“極簡詩詞”app開發背後:Flutter移動應用快速構建實踐——狀態管理、國際化、資料持久化、效能優化(一)

畫星星高手發表於2020-01-31

前言

在開始之前的提示:雖然Flutter背靠Google這棵大樹,但是畢竟還是一個年輕的技術,專案還處於初期階段,更新非常快,問題也不是一般的多,使用Flutter的話就意味著必須忍受各種奇怪的bug和沒有豐富中文資料的頭疼,本文不是安利同學們入坑,只是對“極簡詩詞”app的開發過程進行記錄。

另外app已經上架,有興趣的同學可以下載試試:www.coolapk.com/apk/251155

主要介面截圖:

主頁 暗黑版主頁 詩集 詩集瀏覽
“極簡詩詞”app開發背後:Flutter移動應用快速構建實踐——狀態管理、國際化、資料持久化、效能優化(一)
“極簡詩詞”app開發背後:Flutter移動應用快速構建實踐——狀態管理、國際化、資料持久化、效能優化(一)
“極簡詩詞”app開發背後:Flutter移動應用快速構建實踐——狀態管理、國際化、資料持久化、效能優化(一)
“極簡詩詞”app開發背後:Flutter移動應用快速構建實踐——狀態管理、國際化、資料持久化、效能優化(一)
詩集詳情 作者列表 作者詳情 字型選擇
“極簡詩詞”app開發背後:Flutter移動應用快速構建實踐——狀態管理、國際化、資料持久化、效能優化(一)
“極簡詩詞”app開發背後:Flutter移動應用快速構建實踐——狀態管理、國際化、資料持久化、效能優化(一)
“極簡詩詞”app開發背後:Flutter移動應用快速構建實踐——狀態管理、國際化、資料持久化、效能優化(一)
“極簡詩詞”app開發背後:Flutter移動應用快速構建實踐——狀態管理、國際化、資料持久化、效能優化(一)

和Django快速開發實踐的文章一樣,本文不講廢話,直接上步驟。

專案檔案結構

先設計好專案檔案結構,不同的專案有不同的需求,按照自己的實際需要來設計結構就好了,以下是我的專案結構,僅供參考:

lib
├── common
├── i10n
├── models
├── routes
├── states
└── widgets
複製程式碼
資料夾 作用
common 一些工具類,如通用方法類、網路介面類、儲存全域性變數的靜態類等
i10n 存放國際化相關程式碼
models 通過json to models生成的model類檔案都存在這裡
routes 存放專案的所有頁面程式碼
states 儲存app中需要跨元件共享的狀態類
widgets 存放自定義widget

定義好models

在本專案中,我使用json to models來自動生成models類,為什麼使用這個呢?原因很簡單,減少工作量,用json定義好app中使用到的模型,生成model類之後可以很方便序列化成json資料進行持久化和或者從配置檔案中讀取json資料反序列化成model物件,還可以直接根據後臺介面返回的json資料生成model類,非常方便。

使用json定義model,例子如下:

在專案根目錄下建立json資料夾,新增要進行轉換的json檔案,內容大概像這樣。 poem.json

{
    "strains": [
        "平平平仄仄,平仄仄平平。",
        "仄仄平平仄,平平仄仄平。",
        "平平平仄仄,平仄仄平平。",
        "平仄仄平仄,平平仄仄平。"
    ],
    "author": "作者名稱",
    "authorObj": "$author",
    "paragraphs": [
        "秦川雄帝宅,函谷壯皇居。",
        "綺殿千尋起,離宮百雉餘。",
        "連甍遙接漢,飛觀迥凌虛。",
        "雲日隱層闕,風煙出綺疎。"
    ],
    "tags": [
        "戰爭",
        "生活",
        "冬天",
        "愛國",
        "邊塞"
    ],
    "chapter": "國風",
    "section": "周南",
    "rhythmic": "玉樓春",
    "title": "帝京篇十首 一",
    "content": "經傳宜獨坐讀,史鑑宜與友共讀。",
    "comment": [
        "孫愷似曰:深得此中真趣,固難為不知者道。",
        "王景州曰:如無好友,即紅友亦可。"
    ],
    "notes": [
        "1.小山--寫女子的隔夜殘妝。小山:女子畫眉的式樣之一。小山重疊:眉暈褪色。金:額黃,在額上塗黃色。金明滅:褪色的額黃明暗不勻。",
        "2.鬢雲欲度--鬢髮撩亂如雲,低垂下來。香腮雪:潔白如雪的香腮。",
        "3.照花--對鏡簪花。用前鏡、後鏡對照以瞻顧後影。",
        "4.雙雙--羅襦上用金線繡的成雙的鷓鴣鳥。反襯自身孤獨。"
    ],
    "anthology": "所屬詩集名稱",
    "id": "08e41396-2809-423d-9bbc-1e6fb24c0ca1"
}
複製程式碼

新增依賴:

dev_dependencies:
  json_model: ^0.0.2
  build_runner: ^1.0.0
  json_serializable: ^2.0.0
複製程式碼

好了之後執行:

flutter packages pub run json_model
複製程式碼

這樣就會自動在lib/models資料夾下面生成models類啦。

有個坑爹的地方是這個json_model庫只能支援很老版本的build_runnerjson_serializable,這和我後面要用到的intl就衝突了啊,每次用這兩個庫的時候我都要不斷註釋切換依賴的版本,真的麻煩 = =....

狀態管理

狀態管理是app中最重要的一部分,也是後面主題切換和國際化的基礎。 本文是快速開發實踐,不過多深入Flutter的狀態管理,想了解的同學可以看看大佬寫的Flutter教程:book.flutterchina.club/chapter7/pr…

我是使用Provider這個元件來管理app的狀態的,它基於InheritedWidget實現,用起來挺方便。

先新增依賴:

dependencies:
  provider: ^3.2.0
複製程式碼

lib/states資料夾下新增共享狀態的models,例如:

import 'package:flutter/material.dart';
import 'package:minimal_poem/common/global.dart';
import 'package:minimal_poem/models/index.dart';
import 'notifier.dart';

class ProfileChangeNotifier extends ChangeNotifier {
  Profile get profile => Global.profile;

  @override
  void notifyListeners() {
    // 儲存Profile變更
    Global.saveProfile();
    Global.saveAllUsers();
    super.notifyListeners(); // 通知依賴的Widget更新
  }
}

class ThemeModel extends ProfileChangeNotifier {
  // 獲取當前主題,如果未設定主題,則預設使用藍色主題
  ColorSwatch get theme => Global.themes.firstWhere((e) => e.value == profile.theme, orElse: () => Colors.blue);

  // 主題改變後,通知其依賴項,新主題會立即生效
  set theme(ColorSwatch color) {
    if (color != theme) {
      profile.theme = color[500].value;
      notifyListeners();
    }
  }

  bool get darkMode => Global.profile.darkMode;

  set darkMode(bool value) {
    Global.profile.darkMode = value;
    notifyListeners();
  }
}
複製程式碼

這些model繼承自ProfileChangeNotifier,可以提供資料或者管理資料的修改和儲存。

在普通的元件裡可以直接使用獲取或儲存資料,配合provider元件使用可以在model資料改變的時候出發元件的更新動作~

例如我的MyApp類定義,用到了MultiProviderConsumer2

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: <SingleChildCloneableWidget>[
        ChangeNotifierProvider.value(value: ThemeModel()),
        ChangeNotifierProvider.value(value: UserModel()),
        ChangeNotifierProvider.value(value: LocaleModel()),
      ],
      child: Consumer2<ThemeModel, LocaleModel>(
        builder: (BuildContext context, themeModel, localeModel, Widget child) {
          return MaterialApp(
            theme: ThemeData(
              brightness: Global.profile.darkMode ? Brightness.dark : Brightness.light,
              primarySwatch: themeModel.theme,
            ),
            onGenerateTitle: (context) {
              return DaLocalizations.of(context).title;
            },
            home: HomeRoute(),
            //應用主頁
            locale: localeModel.getLocale(),
            //我們只支援美國英語和中文簡體
            supportedLocales: [
              const Locale('zh', 'CN'), // 中文簡體
              const Locale('en', 'US'), // 美國英語
              //其它Locales
            ],
            localizationsDelegates: [
              // 本地化的代理類
              GlobalMaterialLocalizations.delegate,
              GlobalWidgetsLocalizations.delegate,
              // EasyRefresh的多語言支援
              GlobalEasyRefreshLocalizations.delegate,
              // 註冊我們的Delegate
              DaLocalizationsDelegate()
            ],
            localeResolutionCallback: (Locale _locale, Iterable<Locale> supportedLocales) {
              if (localeModel.getLocale() != null) {
                //如果已經選定語言,則不跟隨系統
                return localeModel.getLocale();
              } else {
                Locale locale;
                // APP語言跟隨系統語言,如果系統語言不是中文簡體或美國英語,
                // 則預設使用美國英語
                if (supportedLocales.contains(_locale)) {
                  locale = _locale;
                } else {
                  locale = Locale('en', 'US');
                }
                return locale;
              }
            },
          );
        },
      ),
    );
  }
}
複製程式碼

國際化支援

國際化就是多語言啦,用到了intl包。

在專案根目錄下建立資料夾i10n-arb,在lib/i10n裡建立localization_intl.dart

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'messages_all.dart';

class DaLocalizations {
  String get userNameOrPasswordWrong => null;

  static Future<DaLocalizations> load(Locale locale) {
    final String name = locale.countryCode.isEmpty ? locale.languageCode : locale.toString();
    final String localeName = Intl.canonicalizedLocale(name);
    //2
    return initializeMessages(localeName).then((b) {
      Intl.defaultLocale = localeName;
      return new DaLocalizations();
    });
  }

  static DaLocalizations of(BuildContext context) {
    return Localizations.of<DaLocalizations>(context, DaLocalizations);
  }
  String get auto => Intl.message('auto', name: 'auto', desc: 'set theme mode auto');
}

//Locale代理類
class DaLocalizationsDelegate extends LocalizationsDelegate<DaLocalizations> {
  const DaLocalizationsDelegate();

  //是否支援某個Local
  @override
  bool isSupported(Locale locale) => ['en', 'zh'].contains(locale.languageCode);

  // Flutter會呼叫此類載入相應的Locale資源類
  @override
  Future<DaLocalizations> load(Locale locale) {
    //3
    return DaLocalizations.load(locale);
  }

  // 當Localizations Widget重新build時,是否呼叫load重新載入Locale資源.
  @override
  bool shouldReload(DaLocalizationsDelegate old) => false;
}
複製程式碼

執行命令生成arb檔案:

flutter pub pub run intl_translation:extract_to_arb --output-dir=i10n-arb lib/i10n/localization_intl.dart
複製程式碼

之後會在i10n-arb資料夾下生成intl_messages.arb檔案,這個本質上是一個json檔案,我們還要為不同的語言版本建立對應的翻譯,比如本app支援中文和英文,那麼需要建立兩個檔案:intl_zh.arbintl_en.arb

intl_messages.arb檔案的內容分別複製到對應語言的翻譯檔案中,修改成對應語言的版本即可。

{
  "@@last_modified": "2019-12-17T17:04:43.001945",
  "title": "title",
  "@title": {
    "type": "text",
    "placeholders": {}
  },
}
複製程式碼

上面這些做完之後執行命令生成對應的類:

# 從arb檔案生成dart程式碼
flutter pub pub run intl_translation:generate_from_arb --output-dir=lib/i10n --no-use-deferred-loading lib/i10n/localization_intl.dart i10n-arb/intl_*.arb
複製程式碼

未完待續

原來有這麼多內容,限制於篇幅,我將在接下來的文章中繼續記錄~

歡迎交流

交流問題請在微信公眾號後臺留言,每一條資訊我都會回覆哈~

相關文章