手把手教你在Android專案中接入Flutter,在Flutter中使用Android佈局

安卓小哥發表於2019-07-30

開頭

在flutter開發中,始終會有下面兩個無法避免的問題:

  • 原生專案往flutter遷移,就需要在原生專案中接入flutter
  • flutter專案中要使用到一些比較成熟的應用,就無法避免去用到原生的各種成熟庫,比如音視訊之類的

這篇文章,將會對上面兩種情況,分別進行介紹

在Android中接入flutter介面

在android專案中需要將flutter以module的形式接入

建立flutter module

進入當前android專案,在根目錄執行如下命令:

flutter create -t module my_flutter
複製程式碼

上面表示建立一個名為 my_flutter 的flutter module

之後執行

cd my_flutter
cd .android/
./gradlew flutter:assembleDebug
複製程式碼

同時,確保你的在你的android專案目錄下 app/build.gradle ,有新增如下程式碼:

android {
    compileSdkVersion 28
    defaultConfig {
        ...
    }
    buildTypes {
        ...
    }

    //flutter相關宣告
    compileOptions {
        sourceCompatibility 1.8
        targetCompatibility 1.8
    }
}
複製程式碼

接著,在 android專案 根目錄下的 settings.gradle 中新增如下程式碼

include ':app'
setBinding(new Binding([gradle: this]))
evaluate(new File(
        rootDir.path + '/my_flutter/.android/include_flutter.groovy'
))
複製程式碼

最後,需要在android專案下的 app/build.gradle 中引入 my_flutter

dependencies {
    ...

    //匯入flutter
    implementation project(':flutter')
}
複製程式碼

到這裡,基本上就可以開始接入flutter的內容了

不過這時候還有一個問題需要注意,如果你的android專案已經遷移到了androidx,可能你會遇到下面的這種問題

手把手教你在Android專案中接入Flutter,在Flutter中使用Android佈局

這種問題明顯是因為flutter建立moudle時,並未做到androidx的轉換,因為建立moudle的命令還不支援androidx

可以檢視下面這個issue

Generated Flutter Module Files Do Not Use AndroidX

下面就開始解決這個問題

解決androidx帶來的問題

首先,如果你原先的android專案已經遷移到了androidx,那麼在根目錄下的 grale.properties 一定有如下內容

# 表示使用 androidx
android.useAndroidX=true
# 表示將第三方庫遷移到 androidx
android.enableJetifier=true
複製程式碼

下面進入到 my_flutter 目錄下,在 你的android專案/my_flutter/.android/Flutter/build.gradle 中對庫的依賴部分進行修改

如果預設的內容如下:

dependencies {
    testImplementation 'junit:junit:4.12'
    implementation 'com.android.support:support-v13:27.1.1'
    implementation 'com.android.support:support-annotations:27.1.1'
}
複製程式碼

將所有依賴修改為androidx的版本:

dependencies {
    testImplementation 'junit:junit:4.12'
    implementation 'androidx.legacy:legacy-support-v13:1.0.0'
    implementation 'androidx.annotation:annotation:1.0.0'
}
複製程式碼

在android studio上點選完 Sync Now 同步之後

再進入下面的目錄 你的android專案/my_flutter/.android/Flutter/src/main/java/io/flutter/facade/ 目錄下,對 Flutter.javaFlutterFragment.java 分別進行修改

修改FlutterFragment.java

原本的依賴如下

手把手教你在Android專案中接入Flutter,在Flutter中使用Android佈局

將報錯部分替換為androidx的版本

import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
複製程式碼

修改Flutter.java

原本的依賴如下

手把手教你在Android專案中接入Flutter,在Flutter中使用Android佈局

將報錯部分替換為androidx的版本

import androidx.annotation.NonNull;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.OnLifecycleEvent;
複製程式碼

那麼現在,androidx帶來的問題就解決了,下面就開始準備正式接入Flutter

在flutter中編輯入口

進入 my_flutter 目錄中的lib目錄,可以看到會有系統自帶的 main.dart 檔案,這是一個預設的計數器頁面,我們修改一部分:

void main() => runApp(getRouter(window.defaultRouteName));

Widget getRouter(String name) {
  switch (name) {
    case 'route1':
      return MyApp();
    default:
      return Center(
        child: Text('Unknown route: $name', textDirection: TextDirection.ltr),
      );
  }
}
複製程式碼

將入口更換為通過“route1" 命名進入進入

接下來就是在android中進行操作了

在android中接入flutter

進入到android專案,在MainActivity中,我們做如下操作:

        bt_flutter.setOnClickListener {
            val flutterView = Flutter.createView(
                this@MainActivity,
                lifecycle,
                "route1"
            )
            val layout = ConstraintLayout.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT
            )
            layout.leftMargin = 0
            layout.bottomMargin = 26
            addContentView(flutterView, layout)

        }
複製程式碼

從上面的程式碼可以看到,我們通過一個按鈕的點選事件去展示了flutter的計數器頁面。實際效果如下:

手把手教你在Android專案中接入Flutter,在Flutter中使用Android佈局

那麼android接入flutter就結束了,下面是在flutter中接入android

在Flutter中接入android介面

我們可以新建一個flutter專案,用於測試這個例子

因為用到了kotin,所以使用以下命令

flutter create -a kotlin counter_native
複製程式碼

專案建立好之後,就可以開始了,在開始之前,我們首先可以瞭解以下如何在flutter中拿到android中的資料

獲取android資料

關於如何去獲取資料,主要還是使用 MethodChannel

看一下android中MainActivity的程式碼

class MainActivity: FlutterActivity() {

  private val channelName = "samples.flutter.io/counter_native";

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    GeneratedPluginRegistrant.registerWith(this)
    MethodChannel(flutterView, channelNameTwo).setMethodCallHandler { methodCall, result ->
      when(methodCall.method){
        "getCounterData" -> {
          result.success(getCounterData())
        }
        else -> {
          result.notImplemented();
        }
      }
    }
  }

  private fun getCounterData():Int{
    return 100;
  }
}
複製程式碼

MethodChannel 的結果回撥中,我們進行了篩選,如果方法名是 getCounterData 就直接返回100

接下來在flutter中編寫下面的程式碼:

static const platform =
      const MethodChannel('samples.flutter.io/counter_native');
      
void getCounterData() async {
    int data;
    try {
      final int result = await platform.invokeMethod('getCounterData');
      data = result;
    } on PlatformException catch (e) {
      data = -999;
    }
    setState(() {
      counterData = data;
    });
  }
複製程式碼

效果如下:

手把手教你在Android專案中接入Flutter,在Flutter中使用Android佈局

獲取android的資料就說到這裡,下面就是去獲取android的頁面了

獲取android的佈局

相較於資料而言,拿到android的佈局就要複雜的多

建立android檢視

在android專案裡面,建立一個想要展示在flutter中的佈局,這裡,我們結合xml檔案來建立佈局,不過使用xml的方式,會出現R檔案找不到的情況,這時候編譯器會報錯,暫時不用去管:

class CounterView(context: Context, messenger: BinaryMessenger, id: Int)
    : PlatformView, MethodChannel.MethodCallHandler {
    
    private var methodChannel: MethodChannel =
            MethodChannel(messenger, "samples.flutter.io/counter_view_$id")
    private var counterData: CounterData = CounterData()
    private var view: View = LayoutInflater.from(context).inflate(R.layout.test_layout, null);
    private var myText: TextView


    init {
        methodChannel.setMethodCallHandler(this)
        myText = view.findViewById(R.id.tv_counter)
    }


    override fun getView(): View {
        return view
    }

    override fun dispose() {

    }

    override fun onMethodCall(methodCall: MethodCall, result: MethodChannel.Result) {
        when (methodCall.method) {
            "increaseNumber" -> {
                counterData.counterData++
                myText.text = "當前Android的Text數值是:${counterData.counterData}"
                result.success(counterData.counterData)
            }
            "decreaseNumber" -> {
                counterData.counterData--
                myText.text = "當前Android的Text數值是:${counterData.counterData}"
                result.success(counterData.counterData)
            }
            "decreaseSize" -> {
                if(myText.textSize > 0){
                    val size = myText.textSize
                    myText.setTextSize(TypedValue.COMPLEX_UNIT_PX,size-1)
                    result.success(myText.textSize)
                } else{
                    result.error("出錯", "size無法再小了!", null)
                }
            }
            "increaseSize" -> {
                if(myText.textSize < 100){
                    val size = myText.textSize
                    myText.setTextSize(TypedValue.COMPLEX_UNIT_PX,size+1)
                    result.success(myText.textSize)
                } else{
                    result.error("出錯", "size無法再大了!", null)
                }
            }
            "setText" -> {
                myText.text = (methodCall.arguments as String)
                result.success(myText.text)
            }
            else -> {
                result.notImplemented();
            }
        }
    }
}
複製程式碼

上面的 CounterData 類是用於儲存資料建立的一個類:

class CounterData(var counterData: Int = 0) {
}
複製程式碼

接下來,我們建立一個 CounterViewFactory 類用於獲取到佈局:

class CounterViewFactory(private val messenger: BinaryMessenger)
    : PlatformViewFactory(StandardMessageCodec.INSTANCE) {

    override fun create(context: Context, id: Int, o: Any?): PlatformView {
        return CounterView(context, messenger, id)
    }
}
複製程式碼

最後建立一個 CounterViewPlugin.kt 檔案,它用於註冊檢視,相當於初始化入口

class CounterViewPlugin{
    fun registerWith(registrar: Registrar) {
        registrar.platformViewRegistry().registerViewFactory("samples.flutter.io/counter_view", CounterViewFactory(registrar.messenger()))
    }
}
複製程式碼

建立完成後,在MainActivity中進行檢視註冊:

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    CounterViewPlugin().registerWith(flutterView.pluginRegistry.registrarFor("CounterViewPlugin"))
    ...
  }
複製程式碼

接下來,就是在flutter中需要做的一些事情了

在flutter中獲取android檢視

在flutter裡面,想要拿到android的檢視,需要通過 AndroidView 去獲取

  Widget build(BuildContext context) {
    if (Platform.isAndroid) {
      return AndroidView(
        viewType: 'samples.flutter.io/counter_view',
        onPlatformViewCreated: _onPlatformViewCreated,
      );
    }
    return Text(
        '$defaultTargetPlatform 還不支援這個佈局');
  }
複製程式碼

在 onPlatformViewCreated 方法中,我們需要建立 MethodChannel ,用於呼叫android中編寫的方法,我們可以封裝一個Controller去處理這些邏輯:

  final CounterController counterController;

  void _onPlatformViewCreated(int id) {
    if (widget.counterController == null) {
      return;
    }
    widget.counterController.setId(id);
  }
複製程式碼

下面是 CounterController

typedef void CounterViewCreatedCallBack(CounterController controller);


class CounterController {
  MethodChannel _channel;


  void setId(int id){
    _channel = new MethodChannel('samples.flutter.io/counter_view_$id');
    print("id");
  }

  Future increaseNumber() async {
    final int result = await _channel.invokeMethod(
      'increaseNumber',
    );
    print("result:${result}");
  }

  Future decreaseNumber() async {
    final int result = await _channel.invokeMethod(
      'decreaseNumber',
    );
  }
  Future increaseSize() async {
    final  result = await _channel.invokeMethod(
      'increaseSize',
    );
  }
  Future decreaseSize() async {
    final  result = await _channel.invokeMethod(
      'decreaseSize',
    );
  }

  Future setText(String text) async {
    final  result = await _channel.invokeMethod(
      'setText',text,
    );
  }

}
複製程式碼

效果如下:

手把手教你在Android專案中接入Flutter,在Flutter中使用Android佈局


附錄

一個超適合Flutter入門的Todo-List專案:

Todo-List-App

相關文章