I'm using the Provider package to manage my apps business logic but I've encountered a problem where my entire ListView is rebuilding instead of an individual ListTile. Here's the UI to give you a better understanding:
Currently if I scroll to the bottom of the list, tap the checkbox of the last item, I see no animation for the checkbox toggle and the scroll jumps to the top of the screen because the entire widget has rebuilt. How do I use Provider so that only the single ListTile rebuilds and not every item in the List? Here's some of the relevant code:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiProvider(
child: MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Checklist',
theme: ThemeData(
brightness: Brightness.light,
primaryColor: Colors.indigo[500],
accentColor: Colors.amber[500],
),
home: ChecklistHomeScreen(),
),
providers: [
ChangeNotifierProvider(
create: (ctx) => ChecklistsProvider(),
),
],
);
}
}
class ChecklistHomeScreen extends StatefulWidget {
@override
_ChecklistHomeScreenState createState() => _ChecklistHomeScreenState();
}
class _ChecklistHomeScreenState extends State<ChecklistHomeScreen> {
void createList(BuildContext context, String listName) {
if (listName.isNotEmpty) {
Provider.of<ChecklistsProvider>(context).addChecklist(listName);
}
}
@override
Widget build(BuildContext context) {
final _checklists = Provider.of<ChecklistsProvider>(context).checklists;
final _scaffoldKey = GlobalKey<ScaffoldState>();
ScrollController _scrollController =
PrimaryScrollController.of(context) ?? ScrollController();
return Scaffold(
key: _scaffoldKey,
body: CustomScrollView(
controller: _scrollController,
slivers: <Widget>[
SliverAppBar(
floating: true,
pinned: false,
title: Text('Your Lists'),
centerTitle: true,
actions: <Widget>[
PopupMenuButton(
itemBuilder: (ctx) => null,
),
],
),
ReorderableSliverList(
delegate: ReorderableSliverChildBuilderDelegate(
(ctx, i) => _buildListItem(_checklists[i], i),
childCount: _checklists.length,
),
onReorder: (int oldIndex, int newIndex) {
setState(() {
final checklist = _checklists.removeAt(oldIndex);
_checklists.insert(newIndex, checklist);
});
},
),
],
),
drawer: Drawer(
child: null,
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: null,
),
);
}
Widget _buildListItem(Checklist list, int listIndex) {
return Dismissible(
key: ObjectKey(list.id),
direction: DismissDirection.endToStart,
background: Card(
elevation: 0,
child: Container(
alignment: AlignmentDirectional.centerEnd,
color: Theme.of(context).accentColor,
child: Padding(
padding: EdgeInsets.fromLTRB(0.0, 0.0, 10.0, 0.0),
child: Icon(
Icons.delete,
color: Colors.white,
),
),
),
),
child: Card(
child: ListTile(
onTap: null,
title: Text(list.name),
leading: Checkbox(
value: list.completed,
onChanged: (value) {
Provider.of<ChecklistsProvider>(context)
.toggleCompletedStatus(list.id, list.completed);
},
),
trailing: IconButton(
icon: Icon(Icons.more_vert),
onPressed: null,
),
),
),
onDismissed: (direction) {
_onDeleteList(list, listIndex);
},
);
}
void _onDeleteList(Checklist list, int listIndex) {
Scaffold.of(context).removeCurrentSnackBar();
Scaffold.of(context).showSnackBar(
SnackBar(
action: SnackBarAction(
label: 'UNDO',
onPressed: () {
Provider.of<ChecklistsProvider>(context)
.undoDeleteChecklist(list, listIndex);
},
),
content: Text(
'List deleted',
style: TextStyle(color: Theme.of(context).accentColor),
),
),
);
}
}
class ChecklistsProvider with ChangeNotifier {
final ChecklistRepository _repository = ChecklistRepository(); //singleton
UnmodifiableListView<Checklist> get checklists => UnmodifiableListView(_repository.getChecklists());
void addChecklist(String name) {
_repository.addChecklist(name);
notifyListeners();
}
void deleteChecklist(int id) {
_repository.deleteChecklist(id);
notifyListeners();
}
void toggleCompletedStatus(int id, bool completed) {
final list = checklists.firstWhere((c) => c.id == id);
if(list != null) {
list.completed = completed;
_repository.updateChecklist(list);
notifyListeners();
}
}
}
I should say I understand why this is the current behavior, I'm just not sure of the correct approach to ensure only the list item I want to update gets rebuilt instead of the whole screen.
I've also read about Consumer
but I'm not sure how I'd fit it into my implementation.
A Consumer will essentially allow you to consume any changes made to your change notifier. It's best practice to embed the Consumer as deep down as possible in your build method. This way only the wrapped widget will get re-built. This document explains it well: https://flutter.dev/docs/development/data-and-backend/state-mgmt/simple
Try wrapping your CheckBox widget in a Consumer widget. Only the checkbox should be rebuilt.
Consumer<ChecklistsProvider>(
builder: (context, provider, _) {
return Checkbox(
value: list.completed,
onChanged: (value) {
provider.toggleCompletedStatus(list.id, list.completed);
},
);
},
),
If you'd rather have the ListTile AND the CheckBox be re-built, just wrap the ListTile in the Consumer instead
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With