[toc]
FishRedux完成一個玩安卓客戶端
前言
不知不覺從18年接觸Flutter斷斷續續到現在,說是一直在玩,其實接觸得也都很淺~ 實際說起來,貌似自己一點都不懂... 雖然自己斷斷續續也寫了一些app: 玩安卓 鋼鐵直男版 也在公司app上整合了一個單頁面的flutter首頁 [捂臉] 但是說實話我自己都不想去玩,好垃圾~
所以才會想在年底比較閒的時候,做出一個至少我願意裝在我手機上的app,至少是...對我有用的app,所以才有了這個專案。
希望自己可以一直有恆心完善下去:
已完成
- 首頁文章列表
- banner
- 微信公眾號列表
- 熱門專案
- 搜尋
- 我的收藏(網站,文章)
- 新增&刪除&編輯收藏
- 體系
- 導航
- 積分(收益詳情&排名列表)
- 分享
- 主題換膚
未完成
- 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>[],),
);
}
}
複製程式碼
哇!! 一氣呵成! 渾身通透! 再仔細一看:
),
),
),
),
),
),
),
),
)
],
),
);
}
}
複製程式碼
而且這只是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),
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組成,當時不是很熟就用了上面的方法),我們來看看佈局層級:
其中的Index_view為:
child: CustomScrollView(
slivers: <Widget>[
SliverToBoxAdapter(
child: viewService.buildComponent('banner'),
),
SliverToBoxAdapter(
child: viewService.buildComponent('classify'),
),
SliverToBoxAdapter(
child: viewService.buildComponent('hotArticle'),
),
],
),
複製程式碼
adapter
首頁我們需要關注的是首頁文章的Adapter,它隸屬於DynamicFlowAdapter,其他的還有
- StaticFlowAdapter
- CustomAdapter
- 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);
}
}
複製程式碼
我們稍微分析下:
- 我們在pool中定義了component的路由
- 我們在_ArticleAdapterConnector的get方法中返回了一個ItemBean的List,其type為我們提前定義好的component,而data為各個component的state(各個component的state應該為page的子集)
- over
個人頁面&登入頁面
本來還想寫寫其他頁面的程式碼的,但是其實都是個人主頁頁面的程式碼的擴充,說難點其實沒有,唯一的尷尬點就是程式碼量爆炸,還有一點是一開始用fishRedux會忘記使用方法,比如:
- action怎麼寫?
- 在effect還是reducer裡面寫邏輯??
- 我的分頁要怎麼寫比較好?
- 臥槽,我的tabController咋寫
- ... 這裡把我的葵花寶典奉上,我把下面這段文字寫成了一個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,我會持續改進的。