Flutter AnimatedList с шаблоном поставщика

У меня есть модель, которая реализует ChangeNotifier

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

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

и ListView, который прослушивает эти изменения:

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);
          },
        );
      },
    );
  }
}

пока все хорошо, когда элемент добавляется в список в модели, уведомление об изменении запускает перестройку Listview, и я вижу новые данные. Но я не могу обернуться, используя это с AnimatedList вместо ListView. Желательно, чтобы id оставила мою модель такой, какая она есть, поскольку анимация - это забота пользовательского интерфейса, а не моей логики.

Changenotifier всегда дает мне обновленную версию моих данных, но что мне действительно нужно, так это уведомление "элемент добавлен" или "элемент удален".

Есть ли лучший способ сделать это?

3 ответа

Недавно я начал изучать Flutter и был удивлен, обнаружив, что эта тема нигде должным образом не освещена. Я придумал два подхода, которые я назвал базовым и продвинутым. Начнем с Базового. Он назван так потому, что Provider вызывается в том же виджете, где построен AnimatedList.

      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),
      ),
    );
  }
}

Расширенный подход отличается тем, что он инкапсулирует AnimatedListState. Эту идею я взял из документации Flutter’s AnimatedList .

      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),
    );
  }
}

Весь код опубликован на Github . Теперь я хочу немного уточнить ваше предложение об уведомлениях о добавлении или удалении элементов. Насколько я понимаю, это противоречит философии Flutter, где виджет является конфигурацией пользовательского интерфейса. Когда состояние виджета изменяется, Flutter сравнивает его с предыдущим состоянием и волшебным образом применяет это различие к пользовательскому интерфейсу. Вот почему я не использовал уведомления «элемент добавлен», «элемент удален» в своих реализациях. Однако я думаю, что это должно быть возможно сделать, потому что я видел аналогичный подход в подписке Firestore для документирования изменений, хотя пока я не могу понять, как реализовать то же самое с Provider. Документация провайдера довольно плохая. После внимательного прочтения я могу' Не скажите, как реализовать частичные обновления с помощью Provider. Может быть ProxyProvider со своимupdateможет помочь или может быть ListenableProvider. Дайте мне знать, если вы можете найти решение вашего предложения.

Это результат моего испытания. Это версия для риверпода, но я думаю, что и для провайдеров то же самое.

Есть два момента.

  1. Инициализируйте состояние в родительском виджете виджета, который использует AnimatedList.
  2. Добавлять/удалять AnimatedList и добавлять/удалять состояния асинхронно с помощью async.

основной дротик

      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));
  }
}

провайдер.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();
  }
}

пример репозитория здесь .

Это старый пост, но я добавляю его сюда, если кто-то наткнется на этот вопрос и не найдет правильного решения.

Я хотел, чтобы какая-то форма перехода по высоте работала со списком элементов, добавленных на боковую панель, которая у меня была. По сути, это была корзина для покупок. Я также использую Provider как решение для управления состоянием.

здесь был не лучший вариант, потому что вам нужно контролировать количество элементов с помощью. Мои функции по добавлению и удалению товара находились в другом месте приложения (карточки товаров и т. д.), поэтому привязывать их к ключу было бы слишком сложно.

Поэтому вместо этого я решил анимироватьвиджет, который обертывал мои дочерние элементы, сэто делает то, что называется, автоматически анимирует высоту.

Затем я добавил анимацию к имеющимся у меня виджетам элементов списка. Это обеспечивает мне переход по высоте, когда я добавляю элемент в список, и сам элемент списка прекрасно сливается с ним.

Вот пример:

      class PurchaseRowItem extends StatefulWidget {
  const PurchaseRowItem({Key? key, required this.item}) : super(key: key);

  final PurchaseRow item;

  @override
  State<PurchaseRowItem> createState() => _PurchaseRowItemState();
}

class _PurchaseRowItemState extends State<PurchaseRowItem> {
  bool _visible = false;

  @override
  void initState() {
    super.initState();

    // Opacity needs to start from 0 and transition to 1
    // so we set it in initState by waiting 10ms
    Future.delayed(const Duration(milliseconds: 10), () {
      setState(() {
        _visible = true;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    AnimatedSize(
      curve: Curves.linear,
      // Alignment is important if you want the list to flow from top to bottom so it doesn't jump when adding items
      alignment: Alignment.topCenter, 
      duration: Duration(milliseconds: 250),
      child: Column(
        children: [
          for (var item in purchase.rows)
            AnimatedOpacity(
              opacity: _visible ? 1 : 0,
              duration: const Duration(milliseconds: 250),
              child: Dismissible(
                child: Container() // Add your list item here
              ),
            )
        ],
      ),
    );
  }
}
Другие вопросы по тегам