一、前言
今天學習外掛開發,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…
如有錯誤,歡迎指出指正~