今天在进行插件开发的时候,遇到了各种问题,有 AndroidStudio 版本不对的问题,有插件的 build.gradle 文件配置不对的问题,还有插件中的 kotlin 插件没有配置,导致找不到类的问题,头疼,所以记录下开发的完整流程,以便后期再遇到此种问题的时候,有个解决方案。

一、Flutter插件

Flutter插件是由于 Flutter 自身不具备访问平台特性的能力,所以需要借助插件使用原生API的能力才能具有相应的功能,比如吐司,拍照,位置信息等,所以需要一个通道能够使得Flutter和原生平台之间能够顺利的沟通,这就是 channel, channel有好几种,但是我们一般都是用的 MethodChannel, 后面有时间再补上这块的内容。

二、Flutter插件开发流程

  1. 先在 Flutter 项目中定义好有关 MethodChannel 相应的工具类,并提供静态方法供Flutter项目中其他地方使用
  2. 然后在 Flutter 项目中 进行 插件开发,并添加依赖到宿主工程中
  3. 在宿主工程中开发PluginRegistraint类,并将插件添加到 FlutterEngine的插件列表 plugins 中
  4. 在MainActivity中调用 PluginRegistrant的 registerWith() 方法,最终将插件注册到 FlutterEngine 中
  5. 在Flutter 项目中对第1步创建好的方法进行调用和测试

三、Flutter中工具类

  plugin_toast.dart  

import 'package:flutter/services.dart';

class ToastPlugin {
  static MethodChannel _channel = new MethodChannel('lucky_toast_plugin');

  static Future<void> showToast() async {
    await _channel.invokeMethod('showToast', {'msg': 'hello world'});
    return;
  }

}

四、插件开发

1. 首先把Flutter项目中用 AndroidStudio 打开,这一步很重要,否则不能进行插件开发。

2. 在 AndroidStudio 打开项目之后,搜索 MainActivity, 然后点击右上角的的 Open for Editing in Android Studio。 

3. 单独打开之后,在File中选择创建 Module,  并选择 Android Library,设置好项目名称 plugin_toast,此时注意检查 根目录下 settings.gradle 文件是否配置正确。

4. 查看项目根目录下的build.gradle文件是否正确, 可如下参考:

buildscript {
    ext.kotlin_version = '1.3.72'
    repositories {
        google()
        jcenter()
    }

    dependencies {
        classpath 'com.android.tools.build:gradle:4.1.0'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

allprojects {
    repositories {
        google()
        jcenter()
    }
}

rootProject.buildDir = '../build'
subprojects {
    project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
    project.evaluationDependsOn(':app')
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

5. 查看并确定plugin_toast 插件的 build.gradle 文件是否正确,可参考如下,因为这个配置问题,导致项目编译好了,但是运行是找不到 MainActivity.class 的问题。

def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
    localPropertiesFile.withReader('UTF-8') { reader ->
        localProperties.load(reader)
    }
}

def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
    throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
//
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
    flutterVersionCode = '1'
}

def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
    flutterVersionName = '1.0'
}

apply plugin: 'com.android.library'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"

android {
    compileSdkVersion 30
    buildToolsVersion "30.0.1"

    defaultConfig {
        minSdkVersion 22
        targetSdkVersion 30
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        consumerProguardFiles "consumer-rules.pro"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {

    //implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    //implementation 'androidx.core:core-ktx:1.2.0'
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'com.google.android.material:material:1.1.0'
    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}

flutter {
    source '../..'
}

6. 在宿主 app 的build.gradle 文件中添加插件库 

def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
    localPropertiesFile.withReader('UTF-8') { reader ->
        localProperties.load(reader)
    }
}

def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
    throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}

def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
    flutterVersionCode = '1'
}

def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
    flutterVersionName = '1.0'
}

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"

android {
    compileSdkVersion 30

    sourceSets {
        main.java.srcDirs += 'src/main/kotlin'
    }

    defaultConfig {
        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
        applicationId "com.example.flutter_widgets"
        minSdkVersion 22
        targetSdkVersion 30
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
    }

    buildTypes {
        release {
            // TODO: Add your own signing config for the release build.
            // Signing with the debug keys for now, so `flutter run --release` works.
            signingConfig signingConfigs.debug
        }
    }
}

flutter {
    source '../..'
}

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    //添加插件库
    implementation project(path:':plugin_toast')
}

7. 确定好以上没有问题,就可以正常开发插件了,代码如下

package com.luckyboy.plugin_toast;

import android.content.Context;
import android.widget.Toast;
import androidx.annotation.NonNull;
import java.util.Map;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;

public class ToastPlugin implements FlutterPlugin, MethodChannel.MethodCallHandler {

    private MethodChannel channel;
    private Context context;
    
    @Override
    public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
        context = binding.getApplicationContext();
        channel = new MethodChannel(binding.getBinaryMessenger(), "lucky_toast_plugin");
        channel.setMethodCallHandler(this);
    }

    @Override
    public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
        channel.setMethodCallHandler(null);
    }

    @Override
    public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
         if (call.method.equals("showToast")){
             Map<String, String> map = call.arguments();
             String message = map.get("msg");
             Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
         }
    }
    
}
  1. 创建 ToastPlugin 类,并实现 FlutterPlugin, MethodChannel.MethodCallHandler 接口,总共有三个方法 :onAttachedToEngine, onDetachedFromEngine, onMethodCall 。
  2. 首先在 onAttachedToEngine 中 创建了 MethodChannel 对象 channel,并设置插件的标记 "lucky_toast_plguin",这个字符串就是在Flutter中已经定义好的,方便在Flutter中对插件的查找 ,最后调用 channel.setMethodCallHandler() 设置回调 。
  3. 其次在 onDetachedFromEngine 中将回调设置为 null 。
  4. 在 onMethodCall 方法中 进行业务处理,call.method 获取的是 Flutter 中 invoke 方法设置的调函函数名,call.arguments() 方法返回的是invoke 方法设置的参数。
  5. 可以在result.success("success")。
  6. 插件开发完毕。 

五、添加插件到宿主 

1. 创建 ToastPluginRegistrant 类,用于将插件添加到 FlutterEngine 的插件列表中

package io.flutter.plugins;

import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import com.luckyboy.plugin_toast.ToastPlugin;
import io.flutter.embedding.engine.FlutterEngine;

@Keep
public final class ToastPluginRegistrant {

    public static void registerWith(@NonNull FlutterEngine flutterEngine){
        flutterEngine.getPlugins().add(new ToastPlugin());
    }

}

 2. 在 MainActivity 中进行 registerWith 方法调用,注册插件到插件中

package com.example.flutter_widgets

import android.os.Bundle
import io.flutter.embedding.android.FlutterActivity
import io.flutter.plugins.GeneratedPluginRegistrant
import io.flutter.plugins.ToastPluginRegistrant

class MainActivity: FlutterActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        GeneratedPluginRegistrant.registerWith(flutterEngine!!);
        ToastPluginRegistrant.registerWith(flutterEngine!!);
    }

}

GeneratedPluginRegistrant 是另外一个系统帮我们自动生成的,可以不用管。至此,Android 端的插件开发部分已经完成,剩下的就是Flutter的调用插件部分代码。

六、Flutter 插件调用测试

import 'package:flutter/material.dart';
import 'package:flutter_widgets/plugin/lib/plugin_toast.dart';

class PluginPage extends StatefulWidget {
  @override
  _PluginPageState createState() => _PluginPageState();
}

class _PluginPageState extends State<PluginPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Plugin页面')),
      body: Column(
        children: [
          MaterialButton(
            minWidth: double.infinity,
            height: 48,
            onPressed: () {
              showToast();
            },
            child: Text('点击Toast'),
          )
        ],
      ),
    );
  }

  Future<void> showToast() async {
    await ToastPlugin.showToast();
    return;
  }
}

七、测试结果

如何查看fluter 插件需要支持Android 33 flutter开发插件_插件开发