Prevent background rebuidling when calling emit()

87 Views Asked by At

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,
                          ),
                        ),
                      ),
                    ),
                  ],
                ),
        ],
      ),
    );
  }

}
1

There are 1 best solutions below

1
eamirho3ein On

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.