Kotlin + MVP + Flutter ,让你可以在自己的项目中集成 Flutter 并使用

通过学习本片文章中的知识点,你可以避免掉很多坑,从而轻松的实现 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 就不再赘述,在我的上一篇文章,已经详细介绍过了。

扫描二维码关注公众号,回复: 2893550 查看本文章

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 ~~~

猜你喜欢

转载自juejin.im/post/5b7cf52e51882542c963f0f1