Kotlin + MVP + Flutter ,讓你可以在自己的專案中整合 Flutter 並使用

JD-CP發表於2018-08-22

通過學習本片文章中的知識點,你可以避免掉很多坑,從而輕鬆的實現 Flutter 在 Android 專案中的整合。

簡介

1. Kotlin

Kotlin,由 JetBrains 於 2011.07 推出,一款面向 JVM 在 Java 虛擬機器上執行的靜態型別程式語言。

相比 Java,它可以靜態檢測很多陷阱,比如常見多發的空指標,所以開發效率更高。

而且通過支援variable type inference,higher-order functions (closures),extension functions,

mixins and first-class delegation等實現,使得它比 Java 更加簡潔。雖然它與 Java 語法並不相容,

但 Kotlin 可以和 Java 程式碼相互運作。更為重要的是,

在 2017 年的 Goofle I/O 上,也宣佈 kotlin 為 Android 的官方開發語言。

github 地址:Kotlin

2. MVP

在這裡,MVP 就不再贅述,在我的上一篇文章,已經詳細介紹過了。

demo 裡的是 Kotlin 版,但實現原理都是一樣的。

有興趣的點下方連結:

從 0 到 1,帶你解剖 MVP 的神祕之處,並自己動手實現 MVP !

3. Flutter

Flutter,由 Google 在 2018. 02 推出的移動UI框架,

可以快速在 Android 和 iOS 上構建高質量的原生使用者介面。

Flutter 的優勢,在這裡我也不再多說了。在 Flutter 中文網 都是有的。

優勢有很多,當然劣勢也很多!雖說跨平臺,但是對於適配問題,還需要去優化並解決。

效能相關,經常會出現一些卡頓現象,並且對於動畫的實現效果,也不是那麼的理想。

當然,還有很多其他的問題。畢竟現在釋出的也只是 beta 版,上述的這些問題,也會得到很好的解決的。

ok,下面切入正題,我們如何在專案中,去使用 Flutter。

疑問

在 Android 原有專案的基礎,去整合並使用 Flutter,肯定會有下面幾個疑問?

  1. 如何在原生上,展示 Flutter 介面?

  2. 原生如何給 Flutter 傳送資料?Flutter 如何接收?

  3. Flutter 如何呼叫原生的 method ?通過什麼來呼叫?

  4. 我們知道在 Flutter 中,主入口只有一個 void main()

    如果在原生介面 A,要顯示一個 ListView。在原生介面 B,要顯示一個 webView

    那我們在 Flutter 中,通過什麼來判斷我要載入的是 ListView 還是 webView 呢?

實現

ps:如果電腦前的同學沒有安裝 Flutter,建議先安裝。

Flutter 下載安裝地址

1. 在 Android 原生的專案基礎中,如何整合 Flutter

  1. 開啟你的專案,找到 Terminal,輸入終端命令:flutter channel

    預設分支應該是 beta,現在我們需要切換到 master 分支。

    繼續輸入終端命令:flutter channel master

    等待執行完畢之後,我們就成功的切換到了 master 分支。為什麼要切換到 master 分支?

    因為我們在安裝 Flutter 的時候,預設安裝的是 beta 版本。

    該版本,目前是不支援在現有專案中整合 Flutter Module 模組功能的。

    如果在 beta 版本中,執行了建立 Module 命令:flutter create -t module 你要建立的庫的名字

    它會提示你 "module" is not an allowed value for option "template"

  2. 執行終端命令,建立你的 Flutter Library:flutter create -t module flutter_library

    等待執行,建立成功後,會如下所示:

    這裡寫圖片描述
    注意:命令中的 flutter_library, 是我對 Flutter Library 的命名。你可以替換為你的命名。

  3. 將 flutter_library 新增到 Android 工程

    找到 Project 層 setting.gradle 檔案並開啟,新增如下程式碼:

    setBinding(new Binding([gradle: this]))
    evaluate(new File(
            settingsDir.parentFile,
            '/你的工程目錄名/flutter_library/.android/include_flutter.groovy'
    ))
    複製程式碼

    編譯通過後,在 app 目錄下的 build.gradle,新增依賴:

    dependencies {
        implementation project(':flutter')
    }
    複製程式碼

至此,我麼已經成功將 Flutter Module 新增到 Android 工程中了。是不是很簡單?skr skr skr ......

2. 在原生上,如何展示 Flutter 介面?

開啟我們 app 目錄下的 MainActivity,新增如下程式碼:

addContentView(Flutter.createView(this, lifecycle, "route1"),
                FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT));
複製程式碼

以上程式碼,就是建立了一個寬高均充滿螢幕的 FlutterView,可以將 FlutterView 看作為展示 Flutter Widget 的容器。

”route1“ 是什麼鬼?這個待會兒再解釋,現在你不需要關心。現在執行程式碼,會看到如下所示:

這裡寫圖片描述

現在呢,我們已經成功在原生上,將 Flutter 介面成功的展示出來。

3. 原生如何給 Flutter 傳送資料?Flutter 如何接收?

在這裡,我們需要用到 EventChannel

這個類的作用,可以簡單理解為從原生向 Flutter,push data:主動的推送資料。

修改後的 Activity 程式碼如下:

class MainActivity : AppCompatActivity() {

    companion object {

        val GET_NAME_CHANNEL = "sample.flutter.io/get_name"

    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val flutterView = Flutter.createView(this, lifecycle, "route1")

        addContentView(flutterView, FrameLayout.LayoutParams(
                FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT));

        EventChannel(flutterView, GET_NAME_CHANNEL).setStreamHandler(object : EventChannel.StreamHandler {
            override fun onListen(p0: Any?, events: EventChannel.EventSink?) {
                events?.success(getName())
            }

            override fun onCancel(p0: Any?) {

            }
        })

    }

    fun getName(): String? = "flutter_library"

}
複製程式碼

看 Flutter 端接收的程式碼:

class MyHomePage extends StatefulWidget {
  final String title;

  MyHomePage({Key key, this.title}) : super(key: key);

  @override
  _MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  static const EventChannel eventChannel =
      EventChannel('sample.flutter.io/get_name');

  String _name = 'unknown';

  void _receiveData() {}

  @override
  void initState() {
    super.initState();
    eventChannel.receiveBroadcastStream().listen(_onEvent, onError: _onError);
  }

  void _onEvent(Object event) {
    setState(() {
      _name = event.toString();
    });
  }

  void _onError(Object error) {
    setState(() {
      _name = 'Battery status: unknown.';
    });
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: new Center(
        child: new Column(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: <Widget>[
            new Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                new Text('Flutter', key: const Key('Battery level label')),
                new Padding(
                  padding: const EdgeInsets.all(16.0),
                  child: new RaisedButton(
                    child: const Text('Refresh'),
                    onPressed: _receiveData,
                  ),
                ),
              ],
            ),
            new Text('從原生 push 過來的資料:' + _name),
          ],
        ),
      ),
    );
  }
}
複製程式碼

注意:在建立 EventChannel 物件的時候,傳入的 name,

一定要和你在原生中傳入的 name 對應起來,否則將接收不到。這個很好理解。

4. Flutter 如何呼叫原生的 method ?通過什麼來呼叫?

MethodChannel

Flutter 向原生呼叫方法或獲取資料時,需要用到這個類來實現。

接下來看 Android 端實現程式碼,修改後如下:

class MainActivity : AppCompatActivity() {

    companion object {

        val PUSH_CHANNEL = "sample.flutter.io/push"
        val PULL_CHANNEL = "sample.flutter.io/pull"

    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val flutterView = Flutter.createView(this, lifecycle, "route1")

        addContentView(flutterView, FrameLayout.LayoutParams(
                FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT));

        EventChannel(flutterView, PUSH_CHANNEL).setStreamHandler(object : EventChannel.StreamHandler {
            override fun onListen(p0: Any?, events: EventChannel.EventSink?) {
                events?.success(getName())
            }

            override fun onCancel(p0: Any?) {

            }
        })

        MethodChannel(flutterView, PULL_CHANNEL).setMethodCallHandler { methodCall, result ->
            run {
                if (methodCall.method.equals("refresh")) {
                    refresh()
                    result.success("")
                } else {
                    result.notImplemented()
                }
            }
        }

    }

    fun getName(): String? = "flutter_library"

    fun refresh() {
        showShort("refresh")
    }

}
複製程式碼

當 Flutter 呼叫 refresh 方法時,android 端呼叫 refresh() 方法,這裡實現了一個簡單的吐司,並返回了空字串。

當然你也可以做其他操作,比如跳轉頁面、實現動畫、獲取資料等等。

5. 判斷不同的 route ,載入不同的介面

我們在 MainActivity 載入 FlutterView 時,有傳入一個引數 "route1"

點選進入 createView 的原始碼時,有這樣一句註釋:

The default initialRoute is "/".
複製程式碼

這裡寫圖片描述

通過檢視原始碼得知,initialRoute 的預設值為 "/"。因為入口只有一個:void main()

所以判斷 route ,載入不同介面的邏輯應該也就在這裡了。具體請看程式碼實現:

void main() => runApp(new MyApp(window.defaultRouteName));

class MyApp extends StatelessWidget {
  final String route;

  MyApp(this.route);

  @override
  Widget build(BuildContext context) {
    switch (route) {
      case "route1":
        return new MaterialApp(
          title: "Android-Flutter-Demo",
          home: new MyHomePage(title: 'Android-Flutter-Demo'),
        );
        break;
      default:
        return Center(
          child:
              Text('Unknown route: $route', textDirection: TextDirection.ltr),
        );
    }
  }
}
複製程式碼

怎麼樣,很簡單的吧?到這裡呢,文章開頭說的那四個問題,我們也都一一解決掉了。

下面說一下我的 demo 實現,在 Android 端獲取介面資料,然後轉化成 json 格式,

通過 Flutter 端的呼叫,以列表形式進行展示。最後效果圖如下:

這裡寫圖片描述

demo 中的程式碼實現,沒有考慮實際需求。

只是為了驗證,android 和 flutter 混合開發,這條路是行得通的。

最後,奉上 github demo 地址:

Android-Flutter-Demo

喜歡的同學可以點點 star ~~~

相關文章