I use Stateless Widget alongside BLoC almost everytime. Am I wrong?

Issue

I am having a hard time understanding how to deal with states in some specific situations with Flutter.

For example, say I need a page where the click of a button fetches data from an API. Such a request could take time or any kind of problems could happen. For this reason, I would probably use the BLoC pattern to properly inform the user while the request goes through various "states" such as loading, done, failed and so on.

Now, say I have a page that uses a Timer to periodically (every 1sec) update a Text Widget with the new elapsed time value of a Stopwatch. A timer needs to be properly stopped (timer.cancel()) once it is not used anymore. For this reason, I would use a Stateful Widget and stop the timer directly in the dispose state.

However, what should one do when they have a page which both :

  • Fetches data from API and/or other services which require correctly handling states.
  • Uses a Timer or anything (streams ?) that requires proper canceling/closing/disposing of…

Currently, I have a page which makes API calls and also holds such a Timer. The page is dealt with the BLoC pattern and the presence of the Timer already makes the whole thing a little tedious. Indeed, creating a BLoC "just" to update a Timer feels a little bit like overkill.

But now, I am facing a problem. If the user doesn’t go through the page the "regular" way and decides to use the "back button" of his phone: I never get to cancel the Timer. This, in turn, will throw the following error: Unhandled Exception: Looking up a deactivated widget's ancestor is unsafe.

Indeed, even after having pop() to the previous page, the Timer is still running and trying to complete its every 1sec duty :

Timer.periodic(
      const Duration(seconds: 1),
      (Timer t) {
        context.read<TimerBloc>().add(const Update());
      },
    );
  }

Could someone please explain to me how such "specific" situations should be handled? There must be something, a tiny little concept that I am not totally understand, and I can feel it is slowing me down sometimes.

Thank you very much in advance for any help.

Solution

First off, this is opinionated.

Even though you’ve described a lot, it is a bit tricky to follow your cases and how you’ve (specifically) implemented it. But I’ll give it a shot at describing things to consider.

There are several ways to handle this. I’ll try to answer your questions.

  1. There is nothing wrong with always or only having Stateless widgets and Blocs.
  2. There is nothing wrong with combining Stateful widgets and Blocs.
  3. Consider the case with a page with both a bloc and e.g. a Timer updating a particular text field on that page. Why should one widget handle both? It sounds like the page could be stateless (using the bloc), that has the text field in it, but that text field could/should perhaps be a separate StatefulWidget that only hold the timer, or equivalent. Meaning that sometimes people put to much responsibility in one huge widget, when it in fact should be split into several smaller ones.
  4. I don’t understand why you would face that error, it is no problem having both a bloc and a timer in a stateful widget, with poping and using backbutton with proper disposal and timer being reset. See the full code example below.
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const FirstPage(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('First page'),
      ),
      body: Center(
        child: ElevatedButton(
            onPressed: () => Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => BlocProvider<FetcherCubit>(
                    create: (context) => FetcherCubit(),
                    child: const SecondPage(),
                  ),
                )),
            child: const Text('Second page')),
      ),
    );
  }
}

class SecondPage extends StatefulWidget {
  const SecondPage({super.key});

  @override
  State<SecondPage> createState() => _SecondPageState();
}

class _SecondPageState extends State<SecondPage> {
  late final Timer myTimer;
  int value = 0;

  @override
  void initState() {
    super.initState();
    myTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
      setState(() {
        value++;
      });
    });
  }

  @override
  void dispose() {
    super.dispose();
    myTimer.cancel();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Second page'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('Timer value: ${value.toString()}'),
            ElevatedButton(
              onPressed: () => context.read<FetcherCubit>().fetch(),
              child: const Text('Fetch!'),
            ),
            BlocBuilder<FetcherCubit, FetcherState>(
              builder: (context, state) {
                late final String text;
                if (state is FetcherInitial) {
                  text = 'Initial';
                } else if (state is FetcherLoading) {
                  text = 'Loading';
                } else {
                  text = 'Completed';
                }
                return Text(text);
              },
            )
          ],
        ),
      ),
    );
  }
}

class FetcherCubit extends Cubit<FetcherState> {
  FetcherCubit() : super(FetcherInitial());

  Future<void> fetch() async {
    emit(FetcherLoading());
    await Future.delayed(const Duration(seconds: 3));
    emit(FetcherCompleted());
  }
}

@immutable
abstract class FetcherState {}

class FetcherInitial extends FetcherState {}

class FetcherLoading extends FetcherState {}

class FetcherCompleted extends FetcherState {}

The result if you build it:

enter image description here

Answered By – Robert Sandberg

Answer Checked By – Mildred Charles (FlutterFixes Admin)

Leave a Reply

Your email address will not be published. Required fields are marked *