在Flutter应用程序中分离构建环境

英文地址:Separating build environments in Flutter apps

在开发专业的移动应用程度的时候,我们至少需要两个不同的环境:开发环境和生产环境,通过这种方式, 我们可以在开发环境中进行开发及测试新的特性,而不会意外地破坏用户生产环境的任何东西。

样本代码地址:separating_build_environments

最初的应用程序

lib/main.dart是flutter在新建一个项目的时候,为我们生成默认程序的入口文件,生成的模版代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import 'package:flutter/material.dart';

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Build flavors',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(),
);
}
}

lib/my_home_page.dart:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import 'package:flutter/material.dart';

class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Build flavors'),
),
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
// Every value is hardcoded in the app.
// No development or production variants exist yet.
new Text('This is the production app.'),
new Text('Backend API url is https://api.example.com/'),
],
),
),
);
}
}

my_home_page.dart文件是对默认地模版代码文件稍微做了一些修改。

将应用程序分为两个环境

我们当前的应用配置仅是生产环境的,没有方法为生产环境提供环境配置,让我们做些修改,使我们拥有两套不同的环境:developmentproducation

这里我们有几个地方想要根据当前的环境来拥有不同的行为:

  • 标题栏,生产环境标题是Build flavors DEV,开发环境是Build flavors
  • 第一个文本控件,开发环境显示文本是This is the development app,生产环境是the production app
  • 第二个文本控件,开发环境显示文本:Backend API url is https://dev-api.example.com/,生产环境是Backend API url is https://api.example.com/

我们需要创建一个用于包含所有特定于环境的配置信息的配置类,这是有意义的。

创建配置对象

新建app_config.dart文件,该文件将包含我们所有环境依赖的信息。

在我们这个例子中,将得到这样的结果,如下:

1
2
3
4
5
6
7
8
9
10
11
12
import 'package:meta/meta.dart';

class AppConfig {
AppConfig({
@required this.appName,
@required this.flavorName,
@required this.apiBaseUrl,
});
final String appName;
final String flavorName;
final String apiBaseUrl;
}

你可能正在询问该如何为我们的app提供这些配置信息?你们中的一些人可能早已知晓答案。InheritedWidget使从任务地方获取配置对象变得异常简单。

将AppConfig对象转变为Inherited Widget

为了能够使我们的AppConfig类能够成为Inherited Widget,我们将扩展InheritedWidget类,提供一个静态的of方法来获取该实例,最后重写updateShouldNotify方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import 'package:flutter/material.dart';
import 'package:meta/meta.dart';

class AppConfig extends InheritedWidget {
AppConfig({
@required this.appName,
@required this.flavorName,
@required this.apiBaseUrl,
@required Widget child,
}) : super(child:child);

final String appName;
final String flavorName;
final String apiBaseUrl;

static AppConfig of(BuildContext context) {
return context.inheritFromWidgetOfExactType(AppConfig);
}

@override
bool updateShouldNotify(InheritedWidget oldWidget) => false;
}

这里有几点需要注意:

  • 这个child参数将会是我们整个MaterialApp实例,我们将包裹我们整个应用在AppConfig对象中。
  • 我们创建了一个of静态方法,这个是InheritedWidget的一般约定,这个方法允许我们无论在哪需要,通过调用AppConfig.of(context)来获取特定环境的配置。
  • updateShouldNotify方法中,我只返回false。这个是因为AppConfig在我们创建之后将会不会变化。

接下,让我们为我们的两个环境创建这些文件。

为不同的环境创建启动文件

我们将会为每个环境创建所谓的启动文件,在我们这个例子中,我们仅有两个环境developmentproducation,这样 我将会新建两个文件main_dev.dartmain_prod.dart

在每个文件中,我将会用正确的配置数据来创建一个AppConfig类的实例,我们将传入MyApp新的实例到AppConfig控件中,这样我们应用中任何控件能够轻松地获取到这个配置的实例,然后调用runApp,这个我们整个应用程序的入口点。

lib/main_dev.dart:

1
2
3
4
5
6
7
8
9
10
11
12
13
import 'package:build_flavors/app_config.dart';
import 'package:build_flavors/main.dart';
import 'package:flutter/material.dart';

void main() {
var configuredApp = new AppConfig(
appName: 'Build Flavors DEV',
flavorName:'development',
apiBaseUrl:'https://dev-api.example.com/',
child: new MyApp(),
);
runApp(configuredApp);
}

lib/main_prod.dart:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import 'package:build_flavors/app_config.dart';
import 'package:build_flavors/main.dart';
import 'package:flutter/material.dart';

void main() {
var configuredApp = new AppConfig(
appName: 'Build flavors',
flavorName: 'production',
apiBaseUrl: 'https://api.example.com/',
child: new MyApp(),
);

runApp(configuredApp);
}

生产和开发环境的启动文件不同的仅是配置选项的值不同。

lib/main.dart:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import 'package:build_flavors/app_config.dart';
import 'package:build_flavors/my_home_page.dart';
import 'package:flutter/material.dart';

// We can remove this line here:
// void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Call AppConfig.of(context) anywhere to obtain the
// environment specific configuration
var config = AppConfig.of(context);

return new MaterialApp(
title: config.appName,
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(),
);
}
}

在这里,我们仅仅是获取应用的配置实例,并根据当前的环境正确地设置我们的MaterialApp的标题,移除main,因为在对应的启动文件已经包括这个入口函数。

因为我们的整个应用被包裹在了AppConfig控件中(扩展了InheritedWidget),通过调用AppConfig.of(context)就可以从任何地方来获取这个配置信息,这个在控件树更深地节点也是能有效的。

lib/my_home_page.dart:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import 'package:build_flavors/app_config.dart';
import 'package:flutter/material.dart';

class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
var config = AppConfig.of(context);

return new Scaffold(
appBar: new AppBar(
title: new Text(config.appName),
),
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text('This is the ${config.flavorName} app.'),
new Text('Backend API url is ${config.apiBaseUrl}'),
],
),
),
);
}
}

在不同的环境中运行应用程序

我们可以flutter run命令配合--target-t(缩写)参数来指定。

在我们这个例子中:

  • 运行开发环境的构建,调用flutter run -t lib/main_dev.dart
  • 运行生产环境的构建,调用flutter run -t lib/main_prod.dart

为了创建基于Andriod的发布构建,我可以运行flutter build apk -t lib/main_<environment>.dart,并将根据我们环境得到正确的apk,若是iOS上的发布构建,只需将apk替换成ios