Flutter高級(二)——國際化與換膚

Flutter發佈到如今有一段時間了,目前爲止,不少公司都尚未接受Flutter進行開發,這緣由是多方面的,畢竟在沒有足夠的「探索者」前,貿然使用遇到麻煩時,解決起來太過麻煩。前端

如今市面上感受作的比較好的一款產品,除了官方demo:Gallery,再就是第一款Flutter開發的Github客戶端,詳細信息請移步:gitmeandroid

參照着Flutter中文網以及,大牛正在編寫中的書籍:Flutter實戰,大體梳理了一下Flutter開發流程,不過因爲架構的不一樣,以前iosAndroid最主要的仍是操做dom,而Flutter則與RN相似的採用的響應式操做,這就致使在遷移開發平臺時,會不自主想將之前的經驗進行搬移時遇到麻煩。ios

android、ios以及web端,開發框架流程等都已經很純熟和完善了,而Flutter就目前的生態來講,仍是有些「薄弱」,具體的開發仍是須要進行系統的學習,這裏只是簡單給出一種方式來完成前端很常見的國際化與換膚功能git

完整代碼移步github-demo:flutter_skin_locale程序員

效果圖以下:github

1、基本思路解析

由於Flutter出身同爲Androidgoogle,因此這裏暫時以Android實現方式進行比對;web

一、安卓實現

國際化

安卓中國際化操做比較簡單,由於系統已經提供了這方面的解決方案,咱們只要將對應的資源文件放入不一樣的res資源目錄下面就能夠了,系統會根據當前的locale值查找對應的 res 資源目錄,而後根據資源id查找資源名稱,最後根據資源名稱查找到具體文件。這個流程對於應用層開發人員來講是無感知的,全部要作的只是配置...而後打包。redux

換膚

安卓中原生是不支持換膚的,其實在安卓誕生時候也沒有這方面的需求,後來大廠商爲了某些銷售活動,或者爲了更好適應夜間模式及個性化,纔有了這方面需求。就目前來看使用最多的是換膚框架,好比不少star的Android-skin-support;即使有框架的支持,在涉及大量自定義控件的狀況下,仍須要作不少的適配工做。數組

二、Flutter實現

字符串國際化

頗有意思的是,在 Android 中只須要依照配置就能夠完成的國際化功能,在Flutter中很難行得通,由於Flutter中沒有了Android的 res 系統,全部須要操做的圖片,圖標顏色值,都只能經過文件或者代碼硬性插入,這一點很不舒服,雖然官方給了flutter_localizations庫,但使用起來就知道,真的至關麻煩,咱們不妨拋開Flutter部分平臺的機制,看本身搭輪子是否能夠實現換膚功能;bash

在真正開始以前,仍是得先了解一點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當前的語言環境(等價於設置語言環境爲:"跟隨系統")

咱們大體只須要知道上面所說的部分便可,假設如今須要實現一個語言切換的功能,須要包含如下幾種類型:

  1. 跟隨系統
  2. 簡體中文
  3. 繁體臺灣
  4. 繁體香港
  5. 英文

對於跟隨系統來講,只要將MaterialApplocale字段置爲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到底完成了什麼:

  1. 自動幫助生成 strings_en.arb 默認字符串模版
  2. 提供快捷方式,替換文件出現的字符串到*.arb文件中
  3. 根據*.arb自動生成i18n.dart文件,包含支持語言列表,國際化代理類等
  4. 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,
),
複製代碼

如今應該明白了,若是是想讓國際化時也取值不一樣的圖片,只要相似這樣定義不一樣的文件包,而後將圖片放入便可,這個取值規則是自定義的,根據實際狀況能夠作出修改。

2、模塊依賴項

上面給出了要實現國際化與換膚基本思路,但其中還有不少細節須要思考,好比:

  1. 如何定義主題包?
  2. 字符串獲取時使用S.of(context).***,若是須要在沒有context的地方獲取字符串值,該如何處理?
  3. 當想要修改主題和語言環境時,怎樣通知全部界面進行刷新?
  4. 國際化與換膚是要保持狀態,如何在切換成功後記錄保存?若是是以module的形式混入原生應用,該如何保證原生與Flutter層保持一致?

固然,最後一條能夠能夠不用管,那個屬於混合開發的範疇,真正使用的時候再考慮跨平臺混入bridge;對於記錄保存,使用第三方的庫便可:shared_preferences

針對上面的問題,分別進行討論:

一、如何定義主題包

主題包的定義可使用最簡單的方式,新建一個文件app_theme_config.dart,將全部預約義的主題放在一塊兒:

List<ThemeData> themes = [
  ThemeData(...),
  ThemeData(...),
  ThemeData(...),
  ThemeData(...),
]
複製代碼

裏面每個ThemeData表示一套風格,或者說是一個皮膚;

再新建一個文件app_status_holder.dart,保存當前選擇的皮膚的下標,取值範圍爲:[0,length-1],int類型,這樣的話,每次須要更換皮膚時,只須要修改下標的值,而後從themes數組中取出對應的主題,賦值給MaterialApptheme字段便可。

app_status_holder.dart 文件:

/// 當前系統主題(暫不考慮外部引入主題狀況)
int gCurrentThemeIndex = 0;
複製代碼

二、無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數組列表中默認的語言。

三、如何通知界面刷新?

通常來講,修改語言環境或皮膚後,除了當前界面,已打開或建立的界面也都須要進行刷新,對於安卓平臺來講,系統默認在config變化後重啓界面來達到刷新的目的;

對於Flutter來講,刷新界面不須要重建,只須要調用setState方法便可;方即是方便,但這涉及到事件的推送;消息隊列是最簡單的推送方式,或者說是事件總線EventBus,這個框架在幾乎全部的平臺存在。

爲了方便界面刷新,咱們最好在最頂層監聽事件,而後直接刷新MaterialApp,確保全部的界面均可以觸發重繪操做,監聽操做能夠相似這樣:

// 當通知系統時,刷新一下狀態(換膚/切換語言/漲跌顏色)
eventBus.on<SystemThemeSwitch>().listen((it) {
    setState(() {
        gCurrentThemeIndex = it.currentThemeIndex;
    });
});
複製代碼

在修改系統語言和皮膚切換的界面,若是修改爲功,則須要觸發事件:

/// 切換主題
eventBus.fire(SystemThemeSwitch(currentThemeIndex: news.index));
setState(() {});
複製代碼

3、代碼整合查看效果

如上所述,結合了國際化、換膚、本地持久化、路由跳轉框架以及圖片更改等思路,入口程序應該像這樣:

/// 程序入口
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]],
        );
    }
}
複製代碼

前面還提到,須要在程序第一個界面widgetbuild方法添加以下代碼:

像上面這樣配置後,主流程基本已經完成了,剩下的代碼就是編寫頁面,變量定義等等,事實上在變量少的狀況下,使用event-bus尚可。

若是須要改變變量過多邏輯較大的狀況下,能夠嘗試使用flutter_redux,項目中有提供簡單的使用方式:main_redux.dart;

更多功能請提issues

完成項目請參照flutter_skin_locale

相關文章
相關標籤/搜索