Flutter AnimatedList 与 Provider 模式

问题描述 投票:0回答:3

我有实现 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);
          },
        );
      },
    );
  }
}

到目前为止一切顺利,当将一个项目添加到模型中的列表时,更改通知会触发列表视图的重建,并且我会看到新数据。但我无法全神贯注地使用它与 AnimatedList 而不是 ListView。最好我喜欢保持我的模型原样,因为动画是用户界面的问题而不是我的逻辑的问题。

changenotifier 总是为我提供数据的最新版本,但我真正需要的是“添加项目”或“删除项目”通知。

有这样做的最佳实践方法吗?

flutter provider flutter-animatedlist
3个回答
3
投票

这是我试验的结果。 这是一个 Riverpod 版本,但我认为对于提供商来说是一样的。

有两点。

  1. 初始化使用的小部件的父小部件中的状态 动画列表。
  2. 使用async异步添加/删除AnimatedList以及添加/删除状态。

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

示例存储库位于此处


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

所有代码都发布在Github上。现在我想详细说明一下您关于“添加项目”或“删除项目”通知的提议。据我了解,这违背了 Flutter 的理念,即 widget 是 UI 配置。当小部件的状态发生变化时,Flutter 会与其之前的状态进行比较,并神奇地将差异应用到 UI。这就是为什么我在实现中没有使用“添加项目”、“删除项目”通知。不过,我认为应该可以做到,因为我在 Firestore 订阅文档更改 中看到了类似的方法,尽管现在我不知道如何使用 Provider 实现相同的方法。提供商的文档有点差。仔细阅读之后我也说不出如何用Provider来实现部分更新。可能是 ProxyProvider 及其

update
可以提供帮助,也可能是 ListenableProvider。如果您能找到您的提议的解决方案,请告诉我。


0
投票

这是一篇旧帖子,但如果有人偶然发现这个问题并且没有找到正确的解决方案,我会在这里添加它。

我想要某种形式的高度过渡,用于添加到我的侧边栏的项目列表。它基本上是一个购物篮。我还使用 Provider 作为状态管理解决方案。

AnimatedList
在这里不是一个很好的选择,因为您需要使用
GlobalKey
控制项目数量。我添加项目和删除项目的功能位于应用程序的其他位置(产品卡等),因此将它们挂接到密钥上会太复杂。

因此,我选择对包裹我的子项目的

Column()
小部件进行动画处理,而
AnimatedSize()
正如其名称所示,自动对高度进行动画处理。

然后我向我拥有的列表项小部件添加了一个动画。当我将项目添加到列表中时,这为我提供了高度过渡,并且列表项目本身也随之很好地淡入。

这是一个例子:

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
              ),
            )
        ],
      ),
    );
  }
}
© www.soinside.com 2019 - 2024. All rights reserved.