在 Flutter 中使用 Provider 更新和动画化 AnimatedList

Posted

技术标签:

【中文标题】在 Flutter 中使用 Provider 更新和动画化 AnimatedList【英文标题】:Updating and animating an AnimatedList with Provider in Flutter 【发布时间】:2021-11-17 12:48:12 【问题描述】:

当列表数据存储在拥有列表小部件的同一组件中时,我能够在 Flutter 中成功地为 AnimatedList 的内容制作动画(即,当列表数据发生更改时不会发生重建)。当我尝试使用 ProviderConsumerChangeNotifier 获取列表项目时遇到问题。

拥有AnimatedList 的组件,我们称之为ListPage,是用Consumer<ListItemService> 构建的。我的理解是,只要服务更新列表数据并调用notifyListeners(),就会重建ListPage。发生这种情况时,我不确定在ListPage 内的哪个位置我可以调用AnimatedListState.insertItem 来为列表设置动画,因为在build 期间,列表状态仍然是null。结果是一个不为其内容设置动画的列表。

我认为我的问题归结为“如果实时获取和更新内容,我如何管理此列表的状态?”,理想情况下,我想了解发生了什么,但我愿意接受建议如果这不是完成任务的最佳方式,我应该如何改变它。

下面是一些说明问题的代码:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() 
  runApp(MyApp());


class MyApp extends StatelessWidget 
  const MyApp(Key? key) : super(key: key);

  @override
  Widget build(BuildContext context) 
    return MultiProvider(
      providers: [
        ChangeNotifierProvider<AuthService>(
          create: (_) => AuthService(),
        ),
        ChangeNotifierProxyProvider<AuthService, ListItemService>(
          create: (_) => ListItemService(),
          update: (_, authService, listItemService) =>
              listItemService!..update(authService),
        ),
      ],
      child: MaterialApp(
        home: HomePage(),
      ),
    );
  


class HomePage extends StatelessWidget 
  const HomePage(Key? key) : super(key: key);

  @override
  Widget build(BuildContext context) 
    return Consumer<ListItemService>(
      builder: (context, listItemService, _) =>
          ListPage(items: listItemService.items),
    );
  


// Implementation details aren't really relevant, but
// this only updates if the user logs in or out.
class AuthService extends ChangeNotifier 

class ListItemService extends ChangeNotifier 
  List<Item> _items = [];
  List<Item> get items => _items;

  Future<void> update(AuthService authService) async 
    // Method that subscribes to a Firestore snapshot
    // and calls notifyListeners() after updating _items.
  


class Item 
  Item(required this.needsUpdate, required this.content);

  final String content;
  bool needsUpdate;


class ListPage extends StatefulWidget 
  const ListPage(Key? key, required this.items) : super(key: key);

  final List<Item> items;

  @override
  _ListPageState createState() => _ListPageState();


class _ListPageState extends State<ListPage> 
  final GlobalKey<AnimatedListState> _listKey = GlobalKey();
  late int _initialItemCount;

  @override
  void initState() 
    _initialItemCount = widget.items.length;
    super.initState();
  

  void _updateList() 
    for (int i = 0; i < widget.items.length; i++) 
      final item = widget.items[i];
      if (item.needsUpdate) 
        // _listKey.currentState is null here if called
        // from the build method.
        _listKey.currentState?.insertItem(i);
        item.needsUpdate = false;
      
    
  

  @override
  Widget build(BuildContext context) 
    _updateList();
    return AnimatedList(
      key: _listKey,
      initialItemCount: _initialItemCount,
      itemBuilder: (context, index, animation) => SizeTransition(
        sizeFactor: animation,
        child: Text(widget.items[index].content),
      ),
    );
  

【问题讨论】:

顺便说一句,我看过这个问题,但我的控制流向相反。在问题中,拥有AnimatedList 的组件决定何时删除元素;在我的应用程序中,该组件只是在等待更新(即使它确实添加了元素,它也会通过告诉提供者应该添加一个项目然后监听来自 Firestore 的更改来做到这一点):***.com/questions/61816446/… 【参考方案1】:

您可以使用didUpdateWidget 并检查新旧列表之间的差异。 “检查差异”意味着查看已删除和添加的内容。在您的情况下,Item 小部件应该有一些要识别的东西。例如,您可以使用Equatable,这样Items 之间的相等就是它们的属性之间的相等。

另一个重要方面是您正在处理一个列表,它是可变的,但小部件应该是不可变的。因此,无论何时修改列表,实际上都创建了一个新列表,这一点至关重要。

这里是实现细节,最有趣的部分当然是注释(虽然渲染也很有趣;)):

import 'dart:async';
import 'dart:math';

import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() 
  runApp(MyApp());


class MyApp extends StatelessWidget 
  const MyApp(Key? key) : super(key: key);

  @override
  Widget build(BuildContext context) 
    return MultiProvider(
      providers: [
        ChangeNotifierProvider<AuthService>(
          create: (_) => AuthService(),
        ),
        ChangeNotifierProxyProvider<AuthService, ListItemService>(
          create: (_) => ListItemService(),
          update: (_, authService, listItemService) => listItemService!..update(authService),
        ),
      ],
      child: MaterialApp(
        home: HomePage(),
      ),
    );
  


class HomePage extends StatelessWidget 
  const HomePage(Key? key) : super(key: key);

  @override
  Widget build(BuildContext context) 
    return Material(
      child: SafeArea(
        child: Consumer<ListItemService>(
          builder: (context, listItemService, _) => ListPage(
            // List.from is very important because it creates a new list instead of
            // giving the old one mutated
            items: List.from(listItemService.items),
          ),
        ),
      ),
    );
  


// Implementation details aren't really relevant, but
// this only updates if the user logs in or out.
class AuthService extends ChangeNotifier 

class ListItemService extends ChangeNotifier 
  List<Item> _items = [];

  List<Item> get items => _items;

  Future<void> update(AuthService authService) async 
    // Every 5 seconds
    Timer.periodic(Duration(seconds: 5), (timer) 
      // Either create or delete an item randomly
      if (Random().nextDouble() > 0.5 && _items.isNotEmpty) 
        _items.removeAt(Random().nextInt(_items.length));
       else 
        _items.add(
          Item(
            needsUpdate: true,
            content: 'This is item with random number $Random().nextInt(10000)',
          ),
        );
      
      notifyListeners();
    );
  


class Item extends Equatable 
  Item(required this.needsUpdate, required this.content);

  final String content;
  bool needsUpdate;

  @override
  List<Object?> get props => [content]; // Not sure you want to include needsUpdate?


class ListPage extends StatefulWidget 
  const ListPage(Key? key, required this.items) : super(key: key);

  final List<Item> items;

  @override
  _ListPageState createState() => _ListPageState();


class _ListPageState extends State<ListPage> 
  final _listKey = GlobalKey<AnimatedListState>();

  // You can use widget if you use late
  late int _initialItemCount = widget.items.length;

  /// Handles any removal of [Item]
  _handleRemovedItems(
    required List<Item> oldItems,
    required List<Item> newItems,
  ) 
    // If an [Item] was in the old but is not in the new, it has
    // been removed
    for (var i = 0; i < oldItems.length; i++) 
      final _oldItem = oldItems[i];
      // Here the equality checks use [content] thanks to Equatable
      if (!newItems.contains(_oldItem)) 
        _listKey.currentState?.removeItem(
          i,
          (context, animation) => SizeTransition(
            sizeFactor: animation,
            child: Text(oldItems[i].content),
          ),
        );
      
    
  

  /// Handles any added [Item]
  _handleAddedItems(
    required List<Item> oldItems,
    required List<Item> newItems,
  ) 
    // If an [Item] is in the new but was not in the old, it has
    // been added
    for (var i = 0; i < newItems.length; i++) 
      // Here the equality checks use [content] thanks to Equatable
      if (!oldItems.contains(newItems[i])) 
        _listKey.currentState?.insertItem(i);
      
    
  

  // Here you can check any update
  @override
  void didUpdateWidget(covariant ListPage oldWidget) 
    super.didUpdateWidget(oldWidget);
    _handleAddedItems(oldItems: oldWidget.items, newItems: widget.items);
    _handleRemovedItems(oldItems: oldWidget.items, newItems: widget.items);
  

  @override
  Widget build(BuildContext context) 
    return AnimatedList(
      key: _listKey,
      initialItemCount: _initialItemCount,
      itemBuilder: (context, index, animation) => SizeTransition(
        sizeFactor: animation,
        child: Padding(
          padding: const EdgeInsets.all(8.0),
          child: Text(widget.items[index].content),
        ),
      ),
    );
  

【讨论】:

出于好奇,为什么从提供程序中的项目创建一个新列表比重复使用同一个列表更好?我猜这与 Flutter 的内部结构有关?另外,如果使用较晚,可以使用小部件的好提示。 进行了更改,这就是我注意到的:didUpdateWidget 在构建之前运行(对吗?),但不知何故 _listKey.currentState 现在不为空。所以我尝试在构建中检查它,不知何故它也不为空。我认为整个问题是由 build 中的 AnimatedListState 始终为 null 引起的,但现在我意识到在构建的 first 调用中它只是 null。但是,动画仍然没有显示,我的直觉是这是因为didUpdateWidgetbuild 之前被调用。那么,即使调用了insertItem,小部件也会很快重建,从而取消动画? 最后一个想法(对不起,这已经困扰了我整整 3 天,我需要把它拿出来大声笑):即使完全跳过对 insertItem 的调用,列表仍然会呈现它(没有动画),这进一步指出了小部件重新渲染“覆盖”动画的方向。我想知道抬起动画状态键是否会有所作为?这会使 ListPage 小部件的父级拥有并创建该状态吗?我应该提到 ListPage 是由 MaterialRoute 中的 Navigator 推送的,尽管我没有理由相信这会搞砸。 使用同一个列表而不是创建一个新列表会导致 oldWidget._items 指向与 widget._items 相同的列表(因为您没有创建一个新列表,所以它们是相同的)。因此您将无法检查差异;)didUpdateWidget 在构建之前运行,但仅在小部件更新时运行,因此不是第一次调用构建。这就是 _listKey.currentState 不为空的原因。我不知道为什么在 build 中调用 insertItem 不起作用,这看起来很奇怪,但无论如何将这个调用放在 build 中是不好的做法,因为它需要的检查在 didUpdateWidget 关于您的最后评论,我无法重现您所描述的内容。如果我从我给你的代码中注释掉_handleAddedItems_handleRemovedItems,那么正如预期的那样,我什么也不会发生。考虑到您用钥匙描述的内容,我认为您并不真正了解它们是什么以及它们做什么。这是一个相当复杂的话题,但如果您有兴趣,请参阅:youtube.com/watch?v=kn0EOS-ZiIc&t=299s

以上是关于在 Flutter 中使用 Provider 更新和动画化 AnimatedList的主要内容,如果未能解决你的问题,请参考以下文章

Flutter Provider,当他的参数改变时更新 StreamProvider

Flutter Provider:拆分通知的正确方法

Provider 调用 notifyListeners() 时 Flutter View 不更新视图

Flutter Provider的使用

使用 Provider (Flutter) 包进行异步异常处理 (Firebase)

flutter中的状态管理Provider