Flutter 超简单状态管理

Flutter 状态管理

目前Flutter已经有许多状态管理的方案,但就我个人而言,并不能完全满足我的要求。我希望状态管理更加简单,而不是成为负担,我希望状态管理更加可靠,而不是使用过于复杂的实现。譬如目前最为简洁的get库,为了实现一些黑科技语法糖,其实现就较为复杂。我们知道一台机器越复杂,可靠性就会降低。道理就如同在一些动乱地区,非常流行一些傻大黑粗的皮卡车,结构简单,皮实耐用。

为了兼具简洁和可靠性,同时摆脱对InheritedWidget的限制,我使用注解和依赖注入库来实现,原理上类似Bloc,但更加简洁,他们都是使用Stream来做状态管理,这是就是EbloX库。

简单示例

首先来看一个大家喜闻乐见的计数器示例:

1. 添加依赖

dependencies:
  eblox:
  eblox_annotation:

dev_dependencies:
  build_runner:
  eblox_generator:

2. 编写我们的ViewModel类来处理业务逻辑

import 'package:eblox/blox.dart';
import 'package:eblox_annotation/blox.dart';
import 'package:flutter/cupertino.dart';

part 'counter_view_model.g.dart';

@bloX
class _CounterVModel extends Blox{

  @StateX(name:'CounterState')
  int _counter = 0;

  @ActionX(bind: 'CounterState')
  void _add() async{
    _counter ++;
  }

  @ActionX(bind: 'CounterState')
  void _sub(){
    _counter--;
  }

  @override
  void dispose() {
    super.dispose();
    debugPrint('CounterVModel dispose...');
  }
}

首先从Blox继承一个以下划线开头的ViewModel类,该类需要使用@bloX注解修饰 。接下来定义一个UI需要的状态数据_counter,也必须以下划线_开头,然后使用@StateX注解修饰该数据,这样就能自动生成一个State类来包装该数据。@StateX注解可以传name参数生成指定名字的State类,也可以缺省,默认生成规则会去除变量名的下划线,然后首字母大写+State,譬如变量_color,则会生成ColorState类。

接下来,我们需要定义动作,也就是对这个状态的操作。使用@ActionX注解修饰下划线_开头的方法,就是一个动作,它会生成对应的Action类,生成规则与State类似,可以传name指定名称,也可以缺省,如上例,将会生成AddAction类和SubAction类。bind参数用于指定该动作关联的State类名称。

我们定义的两个方法分别用来自增、自减计数变量,但这两个方法不需要我们去调用,而且它们也是私有的。当UI上发出AddActionSubAction动作时,对应的Action方法会自动调用。

3. 生成代码

在项目根路径下执行flutter pub run build_runner watch --delete-conflicting-outputs命令,会生成counter_view_model.g.dart文件

4. 编写UI

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child:BloxBuilder<CounterVModel,CounterState>(
            create:()=>CounterVModel(),
            builder: (count) {
              return Center(
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    Text("$count"),
                    ElevatedButton(
                      onPressed: () {
                        AddAction().to<CounterVModel>();
                      },
                      child: const Text("+"),
                    ),
                    ElevatedButton(
                      onPressed: () {
                        SubAction().to<CounterVModel>();
                      },
                      child: const Text("-"),
                    ),
                  ],
                ),
              );
            }),
      ),
    );
  }
}

在UI上,可以使用BloxBuilder组件来获取状态,它需要指定两个泛型,第一个是我们的ViewModel类CounterVModel,第二个是我们的状态类CounterState,接下来实现create回调,可在此处实例化CounterVModel。如果你不喜欢用create实例化,也可以调用I.put(CounterVModel())方法在其他任意地方实例化。builder回调用于返回Widget,它的参数就是State。

当我们需要发起动作时,直接实例化相应的动作,并调用to方法传入泛型,发起动作,触发计数器的状态改变。当状态改变时,UI能自动感知,也发生对应变化。

可以看到使用Eblox库的整体流程非常简单,开发者只需要定义状态和动作,然后在界面上发起动作即可。完整示例工程,请查看 这里

原理说明

状态管理不是必须品,使用状态管理框架的原因是希望开发更加简单,同时将业务逻辑与UI创建分离,使得项目可以长期维护下去,而不会把代码变成一座屎山。

代码之所以会变成屎山,大部分原因就是职责不明,代码相互耦合。我们以上述的计数器为例,如果在按钮的onPressed中可以直接调用_add()方法修改计数器,这就耦合了。一旦后续_add方法内部发生修改,就会影响外部所有调用该方法的地方。onPressed调用_add也是一种不明确的行为,这种调用意味着什么呢?对于后续接手代码维护的人而言,代码中大量的这种不明确行为是让人崩溃的。

基于这些原因,我们需要状态管理框架,提升代码的可维护性。

现在,我们将业务逻辑写到Blox层,UI与业务逻辑之间的联系,由State和Action两个概念维系。这样就实现了业务逻辑与UI的分离。UI主要是接受用户的操作的,这些操作就是一个个动作,而UI的创建则需要状态,不同的状态决定了不同的UI,UI界面的变化,其实就是状态的变化。这样,我们只需要修改状态即可,UI就能自动感知,从而发生变化。

大多时候,数据可能来自于服务器,所以我们需要在Blox之下增加一层Service,由Service来封装与服务器的交互逻辑,Service隔离了具体的数据源,对于ViewModel而言,Service就是数据源。

常见案例

计数器例子过于简单,我们来看一个更加常见的案例:

这是一个模拟歌曲搜索的界面,基于上述的State和Action概念来分析,我们首先需要明确State和Action。很明显,点击搜索按钮搜索就是一个动作,而搜索结果就是一个状态。

不同的状态,对应不同的UI界面,结果为空时,UI显示Empty标签,有结果时,就显示结果项。

我们来看Eblox如何实现:

1. 定义状态和动作

part 'search_view_model.g.dart';

@bloX
class _SearchVModel extends Blox{

  @AsyncX(name: 'SongListState')
  SongListModel _songModel = SongListModel();

  @bindAsync
  @ActionX(bind: 'SongListState')
  BloxAsyncTask<SongListModel> _search(String name){
    return (){
      return  SearchService.search(name);
    };
  }
}

注意,可以使用@AsyncX注解来修饰异步状态。这里因为我们模拟数据是耗时加载的,因此需要异步加载。还有一点要注意,这里的_search方法声明了参数。被@ActionX注解的方法是可以声明参数的,不仅可以声明参数,还可以声明位置参数或命名参数(花括号中声明参数),此处是声明的位置参数。这些参数会自动包含到动作中,从而在发起动作时传参。

定义动作时,这里增加了一个注解@bindAsync,用于修饰与异步状态关联的动作。需注意,@bindAsync修饰的方法,返回值必须是BloxAsyncTask<T>类型,这里泛型T是我们需要的异步状态的类型。它的原型其实是typedef BloxAsyncTask<T> = Future<T> Function();

接下来,就可以在_search方法中编写加载数据的逻辑。前文已经说过,增加一个Service层封装数据源,这里直接调用Service提供的搜索接口搜索歌曲,但要注意返回值必须是一个Future<T> Function()的方法类型,这里闭包包装一下即可。

2. 解析注解,生成代码
这里,可以使用flutter pub run build_runner build命令手动生成,但每次新增或修改Blox里的代码时都要手敲一遍,因此推荐使用flutter pub run build_runner watch --delete-conflicting-outputs命令,它可以开启监控,每次修改Blox代码时都会自动重新生成。

3. 编写UI

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

  final TextEditingController _controller = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Song search'),),
      body: SafeArea(
        child: Column(
          children: [
            TextField(
              controller: _controller,
              decoration: InputDecoration(
                contentPadding: const EdgeInsets.symmetric(horizontal: 16),
                  suffix: IconButton(
                icon: const Icon(Icons.search_rounded),
                onPressed: (){
                  if(_controller.text.isNotEmpty) {
                    SearchAction(_controller.text).to<SearchVModel>();
                  }
                },
              )),
            ),
            Flexible(
                child: BloxView<SearchVModel, SongListState<SongListModel>>(
              create: () => SearchVModel(),
              onLoading: () => const Center(child: CircularProgressIndicator()),
              onEmpty: ()=> const Center(child: Text("Empty")),
              builder: (state) {
                return ListView.builder(
                    itemCount: state.data.songs.length,
                    itemBuilder: (ctx, i) {
                      return Container(
                        alignment: Alignment.center,
                        height: 40,
                        child: Text(state.data.songs[i],style: const TextStyle(color: Colors.blueGrey,fontSize: 20),),
                      );
                    });
              },
            )),
          ],
        ),
      ),
    );
  }
}

这里界面比较简单,上下结构,上面一个输入框,下面是列表部分。当用户点击搜索按钮时,发起SearchAction动作。注意,这里实例化SearchAction类的参数就对应_search方法中的参数。

对于异步状态,可以使用BloxView来获取。它提供了onLoading, onEmpty, onError,builder等回调来处理数据加载过程中的状态。开始加载时,回调onLoading,我们可以在此创建相应的加载动画,加载完成后,成功获取数据,回调builder来构建界面,如果没有数据回调onEmpty创建相应页面,加载报错,回调onError

这里需要小心,如果状态中包装的是自定义的数据类型,如此处的SongListModel,你仍然希望onEmpty起作用,那么该数据类需要混入BloxData并实现isEmpty方法:

class SongListModel with BloxData{

  SongListModel({UnmodifiableListView<String>? songs}){
    if(songs !=null) this.songs = songs;
  }

  UnmodifiableListView<String> songs = UnmodifiableListView([]);

  @override
  bool get isEmpty => songs.isEmpty;
}

用EbloX实现此案例,代码仍然十分简洁清晰。完整代码,请查看这里

其他

使用Eblox实现业务逻辑与UI的分离后,代码测试变得更加简单方便,以一个简单的单元测试为例:

void main(){
  const len = 10;
  group('Counter test', () {
    setUp(() {
      I.put(CounterVModel());
    });

    tearDown(() {
      I.delete<CounterVModel>();
    });

    test('test add counter', () {
      for(var i = 0;i<len;i++){
        AddAction().to<CounterVModel>();
      }
      expect(Future(() => $<CounterVModel>().counter.data), completion(len));
    });

    test('test sub counter', () {
      for(var i = 0;i<len;i++){
        SubAction().to<CounterVModel>();
      }
      expect(Future(() => $<CounterVModel>().counter.data), completion(-10));
    });
  });
}

另外,如果UI上需要多个状态,那么可以使用MultiBuilder来处理。BloxBuilderBloxView仅能处理单个状态。

目前Eblox处于0.0.2版本,仅测试了一些有限范围的使用,后续有时间会完善更多功能,欢迎大家测试BUG,不胜感激!


关注公众号:编程之路从0到1

了解更多技术干货

编程之路从0到1

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>