一、前言
今天学习插件开发,Flutter
使用一个灵活的系统,允许调用特定平台(iOS/Android)的API,无论在Android
上的Java
或者Kotlin
代码中,还是iOS
上的Object-C
或者Swift
代码中均可使用。Flutter
平台特定的API
支持不依赖于代码生成,而是依赖于灵活的消息传递方式:
- 应用的
Flutter
部分通过平台通道(platform channel)将消息发送到应用程序得所在宿主(iOS或Android)。 - 宿主监听的平台通道,并接受该消息,然后它会调用特定于该平台的API(使用原生编程语言)-并响应发送客户端(即应用程序的
Flutter
部分)。
二、插件实例
1.插件的基本原理
要使用和创建一个Flutter
插件,得要首先知道平台通道在客户端(Flutter UI)和宿主(平台)之间传递消息,用官方的图,下图:
MethodChannel
在
Flutter客户端
和
主机(iOS/Android)
之间传递消息,消息和响应都是异步传递的,这样确保用户界面(UI)保持响应,在
Flutter客户端
,
Flutter
通过
MethodChannel
类发送与方法调用相对应的消息。在平台上,
Android
上通过
MethodChannel
类接收方法调用并发送结果,
iOS
上则可以通过
FlutterMethodChannel
类接收方法调用并发送结果。这些类允许开发者开发一个平台插件,在上图可以发现,箭头是双向的,也就是方法调用也可以朝反方向发送,简而言之:可以从
Flutter
调用
Android/iOS
的代码,也可以从
Android/iOS
调用
Flutter
。标准平台通道使用的是标准消息解码器,支持简单高效的将
JSON
格式的值二进制序列化,如布尔值、数字、字符串、字节缓冲区以及这些数据的列表和映射,发送和接收值会自动对这些值进行序列化和反序列化,下面表格列出展示平台端如何接收
Dart
,反过来也是一样。
Dart | Android | iOS |
---|---|---|
null | null | nil(NSNull when nested) |
bool | java.lang.Boolean | NSNumber numberWithBool: |
int | java.lang.Integer | NSNumber numberWithInt: |
int, if 32 bits not enough | java.lang.Long | NSNumber numberWithLong: |
int, if 64 bits not enough | java.math.BigInteger | FlutterStandardBigInteger |
double | java.lang.Double | NSNumber numberWithDouble: |
String | java.lang.String | NSString |
Uint8List | byte[] | FlutterStandardTypedData typedDataWithBytes: |
Int32List | int[] | FlutterStandardTypedData typedDataWithInt32: |
Int64List | long[] | FlutterStandardTypedData typedDataWithInt64: |
Float64List | double[] | FlutterStandardTypedData typedDataWithFloat64: |
List | java.util.ArrayList | NSArray |
Map | java.util.HashMap | NSDictionary |
2.简单例子1-返回数值
了解原理,下面简单实现平台和客户端传递数据的Flutter
平台插件。
2.1.Flutter平台客户端
首先,需要创建Flutter
平台客户端,构建通道,使用具有基本传递数据功能的单平台方法MethodChannel
,通道的客户端和宿主通过通道构造函数中传递的通道名称进行连接,单个应用中使用的所有通道名称必须是唯一的,官方建议是通道名称前加一个唯一的“域名前缀”,例如samoles.flutter.io/battery
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class _PluginTestState extends State<PluginTest> {
//创建通道名称 必须唯一
static const platform = const MethodChannel('sample.flutter.io/data');
}
复制代码
下面在MethodChannel
上调用一个方法,指定通过String
标识符data
调用的具体方法。如果当前平台不支持API
那么调用会失败,因此需要将invokeMethod
调用包含在try-catch
语句中,返回的数值来更新_data
:
class _PluginTestState extends State<PluginTest> {
//创建通道名称 必须唯一
static const platform = const MethodChannel('sample.flutter.io/data');
String _data;
Future<Null> _returndata() async{
String data;
try{
//1.invokeMethod('xxxx') xxx可以自己命名
final int resultData = await platform.invokeMethod('data');
data = "平台返回数值:$resultData";
}catch(e){
data = "错误:${e.message}";
}
//状态更新
setState(() {
_data = data;
});
}
}
复制代码
主界面添加一个返回数值的文本,和一个浮动按钮:
class _PluginTestState extends State<PluginTest> {
@override
Widget build(BuildContext context) {
return new Scaffold(
//appBar
appBar: AppBar(
title: Text("插件例子"),
//标题居中
centerTitle: true,
),
body:new Center(
child: Text("$_data"),
),
floatingActionButton : FloatingActionButton(
onPressed: _returndata,
tooltip: "获取平台返回的值",
child: new Icon(Icons.audiotrack)
),
);
}
}
复制代码
2.2.使用Java添加Android平台特定的实现
首先在Android Studio打开Flutter
应用的Android
部分:
- Android Studio 选择
File > Open
- 定位到自己的项目根目录,然后选择里面的
android文件夹
,点击OK 如下:
java
目录下打开
MainActivity.java
,我打开项目编译报错,没管。 下面,在
onCreate
里创建MethodChannel并设置一个
MethodCallHandler
。确保使用在
Flutter客户端
使用的通道名称相同:
public class MainActivity extends FlutterActivity {
//1.通道名称
private static final String CHANNEL = "sample.flutter.io/data";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//2.创建MethodChannel 并且设置MethodCallHandler
new MethodChannel(getFlutterView(), CHANNEL).setMethodCallHandler(new MethodChannel.MethodCallHandler(){
@Override
public void onMethodCall(MethodCall call, MethodChannel.Result result){
}
});
GeneratedPluginRegistrant.registerWith(this);
}
}
复制代码
编写Java代码,用于调用Android
上的随机函数,和在Android
项目上编写代码完全一样,在MainActivity
方法添加下面方法:
//返回特定的数值
private int getData() {
return 7;
}
复制代码
最后,在完成之前添加的onMethodCall
方法后,还需要处理一个平台方法data
,所以需要在call
参数中测试它,这个方法里面的逻辑只是调用getData
这个方法,并使用response
参数返回成功和错误情况的响应,如果调用未知的方法,会报告错误信息:
new MethodChannel(getFlutterView(), CHANNEL).setMethodCallHandler(new MethodChannel.MethodCallHandler() {
@Override
public void onMethodCall(MethodCall call, MethodChannel.Result result) {
//3.处理一个平台方法data 和在平台上invokeMethod(xxxx)对应
if (call.method.equals("data")) {
int data = getData();
result.success(data);
} else {
result.notImplemented();
}
}
});
复制代码
现在就可以运行这应用程序,点击按钮,就能获取Android
主机返回的数值7
,效果图如下:
2.3.使用Object-C添加iOS平台特定的实现
首先打开Xcode
中Flutter
应用程序得iOS部分:
- 启动Xcode
- 选择
File > Open...
- 定位到
Flutter app
目录,然后选择里面的ios
文件夹,点击OK - 确保Xcode项目的构建没有错误
- 选择
Runner > Runner
,然后打开AppDelegate.swift
接下来,覆盖application
方法创建一个FlutterMethodChannel
并在里面添加一个aoolication didFinishLaunchingWithOptions:
方法,这里需要确保和Flutter客户端使用的是同一个通道名称
:
#include "AppDelegate.h"
#include "GeneratedPluginRegistrant.h"
#import <Flutter/Flutter.h>
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[GeneratedPluginRegistrant registerWithRegistry:self];
// Override point for customization after application launch.
FlutterViewController* controller = (FlutterViewController*)self.window.rootViewController;
//methodChannelWithName:xxx xxx要和flutter平台定义的通道m名称一样
FlutterMethodChannel* batteryChannel = [FlutterMethodChannel
methodChannelWithName:@"sample.flutter.io/data"
binaryMessenger:controller];
[batteryChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
// TODO
}];
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
@end
复制代码
下面,使用Object-C
代码添加获取具体数值的方法,这个方法在iOS
应用程序写的代码一样,在AppDelegate
类添加getData
方法:
//返回时整形
- (int)getData{
return 7;
}
@end
复制代码
最后,在完成之前添加的setMethodCallHandler
方法之后,还需要处理一个平台方法getData
,所以要在call
参数中测试,该平台方法的实现只需调用上一步编写的iOS
代码,并使用response
参数返回成功和错误情况的响应,如果调用一个未知的方法,会报告信息。
[batteryChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
// TODO
//在这里处理逻辑
if([@"data" isEqualToString:call.method]){
int data = [self getData];
result (@(data));
} else {
result(FlutterMethodNotImplemented);
}
}];
复制代码
现在运行在iOS
上,看看效果:
3.简单例子2-返回当前电池电量
3.1.创建Flutter平台客户端
static const platform = const MethodChannel('samples.flutter.io/battery');
//电池电量
String _batteryLevel = 'Unknown battery level.';
Future<Null> _getBatteryLevel() async {
String batteryLevel;
try{
final int result = await platform.invokeMethod('getBatteryLevel');
batteryLevel = "Battery level at $result%.";
} on PlatformException catch (e){
batteryLevel = "Failed to get battery level: '${e.message}'.";
}
//状态更新
setState((){
_batteryLevel = batteryLevel;
});
}
复制代码
3.2.使用Java添加Android平台特定实现
其实不用直接再打开一个Android stdui
,在本项目直接打开MainActivity
修改即可:
import android.os.Bundle;
import java.util.Random;
import io.flutter.app.FlutterActivity;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugins.GeneratedPluginRegistrant;
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.BatteryManager;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
public class MainActivity extends FlutterActivity {
//1.通道名称
private static final String CHANNEL = "samples.flutter.io/battery";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//2.创建MethodChannel 并且设置MethodCallHandler
new MethodChannel(getFlutterView(), CHANNEL).setMethodCallHandler(new MethodChannel.MethodCallHandler() {
@Override
public void onMethodCall(MethodCall call, MethodChannel.Result result) {
//3.处理一个平台方法data 和在平台上invokeMethod(xxxx)对应
if (call.method.equals("getBatteryLevel")) {
int batteryLevel = getBatteryLevel();
if(batteryLevel != -1){
result.success(batteryLevel);
}else{
result.error("UNAVAILABLE", "Battery level not available.", null);
}
} else {
result.notImplemented();
}
}
});
GeneratedPluginRegistrant.registerWith(this);
}
//返回电量
private int getBatteryLevel() {
int batteryLevel = -1;
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
BatteryManager batteryManager = (BatteryManager) getSystemService(BATTERY_SERVICE);
batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY);
} else {
Intent intent = new ContextWrapper(getApplicationContext()).
registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
batteryLevel = (intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100) /
intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
}
return batteryLevel;
}
}
复制代码
效果就是点击右下角按钮,界面中央会显示当前手机电池电量,iOS
也是一样的流程,就不贴代码了,效果如下:
三、Packages
用过Flutter
的开发者都知道,Flutter
的库是以包(package)的方式来管理,使用package
可以创建可轻松共享的模块化代码。一个最小的package
包括:
- 一个
pubspec.yaml
文件:声明了package
的名称、版本、作者等的元数据文件 - 一个
lib
文件夹:包括包中公开的(public)代码,最少应有一个<package-name>.dart
文件
1.Package类型
Packages可以包含多种内容:
- Dart包(library package):其中包含一些
Flutter
特定功能,因此对Flutter
框架具有依赖性,仅将用于Flutter
,例如Fluro
包,也就是我们平常说的Flutter
包。 - 插件包(plugin package):当我们说
Flutter
插件的时候就是指这,一种专用的Dart
包,其中包含用Dart
代码编写的API,以及针对Android
(使用Java或者Kotlin)和/或针对iOS
(使用Object-C或者Swift)平台的特定实现,一个具体例子就是battery
插件包。
2.包的使用
我们在平时中经常使用库,流程是在pubspec.yaml
里声明一个依赖:
path_provider: ^0.4.1
cached_network_image: ^0.5.0+1
复制代码
这里简单说明一下,之前没有讲解,后来查了一下,^x.x.x
这个是库对应的版本号,^0.4.1
表示和0.4.1
版本兼容,也可以指定特定的版本:
0.4.1:特定的版本
any:任意版本
<0.4.1:小于0.4.4的版本
>0.4.1:大于0.4.1的版本
<=0.4.1:小于等于0.4.1的版本
>=0.4.1:大于等于0.4.1的版本
>=0.4.1<=0.5.0:在0.4.1和0.5.0版本之间(包含0.4.1和0.5.0),也可以用<,>
当添加依赖,使用时把相关的包导入就可使用,就好像导入dio
库:
import 'package:dio/dio.dart';
复制代码
就可以使用它里面提供的API:
dio_get() async{
try{
Response response;
response = await Dio().get("http://gank.io/api/data/福利/10/1");
if(response.statusCode == 200){
print(response);
}else{
print("error");
}
}catch(e){
print(e);
}
}
复制代码
3.开发插件包(plugin package)
下面就简单实现一个Toast
的插件包:
- 选择
Flie > New > New FLutter Project
- 在目录面板中选择第二个
Flutter Plugin
,点击next
,Android stdio 会有显示Select "plugin" when exposing an Android or iOS API for develops
Project name
填写knight_toast_plugin
,这个名字随意,但是要防止和pub
上的库名字冲突
看看项目的目录:
主要看四个目录就可以了:- android:插件包API的
Android
端实现 - example:一个依赖该插件的Flutter应用程序,来说明如何使用它
- ios:插件包API的
iOS
端实现 - lib:
Dart
包的API,插件的客户端会使用这里实现的接口
项目创建就是一个完整的简单插件例子,这个例子是实现了platformVersion
。把android
目录打开:
/** KnightToastPlugin */
public class KnightToastPlugin implements MethodCallHandler {
/** Plugin registration. */
public static void registerWith(Registrar registrar) {
final MethodChannel channel = new MethodChannel(registrar.messenger(), "knight_toast_plugin");
channel.setMethodCallHandler(new KnightToastPlugin());
}
@Override
public void onMethodCall(MethodCall call, Result result) {
if (call.method.equals("getPlatformVersion")) {
result.success("Android " + android.os.Build.VERSION.RELEASE);
} else {
result.notImplemented();
}
}
}
复制代码
发现和一开始使用平台通道编写平台特定的代码很像,从上面知道knightToastPlugin
这个插件实现了MethodCallHandler
,先看看这个MethodCallHandler
接口:
//返回结果接口
public interface Result {
//成功
void success(@Nullable Object var1);
//失败
void error(String var1, @Nullable String var2, @Nullable Object var3);
//没有实现接口时回调 通常是调用了未知的方方法
void notImplemented();
}
//处理本地方法的请求接口
public interface MethodCallHandler {
void onMethodCall(MethodCall var1, MethodChannel.Result var2);
}
复制代码
反正实现一个插件时需要实现这个接口,下面实现弹出吐司这个功能:
3.1.实现MethodCallHandler接口
public class KnightToastPlugin implements MethodCallHandler{
//插件注册
public static void registerWith(Registrar registrar){
//samples.flutter/knight_toast_plugin 这是Method channel的名字 上面是有说过,这里并且添加了域名,为了防止冲突
final MethodChannel channel = new MethodChannel(registrar.messenger(), "samples.flutter/knight_toast_plugin");
channel.setMethodCallHandler(new KnightToastPlugin());
}
@Override
public void onMethodCall(MethodCall methodCall, Result result) {
}
}
复制代码
因为使用过Toast
都知道,Android
需要一个上下文环境(Context),把Context
参数加上:
private Context mContext;
public KnightToastPlugin(Context mContext){
this.mContext = mContext;
}
//插件注册
public static void registerWith(Registrar registrar){
....
//从Registrar获得context
channel.setMethodCallHandler(new KnightToastPlugin(registrar.context()));
}
复制代码
3.2.完善onMethodCall方法
@Override
public void onMethodCall(MethodCall methodCall, Result result) {
//首先判断方法名是否为"showToast"
if(methodCall.method.equals("showToast")){
//因为调用原生,只能传递一个参数,如果想要传递多个,那就放在map里,用map传递
//用MethodCall.argument("xxxx")来取值
//显示内容
String message = methodCall.argument("message");
//时间为short 还是 long
String duration = methodCall.argument("duration");
//调用原生弹出吐司
Toast.makeText(mContext,message,duration.equals("length_short") ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG).show;
//成功
result.success(true);
} else {
//没这个方法
result.notImplemented();
}
}
复制代码
3.3.Flutter客户端
在FLutter客户端
需要做有两步:
- 生成一个
MethodChannel
,例子已经帮生成了。 - 通过这个
MethodChannel
调用showToast
方法。
import 'dart:async';
import 'package:flutter/services.dart';
enum Duration{
length_short,
length_long
}
class KnightToastPlugin {
//这里要和你在android目录下写的插件通道要对应 new MethodChannel(registrar.messenger(), "samples.flutter/knight_toast_plugin");
static const MethodChannel _channel =
const MethodChannel('samples.flutter/knight_toast_plugin');
// 不需要自带的例子
// static Future<String> get platformVersion async {
// final String version = await _channel.invokeMethod('getPlatformVersion');
// return version;
// }
static Future<bool> showToast(String message,Duration duration) async{
//参数封装
var argument = {'message':message,'duration':duration.toString()};
//这个方法是异步调用 "showToast"对应在上面所写的原生代码的methodCall.method.equals("showToast")
var success = await _channel.invokeMethod('showToast',argument);
return success;
}
}
复制代码
3.4.使用插件
把example > lib
目录下的main.dart修改
如下:
import 'package:flutter/material.dart';
import 'package:knight_toast_plugin/knight_toast_plugin.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
_showToast(){
KnightToastPlugin.showToast("吐司出来~", Duration.length_short);
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Plugin example app'),
),
body: Center(
child: Text('吐司例子'),
),
floatingActionButton : FloatingActionButton(
onPressed: _showToast,
tooltip: "可以弹出toast",
child: new Icon(Icons.audiotrack)
),
),
);
}
}
复制代码
效果如下:
3.5.发布插件
插件功能做出来,下面就等发布了,下面把插件发布到pub.dartlang.org上,发布需要科学上网。。,检查pubspec.yaml
,这里需要补一下基本信息:
name: knight_toast_plugin ->插件名字
description: toast_plugin ->插件描述
version: 0.0.1 ->插件版本
author: 15015706912@163.com ->作者
homepage: https://github.com/KnightAndroid ->主页
复制代码
建议将下面文档添加到插件包:
README.md
:结束插件的文件CHANGELOG
:记录每个版本中的更改LICENSE
:包含插件许可条款的文件
flutter packages pub publish --dry-run
复制代码
如果显示包太大,就把build
、.idea
删除,并且把一些警告解决,最后输出:
Package has 0 warnings.
复制代码
下面就可以真正发布插件了,命令如下:
flutter packages pub publish
复制代码
会提示验证Google
账号,授权后就可以继续上传,但是这边我已经授权了,还是卡住:
Looks great! Are you ready to upload your package (y/n)? y
Pub needs your authorization to upload packages on your behalf.
In a web browser, go to https://accounts.google.com/o/oauth2/auth?access_type=offline&approval_prompt=force&response_type=code&client_id=818368855108-8grd2eg9tj9f38os6f1urbcvsq399u8n.apps.googleusercontent.com&redirect_uri=http%3A%2F%2Flocalhost%3A53663&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email
Then click "Allow access".
Waiting for your authorization...
Authorization received, processing...
复制代码
应该是pub
服务器访问不了Google
:
It looks like accounts.google.com is having some trouble.
Pub will wait for a while before trying to connect again.
OS Error: Operation timed out, errno = 60, address = accounts.google.com, port = 53165
复制代码
当成功发布能在pub.dartlang.org/packages上找到自己的插件包。
四、使用字体
有时候要在Flutter
应用程序中使用不同的字体,就好像会使用UI
创建的自定义字体,或者可能会使用Google Flonts
中的字体。在Flutter
应用程序中使用字体分两步完成:
- 在
pubspec.yaml
中声明它们,以确保它们包含在应用程序中 - 通过
TextStyle
属性使用字体
1.在pubsec.yaml声明字体
name: my_application
description: A new Flutter project.
dependencies:
flutter:
sdk: flutter
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
fonts:
- family: NotoSns
fonts:
# https://fonts.google.com/specimen/Noto+Sans+TC -->对应字体下载地址 这里可以不填 只是注释
- asset: fonts/NotoSansTC-Black.otf
- family: Sriaskdi
fonts:
# https://fonts.google.com/specimen/Srisakdi
- asset: fonts/Srisakdi-Regular.ttf
- family: NotoSerifTC
fonts:
# https://fonts.google.com/specimen/Noto+Serif+TC
- asset: fonts/NotoSerifTC-Black.ttf
复制代码
上面格式不能错一点,否则会编译不通过,上面还添加了对应字体的下载地址。把下载好的字体文件放到fonts
下:
family
是字体的名称,可以在
TextStyle
的
fontFamily
属性中使用,
asset
是相对于
pubspec.yaml
文件的路径,这些文件包含字体中字形的轮廓,在构建应用程序时,这些文件会包含在应用程序的asset包中。 可以给字体设置粗细、倾斜等样式
weight
属性指定字体的粗细,取值范围是100到900之间的整百数(100d的倍数),这些值对应FontWeight
,可以用于TextStyle
的fontWeight
属性style
指定字体是倾斜还是正常,对应的值为italic
和normal
,这些值对应fontStyle
可以用于TextStyle
的fontStyle
的TextStyle
属性。
具体代码:
import 'package:flutter/material.dart';
//显示的内容
const String words1 = "Almost before we knew it, we had left the ground.";
const String words2 = "A shining crescent far beneath the flying vessel.";
const String words3 = "A red flair silhouetted the jagged edge of a wing.";
const String words4 = "Mist enveloped the ship three hours out from port.";
void main() {
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Fonts',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new FontsPage(),
);
}
}
class FontsPage extends StatefulWidget {
@override
_FontsPageState createState() => new _FontsPageState();
}
class _FontsPageState extends State<FontsPage> {
@override
Widget build(BuildContext context) {
// https://fonts.google.com/specimen/Noto+Sans+TC
var NotoSnsContainer = new Container(
child: new Column(
children: <Widget>[
new Text(
"NotoSns",
),
new Text(
words2,
textAlign: TextAlign.center,
style: new TextStyle(
fontFamily: "NotoSns",-->务必和pubspec.yaml定义的标识对应
fontSize: 17.0,
),
),
],
),
margin: const EdgeInsets.all(10.0),
padding: const EdgeInsets.all(10.0),
decoration: new BoxDecoration(
color: Colors.grey.shade200,
borderRadius: new BorderRadius.all(new Radius.circular(5.0)),
),
);
// https://fonts.google.com/specimen/Noto+Serif+TC
var NotoSerifTCContainer = new Container(
child: new Column(
children: <Widget>[
new Text(
"NotoSerifTC",
),
new Text(
words3,
textAlign: TextAlign.center,
style: new TextStyle(
fontFamily: "NotoSerifTC",
fontSize: 25.0,
),
),
],
),
margin: const EdgeInsets.all(10.0),
padding: const EdgeInsets.all(10.0),
decoration: new BoxDecoration(
color: Colors.grey.shade200,
borderRadius: new BorderRadius.all(new Radius.circular(5.0)),
),
);
// https://fonts.google.com/specimen/Srisakdi
var SriaskdiContainer = new Container(
child: new Column(
children: <Widget>[
new Text(
"Sriaskdi",
),
new Text(
words4,
textAlign: TextAlign.center,
style: new TextStyle(
fontFamily: "Sriaskdi",
fontSize: 25.0,
),
),
],
),
margin: const EdgeInsets.all(10.0),
padding: const EdgeInsets.all(10.0),
decoration: new BoxDecoration(
color: Colors.grey.shade200,
borderRadius: new BorderRadius.all(new Radius.circular(5.0)),
),
);
// Material Icons font - included with Material Design
String icons = "";
// https://material.io/icons/#ic_accessible
// accessible:  or 0xE914 or E914
icons += "\u{E914}";
// https://material.io/icons/#ic_error
// error:  or 0xE000 or E000
icons += "\u{E000}";
// https://material.io/icons/#ic_fingerprint
// fingerprint:  or 0xE90D or E90D
icons += "\u{E90D}";
// https://material.io/icons/#ic_camera
// camera:  or 0xE3AF or E3AF
icons += "\u{E3AF}";
// https://material.io/icons/#ic_palette
// palette:  or 0xE40A or E40A
icons += "\u{E40A}";
// https://material.io/icons/#ic_tag_faces
// tag faces:  or 0xE420 or E420
icons += "\u{E420}";
// https://material.io/icons/#ic_directions_bike
// directions bike:  or 0xE52F or E52F
icons += "\u{E52F}";
// https://material.io/icons/#ic_airline_seat_recline_extra
// airline seat recline extra:  or 0xE636 or E636
icons += "\u{E636}";
// https://material.io/icons/#ic_beach_access
// beach access:  or 0xEB3E or EB3E
icons += "\u{EB3E}";
// https://material.io/icons/#ic_public
// public:  or 0xE80B or E80B
icons += "\u{E80B}";
// https://material.io/icons/#ic_star
// star:  or 0xE838 or E838
icons += "\u{E838}";
var materialIconsContainer = new Container(
child: new Column(
children: <Widget>[
new Text(
"Material Icons",
),
new Text(
icons,
textAlign: TextAlign.center,
style: new TextStyle(
inherit: false,
fontFamily: "MaterialIcons",
color: Colors.black,
fontStyle: FontStyle.normal,
fontSize: 25.0,
),
),
],
),
margin: const EdgeInsets.all(10.0),
padding: const EdgeInsets.all(10.0),
decoration: new BoxDecoration(
color: Colors.grey.shade200,
borderRadius: new BorderRadius.all(new Radius.circular(5.0)),
),
);
return new Scaffold(
appBar: new AppBar(
title: new Text("Fonts"),
),
body: new ListView(
//主界面
children: <Widget>[
//字体样式一
NotoSnsContainer,
//字体样式二
NotoSerifTCContainer,
//字体样式三
SriaskdiContainer,
//material图标
materialIconsContainer,
],
),
);
}
}
复制代码
效果如下:
五、国际化
1.跟随手机系统语言
一个app中使用国际化已经很普遍的操作了,如果应用可能会给另一种语言的用户(美国,英国)使用,他们看不懂中文,那这时候就要提供国际化功能,使应用的语言切到英文环境下。下面举个弹出日期控件例子:
//弹出时间框
void _showTimeDialog(){
//DatePacker 是flutter自带的日期组件
showDatePicker(
context: context,//上下文
initialDate: new DateTime.now(),//初始今天
firstDate: new DateTime.now().subtract(new Duration(days: 30)),//日期范围,什么时候开始(距离今天前30天)
lastDate: new DateTime.now().add(new Duration(days: 30)),//日期范围 结束时间,什么时候结束(距离今天后30天)
).then((DateTime val){
print(val);
}).catchError((e){
print(e);
});
}
复制代码
系统默认的语言环境是中文,但是实际运行的显示文字是英文的,效果如下:
下面一步一实现组件国际化:1.1.添加依赖flutter_localizations
在默认情况下,Flutter仅提供美国英语本地化,就是默认不支持多语言,即使用户在中文环境下,显示的文字仍然是英文。要添加对其他语言的支持,应用必须制定其他MaterialApp属性,并在pubspec.yaml
下添加依赖:
dependencies:
flutter:
sdk: flutter
flutter_localizations: ----->添加,这个软件包可以支持接近20种语言
sdk: flutter -----》添加
复制代码
记得运行点击右上角的Packages get
或者直接运行flutter packages get
1.2.添加localizationsDelegates和supportedLocales
在MaterialApp
里指定(添加)localizationsDelegates和supportedLocales,如下:
import 'package:flutter_localizations/flutter_localizations.dart';--->记得导库
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
//添加-----
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: [
const Locale('en','US'), //英文
const Locale('zh','CH'), //中文
],
//--------添加结束
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
复制代码
然后重新运行,效果如下:
,发现了确实变成中文了,系统语言中文下会显示中文,系统语言下英文下会显示英文,但是这里也发现两个问题:- 3月21日周四高度太高了,溢出,到时候要看源码来解决了,实在不行后面自己写个组件。
- Titlebar也就是
Flutter Demo Home Page
没有变成中文,这里可以想的到,因为框架不知道翻译这句话。
1.3.多国语言资源整合
那下面来实现多语言,需要用到GlobalMaterialLocalizations
,首先要准备在应用中用到的字符串,针对上述例子,用到了下面这个字符串:
- Flutter Demo Home Page
- Increment
下面只增加中文类型的切换,那么上面的英文依次对应:
- Flutter 例子主页面
- 增加 下面为应用的本地资源定义一个类,将所有这些放在一起用于国际化应用程序通常从封装应用程序本地化值的类开始,下面
DemoLocalizations
这个类包含程序的字符串,该字符串被翻译应用程序所支持的语言环境:
//DemoLocalizations类 用于语言资源整合
class DemoLocalizations{
final Locale locale;//该Locale类是用来识别用户的语言环境
DemoLocalizations(this.locale);
//根据不同locale.languageCode 加载不同语言对应
static Map<String,Map<String,String>> localizedValues = {
//中文配置
'zh':{
'titlebar_title':'Flutter 例子主页面',
'increment':'增加'
},
//英文配置
'en':{
'titlebar_title':'Flutter Demo Home Page',
'increment':'Increment'
}
};
//返回标题
get titlebarTitle{
return localizedValues[locale.languageCode]['titlebar_title'];
}
//返回增加
get increment{
return localizedValues[locale.languageCode]['increment'];
}
}
复制代码
当拿到Localizations实例对象,就可以调用titlebarTitle
、increment
方法来获取对应的字符串。
1.4.实现LocalizationsDelegate类
当定义完DemoLocalizations类后,下面就是要初始化,初始化是交给LocalizationsDelegate
这个类,而这个类是抽象类,需要实现:
//这个类用来初始化DemoLocalizations对象
//DemoLocalizationsDelegate略有不同。它的load方法返回一个SynchronousFuture, 因为不需要进行异步加载。
class DemoLocalizationsDelegate extends LocalizationsDelegate<DemoLocalizations>{
const DemoLocalizationsDelegate();
@override
bool isSupported(Locale locale) {
return ['en','zh'].contains(locale.languageCode);
}
//DemoLocalizations就是在此方法内被初始化的。
//通过方法的 locale 参数,判断需要加载的语言,然后返回自定义好多语言实现类DemoLocalizations
//最后通过静态 delegate 对外提供 LocalizationsDelegate。
@override
Future<Localizations> load(Locale locale) {
return new SynchronousFuture<DemoLocalizations>(new DemoLocalizations(locale));
}
@override
bool shouldReload(LocalizationsDelegate<DemoLocalizations> old) {
return false;
}
static LocalizationsDelegate delegate = const DemoLocalizationsDelegate();
}
复制代码
1.5.添加DemoLocalizationsDelegate 添加进 MaterialApp
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
DemoLocalizationsDelegate.delegate,//添加
],
supportedLocales: [
const Locale('en','US'), //英文
const Locale('zh','CH'), //中文
],
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
复制代码
1.6.设置Localizations widget
那下面怎么使用DemoLocalizations
呢,这时候就要用到Localizations
,Localizations
用于加载和查找包含本地化值的集合的对象,应用程序通过Localizations.of(context,type)
来引用这些对象,如果区域设备的区域设置发生更改,则Localizations
这个组件会自动加载新区域设置的值,然后重新构建使用它们的widget
。DemoLocalizationsDelegate
这个类的对象虽然被传入了 MaterialApp
,但由于 MaterialApp 会在内部嵌套Localizations
,而上面LocalizationsDelegates
是构造函数的参数:
Localizations({
Key key,
@required this.locale,
@required this.delegates,//需要传入LocalizationsDelegates
this.child,
}) : assert(locale != null),
assert(delegates != null),
assert(delegates.any(
(LocalizationsDelegate<dynamic> delegate)//构造DemoLocalizations实例
=> delegate is LocalizationsDelegate<WidgetsLocalizations>)
),
super(key: key);
复制代码
通过上面可以知道,要使用DemoLocalizations
需要通过Localizations
中的LocalizationsDelegate
实例化,应用中要使用DemoLocalizations
就要通过Localizations
来获取:
Localizations.of(context, DemoLocalizations);
复制代码
将上面的代码放进DemoLocalizations
中:
....
//返回标题
get titlebarTitle{
return localizedValues[locale.languageCode]['titlebar_title'];
}
//返回增加
get increment{
return localizedValues[locale.languageCode]['increment'];
}
//加入这个静态方法,方法返回DemoLocalizations实例
static DemoLocalizations of(BuildContext context){
return Localizations.of(context, DemoLocalizations);
}
复制代码
下面就要使用DemoLocalizations
了,把代码字符串换成如下:
home: MyHomePage(title: DemoLocalizations.of(context).titlebarTitle),//这里需要更改
...
tooltip: DemoLocalizations.of(context).increment,//这里需要替换
复制代码
替换完,运行看看效果:
报空指针异常:NoSuchMethodError:The getter 'titlebarTitle' was called on null,也就是没有拿到DemoLocalizations
对象,问题肯定出在Localizations.of
,进去源码:
static T of<T>(BuildContext context, Type type) {
assert(context != null);
assert(type != null);
final _LocalizationsScope scope = context.inheritFromWidgetOfExactType(_LocalizationsScope);
return scope?.localizationsState?.resourcesFor<T>(type);
}
复制代码
注意看context.inheritFromWidgetOfExactType(_LocalizationsScope);这一行代码,继续点进去看:
InheritedWidget inheritFromWidgetOfExactType(Type targetType, { Object aspect });
,然后到这里再查_LocalizationsScope
对象的类型:
//继承InheritedWidget
class _LocalizationsScope extends InheritedWidget {
const _LocalizationsScope ({
Key key,
@required this.locale,
@required this.localizationsState,
@required this.typeTo
....
复制代码
那报错的信息很明显了:也就是找不到
_LocalizationsScope
,调用
titlebarTitle
的方法的context是最外层build方法传入的,而在之前说过 Localizations 这个组件是在 MaterialApp 中被嵌套的,也就是说能找到 DemoLocalizations 的 context 至少需要是 MaterialApp 内部的,而此时的 context 是无法找到 DemoLocalizations 对象的。那下面就简单了,去掉
MyHomePage
构造方法和把
title
去掉,放进
AppBar
里赋值:
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(DemoLocalizations.of(context).titlebarTitle),//这里增加
),
floatingActionButton: FloatingActionButton(
onPressed: _showTimeDialog,
tooltip: DemoLocalizations.of(context).increment,//这里需要替换
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
复制代码
效果图如下:
2.应用内切换语言
下面简单实现在应用内自由切换语言的功能,首先自定义ChangeLocalizations
的Widget,然后通过Localizations.override
来嵌套需要构建的页面,里面需要实现一个切换语言的方法,也就是根据条件来改变Locale
,初始化设置为中文:
//自定义类 用来应用内切换
class ChangeLocalizations extends StatefulWidget{
final Widget child;
ChangeLocalizations({Key key,this.child}) : super(key:key);
@override
ChangeLocalizationsState createState() => ChangeLocalizationsState();
}
class ChangeLocalizationsState extends State<ChangeLocalizations>{
//初始是中文
Locale _locale = const Locale('zh','CH');
changeLocale(Locale locale){
setState(() {
_locale = locale;
});
}
//通过Localizations.override 包裹我们需要构建的页面
@override
Widget build(BuildContext context){
//通过Localizations 实现实时多语言切换
//通过 Localizations.override 包裹一层。---这里
return new Localizations.override(
context: context,
locale:_locale,
child: widget.child,
);
}
}
复制代码
接着当调用changeLocale
方法就改变语言,ChangeLocalizations
外部去调用其方法需要使用到GlobalKey
的帮助:
//创建key值,就是为了调用外部方法
GlobalKey<ChangeLocalizationsState> changeLocalizationStateKey = new GlobalKey<ChangeLocalizationsState>();
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
DemoLocalizationsDelegate.delegate,//添加
],
supportedLocales: [
const Locale('en','US'), //英文
const Locale('zh','CH'), //中文
],
theme: ThemeData(
primarySwatch: Colors.blue,
),
home:new Builder(builder: (context){
//将 ChangeLocalizations 使用到 MaterialApp 中
return new ChangeLocalizations(
key:changeLocalizationStateKey,
child: new MyHomePage(),
);
}),
// home: MyHomePage(),//这里需要更改
);
}
}
复制代码
最后调用:
//语言切换
void changeLocale(){
if(flag){
changeLocalizationStateKey.currentState.changeLocale(const Locale('zh','CH'));
}else{
changeLocalizationStateKey.currentState.changeLocale(const Locale('en','US'));
}
flag = !flag;
}
复制代码
最后效果:
六、打包
1.生成key
编写完应用后,最后就是打包了,因为我是用Android studio
开发的,所以直接在Terminal
输入:
keytool -genkey -v -keystore ~/key.jks -keyalg RSA -keysize 2048 -validity 100000 -alias key
复制代码
这里记住 -alias key key是别名,可以自己随意更改,弹出:
输入密钥库口令:
再次输入新口令:
您的名字与姓氏是什么?
[Unknown]: knight
您的组织单位名称是什么?
[Unknown]: knight
您的组织名称是什么?
[Unknown]: knight
您所在的城市或区域名称是什么?
[Unknown]: knight
您所在的省/市/自治区名称是什么?
[Unknown]: knight
该单位的双字母国家/地区代码是什么?
[Unknown]: C
CN=knight, OU=knight, O=knight, L=knight, ST=knight, C=C是否正确?
[否]: Y
正在为以下对象生成 2,048 位RSA密钥对和自签名证书 (SHA256withRSA) (有效期为 100,000 天):
CN=knight, OU=knight, O=knight, L=knight, ST=knight, C=C
[正在存储/Users/luguian/key.jks] -->生成对应的签名文件
复制代码
我把它复制到android
目录下。
2.创建key.properties
在android
目录下创建一个key.properties
:
3.更改build.gradle
----->增加
def keystorePropertiesFile = rootProject.file("key.properties")
def keystoreProperties = new Properties()
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
----->
android {
compileSdkVersion 28
lintOptions {
disable 'InvalidPackage'
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.flutterdemo"
minSdkVersion 16
targetSdkVersion 28
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
----->增加
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
}
}
buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled true
useProguard true
// proguard文件是混淆
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
------>增加
}
复制代码
4.添加混淆文件
默认情况下,Flutter不会混淆或缩小Android主机。如果您打算使用第三方Java或Android库,您可能希望减小APK的大小或保护该代码免受逆向工程,那就在在android/app/下添加proguard-rules.pro:
最后在项目根目录执行:flutter build apk
复制代码
Initializing gradle... 0.6s
Resolving dependencies... 1.3s
Gradle task 'assembleRelease'...
Gradle task 'assembleRelease'... Done 7.2s
Built build/app/outputs/apk/release/app-release.apk (15.8MB).
复制代码
最后输出在build-app-release下
:
ios
怎么打包就不说了,具体查看
flutter.dev/docs/deploy…。
七、总结
- 跨平台的开发终究逃不过原生。
- 国际化流程有点复杂,不太好理解。
- 打出来的安装包确实有点大。
资料参考_国际化:www.jianshu.com/p/8356a3bc8…
资料参考_Flutter:flutterchina.club/tutorials/i…
国际化demo地址:github.com/KnightAndro…
如有错误,欢迎指出指正~