I'm able to successfully animate an AnimatedList
's contents in Flutter when the list data is stored in the same component that owns the list widget (i.e., there's no rebuild happening when there's changes to the list data). I run into issues when I try to get the items for the list from a ChangeNotifier
using Provider
and Consumer
.
The component that owns the AnimatedList
, let's call it ListPage
, is built with a Consumer<ListItemService>
. My understanding is that ListPage
is then rebuilt whenever the service updates the list data and calls notifyListeners()
. When that happens, I'm not sure where within ListPage
I could call AnimatedListState.insertItem
to animate the list, since during the build
the list state is still null
. The result is a list that doesn't animate its contents.
I think my question boils down to "how do I manage state for this list if the contents are fetched and updated in real time?", and ideally I'd like to understand what's going on but I'm open to suggestions on how I should change this if this isn't the best way to approach the task.
Here's some code that illustrates the problem:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<AuthService>(
create: (_) => AuthService(),
),
ChangeNotifierProxyProvider<AuthService, ListItemService>(
create: (_) => ListItemService(),
update: (_, authService, listItemService) =>
listItemService!..update(authService),
),
],
child: MaterialApp(
home: HomePage(),
),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Consumer<ListItemService>(
builder: (context, listItemService, _) =>
ListPage(items: listItemService.items),
);
}
}
// Implementation details aren't really relevant, but
// this only updates if the user logs in or out.
class AuthService extends ChangeNotifier {}
class ListItemService extends ChangeNotifier {
List<Item> _items = [];
List<Item> get items => _items;
Future<void> update(AuthService authService) async {
// Method that subscribes to a Firestore snapshot
// and calls notifyListeners() after updating _items.
}
}
class Item {
Item({required this.needsUpdate, required this.content});
final String content;
bool needsUpdate;
}
class ListPage extends StatefulWidget {
const ListPage({Key? key, required this.items}) : super(key: key);
final List<Item> items;
@override
_ListPageState createState() => _ListPageState();
}
class _ListPageState extends State<ListPage> {
final GlobalKey<AnimatedListState> _listKey = GlobalKey();
late int _initialItemCount;
@override
void initState() {
_initialItemCount = widget.items.length;
super.initState();
}
void _updateList() {
for (int i = 0; i < widget.items.length; i ) {
final item = widget.items[i];
if (item.needsUpdate) {
// _listKey.currentState is null here if called
// from the build method.
_listKey.currentState?.insertItem(i);
item.needsUpdate = false;
}
}
}
@override
Widget build(BuildContext context) {
_updateList();
return AnimatedList(
key: _listKey,
initialItemCount: _initialItemCount,
itemBuilder: (context, index, animation) => SizeTransition(
sizeFactor: animation,
child: Text(widget.items[index].content),
),
);
}
}
CodePudding user response:
You can use didUpdateWidget
and check the difference between the old and new list. "Checking the difference" means looking at what has been removed vs added. In you case the Item
widget should have something to be identified. You can use Equatable
for example so that an equality between Item
s is an equality between their properties.
One other important aspect is that you are dealing with a list, which is mutable, but Widgets should be immutable. Therefore it is crucial that whenever you modify the list, you actually create a new one.
Here are the implementations details, the most interesting part being the comment of course (though the rendering is fun as well ;)):
import 'dart:async';
import 'dart:math';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<AuthService>(
create: (_) => AuthService(),
),
ChangeNotifierProxyProvider<AuthService, ListItemService>(
create: (_) => ListItemService(),
update: (_, authService, listItemService) => listItemService!..update(authService),
),
],
child: MaterialApp(
home: HomePage(),
),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Material(
child: SafeArea(
child: Consumer<ListItemService>(
builder: (context, listItemService, _) => ListPage(
// List.from is very important because it creates a new list instead of
// giving the old one mutated
items: List.from(listItemService.items),
),
),
),
);
}
}
// Implementation details aren't really relevant, but
// this only updates if the user logs in or out.
class AuthService extends ChangeNotifier {}
class ListItemService extends ChangeNotifier {
List<Item> _items = [];
List<Item> get items => _items;
Future<void> update(AuthService authService) async {
// Every 5 seconds
Timer.periodic(Duration(seconds: 5), (timer) {
// Either create or delete an item randomly
if (Random().nextDouble() > 0.5 && _items.isNotEmpty) {
_items.removeAt(Random().nextInt(_items.length));
} else {
_items.add(
Item(
needsUpdate: true,
content: 'This is item with random number ${Random().nextInt(10000)}',
),
);
}
notifyListeners();
});
}
}
class Item extends Equatable {
Item({required this.needsUpdate, required this.content});
final String content;
bool needsUpdate;
@override
List<Object?> get props => [content]; // Not sure you want to include needsUpdate?
}
class ListPage extends StatefulWidget {
const ListPage({Key? key, required this.items}) : super(key: key);
final List<Item> items;
@override
_ListPageState createState() => _ListPageState();
}
class _ListPageState extends State<ListPage> {
final _listKey = GlobalKey<AnimatedListState>();
// You can use widget if you use late
late int _initialItemCount = widget.items.length;
/// Handles any removal of [Item]
_handleRemovedItems({
required List<Item> oldItems,
required List<Item> newItems,
}) {
// If an [Item] was in the old but is not in the new, it has
// been removed
for (var i = 0; i < oldItems.length; i ) {
final _oldItem = oldItems[i];
// Here the equality checks use [content] thanks to Equatable
if (!newItems.contains(_oldItem)) {
_listKey.currentState?.removeItem(
i,
(context, animation) => SizeTransition(
sizeFactor: animation,
child: Text(oldItems[i].content),
),
);
}
}
}
/// Handles any added [Item]
_handleAddedItems({
required List<Item> oldItems,
required List<Item> newItems,
}) {
// If an [Item] is in the new but was not in the old, it has
// been added
for (var i = 0; i < newItems.length; i ) {
// Here the equality checks use [content] thanks to Equatable
if (!oldItems.contains(newItems[i])) {
_listKey.currentState?.insertItem(i);
}
}
}
// Here you can check any update
@override
void didUpdateWidget(covariant ListPage oldWidget) {
super.didUpdateWidget(oldWidget);
_handleAddedItems(oldItems: oldWidget.items, newItems: widget.items);
_handleRemovedItems(oldItems: oldWidget.items, newItems: widget.items);
}
@override
Widget build(BuildContext context) {
return AnimatedList(
key: _listKey,
initialItemCount: _initialItemCount,
itemBuilder: (context, index, animation) => SizeTransition(
sizeFactor: animation,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(widget.items[index].content),
),
),
);
}
}