不知不觉从18年接触Flutter断断续续到现在,说是一直在玩,其实接触得也都很浅~ 实际说起来,貌似自己一点都不懂... 虽然自己断断续续也写了一些app: 玩安卓 钢铁直男版 也在公司app上集成了一个单页面的flutter首页 [捂脸] 但是说实话我自己都不想去玩,好垃圾~
所以才会想在年底比较闲的时候,做出一个至少我愿意装在我手机上的app,至少是...对我有用的app,所以才有了这个项目。
希望自己可以一直有恒心完善下去:
Flutter开发的一个爽点是:无脑堆代码(大雾),而最大的痛点也是这个,很多时候你会发现自己哼哧哼哧一通代码写下来:
class _TestPageState extends State<TestPage> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( leading: null, title: null, actions: <Widget>[], ), body: Column(children: <Widget>[],), bottomNavigationBar: Row(children: <Widget>[],), ); } } 复制代码
哇!! 一气呵成! 浑身通透! 再仔细一看:
), ), ), ), ), ), ), ), ) ], ), ); } } 复制代码
而且这只是v的代码,更别说还有mc的代码,一个稍微复杂点的页面,轻而易举就上了几百行代码,更别说没有提供页面预览功能(新版as已经提供了),这给日后的界面修改和业务修改都增加了难度,这其实就是很多人被劝退的直接原因了。 有没有解决办法呢? 其实是有的,页面拆分就是一个不错的办法,把一个页面进行业务级的拆分,多个cell组成一个页面,单个cell可以独立,其实就是组件化的思想,但是!还是麻烦!!! 而且我也不满足于原生的方法,因为群里大佬已经在疯狂安利FishRedux了,而我想着说,反正是个2019的句号,索性我也画得疯狂一点,就用fisnRedex了。
###提前总结
代码量爆炸! 但是爽!!!! 爽得可以边写代码边喝酒边唱歌! 有坑!!!! 坑巨多!! 文档贼少!!! 大部份坑都是可以解决的,而且很爽
如果不是很了解 fishRedux 的可以去看下 fishRedux地址 用FishRedux完成一个登录页面
/// 创建应用的根 Widget /// 1. 创建一个简单的路由,并注册页面 /// 2. 对所需的页面进行和 AppStore 的连接 /// 3. 对所需的页面进行 AOP 的增强 class AppRoute { static AbstractRoutes _global; static AbstractRoutes get global { if (_global == null) { _global = PageRoutes( pages: <String, Page<Object, dynamic>>{ /// 闪屏页 'splash': SplashPage(), /// 首页 'home': MainPage(), /// 登录页面 'login': LoginPage(), /// 注册页面 'register': RegisterPage(), /// 首页的第二个tab 'second': SecondPage(), /// 首页的第一个tab 'index': IndexPage(), ///项目目录 'project_list': ProjectListPage(), /// 项目子目录 'project_child_list': ProjectChildPage(), /// webView页面 'webView': WebLoadPage(), /// 微信公众号列表页面 'wechat_author': AuthorPage(), /// 微信公众号文章列表页面 'wechat_author_article': AuthorArticlePage(), /// 用户积分 'user_point': UserPointPage(), /// 用户排名 'user_rank': UserRankPage(), /// 网址收藏 'web_collection': WebCollectionPage(), ///文章收藏 'article_collection': ArticleCollectionPage(), /// 体系列表 'system': SystemPage(), /// 体系列表下属文章 'system_child': SystemChildPage(), /// 导航体系 'navi': NaviPage(), /// 侧滑页面 'draw': DrawPage(), /// 主题颜色修改 'theme_change': ThemeChangePage(), /// 搜索页面 'search': SearchPage(), }, visitor: (String path, Page<Object, dynamic> page) { /// 只有特定的范围的 Page 才需要建立和 AppStore 的连接关系 /// 满足 Page<T> ,T 是 GlobalBaseState 的子类 if (page.isTypeof<GlobalBaseState>()) { /// 建立 AppStore 驱动 PageStore 的单向数据连接 /// 1. 参数1 AppStore /// 2. 参数2 当 AppStore.state 变化时, PageStore.state 该如何变化 page.connectExtraStore<GlobalState>(GlobalStore.store, (Object pageState, GlobalState appState) { final GlobalBaseState p = pageState; // if (p.themeColor != appState.themeColor && // p.ifLogin != appState.ifLogin) { if (pageState is Cloneable) { print('修改--进行复制'); final Object copy = pageState.clone(); final GlobalBaseState newState = copy; newState.themeColor = appState.themeColor; newState.ifLogin = appState.ifLogin; newState.screenH = appState.screenH; newState.screenW = appState.screenW; newState.userPoint = appState.userPoint; return newState; // } } return pageState; }); } /// AOP /// 页面可以有一些私有的 AOP 的增强, 但往往会有一些 AOP 是整个应用下,所有页面都会有的。 /// 这些公共的通用 AOP ,通过遍历路由页面的形式统一加入。 page.enhancer.append( /// View AOP viewMiddleware: <ViewMiddleware<dynamic>>[ safetyView<dynamic>(), ], /// Adapter AOP adapterMiddleware: <AdapterMiddleware<dynamic>>[ safetyAdapter<dynamic>() ], /// Effect AOP effectMiddleware: <EffectMiddleware<dynamic>>[ _pageAnalyticsMiddleware<dynamic>(), ], /// Store AOP middleware: <Middleware<dynamic>>[ logMiddleware<dynamic>(tag: page.runtimeType.toString()), ], ); }, ); } return _global; } } Widget createApp() { final AbstractRoutes routes = AppRoute.global; return MaterialApp( title: '玩安卓', debugShowCheckedModeBanner: false, theme: ThemeData( indicatorColor: ColorConf.ColorFFFFFF, primarySwatch: ColorConf.themeColor, ), home: routes.buildPage('splash', null), onGenerateRoute: (RouteSettings settings) { return MaterialPageRoute<Object>(builder: (BuildContext context) { return routes.buildPage(settings.name, settings.arguments); }); }, ); } /// 简单的 Effect AOP /// 只针对页面的生命周期进行打印 EffectMiddleware<T> _pageAnalyticsMiddleware<T>({String tag = 'redux'}) { return (AbstractLogic<dynamic> logic, Store<T> store) { return (Effect<dynamic> effect) { return (Action action, Context<dynamic> ctx) { if (logic is Page<dynamic, dynamic> && action.type is Lifecycle) { print('${logic.runtimeType} ${action.type.toString()} '); } return effect?.call(action, ctx); }; }; }; } 复制代码
根据FishRedux的思想,我们把首页架构定义为: 一个大的page(MainPage),里面用pageView装载了两个大的page(SecondPage&IndexPage),
Widget buildView(MainState state, Dispatch dispatch, ViewService viewService) { /// 渲染appBar AppBar _renderAppBar() { return AppBar( backgroundColor: state.themeColor, centerTitle: true, titleSpacing: 60, title: TabBar( tabs: state.menuList .map((e) => Tab( text: e, )) .toList(), labelColor: Colors.white, controller: state.tabControllerForMenu, labelPadding: const EdgeInsets.all(0), labelStyle: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), unselectedLabelStyle: TextStyle(fontSize: 14), indicatorPadding: const EdgeInsets.all(0), indicatorSize: TabBarIndicatorSize.label, ), leading: Builder(builder: (ctx) { return IconButton( onPressed: () { dispatch(MainActionCreator.onOpenDraw(ctx)); }, icon: Image.asset( 'images/icon_more.png', color: Colors.white, height: 24, ), ); }), actions: <Widget>[ IconButton( onPressed: () { dispatch(MainActionCreator.onToSearch()); }, icon: Icon(Icons.search), ) ], ); } return Scaffold( primary: true, appBar: _renderAppBar(), body: TabBarView( controller: state.tabControllerForMenu, children: <Widget>[ KeepAliveWidget(AppRoute.global.buildPage('second', null)), KeepAliveWidget(AppRoute.global.buildPage('index', null)), ], ), drawer: AppRoute.global.buildPage('draw', null), ); } 复制代码
好像也没有其他什么需要注意的了,只有一个难点是 TabController ,以及page页面需要如何 保活 :
这个可以参考下之前的文章: 在fishRedux中使用TabController
在普通的stf页面中,我们需要页面保持,只需要实现**AutomaticKeepAliveClientMixin **:
class _TestPageState extends State<testPage> with AutomaticKeepAliveClientMixin { @override Widget build(BuildContext context) { /// 实现super方法 super.build(context); return Container(); } /// 返回true @override bool get wantKeepAlive => true; } 复制代码
而在 fishRedux 中就比较麻烦,我们需要把这个page用keepWidget包裹起来:
import 'package:flutter/material.dart'; /// 保持状态的包裹类 class KeepAliveWidget extends StatefulWidget { final Widget child; const KeepAliveWidget(this.child); @override State<StatefulWidget> createState() => _KeepAliveState(); } class _KeepAliveState extends State<KeepAliveWidget> with AutomaticKeepAliveClientMixin { @override bool get wantKeepAlive => true; @override Widget build(BuildContext context) { super.build(context); return widget.child; } } Widget keepAliveWrapper(Widget child) => KeepAliveWidget(child) 复制代码
我们看下首页的布局,很明显由几个cell组成:
如果在Android里面,那很明显就是一个RecyclerView+itemType组成; 如果是在Flutter原生里面,那很明显就是一个ListView+ItemBuilder里面按item划分 而我们在FishRedux里面,我们把页面做了一个拆分,页面是由一个SingleScrollView组成,而无论bannerComponent,classifyComponent,projectComponent,都是它的一个cell,而重头戏是articleComponent,它带有了父组件带来的loadMore和Refresh(其实整个页面都可以由一个ListView组成,当时不是很熟就用了上面的方法),我们来看看布局层级:
其中的Index_view为:
child: CustomScrollView( slivers: <Widget>[ SliverToBoxAdapter( child: viewService.buildComponent('banner'), ), SliverToBoxAdapter( child: viewService.buildComponent('classify'), ), SliverToBoxAdapter( child: viewService.buildComponent('hotArticle'), ), ], ), 复制代码
首页我们需要关注的是首页文章的 Adapter ,它隶属于 DynamicFlowAdapter ,其他的还有
class ArticleAdapter extends DynamicFlowAdapter<HotArticleState> { ArticleAdapter() : super( pool: <String, Component<Object>>{ "article_cell": ArticleCellComponent(), "comm_article_cell": CommArticleCellComponent(), "hot_project_cell": ProjectComponent(), }, connector: _ArticleAdapterConnector(), reducer: buildReducer(), ); } class _ArticleAdapterConnector extends ConnOp<HotArticleState, List<ItemBean>> { @override List<ItemBean> get(HotArticleState state) { List<ItemBean> _tempList = []; _tempList.addAll(state.hotArticleDataSource .map((e) => ItemBean( "article_cell", ArticleCellState()..hotArticleCellBean = e)) .toList()); _tempList.add(ItemBean( "hot_project_cell", ProjectState() ..projectListDataSource = state.projectDataSource ..screenW = state.size?.width ..screenH = state.size?.height)); _tempList.addAll(state.commArticleDataSource .map((e) => ItemBean("comm_article_cell", CommArticleCellState()..cellBean = e)) .toList()); return _tempList; } @override void set(HotArticleState state, List<ItemBean> items) {} @override subReducer(reducer) { return super.subReducer(reducer); } } 复制代码
我们稍微分析下:
本来还想写写其他页面的代码的,但是其实都是个人主页页面的代码的拓展,说难点其实没有,唯一的尴尬点就是代码量爆炸,还有一点是一开始用fishRedux会忘记使用方法,比如:
action 用来定义在这个页面中发生的动作,例如:登录,清理输入框,更换验证码框等。 同时可以通过payload参数传值,传递一些不能通过state传递的值。 effect 这个dart文件在fish_redux中是定义来处理副作用操作的,比如显示弹窗,网络请求,数据库查询等操作。 page 这个dart文件在用来在路由注册,同时完成注册effect,reducer,component,adapter的功能。 reducer 这个dart文件是用来更新View,即直接操作View状态。 state state用来定义页面中的数据,用来保存页面状态和数据。 view view很明显,就是flutter里面当中展示给用户看到的页面。 复制代码