禁止转载,侵权必究! update 2021.9.28

更新了Null Safety兼容

前言

之前的教程是教大家如何利用别人开发好的flutter的库来快速完成需求。但是客户需求并不总是能用第三方的库来满足,我们需要编写自己的plugin。从本教程开始,我们进入Java和Objective-C本地flutter plugin的开发学习。

创建plugin项目

命令行执行以下命令:

flutter create --org cn.abilitygame -t plugin -i objc -a java 
 --platforms android,ios ability_device_info_plugin

在这里我们用参数 -t plugin来标识我们创建的是flutter的插件。

build一下项目:

cd ability_device_info_plugin/example
flutter build apk --target-platform android-arm,android-arm64 --split-per-abi

可以看到以下build结果

注: /Users/ouyang/app/flutter/abilitygame_projects/ability_device_info_plugin/android/src/main/java/cn/abilitygame/ability_device_info_plugin/AbilityDeviceInfoPlugin.java使用或覆盖了已过时的 API。
注: 有关详细信息, 请使用 -Xlint:deprecation 重新编译。                  
Removed unused resources: Binary resource data reduced from 46KB to 36KB: Removed 20%
Removed unused resources: Binary resource data reduced from 46KB to 36KB: Removed 20%
Running Gradle task 'assembleRelease'...                                
Running Gradle task 'assembleRelease'... Done                      34.0s
✓ Built build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk (4.9MB).

打开Android Studio,选择import project,一路选到项目中的example的android目录下,点击Open。

注意:是example目录下。

按这个流程,Android Studio帮我们配置好了flutter plugin的Android开发环境。非常轻松。

扩展阅读:如果按上述步骤,你的项目还报AndroidX错误,需要升级到flutter v1.12.13以上版本。如果你用的第三方的库是旧版本。那你只能选其他库或者帮它升级版本了。学完这个系列教程,你应该具备升级flutter plugin的能力了。

编写plugin-Android部分

Android Studio应该给你展示的是Android项目结构,如下图:

1. Java的类定义

public class AbilityDeviceInfoPlugin implements FlutterPlugin, MethodCallHandler

它实现了官方的FlutterPlugin接口。为了兼容旧版,它包含了flutter旧版静态方法:

public static void registerWith(Registrar registrar){}

它还包含了flutter新的API方法:

public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {}
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {}

以上都是约定写法,我们把重点关注到它实现的第二个接口MethodCallHandler:

@Override
  public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
    if (call.method.equals("getAndroidDeviceInfo")) {
      Map<String, Object> build = new HashMap<>();
      build.put("model", Build.MODEL);
      Map<String, Object> version = new HashMap<>();
      version.put("release", Build.VERSION.RELEASE);
      version.put("sdkInt", Build.VERSION.SDK_INT);
      build.put("version", version);
      result.success(build);
    } else {
      result.notImplemented();
    }
  }

重点就是这个方法。我们也可以用单独的Java类来实现onMethodCall方法。它就是flutter App调用Java本地方法的入口函数。它首先判断了方法名是getAndroidDeviceInfo,然后从Android系统自带的Build类中获取了相关的系统参数并以Map<String, Object>形式返回给Flutter。

编写plugin-Flutter部分

关闭Android Studio。重新打开Android Studio,选择Open an exist Android Studio Project。

注意:打开根目录,不是example目录,跟本教程速成篇一样。

我们用dart来定义plugin,如下图:

注意:因为Java返回的是Map,这里要使用invokeMapMethod方法。

1. 定义两个类AndroidDeviceInfo & AndroidBuildVersion

对应Java插件中的两个Map。

class AndroidDeviceInfo {
  final String model;
  final AndroidBuildVersion version;

  AndroidDeviceInfo._({
    this.model,
    this.version,
  });

  static AndroidDeviceInfo _fromMap(Map<String, dynamic> map) {
    return AndroidDeviceInfo._(
      version: AndroidBuildVersion._fromMap(map['version']?.cast<String, dynamic>()),
      model: map['model'],
    );
  }
}

注意:它的_fromMap静态私有方法,从Java Map中转换KV为类属性

class AndroidBuildVersion {
  final String release;
  final int sdkInt;

  AndroidBuildVersion._({
    this.release,
    this.sdkInt,
  });

  static AndroidBuildVersion _fromMap(Map<String, dynamic> map) {
    return AndroidBuildVersion._(
      release: map['release'],
      sdkInt: map['sdkInt'],
    );
  }
}

注意:它的_fromMap静态私有方法,从Java Map中转换KV为类属性

定义dart的plugin的私有类变量,缓存device info

AndroidDeviceInfo _cachedAndroidDeviceInfo;

2. 定义MethodChannel

MethodChannel是Java和dart沟通的桥梁,它用name属性来唯一标识。

  static const MethodChannel _channel =
      const MethodChannel('abilitygame.cn/device_info');

3. 定义dart调用java的方法deviceInfo

  Future<AndroidDeviceInfo> get deviceInfo async =>
    _cachedAndroidDeviceInfo ??= AndroidDeviceInfo._fromMap(await _channel.invokeMapMethod<String, dynamic>('getAndroidDeviceInfo'));

它是一个异步方法。使用第2步的命名MethodChannel,它告诉java约定的方法名是:getAndroidDeviceInfo。我们可以回头看看,java侧代码有判断此方法名的逻辑。

测试自定义插件

打开example/lib/main.dart,如下图:

1. import依赖包

import 'package:flutter/material.dart';
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:ability_device_info_plugin/ability_device_info_plugin.dart';

2. 定义类变量

Map<String, dynamic> _deviceData = <String, dynamic>{};

3. 定义initDeviceState方法

  Future<void> initDeviceState() async {
    Map<String, dynamic> deviceInfo;
    try {
      final AbilityDeviceInfoPlugin plugin = AbilityDeviceInfoPlugin();
      deviceInfo = _readAndroidBuildData(await plugin.deviceInfo);
    } on PlatformException {
      print('failed to get device info.');
    }

    if (!mounted) return;

    setState(() {
      _deviceData = deviceInfo;
    });
  }

4. 把AndroidDeviceInfo重新转换回Map对象

  Map<String, dynamic> _readAndroidBuildData(AndroidDeviceInfo build) {
    return <String, dynamic>{
      'version.sdkInt': build.version.sdkInt,
      'version.release': build.version.release,
      'model': build.model,

    };
  }

5. 定义Flutter的UI

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Plugin example app'),
        ),
        body: ListView(
          children: _deviceData.keys.map((String property){
            return Row(
              children: <Widget>[
                Container(
                  padding: const EdgeInsets.all(10),
                  child: Text(property),
                ),
                Expanded(child: Text('${_deviceData[property]}'))
              ],
            );
        }).toList(),
        ),
      ),
    );
  }

真机调试

用数据线连接上Android手机,点击运行。

可以看到我的手机型号是NX563J;Android 9; sdk 28。

机型适配问题

适配所有Android机型

示例源代码

源码地址

NULL SAFETY源码地址