Search code examples
listflutterlistviewdartkey

how can i update list item without rebuilding whole list


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 Widgets possibly from an API


Solution

  • Providing a Key to the Widgets 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);
      }
    }