Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Flutter AnimatedList with Provider Pattern

I have model which implements ChangeNotifier

class DataModel with ChangeNotifier{
   List<Data> data = List<Data>();

   void addData(Data data){
      data.add(data);
      notifyListeners();
   }
}

and a ListView which listens to those changes:

class DataListView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<DataModel>(
      builder: (context, model, child) {
        return ListView.builder(
          itemCount: model.data.length,
          itemBuilder: (context, index) {
            return Text(model.data[index].value);
          },
        );
      },
    );
  }
}

so far so good, when an item is added to the list in the model, the change notification triggers a rebuild of the Listview and I see the new data. But I cant wrap my head around using this with a AnimatedList instead of a ListView. Preferably id like to keep my model as it is, seeing as the animation is a concern of the ui and not of my logic.

The changenotifier always gives me a uptodate version of my data, but what i really need is a "item added" or "item removed" notification.

Is there a best practice way of doing this?

like image 712
Claude Hasler Avatar asked May 15 '20 09:05

Claude Hasler


2 Answers

This is the result of my trial. It's a riverpod version, but I think it's the same for providers.

There are two points.

  1. Initialize the state in the parent widget of the widget that uses AnimatedList.
  2. Add / delete AnimatedList and add / delete states asynchronously by using async.

main.dart

import 'package:animatedlist_riverpod_sample/provider.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:hooks_riverpod/all.dart';

void main() {
  runApp(ProviderScope(child: MyApp()));
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: Home(),
    );
  }
}

class Home extends HookWidget {
  const Home({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final todoList = useProvider(todoListProvider.state);
    return Scaffold(appBar: AppBar(title: Text('Todo[${todoList.length}]')), body: TodoListView());
  }
}

class TodoListView extends HookWidget {
  TodoListView({Key key}) : super(key: key);
  final GlobalKey<AnimatedListState> _listKey = GlobalKey<AnimatedListState>();
  final todoList = useProvider(todoListProvider.state);

  @override
  Widget build(BuildContext context) {
    return AnimatedList(
      key: _listKey,
      initialItemCount: todoList.length,
      itemBuilder: (context, index, animation) =>
          _buildItem(todoList[index], animation, index, context),
    );
  }

  Slidable _buildItem(Todo todo, Animation<double> animation, int index, BuildContext context) {
    return Slidable(
      actionPane: SlidableDrawerActionPane(),
      child: SizeTransition(
          sizeFactor: animation,
          axis: Axis.vertical,
          child: ListTile(title: Text(todo.description), subtitle: Text(todo.id), onTap: () => {})),
      secondaryActions: <Widget>[
        IconSlideAction(
          caption: 'Delete',
          color: Colors.red,
          icon: Icons.delete,
          onTap: () {
            _listKey.currentState.removeItem(
                index, (context, animation) => _buildItem(todo, animation, index, context),
                duration: Duration(milliseconds: 200));
            _removeItem(context, todo);
          },
        ),
      ],
    );
  }

  void _removeItem(BuildContext context, Todo todo) async {
    await Future.delayed(
        Duration(milliseconds: 200), () => context.read(todoListProvider).remove(todo));
  }
}

provider.dart

import 'package:hooks_riverpod/all.dart';

final todoListProvider = StateNotifierProvider<TodoList>((ref) {
  return TodoList([
    Todo(id: '0', description: 'Todo1'),
    Todo(id: '1', description: 'Todo2'),
    Todo(id: '2', description: 'Todo3'),
  ]);
});

class Todo {
  Todo({
    this.id,
    this.description,
  });

  final String id;
  final String description;
}

class TodoList extends StateNotifier<List<Todo>> {
  TodoList([List<Todo> initialTodos]) : super(initialTodos ?? []);

  void add(String description) {
    state = [
      ...state,
      Todo(description: description),
    ];
  }

  void remove(Todo target) {
    state = state.where((todo) => todo.id != target.id).toList();
  }
}

sample repository is here.

like image 73
mrym Avatar answered Nov 16 '22 19:11

mrym


I recently started to learn Flutter and was surprised to find that this topic isn't covered properly anywhere. I came up with two approaches which I called Basic and Advanced. Let's start from Basic. It's named like that because Provider is called within the same widget where AnimatedList is built.

class Users extends ChangeNotifier {
  final _list = ['0', '1', '2', '3', '4'];

  int get length => _list.length;

  operator [](index) => _list[index];

  int add() {
    final int index = length;
    _list.add('$index');
    notifyListeners();
    return index;
  }

  String removeAt(int index) {
    String user = _list.removeAt(index);
    notifyListeners();
    return user;
  }
}

class BasicApp extends StatelessWidget {
  const BasicApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        home: ChangeNotifierProvider(create: (_) => Users(), child: AnimatedListDemo()));
  }
}

class AnimatedListDemo extends StatelessWidget {
  final GlobalKey<AnimatedListState> _listKey = GlobalKey();

  AnimatedListDemo({Key? key}) : super(key: key);

  void addUser(Users users) {
    final int index = users.add();
    _listKey.currentState!.insertItem(index, duration: const Duration(seconds: 1));
  }

  void deleteUser(Users users, int index) {
    String user = users.removeAt(index);
    _listKey.currentState!.removeItem(
      index,
      (context, animation) {
        return SizeTransition(sizeFactor: animation, child: _buildItem(users, user));
      },
      duration: const Duration(seconds: 1),
    );
  }

  Widget _buildItem(Users users, String user, [int? removeIndex]) {
    return ListTile(
      key: ValueKey<String>(user),
      title: Text(user),
      leading: const CircleAvatar(
        child: Icon(Icons.person),
      ),
      trailing: (removeIndex != null)
          ? IconButton(
              icon: const Icon(Icons.delete),
              onPressed: () => deleteUser(users, removeIndex),
            )
          : null,
    );
  }

  @override
  Widget build(BuildContext context) {
    Users users = Provider.of<Users>(context, listen: false);
    return Scaffold(
      appBar: AppBar(
        title: const Text('Basic AnimatedList Provider Demo'),
      ),
      body: AnimatedList(
        key: _listKey,
        initialItemCount: users.length,
        itemBuilder: (context, index, animation) {
          return FadeTransition(
            opacity: animation,
            child: _buildItem(users, users[index], index),
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => addUser(users),
        tooltip: 'Add an item',
        child: const Icon(Icons.add),
      ),
    );
  }
}

Advanced approach differs in that it encapsulates AnimatedListState. I took this idea from Flutter's AnimatedList docs.

typedef RemovedItemBuilder = Widget Function(
    String user, BuildContext context, Animation<double> animation);

class Users extends ChangeNotifier {
  final _list = ['0', '1', '2', '3', '4'];
  final GlobalKey<AnimatedListState> _listKey = GlobalKey();
  final RemovedItemBuilder _removedItemBuilder;

  Users(this._removedItemBuilder);

  int get length => _list.length;

  operator [](index) => _list[index];

  GlobalKey<AnimatedListState> get listKey => _listKey;

  int add() {
    final int index = length;
    _list.add('$index');
    _listKey.currentState!.insertItem(index, duration: const Duration(seconds: 1));
    notifyListeners();
    return index;
  }

  String removeAt(int index) {
    String user = _list.removeAt(index);
    _listKey.currentState!.removeItem(
      index,
      (BuildContext context, Animation<double> animation) {
        return _removedItemBuilder(user, context, animation);
      },
      duration: const Duration(seconds: 1),
    );
    notifyListeners();
    return user;
  }
}

class AdvancedApp extends StatelessWidget {
  const AdvancedApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(home: AnimatedListDemo());
  }
}

class AnimatedListDemo extends StatelessWidget {
  const AnimatedListDemo({Key? key}) : super(key: key);

  Widget _buildItem(BuildContext context, String user, [int? removeIndex]) {
    Users users = Provider.of<Users>(context, listen: false);
    return ListTile(
      key: ValueKey<String>(user),
      title: Text(user),
      leading: const CircleAvatar(
        child: Icon(Icons.person),
      ),
      trailing: (removeIndex != null)
          ? IconButton(
              icon: const Icon(Icons.delete),
              onPressed: () => users.removeAt(removeIndex),
            )
          : null,
    );
  }

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(create: (_) => Users((user, context, animation) {
      return SizeTransition(sizeFactor: animation, child: _buildItem(context, user));
    }), child: Scaffold(
      appBar: AppBar(
        title: const Text('Advanced AnimatedList Provider Demo'),
      ),
      body: Consumer<Users>(builder: (BuildContext context, Users users, _){
        return AnimatedList(
          key: users.listKey,
          shrinkWrap: true,
          initialItemCount: users.length,
          itemBuilder: (context, index, animation) {
            return FadeTransition(
              opacity: animation,
              child: _buildItem(context, users[index], index),
            );
          },
        );
      }),
      floatingActionButton: const AddButtonSeparateWidget(),
    ));
  }
}

class AddButtonSeparateWidget extends StatelessWidget {
  const AddButtonSeparateWidget({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    Users users = Provider.of<Users>(context, listen: false);
    return FloatingActionButton(
      onPressed: users.add,
      tooltip: 'Add an item',
      child: const Icon(Icons.add),
    );
  }
}

All code is published on Github. Now I want to elaborate a bit on your proposition of having "item added" or "item removed" notifications. From what I understand it goes against Flutter's philosophy where widget is a UI config. When a widget's state changes, Flutter diffs against its previous state and magically applies the diff to UI. That's why I didn't use "item added", "item removed" notifications in my implementations. However I think it should be possible to do because I saw a similar approach in Firestore subscription to document changes although for now I can't figure how to implement the same with Provider. Provider's documentation is kind of poor. After a careful reading I can't say how to implement partial updates with Provider. May be ProxyProvider with its update could help or may be ListenableProvider. Let me know if you could find the solution to your proposition.

like image 2
vogdb Avatar answered Nov 16 '22 19:11

vogdb