Android 原生项目集成Flutter

可以将两个Flutter module宿主App克隆到同一个目录下面运行看看效果。

创建Flutter module

假定存在的Android原生项目目录是some/path/MyApp,在MyApp所在的目录下创建一个flutter module

$ cd some/path/
$ flutter create -t module my_flutter

上面的命令会创建一个名为my_flutter的flutter module,并且会在my_flutter目录下生成一个./android的隐藏子目录,这个目录的作用就是将创建好的flutter module包装成一个 android library

创建好的flutter module会为我们自动生成一个 main.dart。我们可以直接使用Android studio打开创建好的flutter module,先运行一下看下效果。

main.jpg

我们修改一下代码去掉顶部的AppBar,修改后页面如下图所示。

remove_appbar.jpg

宿主app的要求

在连接flutter module和宿主app之前,你需要确保在宿主app的build.gradle文件中声明如下

android {
    //...
    compileOptions {
        sourceCompatibility 1.8
        targetCompatibility 1.8
    }
}

让宿主app依赖flutter module

在宿主app的settings.gradle中把flutter module包含进来作为子项目

// MyApp/settings.gradle
include ':app'                                     // assumed existing content
setBinding(new Binding([gradle: this]))                                 // new
evaluate(new File(                                                      // new
  settingsDir.parentFile,                                               // new
  'my_flutter/.android/include_flutter.groovy'                          // new
))                                                                      // new

然后在app/build.gradle文件中声明如下

// MyApp/app/build.gradle

dependencies {
  implementation project(':flutter')
  //...
}

然后同步一下项目宿主工程MyApp。

在Java代码中使用Flutter module

使用 flutter module

使用flutter module生成的Java API 来添加 Flutter views 到宿主app中。flutter module生成的Java API 路径如下图所示。

path.png
  1. 可以直接使用Flutter.createView实现添加 Flutter views 到宿主app中。

宿主app的MainActivity的布局文件如下

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/btnFlutterCreateView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="使用Flutter.createView方法"
        android:textAllCaps="false"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/btnFlutterFragment"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="使用FlutterFragment"
        android:textAllCaps="false"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/btnFlutterCreateView" />

    <FrameLayout
        android:id="@+id/flContainer"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/btnFlutterFragment" />

</android.support.constraint.ConstraintLayout>

当点击btnFlutterCreateView按钮的时候,我们创建一个flutterView,然后添加到布局文件中的flContainer中去。

// MyApp/app/src/main/java/some/package/MainActivity.java
fab.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        //创建flutterView
        View flutterView = Flutter.createView(MainActivity.this, getLifecycle(), null);
        FrameLayout.LayoutParams layout = new FrameLayout.LayoutParams(
        FrameLayout.LayoutParams.MATCH_PARENT, 800);
        addContentView(flutterView, layout);
    }

});

看下运行效果


createView.jpg
  1. 使用Flutter.createFragment方法创建一个Fragment,使用Flutter.createFragment方法我们不需要传递一个Lifecycle对象,FlutterFragment可以自己处理生命周期。
// MyApp/app/src/main/java/some/package/SomeActivity.java
  private fun createFragment() {
      supportFragmentManager
              .beginTransaction()
              .replace(R.id.flContainer, Flutter.createFragment(null))
              .commit()
}

运行效果是和上面的一样的,就不贴图了。

我们看一下Flutter.createViewFlutter.createFragment的方法签名。

public static FlutterView createView(final Activity activity, final Lifecycle lifecycle, 
final String initialRoute) {
    //...
}

public static FlutterFragment createFragment(String initialRoute) {
    //...
}

这两个方法都有一个initialRoute参数,这个是标记加载的flutter的初始界面。我们可以传递不同的initialRoute来打开不同的界面。在flutter中,可以通过window.defaultRouteName获取这个参数值。下面展示一个打开不同界面的例子。

  1. 在flutter_module中新建一个route1.dart
import 'package:flutter/material.dart';

///
/// Created by dumingwei on 2019/4/9.
/// Desc:  rout1
///

class Route1App extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Route1(),
    );
  }
}

class Route1 extends StatefulWidget {
  String text;

  Route1({Key key, this.text}) : super(key: key);

  @override
  State createState() {
    return Route1State();
  }
}

class Route1State extends State<Route1> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Center(
          child: Text(
            'Hello world ,i am route1',
          ),
        ),
      ),
    );
  }
}
  1. 修改一下flutter module的main.dart

flutter module的入口是lib/main.dart。默认创建的widget是MyApp

import 'package:flutter/material.dart';

///lib/main.dart

void main() => runApp(MyApp());

class MyApp extends StatelessWidget{
   ///...
}

现在我们要根据传入的initialRoute值来初始化widget。

import 'dart:ui';

import 'package:flutter/material.dart';

import 'route1.dart';

//调用window.defaultRouteName获取传入的传入的`initialRoute`值
void main() => runApp(_widgetForRoute(window.defaultRouteName));

Widget _widgetForRoute(String route) {
  switch (route) {
    case 'route1'://创建Route1App
      return Route1App();
    default:
      return MyApp();
  }
}
//...

在上面的代码中如果传入的initialRoute值是'route1',我们就初始化Route1App。

下面修改宿主app中MainActivity的代码。

// MyApp/app/src/main/java/some/package/SomeActivity.java

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

        btnFlutterFragment.setOnClickListener {
            createFragment()
        }
    }

  private fun createFragment() {
      supportFragmentManager
              .beginTransaction()
              .replace(R.id.flContainer, Flutter.createFragment("route1"))
              .commit()
  }

在上面的代码中Flutter.createFragment()方法我们传入的initialRoute是"route1"。

现在我们重新运行一下MyApp,效果如下。

route1.jpg

热加载/热重启和调试Dart代码

完整的IDE集成以支持使用混合应用程序的Flutter / Dart代码的工作正在进行中。但是flutter命令行工具和Dart Observatory web user interface已经提供了一些基本功能。

连接真机或者模拟器。然后使Flutter 命令行工具监听您的应用程序。

切换到flutter module的目录下,在命令行输入下面的命令

$ cd some/path/my_flutter
$ flutter attach
Waiting for a connection from Flutter on LLD AL20...

然后使用debug模式启动宿主app,然后导航到使用flutter的地方。然后回到命令行(我这里使用的命令行就是用Android Studio 打开 my_flutter,Android Studio中自带的命令行),可以看到类似下面输出的信息。

Done.
Syncing files to device LLD AL20...                              1,752ms

🔥  To hot reload changes while running, press "r". To hot restart (and rebuild state), press "R".
An Observatory debugger and profiler on LLD AL20 is available at: http://127.0.0.1:54043/
For a more detailed help message, press "h". To detach, press "d"; to quit, press "q".

现在你可以修改flutter中的代码,然后在命令行里输入r来进行热更新。输入'R'来进行热重启(重新构建flutter widget的状态)。你也可以黏贴上面输出信息中的URL到浏览器中来使用Dart Observatory来设置断点,分析内存保留以及其他调试任务。更多详细的帮助信息请输入h,断开连接请输入d,退出请输入q

我们先来试一试热更新。按照上面的步骤,使用debug的模式启动MyApp以后,我们点击使用Flutter.createView方法按钮,输出如下。

route1.jpg

然后我们修改my_flutter中的route1.dart中显示的文字。

class Route1App extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Route1(),
    );
  }
}

class Route1 extends StatefulWidget {
  String text;

  Route1({Key key, this.text}) : super(key: key);

  @override
  State createState() {
    return Route1State();
  }
}

class Route1State extends State<Route1> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Center(
          child: Text(
            //修改文字
            'Hello world ,i am route1 haha',
          ),
        ),
      ),
    );
  }
}

然后在命令行里输入r。发现热更新起作用了。

perss_r.jpg

关于Dart Observatory以后再研究。

Flutter module源码
宿主App源码

参考链接

  1. Android 原生项目集成Flutter

  2. Add Flutter to existing apps

  3. 原生App项目集成flutter混合开发详细指南

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容