Android 项目嵌入Flutter Module(一)

2,036 阅读5分钟

前言

前面两篇文章主要是对Flutter 项目 Android 端项目应用层大概启动流程源码的分析,这篇文章我们就重点来看下一个纯Android项目如何嵌入Flutter Module,以及如何通过FlutterActivity展示Flutter UI

将 Flutter 集成到现有应用

可以按照上面那个链接手动配置一下依赖,下面我们先看下大致效果(下面的图都有点大,凑合看吧)

下面我们分析一下整个流程,配置混合开发环境就不说了,按照文档配置就行

正文

之前分析FlutterActivity的时候,有说到这个类有几个静态方法,现在我们来分析一下

  
  @NonNull
  public static Intent createDefaultIntent(@NonNull Context launchContext) {
    return withNewEngine().build(launchContext);
  }

  @NonNull
  public static NewEngineIntentBuilder withNewEngine() {
    return new NewEngineIntentBuilder(FlutterActivity.class);
  }
  
  public static class NewEngineIntentBuilder {
    private final Class<? extends FlutterActivity> activityClass;
    private String initialRoute = DEFAULT_INITIAL_ROUTE;
    private String backgroundMode = DEFAULT_BACKGROUND_MODE;

    protected NewEngineIntentBuilder(@NonNull Class<? extends FlutterActivity> activityClass) {
      this.activityClass = activityClass;
    }

    @NonNull
    public NewEngineIntentBuilder initialRoute(@NonNull String initialRoute) {
      this.initialRoute = initialRoute;
      return this;
    }

    @NonNull
    public NewEngineIntentBuilder backgroundMode(@NonNull BackgroundMode backgroundMode) {
      this.backgroundMode = backgroundMode.name();
      return this;
    }

    @NonNull
    public Intent build(@NonNull Context context) {
      return new Intent(context, activityClass)
          .putExtra(EXTRA_INITIAL_ROUTE, initialRoute)
          .putExtra(EXTRA_BACKGROUND_MODE, backgroundMode)
          .putExtra(EXTRA_DESTROY_ENGINE_WITH_ACTIVITY, true);
    }
  }

  public static CachedEngineIntentBuilder withCachedEngine(@NonNull String cachedEngineId) {
    return new CachedEngineIntentBuilder(FlutterActivity.class, cachedEngineId);
  }

  public static class CachedEngineIntentBuilder {
    private final Class<? extends FlutterActivity> activityClass;
    private final String cachedEngineId;
    private boolean destroyEngineWithActivity = false;
    private String backgroundMode = DEFAULT_BACKGROUND_MODE;

    protected CachedEngineIntentBuilder(
        @NonNull Class<? extends FlutterActivity> activityClass, @NonNull String engineId) {
      this.activityClass = activityClass;
      this.cachedEngineId = engineId;
    }

    public CachedEngineIntentBuilder destroyEngineWithActivity(boolean destroyEngineWithActivity) {
      this.destroyEngineWithActivity = destroyEngineWithActivity;
      return this;
    }

    @NonNull
    public CachedEngineIntentBuilder backgroundMode(@NonNull BackgroundMode backgroundMode) {
      this.backgroundMode = backgroundMode.name();
      return this;
    }

    @NonNull
    public Intent build(@NonNull Context context) {
      return new Intent(context, activityClass)
          .putExtra(EXTRA_CACHED_ENGINE_ID, cachedEngineId)
          .putExtra(EXTRA_DESTROY_ENGINE_WITH_ACTIVITY, destroyEngineWithActivity)
          .putExtra(EXTRA_BACKGROUND_MODE, backgroundMode);
    }
  }

这几个方法就是为我们Android 跳转到一个FlutterActivity准备的

可以看到createDefaultIntent这个方法实际上是 实例化了一个NewEngineIntentBuilder,然后实例化一个Intent,目标class就是FlutterActivity.class,,然后我们传递值都是默认的

  return new Intent(context, activityClass)
            .putExtra(EXTRA_INITIAL_ROUTE, initialRoute)
            .putExtra(EXTRA_BACKGROUND_MODE, backgroundMode)
            .putExtra(EXTRA_DESTROY_ENGINE_WITH_ACTIVITY, true);

  private String initialRoute = DEFAULT_INITIAL_ROUTE;
  private String backgroundMode = DEFAULT_BACKGROUND_MODE;


	static final String EXTRA_DESTROY_ENGINE_WITH_ACTIVITY =
      "destroy_engine_with_activity";
      
  static final String DEFAULT_INITIAL_ROUTE = "/";
  static final String DEFAULT_BACKGROUND_MODE = BackgroundMode.opaque.name();

  /** The mode of the background of a Flutter {@code Activity}, either opaque or transparent. */
  public enum BackgroundMode {
    /** Indicates a FlutterActivity with an opaque background. This is the default. */
    opaque,
    /** Indicates a FlutterActivity with a transparent background. */
    transparent
  }

可以看到初始化路由默认是“/”

默认背景模式为BackgroundMode.opaque,选择BackgroundMod.ransparent的背景模式会将此{FlutterActivity的内部FlutterView配置为使用FlutterTextureView来支持透明性。此选择对性能有重要影响,仅在实现应用程序设计时,才应使用透明背景。

下面我们看一下这个值EXTRA_DESTROY_ENGINE_WITH_ACTIVITY

   // FlutterActivity

   @Override
    public boolean shouldDestroyEngineWithHost() {
     // 1
      boolean explicitDestructionRequested =
          getIntent().getBooleanExtra(EXTRA_DESTROY_ENGINE_WITH_ACTIVITY, false);
      if (getCachedEngineId() != null || delegate.isFlutterEngineFromHost()) {
        // Only destroy a cached engine if explicitly requested by app developer.
        return explicitDestructionRequested;
      } else {
        // If this Activity created the FlutterEngine, destroy it by default unless
        // explicitly requested not to.
        return getIntent().getBooleanExtra(EXTRA_DESTROY_ENGINE_WITH_ACTIVITY, true);
      }
    }
  	
    @Override
    protected void onDestroy() {
      super.onDestroy();
      delegate.onDestroyView();
      delegate.onDetach();
      lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY);
    }
  
  	// FlutterActivityAndFragmentDelegate
  	
     void onDetach() {
      // 省略部分源码
      if (host.shouldDestroyEngineWithHost()) {
        flutterEngine.destroy();
		
        if (host.getCachedEngineId() != null) {
          FlutterEngineCache.getInstance().remove(host.getCachedEngineId());
        }

        flutterEngine = null;
      }
  }

从上面源码我们看到无论是纯Flutter 项目还是NewEngineIntentBuilder.build方法跳转到的新FlutterActivity默认都是设置为true,即FlutterActivity销毁的时候会将此Engine一并销毁。

然后我们看下原生代码(Kotlin)

val button: Button = root.findViewById(R.id.btnGoFlutter)
        button.setOnClickListener(fun(_: View) {
            startActivity(
                FlutterActivity
                    .withNewEngine()
                    .build(root.context)
            )
        })

其实很简单,就是一个startActivity方法,传递NewEngineIntentBuilder.build 实例化的Intent。

然后我们指定一个初始化路由路径

 startActivity(
                FlutterActivity
                    .withNewEngine()
                    .initialRoute("/second")
                    .backgroundMode(FlutterActivityLaunchConfigs.BackgroundMode.opaque)
                    .build(root.context)
            )

然后在Flutter端配置这个路由

Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      routes: {
        "/second": (context) => new SecondPage(),
      },
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'DartEntrypoint is Main'),
    );
  }
  class SecondPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Second Page"),
      ),
      body: Center(
        child: Text(
          "This is Second Page",
          style: TextStyle(fontSize: 20, color: Colors.red),
        ),
      ),
    );
  }
}

然后看下启动效果

可以看到配置initialRoute就是这么一个效果

我们之前提到过DartEntrypointFunctionName这个方法,用来配置dart首次启动默认执行的方法名字,默认的是main方法。

接下来我们自己创建一个FlutterEngine,指定其DartEntrypoint,而我们的启动方法也要换成withCachedEngine进行构建

  1. 首先我们要创建自定义的applicaiton 继承android.app.Application

    class MyApplication : Application() {
        private lateinit var flutterEngine :FlutterEngine
        override fun onCreate() {
            super.onCreate()
            flutterEngine = FlutterEngine(this,null,false)
            flutterEngine.dartExecutor.executeDartEntrypoint(
                DartExecutor.DartEntrypoint(FlutterMain.findAppBundlePath(), "main1")
            )
            FlutterEngineCache.getInstance().put("my_engine_id",flutterEngine)
        }
    
    

    然后指定

    <application
        android:name=".MyApplication"
    

    从上面代码可以看到,我们在onCreate里实例化一个新的FlutterEngine,同时指定flutterEngine.dartExecutor的默认执行方法名为main1,然后将其添加到FlutterEngineCache缓存池中。

  2. 接着我们要修改启动方式

                startActivity(
    //                FlutterActivity
    //                    .withNewEngine()
    //                    .initialRoute("/second")
    //                    .backgroundMode(FlutterActivityLaunchConfigs.BackgroundMode.opaque)
    //                    .build(root.context)
                    FlutterActivity
                        .withCachedEngine("my_engine_id")
                        .destroyEngineWithActivity(true)
                        .build(root.context)
                )
            })
    

    可以看到使用了Id为my_engine_id的缓存engine进行启动

  3. 接着我们配置Flutter端

    void main1() => runApp(MyApp1());
    
    class MyApp1 extends StatelessWidget {
      // This widget is the root of your application.
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            primarySwatch: Colors.red,
          ),
          home: MyHomePage(title: 'DartEntrypoint is Main1'),
        );
      }
    }
    

    可以看到我们创建了一个main1的方法,接下来看下效果

可以看到确实是通过main1入口进去的Flutter Module

关于初始化路由和启动方法源码介绍可以看Flutter Android启动源码分析(一)及后续文章

上面使用缓存Engine这里是有一个bug的,我们先看下效果和Log

可以看到报错提示我们这个缓存Id对应的Engine不存在...

那好吧,我们再来看下这个方法

// FlutterActivity

   @Override
    public boolean shouldDestroyEngineWithHost() {
     // 1
      boolean explicitDestructionRequested =
          getIntent().getBooleanExtra(EXTRA_DESTROY_ENGINE_WITH_ACTIVITY, false);
      if (getCachedEngineId() != null || delegate.isFlutterEngineFromHost()) {
        // Only destroy a cached engine if explicitly requested by app developer.
        return explicitDestructionRequested;
      } else {
        // If this Activity created the FlutterEngine, destroy it by default unless
        // explicitly requested not to.
        return getIntent().getBooleanExtra(EXTRA_DESTROY_ENGINE_WITH_ACTIVITY, true);
      }
    }
    
    
    // FlutterActivityAndFragmentDelegate
  	
     void onDetach() {
      // 省略部分源码
      if (host.shouldDestroyEngineWithHost()) {
        flutterEngine.destroy();
		
        if (host.getCachedEngineId() != null) {
          FlutterEngineCache.getInstance().remove(host.getCachedEngineId());
        }

        flutterEngine = null;
      }
    }

如果我们构造一个使用缓存Engine的时候,传递EXTRA_DESTROY_ENGINE_WITH_ACTIVITY值为true的时候,FlutterActivity和FlutterEngineCache会在onDestroy方法中一起销毁,所以再次Intent就会报错

将这个值改为false,或者删除缓存池中的缓存ID,重新操作engine并实例化,然后添加到缓存里就可以解决这个问题。

FlutterFragment的使用方式和FlutterActivity大同小异,这里就不做介绍了,可以看文档

最后我强烈建议大家看一下这篇文章,

控制加载顺序,优化性能与内存

总结

通过这篇文章我们可以学习到

  • Android 嵌入Flutter module 的基本用法以及使用注意事项
  • 目前比较成熟的混合开发的库,阿里的flutter_boost

笔者之前也没有做过混合开发,也是一头雾水。不过通过分析Flutter 在Android 应用层上的启动源码、然后结合官方提供的文档做一个demo以及博客的总结,这些流程下来的话基本掌握的差不多。

所以我们在平时开发过程中也应该养成多看源码,多思考的好习惯。确实能在源码里看到一些很好的编码规范和编程思想

下篇文章会讲一下我是如何解决这个Android启动Flutter后白屏和黑屏的问题。