Someone asked me this question and I'd like to make the answer accessible
how can i update list item without rebuilding whole list?
A typical use case (that I will reproduce in the following answer)
could be a ListView
that receives a List
of Widget
s possibly from an API
Providing a Key
to the Widget
s in the List
will prevent those
from being removed from the widget tree and, consequently, being needlessly rebuilt
you may try yourself running this app in dartpad
note the logs in the terminal;
the code is posted below
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final _navigatorKey = GlobalKey<NavigatorState>();
FakeApi _api;
@override
void initState() {
_api = FakeApi(_navigatorKey);
super.initState();
}
@override
void dispose() {
_api?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => MaterialApp(
navigatorKey: _navigatorKey,
home: MyInheritedWidget(
api: _api,
child: const MyHomePage(),
),
);
}
class MyInheritedWidget extends InheritedWidget {
const MyInheritedWidget({
@required Widget child,
@required this.api,
}) : super(
key: const Key('MyInheritedWidget'),
child: child,
);
final FakeApi api;
static MyInheritedWidget of(BuildContext context) =>
context.dependOnInheritedWidgetOfExactType<MyInheritedWidget>();
@override
bool updateShouldNotify(MyInheritedWidget old) => false;
}
class MyHomePage extends StatelessWidget {
const MyHomePage() : super(key: const Key('MyHomePage'));
@override
Widget build(BuildContext context) => Builder(
builder: (context) => Scaffold(
backgroundColor: Colors.blueGrey,
body: StreamBuilder<List<ItemWidget>>(
stream: MyInheritedWidget.of(context).api.stream,
initialData: [],
builder: (context, list) => list.hasError
? const Center(child: Icon(Icons.error))
: !list.hasData
? const Center(child: CircularProgressIndicator())
: list.data.isEmpty
? const Center(
child: Text(
'the list is empty',
textScaleFactor: 1.5,
))
: ListView.builder(
itemCount: list.data.length,
itemBuilder: (context, index) => list.data[index],
),
),
floatingActionButton: FloatingActionButton(
backgroundColor: Colors.white,
child: const Icon(Icons.add, color: Colors.blueGrey),
onPressed: MyInheritedWidget.of(context).api.add,
),
),
);
}
class ItemWidget extends StatelessWidget {
ItemWidget(this.text) : super(key: UniqueKey());
final String text;
@override
Widget build(BuildContext context) {
print('Item $text is building');
return Center(
child: Container(
padding: const EdgeInsets.only(bottom: 20),
width: MediaQuery.of(context).size.width * .5,
child: Card(
elevation: 10,
child: ListTile(
leading: GestureDetector(
child: const Icon(Icons.edit),
onTap: () => MyInheritedWidget.of(context).api.edit(key),
),
trailing: GestureDetector(
child: const Icon(Icons.delete),
onTap: () => MyInheritedWidget.of(context).api.delete(key),
),
title: Text(text),
),
),
),
);
}
}
class ItemDialog extends StatefulWidget {
const ItemDialog({this.text});
final String text;
@override
_ItemDialogState createState() => _ItemDialogState();
}
class _ItemDialogState extends State<ItemDialog> {
TextEditingController _controller;
@override
void initState() {
_controller = TextEditingController()..text = widget.text;
super.initState();
}
@override
void dispose() {
_controller?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => AlertDialog(
content: Stack(
alignment: Alignment.center,
children: <Widget>[
Container(
width: double.infinity,
height: MediaQuery.of(context).size.height * .3,
child: Center(
child: TextField(
autofocus: true,
controller: _controller,
),
),
),
],
),
actions: <Widget>[
IconButton(
onPressed: () => Navigator.pop(context, _controller.text ?? ''),
icon: const Icon(Icons.save),
),
],
);
}
class FakeApi {
FakeApi(this.navigatorKey);
final GlobalKey<NavigatorState> navigatorKey;
final _list = <ItemWidget>[];
StreamController<List<ItemWidget>> _controller;
StreamController<List<ItemWidget>> get _c =>
_controller ??= StreamController<List<ItemWidget>>.broadcast();
Stream<List<ItemWidget>> get stream => _c.stream;
void dispose() => _controller?.close();
void delete(Key key) {
_list.removeWhere((ItemWidget item) => item.key == key);
_c.sink.add(_list);
}
void edit(Key key) async {
final _item = _list.firstWhere((ItemWidget item) => item.key == key);
final _index = _list.lastIndexOf(_item);
final _text = await showDialog<String>(
context: navigatorKey.currentState.overlay.context,
builder: (context) => ItemDialog(
text: _item.text,
),
);
_list.removeAt(_index);
_list.insert(_index, ItemWidget(_text));
_c.sink.add(_list);
}
void add() async {
final _text = await showDialog<String>(
context: navigatorKey.currentState.overlay.context,
builder: (context) => ItemDialog(),
);
_list.add(ItemWidget(_text));
_c.sink.add(_list);
}
}