Flutter之状态管理

310 阅读2分钟

状态管理

刚接触Flutter开发时,不管是为了性能还是项目代码简洁等原因,总归还是会选择一个合适的状态管理工具。但是看着茫茫多的选择还是感觉无从下手,当然我也一样。下方记录一下各种状态管理(Riverpod、Provider、Getx)的使用,以便后续遇到其他项目使用不同状态管理时能快速上手。也希望为看到这篇文章的同学提供一些参考。

所以此篇不适合老司机们, 可以 退🤺! 退🤺! 退🤺!。

Riverpod

使用的前提需要注意,需要使用ProviderScope包裹父级节点

Provider

Provider可以简单的用来共享数据,也可以用来拆分逻辑用来监听一些值的变化。

///简单共享数据
final _counterProvider = Provider<int>((ref) => 1);

/// 使用的地方
Consumer(builder: (context, ref, child) {
        return Text(ref.read(_counterProvider).toString());
    },
)

用来拆分逻辑

class _UserInfo {
  String phone;
  String pwd;
  _UserInfo({
    this.phone = '',
    this.pwd = '',
  });
}

final _userInfoProvider = StateNotifierProvider<_UserInfoNotifier, _UserInfo>(
    (ref) => _UserInfoNotifier());

final _isComplete = Provider<bool>((ref) {
  final info = ref.watch(_userInfoProvider);
  return info.phone.length == 11 && info.pwd.length >= 8;
});

Consumer(builder: (context, ref, _) {
    final isComplete = ref.watch(_isComplete);
    return TextButton(
        onPressed: isComplete ? () {} : null,
        child: const Text('登录'));
})

StateProvider

对于简单数据的状态管理,可以直接使用StateProvider,相比于Provider提供了直接修改数据的能力,相比于StateNotifierProvider,避免了为实现简单逻辑还要编写一个StateNotifier类。

final _counter = StateProvider((ref) => 0);

/// 改变状态值
ref.read(_counter.state).state += 1;
/// 重新设置为默认值
ref.refresh(_counter);
/// 监听值的改变
ref.listen(_counter, (_, value) {
    print(value);
});
Consumer(builder: (context, ref, _) {
    return Text(ref.watch(_counter).toString());
})

StateNotifierProvider

一般页面都会有比较复杂的逻辑,这个时候就推荐创建 CustomStateCustomStateNotifier\<CustomState>, 以及提供StateNotifierProvider<CustomStateNotifier, CustomState> 下方代码可能太多,具体例子可以在此处查看

mixin CodeLoginProviders {
  final manager =
      StateNotifierProvider.autoDispose<CodeLoginStateNotifier, CodeLoginState>(
          (ref) {
    return CodeLoginStateNotifier();
  });

  late final canSendCode = Provider.autoDispose<bool>((ref) {
    final state = ref.watch(manager);
    return state.phone.length == 11;
  });

  ...
}

class CodeLoginStateNotifier extends StateNotifier<CodeLoginState> {
  CodeLoginStateNotifier()
      : super(
          CodeLoginState(phone: DeerStorage.phone),
        );
  Timer? _timer;

  ...

  void sendCode() {
    _startTimer();
  }

  void _startTimer() {
    _stopTimer();
    _hadSendCode = true;

    _timeCount = 60;
    _timer = Timer.periodic(const Duration(seconds: 1), (_) {
      if (state.timeCount == 1) {
        _stopTimer();
      } else {
        _timeCount = state.timeCount - 1;
      }
    });
  }

  ...
}

class CodeLoginState extends Equatable {
  final String phone;
  final String code;
  final int timeCount;
  final bool hadSendCode;

  ...
}

FutureProvider

FutureProvider可以用来进行异步操作,比如网络请求、应用启动加载配置等

/// 网络请求
final request = FutureProvider.autoDispose<int>((ref) async {
  ref.onDispose(() {
    /// 可以在此处取消网络请求
    print('dispose');
  });
  await Future.delayed(const Duration(seconds: 2));
  return Random().nextInt(1000);
});

Consumer(builder: (context, ref, _) {
    return ref.watch(request).map(data: (value) {
            if (value.isLoading) return const CircularProgressIndicator();
            return Text('数据 ${value.value}');
        }, error: (error) {
            return Text(error.error.toString());
        }, loading: (_) {
            return const CircularProgressIndicator();
        });
})

/// 模拟App启动时加载必要的配置
final sharePreference = FutureProvider<void>((ref) async {
  /// 可以保存起来,封装存储 ,或者直接返回,使用此provider
  await SharedPreferences.getInstance();
});

final loadConfig = FutureProvider<void>((ref) async {
  /// 获取一些本地存储的数据等
  await Future.delayed(const Duration(seconds: 2));
});

/// 其他配置
final otherFuture = FutureProvider<void>((ref) async {
  await Future.delayed(const Duration(seconds: 4));
});

/// 统一配置状态完成provider
final appConfig = FutureProvider((ref) async {
  await Future.wait([
    ref.watch(sharePreference.future),
    ref.watch(loadConfig.future),
    ref.watch(otherFuture.future),
  ]);
});

// 可以在应用启动时包裹,提供 Splash页面。
Consumer(builder: (context, ref, _) {
    return ref.watch(appConfig).when(data: (_) {
       print(DateTime.now());
       return Text('程序初始化完成');
    }, error: (_, __) {
       return Text('错误');
    }, loading: () {
       print(DateTime.now());
       return Text('loading');
    });
})

family & dependencies

像商品详情页,如果你的provider定义的是全局变量,可以使用family使用商品的id作为入参,提供不同的StateNotifier。我一般是使用mixin提供页面相关的provider,State混入即可,感觉这样更适合单页面的状态管理。

  /// pageView切换
  static final pageIndex = StateProvider<int>((ref) => 0);
  static final isSelected = Provider.family<bool, int>((ref, arg) {
    final index = ref.watch(pageIndex);
    return index == arg;
  }, dependencies: [pageIndex]);
  
 /// 对应下标的item是否处于点击状态
 final isSelected = ref.watch(HeaderProviders.isSelected(index));

overrideWithValue

可以设置provider的值 如例子中应用初始化配置完成后,为全局Provider设置当前用户的信息

/// 等待本地存储等加载完成后设置用户信息
final userInfo =
      StateNotifierProvider<DeerUserInfoState, DeerUserInfo?>(
          (ref) => throw UnimplementedError())

ProviderScope(
      overrides: [
        UserProviders.userInfo
            .overrideWithValue(DeerUserInfoState(DeerStorage.userInfo)),
      ],
      ...
)

Riverpod的具体使用可以在此处找到

Provider

Provider

简单的提供数据共享,不会刷新UI,使用和InheritedWidget差不多,树

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text(ProviderRouters.counter),
      ),
      body: Provider(
        create: (context) => 2,// 提供数据
        child: const _ProviderContainer(),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Center(
      // 获取数据
      child: Consumer<int>(builder: (context, value, _) {
        return Text(value.toString());
      }),
    );
  }
}

ChangeNotifierProvider

提供数据共享,数据模型需要继承或者混入ChangeNotifier,并且在数据改变时调用notifyListeners,不过在实际开发中很少会只有一个属性变化,需要注意合理使用ConsumerSelector,避免不必要的刷新。

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text(ProviderRouters.changeNotifierProvider),
      ),
      body: ChangeNotifierProvider(
        create: (context) => _State(), // 提供状态
        child: const Center(child: _CounterContainer()),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    final _state = context.read<_State>();
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            // 如果有多个属性值更新,只想在一个值改变时刷新
            Selector<_State, int>(
              builder: (context, value, _) {
                print('text - rebuild');
                return Text(value.toString());
              },
              selector: (_, value) => value.count,
            ),
            TextButton(
              onPressed: _state.increase,
              child: const Text('+'),
            ),
          ],
        ),
        Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            /// 对比Selector
            Consumer<_State>(builder: (context, value, _) {
              print('switch - rebuild');
              return Switch(
                value: value.isOpen,
                onChanged: (_) {},
              );
            }),
            TextButton(
              onPressed: _state.change,
              child: const Text('打开/关闭'),
            ),
          ],
        )
      ],
    );
  }
}

class _State extends ChangeNotifier {
  int count = 0;

  bool isOpen = false;

  void increase() {
    count += 1;
    notifyListeners();
  }

  void change() {
    isOpen = !isOpen;
    notifyListeners();
  }
}

FutureProvider

设置初始值,接收一个Future,在状态更新时刷新child

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text(ProviderRouters.futuerProvider),
      ),
      body: FutureProvider.value(
        value: Future.delayed(
            const Duration(seconds: 2), () => _State()..loadCompleted()),
        initialData: _State(),
        child: Consumer<_State>(builder: (_, value, __) {
          Widget container;
          if (value.isLoading) {
            container = const CircularProgressIndicator();
          } else {
            container = const Text('加载完成');
          }
          return Center(child: container);
        }),
      ),
    );
  }
}

class _State {
  bool isLoading = true;

  void loadCompleted() {
    isLoading = false;
  }
}

StreamProvider

和FutureProvider一致,只是接收变成了一个Stream

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text(ProviderRouters.streamProvider),
      ),
      body: StreamProvider<int>(
        create: (_) {
          return Stream.periodic(const Duration(seconds: 1), (value) {
            return value;
          });
        },
        initialData: 0,
        builder: (context, __) {
          return Center(
            /// 此处使用context.watch,原理和Consumer是一样的
            child: Text(context.watch<int>().toString()),
          );
        },
      ),
    );
  }
}

ProxyProvider

如果一个模型需要依赖另外一个或者多个模型值的改变,则可以使用ProxyProvider

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

  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (context) => _Counter()),
        ProxyProvider<_Counter, _Logic>(
          create: (_) => _Logic(number: 0),
          update: (context, value, _) {
            return _Logic(number: value.count);
          },
        ),
      ],
      child: Scaffold(
        appBar: AppBar(title: const Text('data')),
        body: Center(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Consumer<_Counter>(
                  builder: (_, value, __) => Text(value.count.toString())),
              Consumer<_Logic>(builder: (_, value, __) => Text(value.desc)),
              Builder(builder: (context) {
                return TextButton(
                  onPressed: () {
                    context.read<_Counter>().increase();
                  },
                  child: const Text('+'),
                );
              })
            ],
          ),
        ),
      ),
    );
  }
}

class _Counter extends ChangeNotifier {
  int count = 0;

  void increase() {
    count += 1;
    notifyListeners();
  }
}

class _Logic {
  int number;

  _Logic({
    required this.number,
  });

  String get desc => number.isEven ? "偶数" : "奇数";
}

ChangeNotifierProxyProvider

如果一个数据模型自己是一个ChangeNotifier,并且它需要依赖其他数据模型的数据,则可以使用ChangeNotifierProxyProvider,更适合使用于依赖的数据模型的数据是不变的, 当然依赖的其他数据是ChangeNotifier也是可以的。

可以查看官方demo

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

  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        Provider(create: (context) => _Letters()),
        ChangeNotifierProxyProvider<_Letters, _LikeLetters>(
          create: (_) => _LikeLetters(),
          update: (context, value, like) {
            return like!..updateLetters(value.letters);
          },
        ),
      ],
      child: Scaffold(
        appBar: AppBar(title: const Text('data')),
        body: Column(
          mainAxisSize: MainAxisSize.min,
          children: const [
            Expanded(child: AllListView()),
            Divider(),
            Expanded(child: LikeListView()),
          ],
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    final letters =
        context.select<_Letters, List<String>>((value) => value.letters);
    return ListView.builder(
      padding: const EdgeInsets.symmetric(horizontal: 10),
      itemBuilder: (context, index) {
        final letter = letters[index];
        return Row(children: [
          Expanded(
            child: Text(letter),
          ),
          LikeButton(
            letter: letter,
          ),
        ]);
      },
      itemExtent: 50,
      itemCount: letters.length,
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    final letters = context.watch<_LikeLetters>().like;
    return ListView.builder(
      padding: const EdgeInsets.symmetric(horizontal: 10),
      itemBuilder: (context, index) {
        final letter = letters[index];
        return Text(letter);
      },
      itemExtent: 50,
      itemCount: letters.length,
    );
  }
}

class LikeButton extends StatelessWidget {
  final String letter;
  const LikeButton({
    Key? key,
    required this.letter,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final _like = context.watch<_LikeLetters>();
    final isLike = _like.isLike(letter);
    return TextButton(
        onPressed: () {
          isLike ? _like.removeLetter(letter) : _like.addLetter(letter);
        },
        child: Text(isLike ? "不喜欢" : "喜欢"));
  }
}

class _Letters {
  List<String> letters = [
    'A',
    'B',
    'C',
    'D',
    'E',
  ];
}

class _LikeLetters extends ChangeNotifier {
  late List<String> _letters;

  List<String> get letters => _letters;

  List<String> like = [];

  void updateLetters(List<String> value) {
    _letters = value;
    notifyListeners();
  }

  bool isLike(String letter) => like.contains(letter);

  void addLetter(String letter) {
    like.add(letter);

    notifyListeners();
  }

  void removeLetter(String letter) {
    like.remove(letter);

    notifyListeners();
  }
}

GetX

项目中按照GetxController+GetBuilder基本可以大部分状态管理问题,这里只做简单使用记录,Getx有一篇文章,感兴趣的可以去看一下

Obx

一般 obs 配合 obx 使用

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

  final _count = 0.obs;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text(GetXRouter.obx),
      ),
      body: Center(
          child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Obx(() => Text(_count.value.toString())),
          TextButton(
            onPressed: () => _count.value += 1,
            child: const Text('+'),
          )
        ],
      )),
    );
  }
}

Getbuilder

一般 GetxController 和 Getbuilder配合使用


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

  final controller = Get.put(_Controller());

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text(GetXRouter.getBuilder),
      ),
      body: Center(
          child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          GetBuilder<_Controller>(
            builder: (controller) {
              print('rebuild A');
              return Text('A: ${controller.a}');
            },
            filter: (controller) => controller.a,
          ),
          TextButton(
              onPressed: () {
                controller.increaseA();
              },
              child: const Text('A +')),
          GetBuilder<_Controller>(
            builder: (controller) {
              print('rebuild B');

              return Text('B: ${controller.b}');
            },
            filter: (controller) => controller.b,
          ),
          TextButton(
              onPressed: () {
                controller.increaseB();
              },
              child: const Text('B +')),
        ],
      )),
    );
  }
}

class _Controller extends GetxController {
  var a = 0;

  var b = 0;

  void increaseA() {
    a += 1;
    update();
  }

  void increaseB() {
    b += 1;
    update();
  }
}

最后

还有一些其它就不在此处做记录了,总体来说我是喜欢Riverpod的,配合hooks_riverpod你会感到什么叫做幸福的。Riverpod作为Provider的改进版本(同一作者),Provider的选择优先级可以降一降。当然Getx也很好,毕竟star量在那里。这里有一篇GetX的详细教程,作者还写有flutter_blocfish_redux等框架的使用文章,有需要可以去查看

本篇文章demo在这里