Flutter fish_redux项目架构实践

Flutter fish_redux项目架构实践

技术背景:

  • Flutter 是谷歌开源的新一代跨平台UI框架,其以高性能,响应式而业界闻名
  • fish_redux 是阿里闲鱼事业部开源的一套类似redux的flutte框架,适用中大型项目,用于页面拆分,模块拆分及跨组件通讯的

fish_redux : https://github.com/alibaba/fish-redux

官方文档太low,建议直接看这里 https://blog.csdn.net/weixin_34163553/article/details/91380928

网上有很多关于fish_redux的相关博客,那为什么我还要重复制造轮子?原因有两:

  • Demo 和 实际项目完全不同
  • 有些细节,有些概念必须要彻底理解和消化,那么在实际的编码中才能信手拈来

fish_redux的几个概念:

以下为补充部分,具体的请看完上面的博客,再回来继续阅读

  • store

    和redux里的store类似,用于全局数据共享,如userinfo,token,Theme等,以下代码不做这一部分的演示

  • state

    按官方文档来说,一个Page对应一个state,页面初始化的时候同时初始化相应的PageState,页面销毁的时候会同时释放相应的PageState

  • page

    通常指一个页面,继承自Page,拥有initstate方法,注意只有page才拥有,而component是没有的

  • component

    组件,一个Page 可以由多个component组合而成,用于页面的拆分

  • reducer

    凡是redux类型框架,他们遵守的都是state不可变原则,只有通过reducer去返回新的一个state去刷新试图

  • effect

    fish_redux 新增加的一个概念,用于处理reducer之外的副作用,实际项目中网络请求,业务逻辑也是写在这里的

  • Route

    路由,有AppRoutes,PageRoutes,HybridRoutes

准备工作:

  • 请提前安装fish-redux-template插件,android studio 和 vs code都有对应的插件,用于快速生成Page,Component,Adapter等模版
  • pubspec.yaml 配置文件依赖 fish_redux

项目实战:

example : https://github.com/ikimiler/fish_redux_example

通过项目实战你可以深入了解到connec,adapter,component,page究竟怎么使用,以及有什么区别

项目里有mvp和fish_redux两种架构模式,mvp请自行阅读,重点介绍fish_redx

没图没吸引力:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
Main.dart

//运行fish_redux架构示例
void main() => runApp(createApp());
//运行mvp架构示例
// void main() => runApp(MainPage());

class MainPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "demo",
      theme: ThemeData(
        primaryColor: Colors.white,
      ),
      home: MVPPage(),
    );
  }
}

App.dart

/// 创建应用的根 Widget
/// 1. 创建一个简单的路由,并注册页面
/// 2. 对所需的页面进行和 AppStore 的连接 todo
/// 3. 对所需的页面进行 AOP 的增强 todo
Widget createApp() {
  return MaterialApp(
    title: 'demo',
    debugShowCheckedModeBanner: false,
    theme: ThemeData(
      primaryColor: Colors.white,
    ),
    //配置默认页面为OnePage
    home: RouteConfig.ROUTES.buildPage(RouteConfig.ONE_PAGE_PATH, null),
    onGenerateRoute: (RouteSettings settings) {
      return MaterialPageRoute<Object>(builder: (BuildContext context) {
        return RouteConfig.ROUTES.buildPage(settings.name, settings.arguments);
      });
    },
  );
}

//这里只是起到一个路由映射作用,抽离出来是为了方便统一管理
class RouteConfig {
  static final String ONE_PAGE_PATH = "one_page_path";
  static final String TWO_PAGE_PATH = "two_page_path";
  static final String THREE_PAGE_PATH = "three_page_path";
  static final String COMPONENT_DEMO_PAGE_PATH = "component_demo_page_path";
  static final String LISTVIEW_DEMO_PAGE_PATH = "listview_demo_page_path";
	//这里用到了PageRoutes,当然还有AppRoutes,HybridRoutes
  static final AbstractRoutes ROUTES =
      PageRoutes(pages: <String, Page<Object, dynamic>>{
    RouteConfig.ONE_PAGE_PATH: OnePage(),
    RouteConfig.TWO_PAGE_PATH: TwoPage(),
    RouteConfig.THREE_PAGE_PATH: ThreePage(),
    RouteConfig.LISTVIEW_DEMO_PAGE_PATH: ListviewDemoPage(),
    RouteConfig.COMPONENT_DEMO_PAGE_PATH: ComponentDemoPage(),
  }, visitor: (String path, Page<Object, dynamic> page) {});
}

Page 可以使用fish-redux-template快速生成page,每个page/component都对应6个文件,action,reducer,page/component,effect,state(可有可无,看自己需求),view

这里重点讲component_demo_page,也是体现Page和Component的不同之处

在实际开发中,我们为了代码复用可能一个Page页面由多个Component组装而成,请看最上边图二,它分别由headerComponent,bodyComponent,footerComponent组装而成,代码目录结构为下图

在这里插入图片描述
component_demo_page:

class ComponentDemoPage extends Page<ComponentDemoState, Map<String, dynamic>> {
  ComponentDemoPage()
      : super(
      		//只有page才会有initState方法,普通的component并没有
          initState: initState,
          effect: buildEffect(),
          reducer: buildReducer(),
          //渲染视图
          view: buildView,
          dependencies: Dependencies<ComponentDemoState>(
          		//这里可以映射adapter
              adapter: null,
              slots: <String, Dependent<ComponentDemoState>>{
                //slots这里是一个映射作用以及数据page的state如何和component的state的进行关联,需要用到connect,connect这个概念要深刻理解,它起到一个数据如何关联的作用,牢记。
                //一个页面可以由多个component自由组装,component也可以由其他页面进行复用
                //这里分别映射三个component,其中headercomponent的state 共用当前页面的state,所以采用 NoneConn 连接方式
                //bodycomponent的state用body component本身的state,所以需要connect 从ComponentDemoState映射到BodyState
                //footer和header同理,共用当前页面的state
                //注意,component可以共用,比如这是a页面,那么在b页面也可以使用这些component,只要分别提供对应的connec就可以了,
                "header": NoneConn<ComponentDemoState>() + HeaderComponent(),
                "body": BodyComponentConnec() + BodyComponent(),
                "footer": NoneConn<ComponentDemoState>() + FooterComponent(),
              }),
          middleware: <Middleware<ComponentDemoState>>[],
        );
}

Widget buildView(
    ComponentDemoState state, Dispatch dispatch, ViewService viewService) {
  return Scaffold(
    appBar: AppBar(
      title: Text("component demo"),
    ),
    body: Container(
      child: ListView(
        children: <Widget>[
          //这里直接取映射好的component,也就是page里配置的slots
          viewService.buildComponent("header"),
          viewService.buildComponent("body"),
          viewService.buildComponent("footer"),
          Container(
            color: Colors.grey,
            padding: EdgeInsets.symmetric(vertical: 20),
            alignment: Alignment.center,
            child: Column(
              children: <Widget>[
                Text("ComponentDemoState.header : ${state.header}"),
                Text("ComponentDemoState.body : ${state.body}"),
                Text("ComponentDemoState.footer : ${state.footer}"),
              ],
            ),
          )
        ],
      ),
    ),
  );
}

bodyComponent.dart

class BodyComponent extends Component<BodyState> {
  BodyComponent()
      : super(
          effect: buildEffect(),
          reducer: buildReducer(),
          //渲染具体的视图
          view: buildView,
          dependencies: Dependencies<BodyState>(
              adapter: null, slots: <String, Dependent<BodyState>>{}),
        );
}

//由于bodycomponent用的是自己本身的state,所以需要一个connec来提供如何映射
class BodyComponentConnec extends ConnOp<ComponentDemoState, BodyState> {
  //如何从BodyState 映射到 ComponentDemoState
  //bodystate是自己本身的state
  //ComponentDemoState是页面的state
  @override
  void set(ComponentDemoState state, BodyState subState) {
    // super.set(state, subState);
    state.header = subState.up;
    state.body = subState.mid;
    state.footer = subState.down;
  }

  //如何从ComponentDemoState 映射到 BodyState
  //bodystate是自己本身的state
  //ComponentDemoState是页面的state
  @override
  BodyState get(ComponentDemoState state) {
    // return super.get(state);
    return BodyState()
      ..up = state.header
      ..mid = state.body
      ..down = state.footer;
  }
}

ListviewDemoPage Adapter示例

class ListviewDemoPage extends Page<ListviewDemoState, Map<String, dynamic>> {
  ListviewDemoPage()
      : super(
          initState: initState,
          effect: buildEffect(),
          reducer: buildReducer(),
          //渲染视图
          view: buildView,
          dependencies: Dependencies<ListviewDemoState>(
              //这里映射adapter,由于我们在ListviewDemoState数据源实现了MutableSource接口
              //所以这里可以使用NoneConn连接器,如果数据源没有实现相关接口,那么请按照component的形式,去实现ConnOp,提供具体的数据映射关系
              adapter: NoneConn<ListviewDemoState>() + ListviewAdapter(),
              slots: <String, Dependent<ListviewDemoState>>{
                //同时也可以映射components
                //由于这里就是一个列表,所以不需要其他的component
              }),
          middleware: <Middleware<ListviewDemoState>>[],
        );
}

Widget buildView(
    ListviewDemoState state, Dispatch dispatch, ViewService viewService) {
  return Scaffold(
    appBar: AppBar(
      title: Text("adapter demo"),
    ),
    body: ListView.builder(
      itemBuilder: viewService.buildAdapter().itemBuilder,
      itemCount: viewService.buildAdapter().itemCount,
    ),
  );
}

//page对应的state,实现了MutableSource接口
class ListviewDemoState extends MutableSource
    implements Cloneable<ListviewDemoState> {
  List<String> datas = List.generate(300, (value) => "generate $value");

  @override
  ListviewDemoState clone() {
    return ListviewDemoState();
  }

	//getItemData
  @override
  Object getItemData(int index) {
    return ItemState()..name = datas[index];
  }
	
	//itemCount
  @override
  int get itemCount => datas.length;

	//getItemType,这里可以根据index 获取不同的type,和android原生的recyclerview一样
  @override
  String getItemType(int index) {
    return index % 2 == 0 ? "item" : "line";
  }
	
	//setItemData
  @override
  void setItemData(int index, Object data) {
    datas[index] = data;
  }
}

ListviewAdapter.dart

class ListviewAdapter extends SourceFlowAdapter<ListviewDemoState> {
  ListviewAdapter()
      : super(
          pool: <String, Component<Object>>{
            //这里同样映射component
            //左侧key 对应数据源的getItemType 方法
            "item": ItemComponent(),
            "line": LineComponent(),
          },
          reducer: buildReducer(),
        );
}

ItemComponent.dart

class ItemComponent extends Component<ItemState> {
  ItemComponent()
      : super(
            effect: buildEffect(),
            reducer: buildReducer(),
            //渲染视图
            view: buildView,
            dependencies: Dependencies<ItemState>(
                adapter: null,
                slots: <String, Dependent<ItemState>>{
                }),);

}

Widget buildView(ItemState state, Dispatch dispatch, ViewService viewService) {
  return Container(
    height: 50,
    color: Colors.orangeAccent,
    child: Text("item ---- ${state.name}"),
    alignment: Alignment.center,
  );
}

LineComponent.dart

class LineComponent extends Component<ItemState> {
  LineComponent()
      : super(
          effect: buildEffect(),
          reducer: buildReducer(),
          //渲染视图
          view: buildView,
          dependencies: Dependencies<ItemState>(
              adapter: null, slots: <String, Dependent<ItemState>>{}),
        );
}

Widget buildView(ItemState state, Dispatch dispatch, ViewService viewService) {
  return Container(
    height: 1,
    color: Colors.green,
  );
}

如何进行页面跳转传参数?

//点击按钮发送一个相应的action
RaisedButton(
            onPressed: () {
            	//action也可以携带需要的参数
              dispatch(OneActionCreator.gotoStretchDemoPage());
            },
            child: Text("跨页面进行state通讯以及传参示例"),
 			),
 			
 //页面跳转属于副作用,所以一般是在effect里面处理的
void gotoStretchDemoPage(Action action, Context<OneState> ctx) {
	//使用Navigator进行跳转,arguments传递相关的参数,
	//如果action里有传入参数,也可以从action中获取相关的参数
  Navigator.of(ctx.context).pushNamed(RouteConfig.TWO_PAGE_PATH,
      arguments: {"params": "我是上个页面带过来的参数"});
}

//前面说过每个page页面都有一个initState函数,在对应的state里面
TwoState initState(Map<String, dynamic> args) {
	//所以上面传入的参数,从args里就可以去到,然后赋值给相应的state,view在拿到对应的state,去初始化视图
  return TwoState()..text = args["params"];
}

跨页面或跨component如何通讯?

Effect<ThreeState> buildEffect() {
  return combineEffects(<Object, Effect<ThreeState>>{
    ThreeAction.action: _onAction,
    ThreeAction.sendBroadcast: _onSendBroadcast,
    Lifecycle.initState:_onInitState,//页面的state初始化的action
    Lifecycle.dispose:_onDispose,//页面销毁了
  });
}

void _onAction(Action action, Context<ThreeState> ctx) {}
//state初始化后的action,一般用来进入页面拉取相关数据操作,这里可以执行异步任务
void _onInitState(Action action, Context<ThreeState> ctx) {

}
//页面销毁的action,用来清楚一个数据
void _onDispose(Action action, Context<ThreeState> ctx) {

}
void _onSendBroadcast(Action action, Context<ThreeState> ctx) {
  //跨页面/跨component通讯,可以使用这个发送广播
  //然后再想通讯的页面注册相应的effect,就可以了
  ctx.broadcast(ThreeActionCreator.onReceiveBroadcast());
  //下边这个暂时没有尝试出来,看字面意思是通知所有的effect,但自己验证的时候,并不行 todo
  // ctx.broadcastEffect(ThreeActionCreator.onReceiveBroadcast());
}
发布了35 篇原创文章 · 获赞 73 · 访问量 15万+

猜你喜欢

转载自blog.csdn.net/qq_28268507/article/details/104757530