diff --git a/add_to_app/multiple_flutters/multiple_flutters_android/.gitignore b/add_to_app/multiple_flutters/multiple_flutters_android/.gitignore new file mode 100644 index 000000000..aa724b770 --- /dev/null +++ b/add_to_app/multiple_flutters/multiple_flutters_android/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/add_to_app/multiple_flutters/multiple_flutters_android/README.md b/add_to_app/multiple_flutters/multiple_flutters_android/README.md new file mode 100644 index 000000000..b6b394e88 --- /dev/null +++ b/add_to_app/multiple_flutters/multiple_flutters_android/README.md @@ -0,0 +1,17 @@ +# multiple_flutters_android + +This is an add-to-app sample that uses the Flutter engine group API to host +multiple instances of Flutter in the app. + +## Getting Started + +```sh +cd ../multiple_flutters_module +flutter pub get +cd - +open -a "Android Studio" multiple_flutters_android/ # macOS command +# (build and run) +``` + +For more information see +[multiple_flutters_module](../multiple_flutters_module/README.md). diff --git a/add_to_app/multiple_flutters/multiple_flutters_android/app/.gitignore b/add_to_app/multiple_flutters/multiple_flutters_android/app/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/add_to_app/multiple_flutters/multiple_flutters_android/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/add_to_app/multiple_flutters/multiple_flutters_android/app/build.gradle b/add_to_app/multiple_flutters/multiple_flutters_android/app/build.gradle new file mode 100644 index 000000000..9972035ce --- /dev/null +++ b/add_to_app/multiple_flutters/multiple_flutters_android/app/build.gradle @@ -0,0 +1,50 @@ +plugins { + id 'com.android.application' + id 'kotlin-android' +} + +android { + signingConfigs { + self { + } + } + compileSdkVersion 30 + + defaultConfig { + applicationId "dev.flutter.multipleflutters" + minSdkVersion 24 + targetSdkVersion 30 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + signingConfig debug.signingConfig + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } +} + +dependencies { + + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + implementation 'androidx.core:core-ktx:1.2.0' + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'com.google.android.material:material:1.1.0' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + testImplementation 'junit:junit:4.+' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + implementation project(':flutter') +} \ No newline at end of file diff --git a/add_to_app/multiple_flutters/multiple_flutters_android/app/proguard-rules.pro b/add_to_app/multiple_flutters/multiple_flutters_android/app/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/add_to_app/multiple_flutters/multiple_flutters_android/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/add_to_app/multiple_flutters/multiple_flutters_android/app/src/main/AndroidManifest.xml b/add_to_app/multiple_flutters/multiple_flutters_android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..734b96c27 --- /dev/null +++ b/add_to_app/multiple_flutters/multiple_flutters_android/app/src/main/AndroidManifest.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/add_to_app/multiple_flutters/multiple_flutters_android/app/src/main/java/dev/flutter/multipleflutters/App.kt b/add_to_app/multiple_flutters/multiple_flutters_android/app/src/main/java/dev/flutter/multipleflutters/App.kt new file mode 100644 index 000000000..47cd85705 --- /dev/null +++ b/add_to_app/multiple_flutters/multiple_flutters_android/app/src/main/java/dev/flutter/multipleflutters/App.kt @@ -0,0 +1,18 @@ +package dev.flutter.multipleflutters + +import android.app.Application +import io.flutter.embedding.engine.FlutterEngineGroup + +/** + * Application class for this app. + * + * This holds onto our engine group. + */ +class App : Application() { + lateinit var engines: FlutterEngineGroup + + override fun onCreate() { + super.onCreate() + engines = FlutterEngineGroup(this) + } +} diff --git a/add_to_app/multiple_flutters/multiple_flutters_android/app/src/main/java/dev/flutter/multipleflutters/DataModel.kt b/add_to_app/multiple_flutters/multiple_flutters_android/app/src/main/java/dev/flutter/multipleflutters/DataModel.kt new file mode 100644 index 000000000..d40587014 --- /dev/null +++ b/add_to_app/multiple_flutters/multiple_flutters_android/app/src/main/java/dev/flutter/multipleflutters/DataModel.kt @@ -0,0 +1,41 @@ +package dev.flutter.multipleflutters + +import java.lang.ref.WeakReference + +/** + * Interface for getting notifications when the DataModel is updated. + */ +interface DataModelObserver { + fun onCountUpdate(newCount: Int) +} + +/** + * A singleton/observable data model for the data shared between Flutter and the host platform. + * + * This is the definitive source of truth for all data. + */ +class DataModel { + companion object { + val instance = DataModel() + } + + private val observers = mutableListOf>() + + public var counter = 0 + set(value) { + field = value + for (observer in observers) { + observer.get()?.onCountUpdate(value) + } + } + + fun addObserver(observer: DataModelObserver) { + observers.add(WeakReference(observer)) + } + + fun removeObserver(observer: DataModelObserver) { + observers.removeIf { + if (it.get() != null) it.get() == observer else true + } + } +} diff --git a/add_to_app/multiple_flutters/multiple_flutters_android/app/src/main/java/dev/flutter/multipleflutters/DoubleFlutterActivity.kt b/add_to_app/multiple_flutters/multiple_flutters_android/app/src/main/java/dev/flutter/multipleflutters/DoubleFlutterActivity.kt new file mode 100644 index 000000000..f86e09ac0 --- /dev/null +++ b/add_to_app/multiple_flutters/multiple_flutters_android/app/src/main/java/dev/flutter/multipleflutters/DoubleFlutterActivity.kt @@ -0,0 +1,81 @@ +package dev.flutter.multipleflutters + +import android.content.Intent +import android.os.Bundle +import android.widget.FrameLayout +import android.widget.LinearLayout +import androidx.fragment.app.FragmentActivity +import androidx.fragment.app.FragmentManager +import io.flutter.embedding.android.FlutterFragment +import io.flutter.embedding.engine.FlutterEngineCache + +/** + * An activity that displays 2 FlutterFragments vertically. + */ +class DoubleFlutterActivity : FragmentActivity(), EngineBindingsDelegate { + private val topBindings: EngineBindings by lazy { + EngineBindings(activity = this, delegate = this, entrypoint = "topMain") + } + private val bottomBindings: EngineBindings by lazy { + EngineBindings(activity = this, delegate = this, entrypoint = "bottomMain") + } + private val numberOfFlutters = 2 + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val root = LinearLayout(this) + root.layoutParams = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.MATCH_PARENT + ) + root.orientation = LinearLayout.VERTICAL + root.weightSum = numberOfFlutters.toFloat() + + val fragmentManager: FragmentManager = supportFragmentManager + + setContentView(root) + + val app = applicationContext as App + + for (i in 0 until numberOfFlutters) { + val flutterContainer = FrameLayout(this) + root.addView(flutterContainer) + flutterContainer.id = 12345 + i + flutterContainer.layoutParams = LinearLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT, + 1.0f + ) + val engine = if (i == 0) topBindings.engine else bottomBindings.engine + FlutterEngineCache.getInstance().put(i.toString(), engine) + val flutterFragment = + FlutterFragment.withCachedEngine(i.toString()).build() + fragmentManager + .beginTransaction() + .add( + 12345 + i, + flutterFragment + ) + .commit() + } + + topBindings.attach() + bottomBindings.attach() + } + + override fun onDestroy() { + for (i in 0 until numberOfFlutters) { + FlutterEngineCache.getInstance().remove(i.toString()) + } + + super.onDestroy() + + bottomBindings.detach() + topBindings.detach() + } + + override fun onNext() { + val flutterIntent = Intent(this, MainActivity::class.java) + startActivity(flutterIntent) + } +} diff --git a/add_to_app/multiple_flutters/multiple_flutters_android/app/src/main/java/dev/flutter/multipleflutters/EngineBindings.kt b/add_to_app/multiple_flutters/multiple_flutters_android/app/src/main/java/dev/flutter/multipleflutters/EngineBindings.kt new file mode 100644 index 000000000..6c0ac0d17 --- /dev/null +++ b/add_to_app/multiple_flutters/multiple_flutters_android/app/src/main/java/dev/flutter/multipleflutters/EngineBindings.kt @@ -0,0 +1,86 @@ +package dev.flutter.multipleflutters + +import android.app.Activity +import io.flutter.FlutterInjector +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.embedding.engine.dart.DartExecutor +import io.flutter.plugin.common.MethodChannel + +/** + * This interface represents the notifications an EngineBindings may be receiving from the Flutter + * instance. + * + * What methods this interface has depends on the messages that are sent over the EngineBinding's + * channel in `main.dart`. Messages that interact with the DataModel are handled automatically + * by the EngineBindings. + * + * @see main.dart for what messages are getting sent from Flutter. + */ +interface EngineBindingsDelegate { + fun onNext() +} + +/** + * This binds a FlutterEngine instance with the DataModel and a channel for communicating with that + * engine. + * + * Messages involving the DataModel are handled by the EngineBindings, other messages are forwarded + * to the EngineBindingsDelegate. + * + * @see main.dart for what messages are getting sent from Flutter. + */ +class EngineBindings(activity: Activity, delegate: EngineBindingsDelegate, entrypoint: String) : + DataModelObserver { + val channel: MethodChannel + val engine: FlutterEngine + val delegate: EngineBindingsDelegate + + init { + val app = activity.applicationContext as App + // This has to be lazy to avoid creation before the FlutterEngineGroup. + val dartEntrypoint = + DartExecutor.DartEntrypoint( + FlutterInjector.instance().flutterLoader().findAppBundlePath(), entrypoint + ) + engine = app.engines.createAndRunEngine(activity, dartEntrypoint) + this.delegate = delegate + channel = MethodChannel(engine.dartExecutor.binaryMessenger, "multiple-flutters") + } + + /** + * This setups the messaging connections on the platform channel and the DataModel. + */ + fun attach() { + DataModel.instance.addObserver(this) + channel.invokeMethod("setCount", DataModel.instance.counter) + channel.setMethodCallHandler { call, result -> + when (call.method) { + "incrementCount" -> { + DataModel.instance.counter = DataModel.instance.counter + 1 + result.success(null) + } + "next" -> { + this.delegate.onNext() + result.success(null) + } + else -> { + result.notImplemented() + } + } + } + } + + /** + * This tears down the messaging connections on the platform channel and the DataModel. + */ + fun detach() { + // TODO: Uncomment after https://github.com/flutter/engine/pull/24644 is on stable. + // engine.destroy(); + DataModel.instance.removeObserver(this) + channel.setMethodCallHandler(null) + } + + override fun onCountUpdate(newCount: Int) { + channel.invokeMethod("setCount", newCount) + } +} diff --git a/add_to_app/multiple_flutters/multiple_flutters_android/app/src/main/java/dev/flutter/multipleflutters/MainActivity.kt b/add_to_app/multiple_flutters/multiple_flutters_android/app/src/main/java/dev/flutter/multipleflutters/MainActivity.kt new file mode 100644 index 000000000..8653a9cd0 --- /dev/null +++ b/add_to_app/multiple_flutters/multiple_flutters_android/app/src/main/java/dev/flutter/multipleflutters/MainActivity.kt @@ -0,0 +1,61 @@ +package dev.flutter.multipleflutters + +import android.content.Intent +import androidx.appcompat.app.AppCompatActivity +import android.os.Bundle +import android.view.View +import android.widget.TextView + +/** + * The activity that uses standard platform UI APIs. + * + * This doesn't talk directly to or render Flutter content and represents a part of your app that + * would potentially already be written that wants to interface with Flutter. + */ +class MainActivity : AppCompatActivity(), DataModelObserver { + private lateinit var countView: TextView + private val mainActivityIdentifier: Int + + private companion object { + /** A count that makes every other MainActivity have 1 or 2 Flutter instances. */ + var mainActivityCount = 0 + } + + init { + mainActivityIdentifier = mainActivityCount + mainActivityCount += 1 + } + + /** Implemented from AppCompactActivity. */ + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + DataModel.instance.addObserver(this) + countView = findViewById(R.id.count) + countView.text = DataModel.instance.counter.toString() + } + + /** Implemented from AppCompactActivity. */ + override fun onDestroy() { + super.onDestroy() + DataModel.instance.removeObserver(this) + } + + /** Event from `activity_main.xml`. */ + fun onClickNext(view: View) { + val nextClass = + if (mainActivityIdentifier % 2 == 0) SingleFlutterActivity::class.java else DoubleFlutterActivity::class.java + val flutterIntent = Intent(this, nextClass) + startActivity(flutterIntent) + } + + /** Event from `activity_main.xml`. */ + fun onClickAdd(view: View) { + DataModel.instance.counter = DataModel.instance.counter + 1 + } + + /** Implemented from DataModelObserver. */ + override fun onCountUpdate(newCount: Int) { + countView.text = newCount.toString() + } +} diff --git a/add_to_app/multiple_flutters/multiple_flutters_android/app/src/main/java/dev/flutter/multipleflutters/SingleFlutterActivity.kt b/add_to_app/multiple_flutters/multiple_flutters_android/app/src/main/java/dev/flutter/multipleflutters/SingleFlutterActivity.kt new file mode 100644 index 000000000..7316e5301 --- /dev/null +++ b/add_to_app/multiple_flutters/multiple_flutters_android/app/src/main/java/dev/flutter/multipleflutters/SingleFlutterActivity.kt @@ -0,0 +1,37 @@ +package dev.flutter.multipleflutters + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.engine.FlutterEngine + +/** + * This is an Activity that displays one instance of Flutter. + * + * EngineBindings is used to bridge communication between the Flutter instance and the DataModel. + */ +class SingleFlutterActivity : FlutterActivity(), EngineBindingsDelegate { + private val engineBindings: EngineBindings by lazy { + EngineBindings(activity = this, delegate = this, entrypoint = "main") + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + engineBindings.attach() + } + + override fun onDestroy() { + super.onDestroy() + engineBindings.detach() + } + + override fun provideFlutterEngine(context: Context): FlutterEngine? { + return engineBindings.engine + } + + override fun onNext() { + val flutterIntent = Intent(this, MainActivity::class.java) + startActivity(flutterIntent) + } +} diff --git a/add_to_app/multiple_flutters/multiple_flutters_android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/add_to_app/multiple_flutters/multiple_flutters_android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 000000000..2b068d114 --- /dev/null +++ b/add_to_app/multiple_flutters/multiple_flutters_android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/add_to_app/multiple_flutters/multiple_flutters_android/app/src/main/res/drawable/ic_launcher_background.xml b/add_to_app/multiple_flutters/multiple_flutters_android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..07d5da9cb --- /dev/null +++ b/add_to_app/multiple_flutters/multiple_flutters_android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/add_to_app/multiple_flutters/multiple_flutters_android/app/src/main/res/layout/activity_main.xml b/add_to_app/multiple_flutters/multiple_flutters_android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 000000000..e5ef998fa --- /dev/null +++ b/add_to_app/multiple_flutters/multiple_flutters_android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,52 @@ + + + + + + + +