I'm developping an app which consists in a list of timers. Each of them is stored in a SQLite database and represented by a ListTile in a ListView. Since I need the ability to pause/resume and restart each of the timers independently, each ListTile is a StatefulWidget.
Here is the code for the list :
class TimersList extends StatefulWidget {
const TimersList({Key? key}) : super(key: key);
@override
State<TimersList> createState() => _TimersListState();
}
class _TimersListState extends State<TimersList> {
List<Timer> timers = [];
Future<void> _initTimers() async {
timers = await getAllTimers(); // retrieve from database
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Timers'),
),
body: FutureBuilder<void>(
future: _initTimers(),
builder: (context, snapshot) => ListView.separated(
itemCount: timers.length,
itemBuilder: (BuildContext context, int index) {
final item = timers[index];
return TimerTile(
item: item,
index: index,
);
},
separatorBuilder: (BuildContext context, int index) {
return const Divider(height: 1);
},
)));
}
}
And here is the code for the ListTile :
class TimerTile extends StatefulWidget {
final Timer item;
final int index;
const TimerTile(
{Key? key,
required this.item,
required this.index})
: super(key: key);
@override
State<StatefulWidget> createState() => _TimerTileState();
}
class _TimerTileState extends State<TimerTile> {
@override
Widget build(BuildContext context) {
final countdownController = CountdownController(
duration: widget.item.initialDuration,
);
countdownController.addListener(() {
if (countdownController.currentDuration == Duration.zero) {
NotificationService().showNotifications(widget.item.label);
countdownController.value = widget.item.initialDuration.inMilliseconds;
countdownController.stop();
setState(() {});
}
});
return ListTile(
title:
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
Text(widget.item.label),
Countdown(
countdownController: countdownController,
builder: (
BuildContext context,
Duration currentRemainingTime,
) {
return Text(formatDuration(currentRemainingTime));
},
),
]),
trailing: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return ButtonBar(mainAxisSize: MainAxisSize.min, children: [
IconButton(
onPressed: () {
if (!countdownController.isRunning) {
countdownController.start();
} else {
countdownController.stop();
}
setState(() {});
},
icon: Icon(countdownController.isRunning
? Icons.pause
: Icons.play_arrow)),
IconButton(
onPressed: () {
countdownController.value =
widget.item.duration.inMilliseconds;
countdownController.stop();
setState(() {});
},
icon: const Icon(Icons.restart_alt)),
]);
}),
);
}
}
The timer model object is pretty simple :
class Timer {
final String uuid;
final String label;
final Duration initialDuration;
Timer(this.uuid, this.label, this.initialDuration);
Map<String, dynamic> toMap() {
return {
'uuid': uuid,
'label': label,
'initialDuration': initialDuration.inMilliseconds,
};
}
}
I am using the package flutter_countdown_timer to manage the appearance and state of the timers in the ListTiles.
So far, so good, everything is working.
Now I want to implement a search mechanism. The idea is pretty simple : the user types some text in a TextField and only the timers which label contains the said text should remain in the list.
Nothing too difficult, but the thing is, I'd like the ListTiles to maintain their state while the list is being filtered so that the timer's state remain the same, whether it's ongoing or paused ; and of course, the time left too.
So far, no matter what I tried, the ListTiles are always rebuilt when filtering, which means the state is lost and the timers go back to their original state.
Here's what I tried with no luck :
- Using ListView constructor instead of ListView.separated since, according to the documentation, it builds the children on demand ; which would explain the constant rebuilds.
- Using the AutomaticKeepAliveClientMixin in the TimerTile widget as suggested here.
- Passing a future instead of a method to the FutureBuilder in the list widget and initialize this future in the initState method (I can provide some more code samples if necessary), but it didn't change anything. Getting rid of the FutureBuilder entirely didn't help either.
- Giving keys to my widgets.
Finally, lifting state up and making the TimerTile a Stateless widget as proposed here does not seem to be an option since the CountdownController needs to be in a StatefulWidget.
I'm kind of at the loss here, although I'm pretty sure there is a way to accomplish what I'm trying to do (seems like a pretty standard feature to me). Maybe there's something wrong with my design. In any case, I could really use some guidance.
Thanks.