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 :
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.
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.
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: