就像 React Native 一樣,在 Flutter 應用中,如果需要呼叫第三方庫的方法或者有一些功能需要使用原生的開發來提供,使用 Flutter Plugin 是一種不錯的方式,它本質上就是一個 Dart Package,但與其它的 package 不同點在於,Flutter 外掛中一般都存在兩個特殊的資料夾:android
與 ios
,如果需要編寫Java、Kotlin或者 Object-C 以及 Swift 程式碼,我們就需要在這兩個資料夾專案中進行,然後通過相應的方法將原生程式碼中開發的方法對映到 dart 中。
本文以開發一個微信外掛為例,為Flutter應用提供微信分享、登入、支付等功能,專案程式碼可以直接在下方找到,也已經提交至Pub庫:
建立外掛目錄
要開發外掛,可以使用下面的程式碼快速基於 plugin
模板開始:
flutter create --template=plugin wechat
複製程式碼
上面的程式碼中,表示以 plugin
模板建立一個名為 wechat
的 package
,建立完成之後,整個專案的目錄結構就都提供好了,並且官方還提供了一些基本開發示例。
目錄結構
- android // Android 相關原生程式碼目錄
- ios // ios 相關原生程式碼目錄
- lib // Dart 程式碼目錄
- example // 一個完整的呼叫了我們正在開發的外掛的 Flutter App
- pubspec.yaml // 專案配置檔案
複製程式碼
從 example/lib/main.dart
開始
在開發我們的應用之後,先來了解一下 flutter
為我們生成的檔案們,開啟 example/lib/main.dart
,程式碼如下:
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:wechat/wechat.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
String _platformVersion = 'Unknown';
@override
void initState() {
super.initState();
initPlatformState();
}
// Platform messages are asynchronous, so we initialize in an async method.
Future<void> initPlatformState() async {
String platformVersion;
// Platform messages may fail, so we use a try/catch PlatformException.
try {
platformVersion = await Wechat.platformVersion;
} on PlatformException {
platformVersion = 'Failed to get platform version.';
}
// If the widget was removed from the tree while the asynchronous platform
// message was in flight, we want to discard the reply rather than calling
// setState to update our non-existent appearance.
if (!mounted) return;
setState(() {
_platformVersion = platformVersion;
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Plugin example app'),
),
body: Center(
child: Text('Running on: $_platformVersion\n'),
),
),
);
}
}
複製程式碼
這裡需要特別注意的就是 initPlatformState()
方法中對 Wechat.platformVersion
的呼叫,這裡面的 Wechat
就是我們的外掛,platformVersion
就是外掛提供的 get
方法,跟著這個檔案,找到 lib/wechat.dart
檔案,程式碼如下:
import 'dart:async';
import 'package:flutter/services.dart';
class Wechat {
static const MethodChannel _channel =
const MethodChannel('wechat');
static Future<String> get platformVersion async {
final String version = await _channel.invokeMethod('getPlatformVersion');
return version;
}
}
複製程式碼
在該檔案中,可以看到 class Wechat
定義了一個 get
方法 platformVersion
,它的函式體有點特別:
final String version = await _channel.invokeMethod('getPlatformVersion');
return version;
複製程式碼
我們的 version
是通過 _channel.invokeMethod('getPlatformVersion')
方法的呼叫得到的,這個 _channel
就是我們 Dart 程式碼與 原生程式碼進行通訊的橋了,也是 Flutter 原生外掛的核心(當然,如果你編寫的外掛並不需要原生程式碼相關的功能,那麼,_channel
就是可有可無的了,比如我們可以寫一個下面這樣的方法,返回 兩個數字 a
與 b
的和:
class Wechat {
...
static int calculate (int a, int b) {
return a + b;
}
}
複製程式碼
之後,修改 example/lib/main.dart
程式碼:
class _MyAppState extends State<MyApp> {
String _platformVersion = 'Unknown';
// 定義一個 int 型變數,用於儲存計算結果
int _calculateResult;
@override
void initState() {
super.initState();
initPlatformState();
}
Future<void> initPlatformState() async {
String platformVersion;
try {
platformVersion = await Wechat.platformVersion;
} on PlatformException {
platformVersion = 'Failed to get platform version.';
}
if (!mounted) return;
// init 的時候,計算一下 10 + 10 的結果
_calculateResult = Wechat.calculate(10, 10);
setState(() {
_platformVersion = platformVersion;
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Plugin example app'),
),
body: Container(
padding: EdgeInsets.all(16.0),
child: SingleChildScrollView(
child: Column(
children: <Widget>[
Text('Running on: $_platformVersion\n'),
// 輸出該結果
Text('Calculate Result: $_calculateResult\n'),
],
),
),
),
),
);
}
}
複製程式碼
支援原生編碼提供的方法
很多時候,寫外掛,更多的是因為我們需要讓應用能夠呼叫原生程式碼提供的方法,怎麼做呢?
Android 系統
開啟 android/src/main/java/com/example/wechat/WechatPlugin.java
檔案,看如下程式碼:
package com.example.wechat;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
import io.flutter.plugin.common.PluginRegistry.Registrar;
/** WechatPlugin */
public class WechatPlugin implements MethodCallHandler {
/** Plugin registration. */
public static void registerWith(Registrar registrar) {
final MethodChannel channel = new MethodChannel(registrar.messenger(), "wechat");
channel.setMethodCallHandler(new WechatPlugin());
}
@Override
public void onMethodCall(MethodCall call, Result result) {
if (call.method.equals("getPlatformVersion")) {
result.success("Android " + android.os.Build.VERSION.RELEASE);
} else {
result.notImplemented();
}
}
}
複製程式碼
還記得上面提到的 getPlatformVersion
嗎?還記得 _channel
那麼,是不是在這裡面也看到的對應的存在?沒錯, dart
中的 getPlatformVersion
通過 _channel.invokeMethod
發起一次請求,然後,Java
程式碼中的 onMethodCall
方法回被呼叫,該方法有兩個引數:
MethodCall call
:請求本身Result result
:結果處理方法
然後通過 call.method
可以知到 _channel.invokeMethod
中的方法名,然後通過 result.success
回撥返回成功結果響應。
registerWith
在上面還有一小段程式碼
registerWith
,可以看到裡面有一個呼叫:
final MethodChannel channel = new MethodChannel(registrar.messenger(), "wechat"); channel.setMethodCallHandler(new WechatPlugin()); 複製程式碼
這裡就是在註冊我們的外掛,將
channel
名,這樣,才不會呼叫alipay
外掛的呼叫最後到了
iOS 系統
同樣的,這次我們開啟 ios/Classes/WechatPlugin.m
檔案:
#import "WechatPlugin.h"
@implementation WechatPlugin
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
FlutterMethodChannel* channel = [FlutterMethodChannel
methodChannelWithName:@"wechat"
binaryMessenger:[registrar messenger]];
WechatPlugin* instance = [[WechatPlugin alloc] init];
[registrar addMethodCallDelegate:instance channel:channel];
}
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
if ([@"getPlatformVersion" isEqualToString:call.method]) {
result([@"iOS " stringByAppendingString:[[UIDevice currentDevice] systemVersion]]);
} else {
result(FlutterMethodNotImplemented);
}
}
@end
複製程式碼
雖然語法有所不同,但是,可以看到,跟 android
的 Java 程式碼結構上幾乎是一模一樣的,首先 register
一個名為 wechat
的 channel
,然後去 handleMethodCall
,同樣的通過 call.method
拿到方法名,通過 result
做出響應。
小試牛刀
接下來,我們將前面的 caculate
方法,移到原生程式碼中來提供(雖然這很沒必要,但畢竟,只是為了演示嘛)。
Android
在前面開啟的 android/src/main/java/com/example/wechat/WechatPlugin.java
檔案中,修改 onMethodCall
方法:
@Override
public void onMethodCall(MethodCall call, Result result) {
if (call.method.equals("getPlatformVersion")) {
result.success("Android " + android.os.Build.VERSION.RELEASE);
} else if (call.method.equals("calculate")) {
int a = call.argument("a");
int b = call.argument("b");
int r = a + b;
result.success("" + r);
} else {
result.notImplemented();
}
}
複製程式碼
新增了 call.method.equals("calculate")
判斷,這裡面具體的過程是:
- 呼叫
call.argument()
方法,可以取得由wechat.dart
傳遞過來的引數 - 計算結果
- 呼叫
result.success()
響應結果
然後,我們需要在 lib/wechat.dart
中修改 calculate
方法的實現,程式碼如下:
static Future<int> calculate (int a, int b) async {
final String result = await _channel.invokeMethod('calculate', {
'a': a,
'b': b
});
return int.parse(result);
}
複製程式碼
由於 _channel.invokeMethod
是一個非同步操作,所以,我們需要將 calculate
的返回型別修改為 Future
,同時加上 async
,此時我們就可以直接使用 await
關鍵字了,跟 JavaScript
中的 await
一樣,讓我們用同步的方式編寫非同步程式碼,在新版的 calculate
程式碼中,我們並沒有直接計算 a+b
的結果,而是呼叫 _channel.invokeMethod
方法,將 a
與 b
傳遞給了 Java
端的 onMethodCall
方法,然後返回該方法返回的結果。
_channel.invokeMethod
該方法接受兩個引數,第一個定義一個方法名,它是一個標識,簡單來說,它告訴原生端的程式碼,我們這次是要幹什麼,第二個引數是一個
Map<String, dynamic>
型資料,是引數列表,我們可以在原生程式碼中獲取到。
接著,我們需要更新一下對該方法的呼叫了,回到 example/lib/main.dart
中,修改成如下呼叫:
_calculateResult = await Wechat.calculate(10, 10);
複製程式碼
因為我們現在的 calculate
方法已經是一個非同步方法了。
iOS
如果我們的外掛需要支援 Android
與 IOS
兩端,那麼需要同步的在 ios
中實現上面的方法,開啟 ios/Classes/WechatPlugin.m
檔案,作如下修改:
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
NSDictionary *arguments = [call arguments];
if ([@"getPlatformVersion" isEqualToString:call.method]) {
result([@"iOS " stringByAppendingString:[[UIDevice currentDevice] systemVersion]]);
} else if ([@"calculate" isEqualToString:call.method]) {
NSInteger a = [arguments[@"a"] intValue];
NSInteger b = [arguments[@"b"] intValue];
result([NSString stringWithFormat:@"%d", a + b]);
} else {
result(FlutterMethodNotImplemented);
}
}
複製程式碼
實現過程與 java
端保持一致即可。
新增第三方 SDK
我們的外掛是可以提供微信的分享相關功能的,所以,肯定需要用到第三方SDK,還是從 Android 開始。
Android 端 WechatSDK
按 官方接入指南 所述,我們需要新增依賴:
dependencies {
compile 'com.tencent.mm.opensdk:wechat-sdk-android-with-mta:+'
}
複製程式碼
或
dependencies {
compile 'com.tencent.mm.opensdk:wechat-sdk-android-without-mta:+'
}
複製程式碼
前者帶有統計功能,這很簡單,開啟 android/build.gradle
檔案 ,在最下方貼上以上片段即可:
...
android {
compileSdkVersion 27
defaultConfig {
minSdkVersion 16
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
lintOptions {
disable 'InvalidPackage'
}
}
dependencies {
compile 'com.tencent.mm.opensdk:wechat-sdk-android-with-mta:+'
}
複製程式碼
然後,回到 WechatPlugin.java
檔案,先新增一個 register
方法,它將我們的Appid 註冊給微信,還是接著前面的 onMethodCall
中的 if
判斷:
...
import com.tencent.mm.opensdk.openapi.WXAPIFactory;
...
else if (call.method.equals("register")) {
appid = call.argument("appid");
api = WXAPIFactory.createWXAPI(context, appid, true);
result.success(api.registerApp(appid));
}
...
複製程式碼
然後回到 lib/wechat.dart
新增相應呼叫:
...
/// Register app to Wechat with [appid]
static Future<dynamic> register(String appid) async {
var result = await _channel.invokeMethod(
'register',
{
'appid': appid
}
);
return result;
}
...
複製程式碼
此時,在我們的 example
應該中,就可以呼叫 Wechat.register
方法,來註冊應用了
ios
按照官方 ios 接入指南所述,我們可以通過 pod
新增依賴:
pod 'WechatOpenSDK'
複製程式碼
開啟 ios/wechat.podspec
,可以看到如下內容:
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html
#
Pod::Spec.new do |s|
s.name = 'wechat'
s.version = '0.0.1'
s.summary = 'A new flutter plugin project.'
s.description = <<-DESC
A new flutter plugin project.
DESC
s.homepage = 'http://example.com'
s.license = { :file => '../LICENSE' }
s.author = { 'Your Company' => 'email@example.com' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.public_header_files = 'Classes/**/*.h'
s.dependency 'Flutter'
s.ios.deployment_target = '8.0'
end
複製程式碼
留意到數第三行的 s.dependency
,這就是在指定我們依賴 Flutter
,如果有其它依賴在這裡新增一行即可:
...
s.public_header_files = 'Classes/**/*.h'
s.dependency 'Flutter'
s.dependency 'WechatOpenSDK'
s.ios.deployment_target = '8.0'
end
複製程式碼
然後開啟 ios/Classes/WechatPlugin.h
檔案,修改如下:
#import <Flutter/Flutter.h>
#include "WXApi.h"
@interface WechatPlugin : NSObject<FlutterPlugin, WXApiDelegate>
@end
複製程式碼
再回到 ios/Classes/WechatPlugin.m
,接著前面的 if
條件繼續新增判斷:
...
// Register app to Wechat with appid
else if ([@"register" isEqualToString:call.method]) {
[WXApi registerApp:arguments[@"appid"]];
result(nil);
}
...
複製程式碼
此時,我們的外掛已經支援微信 SDK 的 註冊至微信 功能了,更多實現,本文就不再討論,有興趣,可以直接下載完整專案,後面都是大同小異的實現,唯一需要的是,你需要有一定的 Java
編碼與 Objective-C
編碼能力。