“极简诗词”app开发背后:Flutter移动应用快速构建实践——状态管理、国际化、数据持久化、性能优化(一)
前言
在开始之前的提示:虽然Flutter背靠Google这棵大树,但是毕竟还是一个年轻的技术,项目还处于初期阶段,更新非常快,问题也不是一般的多,使用Flutter的话就意味着必须忍受各种奇怪的bug和没有丰富中文资料的头疼,本文不是安利同学们入坑,只是对“极简诗词”app的开发过程进行记录。
另外app已经上架,有兴趣的同学可以下载试试:https://www.coolapk.com/apk/251155
主要界面截图: | 主页 | 暗黑版主页 | 诗集 | 诗集浏览 | | ----- | ------ | ------ | ------ | | | | 诗集详情 | 作者列表 | 作者详情 | 字体选择 | |
和Django快速开发实践的文章一样,本文不讲废话,直接上步骤。
项目文件结构
先设计好项目文件结构,不同的项目有不同的需求,按照自己的实际需要来设计结构就好了,以下是我的项目结构,仅供参考:
lib
├── common
├── i10n
├── models
├── routes
├── states
└── widgets
定义好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_runner
和json_serializable
,这和我后面要用到的intl
就冲突了啊,每次用这两个库的时候我都要不断注释切换依赖的版本,真的麻烦 = =....
状态管理
状态管理是app中最重要的一部分,也是后面主题切换和国际化的基础。 本文是快速开发实践,不过多深入Flutter的状态管理,想了解的同学可以看看大佬写的Flutter教程:https://book.flutterchina.club/chapter7/provider.html
我是使用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
类定义,用到了MultiProvider
和Consumer2
:
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.arb
和intl_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
未完待续
原来有这么多内容,限制于篇幅,我将在接下来的文章中继续记录~
欢迎交流
交流问题请在微信公众号后台留言,每一条信息我都会回复哈~ - 微信公众号:画星星高手 - 打代码直播间:https://live.bilibili.com/11883038 - 知乎:https://www.zhihu.com/people/dealiaxy - 简书:https://www.jianshu.com/u/965b95853b9f