返回文章列表文章索引阅读全文
STARBLOG / 阅读手记
Flutter

Behind Developing the Minimal Poetry App: Flutter Mobile Rapid Development Practice — State Management, Internationalization, Data Persistence, and Performance Optimization (Part 1)

Preface A note before we start: Although Flutter has Google behind it, it is still a young technology. The project is still in its early stages, updates very quickly, and there are more than a few problems. Using Flutter means you have to put up with all kinds of weird bugs and the headache of scarce Chinese documentation. This article is not trying to talk anyone into jumping in; it is simply a record of the development process of the “Minimal Poetry” app. The app is already on the store. If you’re interested, you can download it and give it a try: https://www.coolapk.com/apk/

曦远 (DealiAxy)约 29 分钟
目录7 个章节

Preface

A note before we start: Although Flutter has Google behind it, it is still a young technology. The project is still in its early stages, updates very quickly, and there are more than a few problems. Using Flutter means you have to put up with all kinds of weird bugs and the headache of scarce Chinese documentation. This article is not trying to talk anyone into jumping in; it is simply a record of the development process of the “Minimal Poetry” app.

The app is already on the store. If you’re interested, you can download it and give it a try: https://www.coolapk.com/apk/251155

Main UI screenshots:

HomeDark Mode HomePoetry CollectionBrowse Collection
Collection DetailsAuthor ListAuthor DetailsFont Selection

As with the Django rapid development practice article, this piece skips the fluff and goes straight to the steps.

Project File Structure

Design the project file structure first. Different projects have different needs, so design the structure according to your actual requirements. The following is my project structure, for reference only:

lib
├── common
├── i10n
├── models
├── routes
├── states
└── widgets
FolderPurpose
commonSome utility classes, such as general helper classes, network API classes, static classes that store global variables, etc.
i10nHolds internationalization-related code
modelsModel class files generated via json to models are all stored here
routesHolds all page code for the project
statesStores state classes that need to be shared across components in the app
widgetsHolds custom widgets

Define the models

In this project, I use json to models to auto-generate model classes. Why this? The reason is simple: it reduces the workload. Define the models used in the app with JSON; after generating the model classes you can conveniently serialize them to JSON for persistence, or read JSON data from config files and deserialize it into model objects. You can also generate model classes directly from JSON returned by backend APIs, which is very convenient.

Define models using JSON, as in the following example:

Create a json folder in the project root and add the JSON files to be converted. The content looks roughly like this. 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"
}

Add the dependencies:

dev_dependencies:
  json_model: ^0.0.2
  build_runner: ^1.0.0
  json_serializable: ^2.0.0

After that, run:

flutter packages pub run json_model

This will automatically generate the model classes under the lib/models folder.

One frustrating thing is that this json_model library only supports very old versions of build_runner and json_serializable, which then conflicts with the intl I need later. Every time I use these two libraries I have to keep commenting things out and switching dependency versions. Really annoying = =....

State Management

State management is the most important part of an app, and also the foundation for theme switching and internationalization later on. This article is a rapid development practice, so it won’t go too deep into Flutter state management. If you want to learn more, you can check out this Flutter tutorial written by an expert: https://book.flutterchina.club/chapter7/provider.html

I use the Provider component to manage app state. It is implemented based on InheritedWidget and is quite convenient to use.

First, add the dependency:

dependencies:
  provider: ^3.2.0

Under the lib/states folder, add models for shared state, for example:

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();
  }
}

These models inherit from ProfileChangeNotifier and can provide data or manage modifications and persistence of data.

In ordinary widgets you can use them directly to get or save data. Combined with the provider component, when the model data changes it can trigger widget updates~

For example, my MyApp class definition uses MultiProvider and 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;
              }
            },
          );
        },
      ),
    );
  }
}

Internationalization Support

Internationalization means multi-language support, using the intl package.

Create a folder i10n-arb in the project root, and create localization_intl.dart in lib/i10n:

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;
}

Run the command to generate arb files:

flutter pub pub run intl_translation:extract_to_arb --output-dir=i10n-arb lib/i10n/localization_intl.dart

After that, an intl_messages.arb file will be generated under the i10n-arb folder. This is essentially a json file. We also need to create corresponding translations for different language versions. For example, this app supports Chinese and English, so we need to create two files: intl_zh.arb and intl_en.arb.

Copy the contents of the intl_messages.arb file into the translation files for the corresponding languages respectively, and modify them into the corresponding language versions.

{
  "@@last_modified": "2019-12-17T17:04:43.001945",
  "title": "title",
  "@title": {
    "type": "text",
    "placeholders": {}
  },
}

After finishing the above, run the command to generate the corresponding classes:

# 从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

To Be Continued

Turns out there's so much content. Limited by length, I will continue documenting in the following articles~

Welcome to Connect

For questions, please leave a message on the WeChat official account backend. I'll reply to every message~

_EOF_

Behind Developing the Minimal Poetry App: Flutter Mobile Rapid Development Practice — State Management, Internationalization, Data Persistence, and Performance Optimization (Part 1)

本文作者:
程序设计实验室
版权声明:
本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

相关文章

从细节入手改善用户体验,Flutter跨平台App开发中设置Android和iOS的启动页

前言 flutter开发App方便快捷,就是冷启动的时候要加载dart引擎有短暂的白屏,严重影响用户体验,这个时候我们就需要设置一个启动图片,像国内大部分App那样(例如微信那个地球),启动App的时候显示这个图片,加载完再进入App,不至于影响用户体验。 Android启动页 首先要准备好启动图,本文命名为splash.png,按照不同的分辨率添加到对应的mipmap-hdpi / mipmap

2021-12-18 · Flutter · 约 1 分钟

有模有样解决Flutter里Webview无法访问HTTP页面的问题

探索过程 Android9(好像是吧)开始谷歌就默认不让开发者访问不安全HTTP内容了,如果非要用HTTP,那必须在networkSecurityConfig里配置cleartextTrafficPermitted才行。谷歌这个设计的出发点本是好的,HTTPS固然更安全咱都懂,但有时候这个后端接口你选择不了要HTTP还是HTTPS,比如要使用没有提供HTTPS的第三方服务,或者是本文的这种情况:在

2021-12-18 · Flutter · 约 1 分钟

讨论

共 0 条

良言一句三冬暖,恶语伤人六月寒。

加载评论中…