「極簡詩詞」app開發背後:Flutter移動應用快速構建實踐——狀態管理、國際化、數據持久化、性能優化(一)

前言

在開始以前的提示:雖然Flutter背靠Google這棵大樹,可是畢竟仍是一個年輕的技術,項目還處於初期階段,更新很是快,問題也不是通常的多,使用Flutter的話就意味着必須忍受各類奇怪的bug和沒有豐富中文資料的頭疼,本文不是安利同窗們入坑,只是對「極簡詩詞」app的開發過程進行記錄。html

另外app已經上架,有興趣的同窗能夠下載試試:www.coolapk.com/apk/251155json

主要界面截圖:bash

主頁 暗黑版主頁 詩集 詩集瀏覽
詩集詳情 做者列表 做者詳情 字體選擇

和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類,很是方便。app

使用json定義model,例子以下:less

在項目根目錄下建立json文件夾,添加要進行轉換的json文件,內容大概像這樣。 poem.jsonide

{
    "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
複製代碼

未完待續

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

歡迎交流

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

相關文章
相關標籤/搜索