Flutter
釋出到現在有一段時間了,目前為止,很多公司都還沒有接受Flutter進行開發,這原因是多方面的,畢竟在沒有足夠的“探索者”前,貿然使用遇到麻煩時,解決起來太過麻煩。
現在市面上感覺做的比較好的一款產品,除了官方demo:Gallery,再就是第一款Flutter開發的Github客戶端,詳細資訊請移步:gitme。
參照著Flutter中文網
以及,大牛正在編寫中的書籍:Flutter實戰,大致梳理了一下Flutter開發流程,不過由於架構的不同,之前ios
和Android
最主要的還是操作dom
,而Flutter則與RN類似的採用的響應式操作,這就導致在遷移開發平臺時,會不自主想將以前的經驗進行搬移時遇到麻煩。
在android、ios以及web端
,開發框架流程等都已經很純熟和完善了,而Flutter就目前的生態來說,還是有些“薄弱”,具體的開發還是需要進行系統的學習,這裡只是簡單給出一種方式來完成前端很常見的國際化與換膚功能
。
完整程式碼移步github-demo:flutter_skin_locale
效果圖如下:
一、基本思路解析
因為Flutter
出身同為Android
的google
,所以這裡暫時以Android
實現方式進行比對;
1、安卓實現
國際化
安卓中國際化操作
比較簡單,因為系統已經提供了這方面的解決方案,我們只要將對應的資原始檔放入不同的res資源目錄
下面就可以了,系統會根據當前的locale
值查詢對應的 res 資源目錄,然後根據資源id
查詢資源名稱
,最後根據資源名稱查詢到具體檔案
。這個流程對於應用層開發人員來說是無感知的,所有要做的只是配置...然後打包。
換膚
安卓中原生是不支援換膚的,其實在安卓誕生時候也沒有這方面的需求,後來大廠商為了某些銷售活動,或者為了更好適應夜間模式及個性化,才有了這方面需求。就目前來看使用最多的是換膚框架,比如很多star的Android-skin-support;即便有框架的支援,在涉及大量自定義控制元件的情況下,仍需要做很多的適配工作。
2、Flutter實現
字串國際化
很有意思的是,在 Android 中只需要依照配置就可以完成的國際化功能,在Flutter中很難行得通,因為Flutter中沒有了Android的 res 系統
,所有需要操作的圖片,圖示顏色值,都只能通過檔案或者程式碼硬性插入,這一點很不舒服,雖然官方給了flutter_localizations
庫,但使用起來就知道,真的相當麻煩,我們不妨拋開Flutter部分平臺
的機制,看自己搭輪子是否可以實現換膚功能;
在真正開始之前,還是得先了解一點Flutter中入口的邏輯,否則肯定找不到頭緒:
return MaterialApp(
title: 'Flutter Title',
routes: ...,
localizationsDelegates: [
S.delegate,
GlobalMaterialLocalizations.delegate, //Material 元件庫所使用的字串
GlobalWidgetsLocalizations.delegate, // 在當前的語言中,文字預設的排列方向
],
supportedLocales: S.delegate.supportedLocales,
localeResolutionCallback: S.delegate.resolution(fallback: const Locale('en', '')),
locale: mapLocales[SupportLocale.values[gCurrentSupportLocale]],
);
複製程式碼
Flutter 入口程式一般都是這個樣子,針對涉及國際化的每個欄位,簡單的說明一下:
欄位 | 含義 |
---|---|
localizationsDelegates | 國際化代理類,這個只需知道是國際化字串資源類的集合即可,一般我們會將自定義的國際化字串物件在這裡宣告;具體功能可以檢視[LocalizationsDelegate]類 |
supportedLocales | 系統支援的語言環境,比如中文簡體,中文繁體等等,注意的是,locale要同時賦值語言和國家,以英文為例:Locale("en", "") |
localeResolutionCallback | 如果當前手機設定的語言環境或者說宿主app設定的語言環境不在 supportedLocales 中,那麼需要預設一個locale 值,不預設也可以,系統會預設取支援列表supportedLocales 中第一個值 |
locale | 自己設定一個當前語言locale,如果不設定或者設定為null,就取宿主app當前的語言環境(等價於設定語言環境為:"跟隨系統") |
我們大致只需要知道上面所說的部分即可,假設現在需要實現一個語言切換的功能,需要包含以下幾種型別:
- 跟隨系統
- 簡體中文
- 繁體臺灣
- 繁體香港
- 英文
對於跟隨系統
來說,只要將MaterialApp
中locale
欄位置為null
,其他四種情況分別對應不同的 locale
即可;至於MaterialApp
中另外幾個欄位,也只需要根據支援列表一一填入;最重要的部分是國際化資源代理類:S
的建立與更新。
雖說Flutter不支援,但只要程式設計師夠懶,總會有適合的工具出現的,這裡給出一個最簡單的用於國際化的外掛:Flutter i18n
有了這個外掛開發起來會方便的多,在安裝此外掛後,專案中會自動生成${project}/res/values/*.arb
以及${project}/lib/generated/i18n.dart
檔案;*.arb
代表多個檔案(工具只會幫忙生成strings_en.arb
),類似於安卓中多個目錄下針對字串的配置,這個可以自行新增或者刪除*.arb
檔案,多新增一個正確的配置檔案,就相當於多一種語言支援,新增*.arb
檔案後編輯器會自己處理剩下的邏輯,或者點選工具欄頂部的這個圖示:
Flutter i18n已經整合了快捷鍵來提取字串,這個和android中的這個功能使用方法相同:
繼續之前工具自動生成的兩部分檔案說,*.arb
檔案類似下面的結構:
注意:strings_en.arb
是預設的arb檔案,其他arb檔案需要根據預設的arb檔案生成對應的字串。
i18n.dart
是國際化的核心程式碼,大致結構如下:
可以看到,裡面包含了不同國家地區(自己配置支援的國際語言,和*.arb
相對應),同樣的,已經替我們生成了MaterialApp
中所需的幾乎所有配置(具體使用配置可參照demo)。
最後使用的話也很簡單,在程式碼中需要字串的地方替換為這種:
S.of(context).label_soft_setting
複製程式碼
S
類是i18n.dart
自動幫助我們生成的,類似於一個代理類,根據不同的語言環境代理$zh_HK、$zh_TW、$en、$zh_CN
這些具體實現類,達到國際化的目的
經過上面的總結,我們來看Flutter i18n
到底完成了什麼:
- 自動幫助生成
strings_en.arb
預設字串模版 - 提供快捷方式,替換檔案出現的字串到
*.arb
檔案中 - 根據
*.arb
自動生成i18n.dart
檔案,包含支援語言列表,國際化代理類等 i18n.dart
提供在執行時提取不同國家語言字串的功能方法
整體來看,該工具沒有依賴任何庫,也就是說相對於官方提供的方法,Flutter i18n
不需要對pubspec.yaml
做出修改。
基本上,如果專案中沒有太過複雜的要求,只提供這種字串國際化足夠,但有些情況下, 針對不同的語言環境,圖片也需要動態進行更替,關於更換圖片的邏輯,放到文章下面介紹換膚功能的時候再考慮。
純顏色換膚
對比其他平臺換膚,我覺得Flutter換膚最為簡單(這裡換膚是指應用內換膚,不支援從網際網路下載皮膚包換膚),因為系統預設提供了Theme
,不得不說,在Flutter中處處可見Android開發的影子,Theme表示主題
,表示應用整體的風格,theme中可定義各種型別用途的背景顏色,文字顏色,高亮;甚至於可以修改文字的字型大小,字型庫,因此通過theme還可以實現應用內更改字型大小的功能
,不過這篇文章先不考慮這個問題,在換膚後功能完成後,要實現應用內換膚是很容易的事情。
對於theme,可以擷取一部分檢視大致情況:
如上所見,對於分割線,主題色,按鈕,高亮等,可以分別定義不同的顏色值,然後我們可以在MaterialApp
中設定不同的theme(跟國際化配置在相同的地方):
return MaterialApp(
title: 'Flutter Mudule',
theme: themes[gCurrentThemeIndex],
routes: ...,
localizationsDelegates: [
S.delegate,
GlobalMaterialLocalizations.delegate, //Material 元件庫所使用的字串
GlobalWidgetsLocalizations.delegate, // 在當前的語言中,文字預設的排列方向
],
supportedLocales: S.delegate.supportedLocales,
localeResolutionCallback: S.delegate.resolution(fallback: const Locale('en', '')), // 不存對應locale時,預設取值英文
locale: mapLocales[SupportLocale.values[gCurrentSupportLocale]],
);
複製程式碼
其中theme欄位即為當前應用或者說介面採用的主題,如果我們可以對其進行更改,就相當於對app進行換膚(這裡的換膚只是指顏色換膚,真正換膚可能還需要涉及圖片更換,情況與國際化類似,下面會提到這種解決方式)。
想要在程式碼中使用某個顏色時(有些顏色值為自定義,因此係統無法動態感知需要使用哪個樣式),可以使用如下方式:
Theme.of(context).textTheme.display1.color
複製程式碼
圖片換膚
到此為止,簡單的顏色換膚和國際化處理思路已經清晰,遵循上面的邏輯,基本可以完成大部分的需求,不過就如上面所提,如果涉及圖片部分,Flutter框架就無法直接處理了,事實上,flutter 如果需要獲取一個圖片,是需要知道具體路徑的,類似這樣:
Image.asset(
"assets/images/icon_test.png",
width: 45,
height: 45,
),
複製程式碼
相比於android讀取圖片,Flutter這種方式麻煩的多,並且沒有任何的智慧提示;但也因為是這種呼叫方式,給了我們很大的自定義圖片讀取的空間,比如這樣:
定義同級兩個資料夾,裡面分別放置不同主題樣式,然後根據當前選中的主題,取圖片時,選擇不同的路徑,類似這樣:
/// 獲取圖片路徑(中轉,用於多環境等情況) [PlatformAssetBundle] 類檢視資源獲取邏輯
///
/// [useDefault] 是否使用預設的主題資源(當多theme使用相同image時,會有這種情況)
/// [picFormat] 圖片格式,預設為png,
String dispatcherPictureByName(String picName, {bool useDefault = false, String picFormat = "png"}) {
RegExp filter = RegExp("^[^.]+\.(png)|(jpg)|(jpeg)|(gif)|(webp)|(bmp)|(wbmp)\$", caseSensitive: false, multiLine: false);
// 新增字尾
picName = filter.hasMatch(picName) ? picName : "$picName.$picFormat";
// 取系統主題顏色
String pathName = "assets/images-$gCurrentThemeIndex/$picName";
// 返回需要的路徑
return useDefault ? "assets/images-1/$picName" : pathName;
}
複製程式碼
其中$gCurrentThemeIndex
表示當前主題下標序號,然後在程式碼中這樣使用:
Image.asset(
dispatcherPictureByName("icon_test",useDefault: true),
width: 45,
height: 45,
),
複製程式碼
現在應該明白了,如果是想讓國際化時也取值不同的圖片,只要類似這樣定義不同的檔案包,然後將圖片放入即可,這個取值規則是自定義的,根據實際情況可以做出修改。
二、模組依賴項
上面給出了要實現國際化與換膚基本思路,但其中還有很多細節需要思考,比如:
- 如何定義主題包?
- 字串獲取時使用
S.of(context).***
,如果需要在沒有context
的地方獲取字串值,該如何處理? - 當想要修改主題和語言環境時,怎樣通知所有介面進行重新整理?
- 國際化與換膚是要保持狀態,如何在切換成功後記錄儲存?如果是以
module
的形式混入原生應用,該如何保證原生與Flutter層保持一致?
當然,最後一條可以可以不用管,那個屬於混合開發的範疇,真正使用的時候再考慮跨平臺混入bridge;對於記錄儲存,使用第三方的庫即可:shared_preferences
針對上面的問題,分別進行討論:
1、如何定義主題包
主題包的定義可以使用最簡單的方式,新建一個檔案app_theme_config.dart
,將所有預定義的主題放在一起:
List<ThemeData> themes = [
ThemeData(...),
ThemeData(...),
ThemeData(...),
ThemeData(...),
]
複製程式碼
裡面每一個ThemeData
表示一套風格,或者說是一個皮膚;
再新建一個檔案app_status_holder.dart
,儲存當前選擇的皮膚的下標,取值範圍為:[0,length-1],int型別
,這樣的話,每次需要更換皮膚時,只需要修改下標的值,然後從themes陣列
中取出對應的主題,賦值給MaterialApp
中theme
欄位即可。
app_status_holder.dart
檔案:
/// 當前系統主題(暫不考慮外部引入主題情況)
int gCurrentThemeIndex = 0;
複製程式碼
2、無context時處理字串?
跟上面的主題處理方式類似,我們也新建一個檔案,儲存當前app可能使用的字串類app_locale_config.dart
:
/// 某些地方無法 獲取context ,但又需要獲取國際化的字串時,但系統切換可能導致文字不會改變,因為字串沒有在 state方法中初始化
List<S> ss = [
S(),
$zh_CN(),
$zh_TW(),
$zh_HK(),
$en(),
];
/// 當context 不存在時,通過SS而非S去獲取字串
S get SS {
return ss[gCurrentSupportLocale];
}
複製程式碼
在沒有context
的情況如果想要獲取到字串,就必須知道當前語言環境到底是什麼,我們模擬代理類S
定義一個代理方法SS
,然後在全域性記錄當前語言環境,間接的讀取到正確的字串值;
當然,只做這個是不夠的,如果設定了語言設定為跟隨系統
,在系統語言進行切換時,呼叫方法SS
獲取到的一直會是S()
,即系統預設的英文形式,那肯定是不行的,因此,必須在合適的地方呼叫一次這樣的程式碼:
// 系統語言改變時,如果當前為跟隨系統,則需要修改字串讀取物件
if (gCurrentSupportLocale == 0) {
print("當前系統語言為:${Localizations.localeOf(context)}");
ss[0] = S.of(context);
}
複製程式碼
也就是說,需要動態的修改ss陣列列表
中預設的語言。
3、如何通知介面重新整理?
一般來說,修改語言環境或皮膚後,除了當前介面,已開啟或建立的介面也都需要進行重新整理,對於安卓平臺來說,系統預設在config
變化後重啟介面來達到重新整理的目的;
對於Flutter來說,重新整理介面不需要重建,只需要呼叫setState
方法即可;方便是方便,但這涉及到事件的推送;訊息佇列是最簡單的推送方式,或者說是事件匯流排EventBus,這個框架在幾乎所有的平臺存在。
為了方便介面重新整理,我們最好在最頂層監聽事件,然後直接重新整理MaterialApp
,確保所有的介面都可以觸發重繪操作,監聽操作可以類似這樣:
// 當通知系統時,重新整理一下狀態(換膚/切換語言/漲跌顏色)
eventBus.on<SystemThemeSwitch>().listen((it) {
setState(() {
gCurrentThemeIndex = it.currentThemeIndex;
});
});
複製程式碼
在修改系統語言和皮膚切換的介面,如果修改成功,則需要觸發事件:
/// 切換主題
eventBus.fire(SystemThemeSwitch(currentThemeIndex: news.index));
setState(() {});
複製程式碼
三、程式碼整合檢視效果
如上所述,結合了國際化、換膚、本地持久化、路由跳轉框架以及圖片更改等思路,入口程式應該像這樣:
/// 程式入口
void main() => runApp(CustomApp());
/// 自定義包裹 app, 實現換膚等功能
class CustomApp extends StatefulWidget {
@override
State createState() => _CustomAppState();
}
class _CustomAppState extends State<CustomApp> {
@override
void initState() {
super.initState();
// 初始化皮膚取值等全域性 所需 引數
SharedPreferences.getInstance().then((it) {
setState(() {
gCurrentThemeIndex = it.getInt(KEY_THEME_MODE) ?? 0;
gCurrentSupportLocale = it.getInt(KEY_SUPPORT_LOCALE) ?? 0;
});
});
// 當通知系統時,重新整理一下狀態(換膚/切換語言/漲跌顏色)
eventBus.on<SystemThemeSwitch>().listen((it) {
setState(() {
gCurrentThemeIndex = it.currentThemeIndex;
});
});
eventBus.on<SupportLocaleSwitch>().listen((it) {
setState(() {
gCurrentSupportLocale = it.currentSupportLocale;
});
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Mudule',
debugShowCheckedModeBanner: false,
theme: themes[gCurrentThemeIndex],
routes: gActivityRoutes,
localizationsDelegates: [
S.delegate,
GlobalMaterialLocalizations.delegate, //Material 元件庫所使用的字串
GlobalWidgetsLocalizations.delegate, //在當前的語言中,文字預設的排列方向
],
supportedLocales: S.delegate.supportedLocales,
localeResolutionCallback: S.delegate.resolution(fallback: const Locale('en', '')), // 不存對應locale時,預設取值英文
locale: mapLocales[SupportLocale.values[gCurrentSupportLocale]],
);
}
}
複製程式碼
前面還提到,需要在程式第一個介面widget
的build方法
新增如下程式碼:
像上面這樣配置後,主流程基本已經完成了,剩下的程式碼就是編寫頁面,變數定義等等,事實上在變數少的情況下,使用event-bus
尚可。
如果需要改變變數過多邏輯較大的情況下,可以嘗試使用flutter_redux
庫,專案中有提供簡單的使用方式:main_redux.dart;
更多功能請提issues
完成專案請參照flutter_skin_locale