看啥推荐读物
专栏名称: 真丶深红骑士
Android开发者
今天看啥  ›  专栏  ›  真丶深红骑士

Flutter学习之插件开发、自定义字体、国际化

真丶深红骑士  · 掘金  · android  · 2019-03-25 03:09
阅读 122

Flutter学习之插件开发、自定义字体、国际化

一、前言

今天学习插件开发,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…

如有错误,欢迎指出指正~




原文地址:访问原文地址
快照地址: 访问文章快照