Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Flutter Provider - rebuild list item instead of list view

Tags:

flutter

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:

enter image description here

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.

like image 396
rejy11 Avatar asked Dec 06 '19 14:12

rejy11


1 Answers

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

like image 106
magee05 Avatar answered Oct 20 '22 04:10

magee05