FishRedux完成一個玩安卓客戶端

[toc]android

FishRedux完成一個玩安卓客戶端

前言

不知不覺從18年接觸Flutter斷斷續續到如今,說是一直在玩,其實接觸得也都很淺~ 實際提及來,貌似本身一點都不懂... 雖然本身斷斷續續也寫了一些app: 玩安卓 鋼鐵直男版 也在公司app上集成了一個單頁面的flutter首頁 [捂臉] 可是說實話我本身都不想去玩,好垃圾~git

因此纔會想在年末比較閒的時候,作出一個至少我願意裝在我手機上的app,至少是...對我有用的app,因此纔有了這個項目。github

但願本身能夠一直有恆心完善下去:web

已完成數據庫

  • 首頁文章列表
  • banner
  • 微信公衆號列表
  • 熱門項目
  • 搜索
  • 個人收藏(網站,文章)
  • 添加&刪除&編輯收藏
  • 體系
  • 導航
  • 積分(收益詳情&排名列表)
  • 分享
  • 主題換膚

未完成redux

  • todo模塊,但願能夠完成一個todo提示,
  • 吃棗藥丸,加入一些比較好玩的東西,看博客膩了能夠看點好玩的
  • 放鬆放鬆,同上
  • 實用工具,(至少我要加入一個千卡轉千焦,千焦轉大卡的計算工具)
  • webViewPlugs和flutter自帶webView的切換(實際上試過,plugs是整個覆蓋在flutter頁面上,實際上體驗通常,不少控件不能本身定義;自帶的webview性能通常)
  • 切換字體
  • and so on

基本架子

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>[],),
    );
  }
}
複製代碼

哇!! 一鼓作氣! 渾身通透! 再仔細一看:markdown

),
                        ),
                      ),
                    ),
                  ),
                ),
              ),
            ),
          )
        ],
      ),
    );
  }
}
複製代碼

臥槽!!!!!!!!!!!!

並且這只是v的代碼,更別說還有mc的代碼,一個稍微複雜點的頁面,垂手可得就上了幾百行代碼,更別說沒有提供頁面預覽功能(新版as已經提供了),這給往後的界面修改和業務修改都增長了難度,這其實就是不少人被勸退的直接緣由了。 有沒有解決辦法呢? 實際上是有的,頁面拆分就是一個不錯的辦法,把一個頁面進行業務級的拆分,多個cell組成一個頁面,單個cell能夠獨立,其實就是組件化的思想,可是!仍是麻煩!!! 並且我也不知足於原生的方法,由於羣裏大佬已經在瘋狂安利FishRedux了,而我想着說,反正是個2019的句號,索性我也畫得瘋狂一點,就用fisnRedex了。網絡

###提早總結架構

代碼量爆炸! 可是爽!!!! 爽得能夠邊寫代碼邊喝酒邊唱歌! 有坑!!!! 坑巨多!! 文檔賊少!!! 大部份坑都是能夠解決的,並且很爽

若是不是很瞭解fishRedux的能夠去看下 fishRedux地址 用FishRedux完成一個登陸頁面

頁面預覽

Screenshot_2020-01-07-23-39-11-008_com.lht.flutter_android_fun.png

Screenshot_2020-01-07-23-39-18-750_com.lht.flutter_android_fun.png

Screenshot_2020-01-07-23-39-21-059_com.lht.flutter_android_fun.png

Screenshot_2020-01-07-23-39-23-285_com.lht.flutter_android_fun.png

Screenshot_2020-01-07-23-39-30-113_com.lht.flutter_android_fun.png

Screenshot_2020-01-07-23-39-36-264_com.lht.flutter_android_fun.png

路由定義:

/// 建立應用的根 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),

view

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),
  );
}
複製代碼

and so on

好像也沒有其餘什麼須要注意的了,只有一個難點是TabController,以及page頁面須要如何保活

定義本身的TabController

這個能夠參考下以前的文章:在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)
複製代碼

Adapter寫法

咱們看下首頁的佈局,很明顯由幾個cell組成:

  • banner
  • 公衆號分類gridView
  • 置頂推薦
  • 項目推薦
  • 首頁分章分頁

若是在Android裏面,那很明顯就是一個RecyclerView+itemType組成; 若是是在Flutter原生裏面,那很明顯就是一個ListView+ItemBuilder裏面按item劃分 而咱們在FishRedux裏面,咱們把頁面作了一個拆分,頁面是由一個SingleScrollView組成,而不管bannerComponent,classifyComponent,projectComponent,都是它的一個cell,而重頭戲是articleComponent,它帶有了父組件帶來的loadMore和Refresh(其實整個頁面均可以由一個ListView組成,當時不是很熟就用了上面的方法),咱們來看看佈局層級:

code

其中的Index_view爲:

child: CustomScrollView(
      slivers: <Widget>[
        SliverToBoxAdapter(
          child: viewService.buildComponent('banner'),
        ),
        SliverToBoxAdapter(
          child: viewService.buildComponent('classify'),
        ),
        SliverToBoxAdapter(
          child: viewService.buildComponent('hotArticle'),
        ),
      ],
    ),
複製代碼

adapter

首頁咱們須要關注的是首頁文章的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);
  }
}
複製代碼

咱們稍微分析下:

  1. 咱們在pool中定義了component的路由
  2. 咱們在_ArticleAdapterConnector的get方法中返回了一個ItemBean的List,其type爲咱們提早定義好的component,而data爲各個component的state(各個component的state應該爲page的子集)
  3. over

我的頁面&登陸頁面

原本還想寫寫其餘頁面的代碼的,可是其實都是我的主頁頁面的代碼的拓展,說難點其實沒有,惟一的尷尬點就是代碼量爆炸,還有一點是一開始用fishRedux會忘記使用方法,好比:

  1. action怎麼寫?
  2. 在effect仍是reducer裏面寫邏輯??
  3. 個人分頁要怎麼寫比較好?
  4. 臥槽,個人tabController咋寫
  5. ... 這裏把個人葵花寶典奉上,我把下面這段文字寫成了一個txt,放在桌面,忘記了就打開看看:
action
用來定義在這個頁面中發生的動做,例如:登陸,清理輸入框,更換驗證碼框等。
同時能夠經過payload參數傳值,傳遞一些不能經過state傳遞的值。

effect
這個dart文件在fish_redux中是定義來處理反作用操做的,好比顯示彈窗,網絡請求,數據庫查詢等操做。

page
這個dart文件在用來在路由註冊,同時完成註冊effect,reducer,component,adapter的功能。

reducer
這個dart文件是用來更新View,即直接操做View狀態。

state
state用來定義頁面中的數據,用來保存頁面狀態和數據。

view
view很明顯,就是flutter裏面當中展現給用戶看到的頁面。
複製代碼

結語

這個app還很粗糙,歡迎提issue,我會持續改進的。

密碼是123456
相關文章
相關標籤/搜索