0

I have a background that is drawn using Flutter custom painter and it uses the Random() function to draw shapes at random positions (ex.: stars), also I have a widget that has a timer, this timer ticks every second and every second I use emit() inside my cubit to update my screen.

My problem is when the screen gets updated every second, the background also gets updated with new positions for my shapes, and every second nearly two rebuilds which means that my background rebuilds itself two times for every second (that's a disaster for the Memory), besides this background must be static, so how to prevent this unnecessary rebuilds?

my stack widget which holds the timer and background behind it: my backgrounds are NightClipper(), LightClipper() and DayTimeline() widgets they are being rebuilt although they're not included in my BlocConsumer()

      Stack(
      alignment: AlignmentDirectional.center,
      fit: StackFit.expand,
      children: [
        ClipPath(
          clipper: NightClipper(
            portrait: _portrait,
            lightRegionFraction: light,
            nightRegionFraction: night,
          ),
          child: CustomPaint(
            painter: DayNightView(
              portrait: _portrait,
              lightRegionFraction: light,
              nightRegionFraction: night,
            ),
          ),
        ),
        ClipPath(
          clipper: LightClipper(
            portrait: _portrait,
            lightRegionFraction: light,
            nightRegionFraction: night,
          ),
          child: CustomPaint(
            painter: DayLightView(
              portrait: _portrait,
              lightRegionFraction: light,
              nightRegionFraction: night,
            ),
          ),
        ),
        CustomPaint(
          painter: DayTimeline(
            portrait: _portrait,
            fajrFraction: light,
            sunriseFraction: 0.2,
            dhuhrFraction: 0.45,
            asrFraction: 0.6,
            maghribFraction: night,
            ishaFraction: 0.8,
          ),
        ),
        BlocConsumer<HomeScreenMainWidgetCubit, HomeScreenMainWidget>(
          listener: (context, value) {
            context.read<HomeScreenMainWidgetCubit>().refresh();
          },
          builder: (context, value) {
            return Stack(
              alignment: AlignmentDirectional.center,
              fit: StackFit.expand,
              children: [
                CustomPaint(
                  foregroundPainter: NextPrayerIndicator(
                    indicator: value.indicatorValue,
                    portrait: _portrait,
                  ),
                  painter: Background(
                    portrait: _portrait,
                  ),
                ),
                CustomPaint(
                  painter: CountdownTimer(
                    timer: value.nextPrayerTimer,
                    portrait: _portrait,
                  ),
                ),
                CustomPaint(
                  painter: PrayerTitle(
                    title: value.nextPrayerTitle,
                    portrait: _portrait,
                  ),
                ),
              ],
            );
          },
        ),
      ],
    );

My provider code

        BlocProvider(
          create: (_) => HomeScreenMainWidgetCubit()..init(),
        ),

My cubit code

class HomeScreenMainWidgetCubit extends Cubit<HomeScreenMainWidget> {
  HomeScreenMainWidgetCubit()
      : super(HomeScreenMainWidget(
          indicatorValue: 0.0,
          nextPrayerTitle: '...',
          nextPrayerTimer: Duration(
            hours: 0,
            minutes: 0,
            seconds: 0,
          ),
        ));

  /*
  0 -> sunrise
  1 -> sunset / maghrib
  2 -> fajr
  3 -> dhuhr
  4 -> asr
  5 -> isha
   */

  Timer? _timer;
  List<Duration>? _prayerTimes;

  void init() {
    bool gps = SharedPreferencesService.getGPSBool() ?? false;
    if (gps == true) {
      Position? position = SharedPreferencesService.getPosition();
      _prayerTimes = PrayerTimes.getAuto(
        position!,
        DateTime.now(),
      );
    } else {
      String country = SharedPreferencesService.getCountry() ?? 'Egypt';
      String state = SharedPreferencesService.getState() ?? 'Cairo';
      _prayerTimes = PrayerTimes.getManual(
        country,
        state,
        DateTime.now(),
      );
    }
    refresh();
  }

  void refresh() {
    _timer = Timer(Duration(seconds: 1), () {
      emit(HomeScreenMainWidget(
        indicatorValue: NextPrayer.getIndicatorValue(
          _prayerTimes!,
          DateTime.now(),
        ),
        nextPrayerTitle: NextPrayer.getTitle(
          _prayerTimes!,
          DateTime.now(),
        ),
        nextPrayerTimer: NextPrayer.getTimer(
          _prayerTimes!,
          DateTime.now(),
        ),
      ));
    });
  }

  @override
  Future<void> close() {
    _timer?.cancel();
    return super.close();
  }
}

Note that in my custom painter, my shouldRepaint function returns false, so I don't know why my background gets updated on each emit.

edits: here's my stateless widget

class HomeScreen extends StatelessWidget {
  const HomeScreen({
    Key? key,
  }) : super(key: key);

  static String id = 'home_screen';

  @override
  Widget build(BuildContext context) {
    Orientation orientation = MediaQuery.of(context).orientation;
    Size size = MediaQuery.of(context).size;
    bool isPortrait = !((size.width > kPhoneMaximumScreenWidth) ||
        (orientation == Orientation.landscape));
    return MultiBlocProvider(
      providers: [
        BlocProvider(
          create: (_) => HomeScreenMainWidgetCubit()..init(),
        ),
      ],
      child: Stack(
        alignment: AlignmentDirectional.center,
        fit: StackFit.expand,
        children: [
          isPortrait
              ? Column(
                  crossAxisAlignment: CrossAxisAlignment.stretch,
                  children: [
                    Expanded(
                      // flex: 5,
                      child: Semantics(
                        // todo build semantics
                        child: ClipRect(
                          clipper: BackgroundClipper(
                            portrait: isPortrait,
                          ),
                          child: HomeScreenStackDesign(
                            portrait: isPortrait,
                          ),
                        ),
                      ),
                    ),
                  ],
                )
              : Row(
                  crossAxisAlignment: CrossAxisAlignment.stretch,
                  children: [
                    Expanded(
                      // flex: 2,
                      child: Semantics(
                        // todo build semantics
                        child: ClipRect(
                          clipper: BackgroundClipper(
                            portrait: isPortrait,
                          ),
                          child: HomeScreenStackDesign(
                            portrait: isPortrait,
                          ),
                        ),
                      ),
                    ),
                  ],
                ),
        ],
      ),
    );
  }

}
Saif
  • 46
  • 4
  • Your build() might get called 60 times per second. It should be idempotent and fast. If you have random items, they should be stored in State, and recomputed only at chosen times. – Randal Schwartz Oct 05 '22 at 19:04
  • okay, I'm listening can you please explain this more? @RandalSchwartz – Saif Oct 05 '22 at 19:08
  • What part is unclear? – Randal Schwartz Oct 05 '22 at 21:55
  • how to store it in State? what do u mean by that? kindly explain it more @RandalSchwartz – Saif Oct 06 '22 at 13:21
  • I'm using stateless widget and its buid() returns my provider as top level in my tree and btw when I converted to Stateful widget I have the same problem, my background keeps updating with each emit() and note that I'm using a Random() function inside my Paint() method inside custom painter with for-loop which loops 100 times to draw 100 shapes at different positions of the screen – Saif Oct 06 '22 at 13:25
  • check out my edits in the question above please @RandalSchwartz – Saif Oct 06 '22 at 13:27

1 Answers1

0

You can define a variable like this:

Widget backgroundWidget;

and fill it in initState like this:

@override
  void initState() {
    super.initState();
    backgroundWidget = Stack(
      children:[
         ClipPath(
          clipper: NightClipper(
            portrait: _portrait,
            lightRegionFraction: light,
            nightRegionFraction: night,
          ),
          child: CustomPaint(
            painter: DayNightView(
              portrait: _portrait,
              lightRegionFraction: light,
              nightRegionFraction: night,
            ),
          ),
        ),
        ClipPath(
          clipper: LightClipper(
            portrait: _portrait,
            lightRegionFraction: light,
            nightRegionFraction: night,
          ),
          child: CustomPaint(
            painter: DayLightView(
              portrait: _portrait,
              lightRegionFraction: light,
              nightRegionFraction: night,
            ),
          ),
        ),
        CustomPaint(
          painter: DayTimeline(
            portrait: _portrait,
            fajrFraction: light,
            sunriseFraction: 0.2,
            dhuhrFraction: 0.45,
            asrFraction: 0.6,
            maghribFraction: night,
            ishaFraction: 0.8,
          ),
        ),
      ]
    );
  }

and pass this backgroundWidget to your main stack widget:

Stack(
      alignment: AlignmentDirectional.center,
      fit: StackFit.expand,
      children: [
        backgroundWidget,
        BlocConsumer<HomeScreenMainWidgetCubit, HomeScreenMainWidget>(
          listener: (context, value) {
            context.read<HomeScreenMainWidgetCubit>().refresh();
          },
          builder: (context, value) {
            return Stack(
              alignment: AlignmentDirectional.center,
              fit: StackFit.expand,
              children: [
                CustomPaint(
                  foregroundPainter: NextPrayerIndicator(
                    indicator: value.indicatorValue,
                    portrait: _portrait,
                  ),
                  painter: Background(
                    portrait: _portrait,
                  ),
                ),
                CustomPaint(
                  painter: CountdownTimer(
                    timer: value.nextPrayerTimer,
                    portrait: _portrait,
                  ),
                ),
                CustomPaint(
                  painter: PrayerTitle(
                    title: value.nextPrayerTitle,
                    portrait: _portrait,
                  ),
                ),
              ],
            );
          },
        ),
      ],
    );

in this way your backgroundWidget always paint once and not change in rebuild.

eamirho3ein
  • 16,619
  • 2
  • 12
  • 23