本課時我會和大家一起來完善 App 的其他功能仗谆,其中包括:我的好友懊直、我的消息扒吁、系統(tǒng)設置和搜索功能。按照我們之前課時所學的技術點室囊,我們可以通過繪制組件樹+布局來實現(xiàn)雕崩,在實現(xiàn)過程中也會介紹一些新的知識點,接下來我們就分別看下這幾個功能的實現(xiàn)過程融撞。
我的好友
我們首先看下要實現(xiàn)的效果圖盼铁,如圖 1 所示。
圖 1 我的好友效果圖
根據(jù)圖 1 的效果圖懦铺,我們繪制出組件樹+布局捉貌,如圖 2 所示。
圖 2 組件樹
圖 2 很清晰地分析出了界面所轉化的組件樹,由于這里都不涉及動態(tài)組件趁窃,因此將 Text 和 Image 作為一個 card 組件即可牧挣。代碼實現(xiàn)邏輯和我們之前介紹的推薦頁面和關注頁面基本一樣,接下來我們看下“我的消息”的實現(xiàn)醒陆。
我的消息
我們先來看下“我的消息”要實現(xiàn)的界面效果瀑构,如圖 3 所示。
圖 3 我的消息界面效果
根據(jù)圖 3 的效果圖刨摩,我們繪制出組件樹+布局寺晌,如圖 4 所示。
圖 4 我的消息組件樹+布局
圖 4 就非常清晰地描述了我們整個 UI 構造:
圖 4 中的 Row-Expanded-1 和 Row-Expanded-5 代表的是使用 flex 布局澡刹,左右屏幕占比 1 比 5呻征;
圖 4 中的 first_line 代表的是圖 3 右側的用戶昵稱和時間一行;
圖 4 中的 spaceBetween 是 Row 的 mainAxisAlignment 屬性罢浇,代表的是兩端對齊陆赋,具體這部分代碼如下。
復制代碼
/// 獲取右側的首行
Widget _getFirstLine() {
? return? Row(
? ? mainAxisAlignment: MainAxisAlignment.spaceBetween,
? ? children: <Widget>[
? ? ? Text(
? ? ? ? userMessage.userInfo.nickName,
? ? ? ? style: TextStyles.commonStyle(0.8, Colors.black),
? ? ? ),
? ? ? _getMessageTimeSection(userMessage.messageTime),
? ? ],
? );
}
由于這里也沒有涉及組件的復用和動態(tài)組件嚷闭,因此這里也建議將整個組件內容設計為一個組件叫作 message_card攒岛。為了代碼維護性,可以使用類函數(shù)來封裝小組件胞锰,為后續(xù)重構抽象為通用組件做準備灾锯,例如這里我們將 first_line 設計為一個類函數(shù),如上代碼中的 _getFirstLine 函數(shù)嗅榕。
系統(tǒng)設置
接下來我們來看下“系統(tǒng)設置”這部分界面效果顺饮,如圖 5 所示。
圖 5 系統(tǒng)設置的效果
看到圖 5 的效果后凌那,其實組件設計可能不是關鍵领突。這里涉及兩個新的知識點:
在 Flutter 上怎么處理表單數(shù)據(jù);
怎么保存系統(tǒng)設置的數(shù)據(jù)案怯。
這里具體的組件樹+布局就不繪制了,我們可以將實現(xiàn)過程分為四部分:第三方庫引入澎办、通用文件存儲嘲碱、model 應用和組件應用。
第三方庫
這里我們需要使用到 Flutter 本地存儲功能局蚀,F(xiàn)lutter 本地存儲功能包含三種:shared_preferences麦锯、path_provider 文件存儲以及 sqflite。這里只介紹 path_provider 文件存儲的實現(xiàn)琅绅,其他兩個大家參照官網(wǎng)的介紹嘗試即可扶欣。使用該 path_provider 庫需要在 pubspec.yaml 中增加庫引入,然后更新本地庫。
通用文件存儲
接下來我們基于 path_provider 實現(xiàn)一個通用的文件內容存儲料祠,代碼在 github 源碼中的 util/tools/local_storage.dart 中骆捧。這里我們主要需要實現(xiàn)兩個方法,一個是文件儲存內容髓绽,一個文件讀取內容敛苇。
文件存儲
我們先來看下文件存儲的邏輯,如下代碼:
復制代碼
/// 將數(shù)據(jù)保存到文件中
static Future<void> save(String content, String filePath) async {
? final directory = await getApplicationDocumentsDirectory();
? File file = new File('${directory.path}/$filePath');
? file.writeAsString(content);
}
因為是異步獲取文件存儲路徑顺呕,因此 save 方法也需要作為異步邏輯枫攀,由于無須等待處理結果,因此返回 void株茶。上面代碼中使用了 path_provider 的 getApplicationDocumentsDirectory 的方法獲取文件存儲目錄来涨,使用 dart:io 獲取具體文件的操作句柄,最后將內容寫入文件启盛,接下來我們看下讀取的過程蹦掐。
文件讀取
讀取的過程和寫的代碼相似,首先是找到文件并獲取文件操作句柄驰徊,然后再使用文件句柄讀取文件具體內容笤闯,代碼如下:
復制代碼
/// 獲取文件數(shù)據(jù)內容
static Future<String> get(String filePath) async {
? try {
? ? final directory = await getApplicationDocumentsDirectory();
? ? File file = new File('${directory.path}/$filePath');
? ? bool exist = await file.exists();
? ? if(!exist){ // 判斷是否存在文件
? ? ? return '';
? ? }
? ? return file.readAsString();
? } catch(e) {
? ? return '';
? }
}
上面代碼增加了一個異常處理,避免讀取失敗返回錯誤數(shù)據(jù)棍厂,因此如果這里判斷異常颗味,則返回空字符串。在 catch 邏輯中是需要增加上報來監(jiān)控告警的牺弹,后續(xù)我們再來介紹這部分內容浦马。
model 應用
因為系統(tǒng)配置是一個全局狀態(tài),需要在多個頁面使用张漂,所以我們需要將系統(tǒng)數(shù)據(jù)保存在 model 中晶默,因此我們在 model 創(chuàng)建
system_config_model.dart 文件。在實現(xiàn)邏輯中航攒,需要先調用 LocalStorage 來獲取初始配置磺陡,代碼如下:
復制代碼
/// 構造函數(shù)
SystemConfigModel.init(){
? LocalStorage.get('tyfapp.system.config').then((configStr){
? ? Map<String, dynamic> configInfo = {};
? ? if(configStr == null || configStr == '') { // 判斷合法性
? ? ? configInfo = {
? ? ? ? "accessMessage" : true,
? ? ? ? "tipsDetail" : true,
? ? ? ? "soundReminder" : true,
? ? ? ? "vibrationReminder" : true
? ? ? };
? ? } else {
? ? ? try { // 嘗試 json 解析,解析失敗直接返回
? ? ? ? configInfo =
? ? ? ? json.decode(configStr) as Map<String, dynamic>;
? ? ? } catch(e){
? ? ? ? return;
? ? ? }
? ? }
? ? systemConfig = StructSystemConfig.fromJson(configInfo);
? });
}
上面代碼 init 為構造函數(shù)漠畜,其中第 3 行是異步讀取文件币他,獲取到文件后存儲在共享狀態(tài)變量 systemConfig 中。為了異炽灸考慮蝴悉,如果沒有獲取到文件內容,則將共享狀態(tài)變量默認設置打開狀態(tài)瘾敢,也就是 true 值拍冠。有了初始化部分尿这,再修改 main.dart 增加一個新的狀態(tài)共享,部分如下代碼:
復制代碼
// 初始化共享狀態(tài)對象
LikeNumModel likeNumModel = LikeNumModel();
NewMessageModel newMessageNum = NewMessageModel(newMessageNum: 0);
// 異步數(shù)據(jù)處理
ApiUserInfoMessage.getUnReadMessageNum(newMessageNum);
// 異步獲取系統(tǒng)配置
SystemConfigModel systemConfigModel = SystemConfigModel.init();
return MultiProvider(
? providers: [
? ? ChangeNotifierProvider(create: (context) => likeNumModel),
? ? ChangeNotifierProvider(
? ? ? ? create: (context) => UserInfoModel(myUserInfo: myUserInfo)),
? ? ChangeNotifierProvider(create: (context) => newMessageNum),
? ? ChangeNotifierProvider(create: (context) => systemConfigModel),
? ],
? child: child,
);
上面代碼的第 7 行就是增加了系統(tǒng)變量的初始化庆杜,第 15 行就是增加到狀態(tài)共享中射众。接下來我們完善下 system_config_model.dart 代碼,為其增加 get 和 save 方法欣福,代碼如下:
復制代碼
/// 轉化為StructSystemConfig結構
StructSystemConfig get() {
? return systemConfig;
}
/// 轉化為StructSystemConfig結構
bool getSwitchItem(String switchItem) {
? if(systemConfig == null) {
? ? return false;
? }
? Map<String,dynamic> systemConfigJson =
? StructSystemConfig.toJson(systemConfig);
? try{
? ? return systemConfigJson[switchItem] as bool;
? }catch(e){
? ? return false;
? }
}
代碼的第 2 到第 18 行中的兩個方法 get 和 getSwitchItem 责球,其作用都是獲取系統(tǒng)配置,前者是獲取所有配置拓劝,后者是獲取具體的某個配置雏逾。我們繼續(xù)看下配置保存的兩個方法,代碼如下郑临。
復制代碼
/// 保存單個數(shù)據(jù)
void saveOne(String key, bool value) {
? Map<String,dynamic> systemConfigJson =
? ? StructSystemConfig.toJson(systemConfig);
? if(systemConfigJson[key] == value) {
? ? return;
? }
? systemConfigJson[key] = value;
? systemConfig = StructSystemConfig.fromJson(systemConfigJson);
? print(systemConfigJson);
? LocalStorage.save(json.encode(systemConfigJson), 'tyfapp.system.config');
? notifyListeners();
}
/// 整體數(shù)據(jù)保存
void save(StructSystemConfig newSystemConfig) {
? if(
? systemConfig.accessMessage == newSystemConfig.accessMessage &&
? ? ? systemConfig.tipsDetail == newSystemConfig.tipsDetail &&
? ? ? systemConfig.soundReminder == newSystemConfig.soundReminder &&
? ? ? systemConfig.vibrationReminder == newSystemConfig.vibrationReminder
? ) {
? ? return;
? }
? systemConfig = newSystemConfig;
? LocalStorage.save(
? ? ? json.encode(StructSystemConfig.toJson(systemConfig)),
? ? ? 'tyfapp.system.config'
? );
? notifyListeners(
代碼 save 和 saveOne栖博,分別對應保存整個系統(tǒng)配置數(shù)據(jù)和保存單個系統(tǒng)配置數(shù)據(jù)。在兩者實現(xiàn)邏輯中厢洞,首先都做了前期數(shù)據(jù)校驗判斷仇让,避免不必要的 build 操作。在 save 代碼邏輯中躺翻,需要將數(shù)據(jù)存儲到本地丧叽,通過調用 LocaStorage.save 來實現(xiàn)。
組件應用
組件應用部分較為簡單公你,我們先看下 pages/system_page/index.dart 的邏輯踊淳,如下:
復制代碼
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:two_you_friend/model/system_config_model.dart';
import 'package:two_you_friend/widgets/system_page/switch_card.dart';
import 'package:two_you_friend/util/struct/system_config.dart';
/// 首頁
class SystemConfigPageIndex extends StatelessWidget {
? /// 構造函數(shù)
? const SystemConfigPageIndex();
? @override
? Widget build(BuildContext context) {
? ? final systemConfigModel = Provider.of<SystemConfigModel>(context);
? ? StructSystemConfig systemConfig = systemConfigModel.get();
? ? return Container(
? ? ? padding: EdgeInsets.all(8),
? ? ? child: Column(
? ? ? ? children: <Widget>[
? ? ? ? ? SystemPageSwitchCard(itemDesc: '新消息提醒', switchItem: 'accessMessage'),
? ? ? ? ? SystemPageSwitchCard(itemDesc: '通知顯示詳情', switchItem: 'tipsDetail'),
? ? ? ? ? SystemPageSwitchCard(itemDesc: '聲音', switchItem: 'soundReminder'),
? ? ? ? ? SystemPageSwitchCard(itemDesc: '振動', switchItem: 'vibrationReminder')
? ? ? ? ],
? ? ? ),
? ? );
? }
}
主要邏輯在 build 中,build 中使用了 widgets/system_page/switch_card.dart 陕靠,我們看下這個子組件的實現(xiàn)迂尝,代碼如下:
復制代碼
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:two_you_friend/model/system_config_model.dart';
import 'package:two_you_friend/styles/text_syles.dart';
/// 單個系統(tǒng)配置
///
/// [title]為帖子詳情內容
class SystemPageSwitchCard extends StatelessWidget {
? /// 傳入的帖子標題
? final String switchItem;
? /// 消息提醒文字
? final String itemDesc;
? /// 構造函數(shù)
? const SystemPageSwitchCard(
? ? ? {Key key, this.itemDesc, this.switchItem}
? ? ? ) : super(key: key);
? @override
? Widget build(BuildContext context) {
? ? // 獲取操作句柄
? ? final systemConfigModel = Provider.of<SystemConfigModel>(context);
? ? return Row(
? ? ? mainAxisAlignment: MainAxisAlignment.spaceBetween,
? ? ? children: <Widget>[
? ? ? ? Text(
? ? ? ? ? itemDesc,
? ? ? ? ? style: TextStyles.commonStyle(1, Colors.black),
? ? ? ? ),
? ? ? ? Switch( // 選擇
? ? ? ? ? ? value: systemConfigModel.getSwitchItem(switchItem),
? ? ? ? ? ? activeTrackColor: Colors.lightBlueAccent,
? ? ? ? ? ? materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
? ? ? ? ? ? onChanged: (newValue) { // 觸發(fā)狀態(tài)變化
? ? ? ? ? ? ? systemConfigModel.saveOne(
? ? ? ? ? ? ? ? ? switchItem,
? ? ? ? ? ? ? ? ? !systemConfigModel.getSwitchItem(switchItem)
? ? ? ? ? ? ? );
? ? ? ? ? ? }
? ? ? ? ),
? ? ? ],
? ? );
? }
}
代碼第 34 行使用了 Switch 這個組件,該組件的 value 是通過 systemConfigModel 狀態(tài)共享類獲取剪芥,在點擊切換時觸發(fā)狀態(tài)改變垄开,并調用 systemConfigModel 中的 saveOne 觸發(fā)依賴組件狀態(tài)變化。
以上就實現(xiàn)了系統(tǒng)設置的功能税肪,相對其他組件的實現(xiàn)溉躲,這部分邏輯較為復雜,涉及了 Flutter 的本地存儲 以及 Provider 的應用技術點益兄。
搜索
最后我們來看下搜索功能签财,前面我們已經(jīng)實現(xiàn)了一個基本的搜索功能,但是其中的接口部分沒有補齊偏塞,我們先來看下實際的效果,如圖 6 所示邦鲫。
圖 6 搜索提示和搜索結果效果
組件樹+布局
根據(jù)圖 6 的頁面效果灸叼,我們來繪制組件樹+布局神汹,搜索提示就是一個列表,這里就不繪制了古今,搜索結果稍微復雜一些屁魏,主要看下這部分,繪制結果如圖 7 所示捉腥。
圖 7 搜索結果頁面組件樹+布局設計
這個組件樹的設計氓拼,包含了我們布局設計思想中的 8 個過程,豎橫抵碟、高寬桃漾、上下和左右,具體細節(jié)就不再贅述拟逮。接下來我們看下這兩部分邏輯的實現(xiàn):搜索提示和搜索結果撬统。
搜索提示
搜索提示較為簡單,主要邏輯是從服務端拉取搜索提示接口敦迄,并返回一個 ListView 列表結果恋追。具體代碼如下:
復制代碼
/// 獲取 suggest list組件列表
Future<Widget> _getSuggestList() async{
? List<String> suggests = await ApiSearchIndex.suggest(query);
? if(suggests == null || suggests.length < 1){ // 異常處理
? ? return Container();
? }
? // 保留前 5 個搜索
? int subLen = suggests.length > 5 ? 5 : suggests.length;
? List<String> subSuggests = suggests.sublist(0, subLen);
? return ListView.builder(
? ? ? scrollDirection: Axis.vertical,
? ? ? shrinkWrap: true,
? ? ? itemCount: subSuggests.length,
? ? ? itemBuilder: (context,index){
? ? ? ? return? ListTile(
? ? ? ? ? ? title: RichText(
? ? ? ? ? ? ? ? text: TextSpan(
? ? ? ? ? ? ? ? ? // 獲取搜索框內輸入的字符串,設置它的顏色并加粗
? ? ? ? ? ? ? ? ? ? text: subSuggests[index],
? ? ? ? ? ? ? ? ? ? style: TextStyles.commonStyle()
? ? ? ? ? ? ? ? )
? ? ? ? ? ? ),
? ? ? ? ? ? onTap: () {
? ? ? ? ? ? ? query = subSuggests[index];
? ? ? ? ? ? ? showResults(context);
? ? ? ? ? ? },
? ? ? ? );
? ? ? }
? );
}
代碼中罚屋,首先使用 query 關鍵詞獲取用戶輸入苦囱,通過 ApiSearchIndex.suggest 方法獲取服務端搜索提示結果,接下來做一些數(shù)據(jù)校驗脾猛,最后根據(jù)搜索提示 build 出相應的組件撕彤。其中的第 26 行至第 28 行代碼的作用是,通過點擊搜索提示觸發(fā)搜索行為尖滚,第 27 行替換搜索提示內容喉刘,第 28 行執(zhí)行搜索并獲取搜索結果。
搜索結果
根據(jù)圖 7 的繪制結果漆弄,我們了解到這里需要設計兩個組件睦裳,組件一是展示搜索到的用戶列表內容,組件二是展示搜索到的帖子列表內容撼唾。我們這里就使用兩個組件函數(shù)來實現(xiàn)廉邑,主要看下用戶部分(帖子部分邏輯相似)。
復制代碼
/// 獲取用戶搜索結果組件
Widget _getUserListWidget(List<StructUserInfo> userList) {
? if(userList == null || userList.length < 1){
? ? return Container();
? }
? int subLen = userList.length > 5 ? 5 : userList.length;
? List<StructUserInfo> subUserList = userList.sublist(0, subLen);
? return ListView.builder(
? ? ? scrollDirection: Axis.vertical,
? ? ? shrinkWrap: true,
? ? ? itemCount: subUserList.length + 1,
? ? ? itemBuilder: (context,index) {
? ? ? ? if(index == 0){
? ? ? ? ? return Row(
? ? ? ? ? ? children: <Widget>[
? ? ? ? ? ? ? Padding(padding: EdgeInsets.only(left: 10)),
? ? ? ? ? ? ? Text(
? ? ? ? ? ? ? ? '用戶',
? ? ? ? ? ? ? ? style: TextStyles.commonStyle(0.9),
? ? ? ? ? ? ? ),
? ? ? ? ? ? ],
? ? ? ? ? );
? ? ? ? }
? ? ? ? return UserPageCard(userInfo: subUserList[index - 1]);
? ? ? });
}
以上組件代碼的實現(xiàn)與我們之前所學習的知識點倒谷,沒有太大的差異性蛛蒙。核心知識點是應用 ListView.builder 組件,來顯示 seaction_name (也就是上面的 Text 組件)和搜索結果中的用戶列表(上面的 UserPageCard 組件)渤愁。
以上就完成了搜索部分的邏輯牵祟,具體代碼查看 github 中的 pages/search_page/custom_delegate.dart 文件。
總結
本課時帶領大家實踐開發(fā)了四個核心頁面(我的好友抖格、我的消息诺苹、系統(tǒng)設置和搜索)咕晋。學完本課時你需要進一步掌握組件樹+布局的設計思想,同時掌握 Flutter 本地存儲的技術點收奔,進一步鞏固 Flutter 編碼風格掌呜。學完本課時之后,我建議你自行去實現(xiàn)“我的消息”中的私信功能和評論相關的部分(后續(xù)會在 github 上提供源碼)坪哄。
本課時之前质蕉,我們對 App 的安全并沒有關注太多,可以說完全放任翩肌。下一課時我們將通過工具化的方式來上報異常模暗,保證我們 App 的質量,提前發(fā)現(xiàn)并解決問題摧阅。
轉自:https://kaiwu.lagou.com/course/courseInfo.htm?courseId=251#/detail/pc?id=3535