Flutter學習之外掛開發、自定義字型、國際化

真丶深紅騎士發表於2019-03-25

一、前言

今天學習外掛開發,Flutter使用一個靈活的系統,允許呼叫特定平臺(iOS/Android)的API,無論在Android上的Java或者Kotlin程式碼中,還是iOS上的Object-C或者Swift程式碼中均可使用。Flutter平臺特定的API支援不依賴於程式碼生成,而是依賴於靈活的訊息傳遞方式:

  • 應用的Flutter部分通過平臺通道(platform channel)將訊息傳送到應用程式得所在宿主(iOS或Android)。
  • 宿主監聽的平臺通道,並接受該訊息,然後它會呼叫特定於該平臺的API(使用原生程式語言)-並響應傳送客戶端(即應用程式的Flutter部分)。

二、外掛例項

1.外掛的基本原理

要使用和建立一個Flutter外掛,得要首先知道平臺通道在客戶端(Flutter UI)和宿主(平臺)之間傳遞訊息,用官方的圖,下圖:

平臺外掛基本原理
上面就是平臺通道的結構大致描述,使用MethodChannelFlutter客戶端主機(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部分:

  1. Android Studio 選擇File > Open
  2. 定位到自己的專案根目錄,然後選擇裡面的android資料夾,點選OK 如下:

開啟android目錄
3. 在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平臺特定的實現

首先開啟XcodeFlutter應用程式得iOS部分:

  1. 啟動Xcode
  2. 選擇File > Open...
  3. 定位到Flutter app目錄,然後選擇裡面的ios資料夾,點選OK
  4. 確保Xcode專案的構建沒有錯誤
  5. 選擇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上,看看效果:

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版本相容,也可以指定特定的版本:

  1. 0.4.1:特定的版本
  2. any:任意版本
  3. <0.4.1:小於0.4.4的版本
  4. >0.4.1:大於0.4.1的版本
  5. <=0.4.1:小於等於0.4.1的版本
  6. >=0.4.1:大於等於0.4.1的版本
  7. >=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的外掛包:

  1. 選擇Flie > New > New FLutter Project
  2. 在目錄皮膚中選擇第二個Flutter Plugin,點選next,Android stdio 會有顯示Select "plugin" when exposing an Android or iOS API for develops
  3. 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客戶端需要做有兩步:

  1. 生成一個MethodChannel,例子已經幫生成了。
  2. 通過這個MethodChannel呼叫showToast方法。

flutter客戶端的步驟

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 ->主頁
複製程式碼

建議將下面文件新增到外掛包:

  1. README.md:結束外掛的檔案
  2. CHANGELOG:記錄每個版本中的更改
  3. 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應用程式中使用字型分兩步完成:

  1. pubspec.yaml中宣告它們,以確保它們包含在應用程式中
  2. 通過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 是字型的名稱,可以在TextStylefontFamily屬性中使用,asset是相對於pubspec.yaml檔案的路徑,這些檔案包含字型中字形的輪廓,在構建應用程式時,這些檔案會包含在應用程式的asset包中。 可以給字型設定粗細、傾斜等樣式

  • weight屬性指定字型的粗細,取值範圍是100到900之間的整百數(100d的倍數),這些值對應FontWeight,可以用於TextStylefontWeight屬性
  • style指定字型是傾斜還是正常,對應的值為italicnormal,這些值對應fontStyle可以用於TextStylefontStyleTextStyle屬性。

具體程式碼:

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: &#xE914; or 0xE914 or E914
    icons += "\u{E914}";

    // https://material.io/icons/#ic_error
    // error: &#xE000; or 0xE000 or E000
    icons += "\u{E000}";

    // https://material.io/icons/#ic_fingerprint
    // fingerprint: &#xE90D; or 0xE90D or E90D
    icons += "\u{E90D}";

    // https://material.io/icons/#ic_camera
    // camera: &#xE3AF; or 0xE3AF or E3AF
    icons += "\u{E3AF}";

    // https://material.io/icons/#ic_palette
    // palette: &#xE40A; or 0xE40A or E40A
    icons += "\u{E40A}";

    // https://material.io/icons/#ic_tag_faces
    // tag faces: &#xE420; or 0xE420 or E420
    icons += "\u{E420}";

    // https://material.io/icons/#ic_directions_bike
    // directions bike: &#xE52F; or 0xE52F or E52F
    icons += "\u{E52F}";

    // https://material.io/icons/#ic_airline_seat_recline_extra
    // airline seat recline extra: &#xE636; or 0xE636 or E636
    icons += "\u{E636}";

    // https://material.io/icons/#ic_beach_access
    // beach access: &#xEB3E; or 0xEB3E or EB3E
    icons += "\u{EB3E}";

    // https://material.io/icons/#ic_public
    // public: &#xE80B; or 0xE80B or E80B
    icons += "\u{E80B}";

    // https://material.io/icons/#ic_star
    // star: &#xE838; 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裡指定(新增)localizationsDelegatessupportedLocales,如下:

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例項物件,就可以呼叫titlebarTitleincrement方法來獲取對應的字串。

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呢,這時候就要用到LocalizationsLocalizations用於載入和查詢包含本地化值的集合的物件,應用程式通過Localizations.of(context,type)來引用這些物件,如果區域裝置的區域設定發生更改,則Localizations這個元件會自動載入新區域設定的值,然後重新構建使用它們的widgetDemoLocalizationsDelegate 這個類的物件雖然被傳入了 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:

建立key.properties

3.更改build.gradle

修改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下

app目錄
這樣就成功了。ios怎麼打包就不說了,具體檢視flutter.dev/docs/deploy…

七、總結

  1. 跨平臺的開發終究逃不過原生。
  2. 國際化流程有點複雜,不太好理解。
  3. 打出來的安裝包確實有點大。

資料參考_國際化:www.jianshu.com/p/8356a3bc8…

資料參考_Flutter:flutterchina.club/tutorials/i…

國際化demo地址:github.com/KnightAndro…

如有錯誤,歡迎指出指正~

相關文章