熟悉我的朋友应该都知道,我好几年前写过一个「Flutter状态管理之路」系列,那个时候介绍的是Provider,这也是官方推荐的状态管理工具,但当时没有写完,因为写着写着,觉得有很多地方不尽人意,用着很别扭,所以在写了7篇文章之后,就暂时搁置了。
一晃时间过了这么久,Flutter内部依然没有一个能够碾压一切的状态管理框架,GetX可能是,但是我觉得不是,InheritedWidget系的状态管理,才应该是正统的状态管理。
最近在留意Provider的后续进展时,意外发现了一个新的库——Riverpod,号称是新一代的状态管理工具,仔细一看,嘿,居然还是Provider的作者,好家伙,这是搬起石头砸自己的脚啊。
就像作者所说,Riverpod就是对Provider的重写,可不是吗,字母都没变,就换了个顺序,这名字也是取的博大精深。
其实Provider在使用上已经非常不错了,只不过随着Flutter的更加深入,大家对它的需求也就越来越高,特别是对Provider中因为InheritedWidget层次问题导致的异常和BuildContext的使用这些问题诟病很多,而Riverpod,正是在Provider的基础上,探索出了一条心的状态管理之路。
大家可以先把官方文档看一看 https://riverpod.dev ,看完之后发现还是一脸懵逼,那就对了,Riverpod和Provider一样,有很多类型的Provider,分别用于不同的场景,所以,理清这些Provider的不同作用和使用场景,对于我们用好Riverpod是非常有帮助的。
官网的文档,虽然是作者精心编写的,但它的教程,站在的是一个创作者的角度,所以很多入门的初学者看上去会有点摸不清方向,所以,这才有了这个系列的文章。
我将在这个系列中,带领大家对文档进行一次精读,进行一次赏析,本文不全是对文档的翻译,而且讲解的顺序也不一样,所以,如果你想入门Riverpod进行状态管理,那么本文一定是你的最佳选择。
首先,我们为什么要进行状态管理,状态管理是解决申明式UI开发,关于数据状态的一个处理操作,例如Widget A依赖于同级的Widget B的数据,那么这个时候,就只能把数据状态上提到它们的父类,但是这样比较麻烦,Riverpod和Provider这样的状态管理框架,就是为了解决类似的问题而产生的。
将一个state包裹在一个Provider中可以有下面一些好处。
首先,我们通过一个简单的例子,来感受下,Riverpod是怎么进行状态管理的。
Provider是Riverpod应用程序中最重要的部分。Provider是一个对象,它封装了一个state并允许监听该state。Provider有很多变体形式,但它们的工作方式都是一样的。
最常见的用法是将它们声明为全局常量,例如下面这样。
final myProvider = Provider((ref) {
return MyValue();
});
不要被Provider的全局变量所吓倒。Provider是完全final的。声明一个Provider与声明一个函数没有什么不同,而且Provider是可测试和可维护的。
这段代码由三个部分组成。
传递给Provider的函数返回的对象的类型,取决于所使用的Provider。例如,一个Provider的函数可以创建任何对象。另一方面,StreamProvider的回调将被期望返回一个Stream。
你可以不受限制地声明你想要的多个Provider。与使用package:provider不同的是,Riverpod允许创建多个暴露相同 "类型 "的状态的provider。
final cityProvider = Provider((ref) => 'London');
final countryProvider = Provider((ref) => 'England');
两个Provider都创建了一个字符串,但这并没有任何问题。
为了使Provider发挥作用,您必须在Flutter应用程序的根部添加ProviderScope。
void main() {
runApp(ProviderScope(child: MyApp()));
}
以上就是Riverpod最简单的使用,我们看下完整的示例代码。
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// We create a "provider", which will store a value (here "Hello world").
// By using a provider, this allows us to mock/override the value exposed.
final helloWorldProvider = Provider((_) => 'Hello world');
void main() {
runApp(
// For widgets to be able to read providers, we need to wrap the entire
// application in a "ProviderScope" widget.
// This is where the state of our providers will be stored.
ProviderScope(
child: MyApp(),
),
);
}
// Extend ConsumerWidget instead of StatelessWidget, which is exposed by Riverpod
class MyApp extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final String value = ref.watch(helloWorldProvider);
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Example')),
body: Center(
child: Text(value),
),
),
);
}
}
可以发现,Riverpod的使用比package:Provider还要简单,申明一个全局变量来管理状态数据,然后就可以在任意地方获取数据了。
在有了一个简单的了解后,我们先来了解下关于状态中的「读」。
在Riverpod中,我们不像package:Provider那样需要依赖BuildContext,取而代之的是一个「ref」变量。这个东西,就是联系存取双方的纽带,这个对象允许我们与Provider互动,不管是来自一个Widget还是另一个Provider。
所有Provider都有一个 "ref "作为参数。
final provider = Provider((ref) {
// use ref to obtain other providers
final repository = ref.watch(repositoryProvider);
return SomeValue(repository);
})
这个参数可以安全地传递给其它Provider或者类,来获取所需要的值。
例如,一个常见的用例是将Provider的 "ref "传递给一个StateNotifier。
final counterProvider = StateNotifierProvider<Counter, int>((ref) {
return Counter(ref);
});
class Counter extends StateNotifier<int> {
Counter(this.ref): super(0);
final Ref ref;
void increment() {
// Counter can use the "ref" to read other providers
final repository = ref.read(repositoryProvider);
repository.post('...');
}
}
这样做,可以使我们的Counter类能够读取Provider。
这种方式是联系组件和Provider的一个重要方式。
Widgets自然没有一个ref参数。但是Riverpod提供了多种解决方案来从widget中获得这个参数。
在widget树中获得一个ref的最常见的方法是用ConsumerWidget代替StatelessWidget。
ConsumerWidget在使用上与StatelessWidget相同,唯一的区别是它的构建方法上有一个额外的参数:"ref "对象。
一个典型的ConsumerWidget看起来像这样。
class HomeView extends ConsumerWidget {
const HomeView({Key? key}): super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
// use ref to listen to a provider
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}
与ConsumerWidget类似,ConsumerStatefulWidget和ConsumerState相当于一个带有状态的StatefulWidget,不同的是,state有一个 "ref "对象。
这一次,"ref "不是作为构建方法的参数传递,而是作为ConsumerState对象的一个属性。
class HomeView extends ConsumerStatefulWidget {
const HomeView({Key? key}): super(key: key);
@override
HomeViewState createState() => HomeViewState();
}
class HomeViewState extends ConsumerState<HomeView> {
@override
void initState() {
super.initState();
// "ref" can be used in all life-cycles of a StatefulWidget.
ref.read(counterProvider);
}
@override
Widget build(BuildContext context) {
// We can also use "ref" to listen to a provider inside the build method
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}
现在我们有了一个 "ref",我们可以开始使用它。
ref "有三个主要用途。
只要有可能,最好使用 ref.watch 而不是 ref.read 或 ref.listen 来实现一个功能。
ref.watch在Widget的构建方法中使用,或者在Provider的主体中使用,以使得Widget/Provider可以监听另一个Provider。
例如,Provider可以使用 ref.watch 来将多个Provider合并成一个新的值。
一个例子是过滤一个todo-list,我们需要两个Provider。
通过使用ref.watch,我们可以制作第三个Provider,结合这两个Provider来创建一个过滤后的任务列表。
final filterTypeProvider = StateProvider<FilterType>((ref) => FilterType.none);
final todosProvider = StateNotifierProvider<TodoList, List<Todo>>((ref) => TodoList());
final filteredTodoListProvider = Provider((ref) {
// obtains both the filter and the list of todos
final FilterType filter = ref.watch(filterTypeProvider);
final List<Todo> todos = ref.watch(todosProvider);
switch (filter) {
case FilterType.completed:
// return the completed list of todos
return todos.where((todo) => todo.isCompleted).toList();
case FilterType.none:
// returns the unfiltered list of todos
return todos;
}
});
有了这段代码,filteredTodoListProvider现在就可以管理过滤后的任务列表。
如果过滤器或任务列表发生变化,过滤后的列表也会自动更新。同时,如果过滤器和任务列表都没有改变,过滤后的列表将不会被重新计算。
类似地,一个Widget可以使用ref.watch来显示来自Provider的内容,并在该内容发生变化时更新用户界面。
final counterProvider = StateProvider((ref) => 0);
class HomeView extends ConsumerWidget {
const HomeView({Key? key}): super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
// use ref to listen to a provider
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}
这段代码显示了一个Widget,它监听了一个存储计数的Provider。如果该计数发生变化,该Widget将重建,用户界面将更新以显示新的值。
ref.watch方法不应该被异步调用,比如在ElevatedButton的onPressed中。也不应该在initState和其他State的生命周期内使用它。在这些情况下,考虑使用 ref.read 来代替。
与ref.watch类似,可以使用ref.listen来观察一个Provider。
它们之间的主要区别是,如果被监听的Provider发生变化,使用ref.listen不会重建widget/provider,而是会调用一个自定义函数。
这对于在某个变化发生时执行某些操作是很有用的,比如在发生错误时显示一个snackbar。
ref.listen方法需要2个参数,第一个是Provider,第二个是当状态改变时我们要执行的回调函数。回调函数在被调用时将被传递2个值,即先前状态的值和新状态的值。
ref.listen方法也可以在Provider的体内使用。
final counterProvider = StateNotifierProvider<Counter, int>((ref) => Counter(ref));
final anotherProvider = Provider((ref) {
ref.listen<int>(counterProvider, (int? previousCount, int newCount) {
print('The counter changed $newCount');
});
// ...
});
或在一个Widget的Build方法中使用。
final counterProvider = StateNotifierProvider<Counter, int>((ref) => Counter(ref));
class HomeView extends ConsumerWidget {
const HomeView({Key? key}): super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
ref.listen<int>(counterProvider, (int? previousCount, int newCount) {
print('The counter changed $newCount');
});
return Container();
}
}
ref.listen也不应该被异步调用,比如在ElevatedButton的onPressed中。也不应该在initState和其他State的生命周期内使用它。
ref.read方法是一种在不监听的情况下获取Provider的状态的方法。
它通常用于由用户交互触发的函数中。例如,当用户点击一个按钮时,我们可以使用ref.read来增加一个计数器的值。
final counterProvider = StateNotifierProvider<Counter, int>((ref) => Counter(ref));
class HomeView extends ConsumerWidget {
const HomeView({Key? key}): super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
// Call `increment()` on the `Counter` class
ref.read(counterProvider.notifier).increment();
},
),
);
}
}
应该尽可能地避免使用ref.read,因为它不是响应式的。
它存在于使用watch或listen会导致问题的情况下。如果可以的话,使用watch/listen几乎总是更好的,尤其是watch。
首先,永远不要在Widget的build函数中直接使用ref.read。
你可能很想使用ref.read来优化一个Widget的性能,例如通过下面的代码来实现。
final counterProvider = StateProvider((ref) => 0);
Widget build(BuildContext context, WidgetRef ref) {
// use "read" to ignore updates on a provider
final counter = ref.read(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.state++,
child: const Text('button'),
);
}
但这是一种非常糟糕的做法,会导致难以追踪的错误。
以这种方式使用 ref.read 通常与这样的想法有关:"Provider所暴露的值永远不会改变,所以使用'ref.read'是安全的"。这个假设的问题是,虽然今天该Provider可能确实从未更新过它的值,但不能保证明天也是如此。
软件往往变化很大,而且很可能在未来,一个以前从未改变的值需要改变。
如果你使用ref.read,当这个值需要改变时,你必须翻阅整个代码库,将ref.read改为ref.watch--这很容易出错,而且你很可能会忘记一些情况。
如果你一开始就使用ref.watch,你在重构时就会减少问题。
但是如果我想用ref.read来减少我的widget重构的次数呢?
虽然这个目标值得称赞,但需要注意的是,你可以用ref.watch代替来达到完全相同的效果(减少构建的次数)。
Provider提供了各种方法来获得一个值,同时减少重建的次数,你可以用这些方法来代替。
例如下面的代码(bad)。
final counterProvider = StateProvider((ref) => 0);
Widget build(BuildContext context, WidgetRef ref) {
StateController<int> counter = ref.read(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.state++,
child: const Text('button'),
);
}
我们可以这样改。
final counterProvider = StateProvider((ref) => 0);
Widget build(BuildContext context, WidgetRef ref) {
StateController<int> counter = ref.watch(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.state++,
child: const Text('button'),
);
}
这两个片段代码都达到了同样的效果:当计数器增加时,我们的按钮将不会重建。
另一方面,第二种方法支持计数器被重置的情况。例如,应用程序的另一部分可以调用。
ref.refresh(counterProvider);
这将重新创建StateController对象。
如果我们在这里使用ref.read,我们的按钮仍然会使用之前的StateController实例,而这个实例已经被弃置,不应该再被使用。
而使用ref.watch则可以正确地重建按钮,使用新的StateController。
根据你想监听的Provider,你可能有多个可能的值可以监听。
作为一个例子,考虑下面的StreamProvider。
final userProvider = StreamProvider<User>(...);
当读取这个userProvider时,你可以像下面这样。
Widget build(BuildContext context, WidgetRef ref) {
AsyncValue<User> user = ref.watch(userProvider);
return user.when(
loading: () => const CircularProgressIndicator(),
error: (error, stack) => const Text('Oops'),
data: (user) => Text(user.name),
);
}
Widget build(BuildContext context, WidgetRef ref) {
Stream<User> user = ref.watch(userProvider.stream);
}
Widget build(BuildContext context, WidgetRef ref) {
Future<User> user = ref.watch(userProvider.future);
}
其他Provider可能提供不同的替代值。
欲了解更多信息,请查阅api参考资料,参考每个Provider的API文档。
最后要提到的一个与读取Provider有关的功能是,能够减少Widget/Provider从ref.watch重建的次数,或者ref.listen执行函数的频率的功能。
这一点很重要,因为默认情况下,监听一个Provider会监听整个对象的状态。但有时,一个Widget/Provider可能只关心一些属性的变化,而不是整个对象。
例如,一个Provider可能暴露了一个User对象。
abstract class User {
String get name;
int get age;
}
但一个Widget可能只使用用户名。
Widget build(BuildContext context, WidgetRef ref) {
User user = ref.watch(userProvider);
return Text(user.name);
}
如果我们简单地使用ref.watch,当用户的年龄发生变化时,这将重建widget。
解决方案是使用select来明确地告诉Riverpod我们只想监听用户的名字属性。
更新后的代码将是这样。
Widget build(BuildContext context, WidgetRef ref) {
String name = ref.watch(userProvider.select((user) => user.name));
return Text(name);
}
通过使用select,我们能够指定一个函数来返回我们关心的属性。
每当用户改变时,Riverpod将调用这个函数并比较之前和新的结果。如果它们是不同的(例如当名字改变时),Riverpod将重建Widget。然而,如果它们是相等的(例如当年龄改变时),Riverpod将不会重建Widget。
这个场景也可以使用select和ref.listen。
ref.listen<String>(
userProvider.select((user) => user.name),
(String? previousName, String newName) {
print('The user name changed $newName');
}
);
这样做也将只在名称改变时调用listener。
另外,你不一定要返回对象的一个属性。任何覆盖==的值都可以使用。例如,你可以这样做。
final label = ref.watch(userProvider.select((user) => 'Mr ${user.name}'));
读取状态,是一个非常重要的部分,什么时候用什么样的方式来读,都会有不同的效果。
ProviderObserver可以监听一个ProviderContainer的变化。
要使用它,你可以扩展ProviderObserver类并覆盖你想使用的方法。ProviderObserver有三个方法。
ProviderObserver的一个简单用例是通过覆盖didUpdateProvider方法来记录Provider的变化。
// A Counter example implemented with riverpod with Logger
class Logger extends ProviderObserver {
@override
void didUpdateProvider(
ProviderBase provider,
Object? previousValue,
Object? newValue,
ProviderContainer container,
) {
print('''
{
"provider": "${provider.name ?? provider.runtimeType}",
"newValue": "$newValue"
}''');
}
}
void main() {
runApp(
// Adding ProviderScope enables Riverpod for the entire project
// Adding our Logger to the list of observers
ProviderScope(observers: [Logger()], child: const MyApp()),
);
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(home: Home());
}
}
final counterProvider = StateProvider((ref) => 0, name: 'counter');
class Home extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Scaffold(
appBar: AppBar(title: const Text('Counter example')),
body: Center(
child: Text('$count'),
),
floatingActionButton: FloatingActionButton(
onPressed: () => ref.read(counterProvider.notifier).state++,
child: const Icon(Icons.add),
),
);
}
}
现在,每当我们的Provider的值被更新时,logger将记录它。
I/flutter (16783): {
I/flutter (16783): "provider": "counter",
I/flutter (16783): "newValue": "1"
I/flutter (16783): }
对于诸如StateController(StateProvider.state的状态)和ChangeNotifier等可改变的状态,previousValue和newValue将是相同的。因为它们引用的是同一个StateController / ChangeNotifier。
这些是对Riverpod的最基本了解,但是却是很重要的部分,特别是如何对状态值进行读取,这是我们用好Riverpod的核心。
向大家推荐下我的网站 https://xuyisheng.top/ 专注 Android-Kotlin-Flutter 欢迎大家访问。
来自:https://xuyisheng.top/riverpod1/
Flutter是一款移动应用程序SDK,一份代码可以同时生成iOS和Android两个高性能、高保真的应用程序。Flutter目标是使开发人员能够交付在不同平台上都感觉自然流畅的高性能应用程序。我们兼容滚动行为、排版、图标等方面的差异。
关注flutter已经好久,因为没有发正式版,所以一直也不想过早的躺浑水,但是最近无意中看到几篇文章,再加上美团和咸鱼等app也一直在做灰度测试,所以上周开始看了一下官方文档,地址:https://flutter.io/docs/get-started/install,然后在此做一下总结。
Flutter默认是单线程任务处理的,如果不开启新的线程,任务默认在主线程中处理。和iOS应用很像,在Dart的线程中也存在事件循环和消息队列的概念,但在Dart中线程叫做isolate。
Flutter 1.5 的发布,同期也宣布发布 Flutter for Web 的 Preview 版本,正式开启了 Flutter 的全平台 UI 框架之路。早在年初发布的 Flutter 2019 Roadmap 中,就有提到,会在今年支持移动设备之外的平台,对 Web 的支持,算是完成了一个新的里程碑吧。
Flutter作为一个可移植的UI框架,已经支持现代Web应用开发了!我们很开心已经发布了SDK预览版,这样你可以在Web浏览器里直接运行你的Flutter UI代码。
Flutter 与原生之间的通信依赖灵活的消息传递方式:1,Flutter 部分通过平台通道将消息发送到其应用程序的所在的宿主环境(原生应用)。2,宿主环境通过监听平台通道,接收消息。
Flutter是借鉴React的开发思想实现的,在子组件的插槽上,React有this.props.children,Vue有<slot></slot>。当然Flutter也有类似的Widget,那就是Navigator,不过是以router的形式实现(像<router-view></router-view>)。
这两个技术在当下如何选择,我之前在公众号上的回复是:如果你已经处于一个比较满意的公司,并考虑长期发展,公司并未使用这两个技术,你可以专心钻研公司当下使用的,或者未来将要使用的,这些才能助你在公司步步高升。
本文对比的是 UIWebView、WKWebView、flutter_webview_plugin(在 iOS 中使用的是 WKWebView)的加载速度,内存使用情况。测试网页打开的速度,只需要获取 WebView 在开始加载网页和网页加载完成时的时间戳
用来构建漂亮、定制化应用的跨平台的 UI 框架 Flutter 现在已经支持 Web 开发了。我们很高兴推出了一个预览版的 SDK 可以让开发者直接使用 Flutter UI 和业务逻辑代码构建 web 应用
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!