diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java index 5bcc91e55ed45..cee5b20b46e8e 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java @@ -5,6 +5,7 @@ package io.flutter.embedding.android; import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.DART_ENTRYPOINT_META_DATA_KEY; +import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.DART_ENTRYPOINT_URI_META_DATA_KEY; import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.DEFAULT_BACKGROUND_MODE; import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.DEFAULT_DART_ENTRYPOINT; import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.DEFAULT_INITIAL_ROUTE; @@ -824,6 +825,32 @@ public String getDartEntrypointFunctionName() { } } + /** + * The Dart library URI for the entrypoint that will be executed as soon as the Dart snapshot is + * loaded. + * + *

Example value: "package:foo/bar.dart" + * + *

This preference can be controlled by setting a {@code } called {@link + * FlutterActivityLaunchConfigs#DART_ENTRYPOINT_URI_META_DATA_KEY} within the Android manifest + * definition for this {@code FlutterActivity}. + * + *

A value of null means use the default root library. + * + *

Subclasses may override this method to directly control the Dart entrypoint uri. + */ + @Nullable + public String getDartEntrypointLibraryUri() { + try { + Bundle metaData = getMetaData(); + String desiredDartLibraryUri = + metaData != null ? metaData.getString(DART_ENTRYPOINT_URI_META_DATA_KEY) : null; + return desiredDartLibraryUri; + } catch (PackageManager.NameNotFoundException e) { + return null; + } + } + /** * The initial route that a Flutter app will render upon loading and executing its Dart code. * diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java index d8b3fc809bea5..8665af6185743 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java @@ -417,12 +417,16 @@ private void doInitialFlutterViewRun() { initialRoute = DEFAULT_INITIAL_ROUTE; } } + @Nullable String libraryUri = host.getDartEntrypointLibraryUri(); Log.v( TAG, "Executing Dart entrypoint: " - + host.getDartEntrypointFunctionName() - + ", and sending initial route: " - + initialRoute); + + host.getDartEntrypointFunctionName() + + ", library uri: " + + libraryUri + == null + ? "\"\"" + : libraryUri + ", and sending initial route: " + initialRoute); // The engine needs to receive the Flutter app's initial route before executing any // Dart code to ensure that the initial route arrives in time to be applied. @@ -435,8 +439,11 @@ private void doInitialFlutterViewRun() { // Configure the Dart entrypoint and execute it. DartExecutor.DartEntrypoint entrypoint = - new DartExecutor.DartEntrypoint( - appBundlePathOverride, host.getDartEntrypointFunctionName()); + libraryUri == null + ? new DartExecutor.DartEntrypoint( + appBundlePathOverride, host.getDartEntrypointFunctionName()) + : new DartExecutor.DartEntrypoint( + appBundlePathOverride, libraryUri, host.getDartEntrypointFunctionName()); flutterEngine.getDartExecutor().executeDartEntrypoint(entrypoint); } @@ -916,6 +923,14 @@ private void ensureAlive() { @NonNull String getDartEntrypointFunctionName(); + /** + * Returns the URI of the Dart library which contains the entrypoint method (example + * "package:foo_package/main.dart"). If null, this will default to the same library as the + * `main()` function in the Dart program. + */ + @Nullable + String getDartEntrypointLibraryUri(); + /** Returns the path to the app bundle where the Dart code exists. */ @NonNull String getAppBundlePath(); diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivityLaunchConfigs.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivityLaunchConfigs.java index a92e342333765..c6aa0aa242f69 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterActivityLaunchConfigs.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivityLaunchConfigs.java @@ -11,6 +11,7 @@ public class FlutterActivityLaunchConfigs { // Meta-data arguments, processed from manifest XML. /* package */ static final String DART_ENTRYPOINT_META_DATA_KEY = "io.flutter.Entrypoint"; + /* package */ static final String DART_ENTRYPOINT_URI_META_DATA_KEY = "io.flutter.EntrypointUri"; /* package */ static final String INITIAL_ROUTE_META_DATA_KEY = "io.flutter.InitialRoute"; /* package */ static final String SPLASH_SCREEN_META_DATA_KEY = "io.flutter.embedding.android.SplashScreenDrawable"; diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java b/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java index 9d34a29c67e6b..910826c960348 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java @@ -105,6 +105,8 @@ public class FlutterFragment extends Fragment /** The Dart entrypoint method name that is executed upon initialization. */ protected static final String ARG_DART_ENTRYPOINT = "dart_entrypoint"; + /** The Dart entrypoint method's URI that is executed upon initialization. */ + protected static final String ARG_DART_ENTRYPOINT_URI = "dart_entrypoint_uri"; /** Initial Flutter route that is rendered in a Navigator widget. */ protected static final String ARG_INITIAL_ROUTE = "initial_route"; /** Whether the activity delegate should handle the deeplinking request. */ @@ -223,6 +225,7 @@ public static NewEngineFragmentBuilder withNewEngine() { public static class NewEngineFragmentBuilder { private final Class fragmentClass; private String dartEntrypoint = "main"; + private String dartLibraryUri = null; private String initialRoute = "/"; private boolean handleDeeplinking = false; private String appBundlePath = null; @@ -256,6 +259,12 @@ public NewEngineFragmentBuilder dartEntrypoint(@NonNull String dartEntrypoint) { return this; } + @NonNull + public NewEngineFragmentBuilder dartLibraryUri(@NonNull String dartLibraryUri) { + this.dartLibraryUri = dartLibraryUri; + return this; + } + /** * The initial route that a Flutter app will render in this {@link FlutterFragment}, defaults to * "/". @@ -410,6 +419,7 @@ protected Bundle createArgs() { args.putBoolean(ARG_HANDLE_DEEPLINKING, handleDeeplinking); args.putString(ARG_APP_BUNDLE_PATH, appBundlePath); args.putString(ARG_DART_ENTRYPOINT, dartEntrypoint); + args.putString(ARG_DART_ENTRYPOINT_URI, dartLibraryUri); // TODO(mattcarroll): determine if we should have an explicit FlutterTestFragment instead of // conflating. if (null != shellArgs) { @@ -1026,6 +1036,20 @@ public String getDartEntrypointFunctionName() { return getArguments().getString(ARG_DART_ENTRYPOINT, "main"); } + /** + * Returns the library URI of the Dart method that this {@code FlutterFragment} should execute to + * start a Flutter app. + * + *

Defaults to null (example value: "package:foo/bar.dart"). + * + *

Used by this {@code FlutterFragment}'s {@link FlutterActivityAndFragmentDelegate.Host} + */ + @Override + @Nullable + public String getDartEntrypointLibraryUri() { + return getArguments().getString(ARG_DART_ENTRYPOINT_URI); + } + /** * A custom path to the bundle that contains this Flutter app's resources, e.g., Dart code * snapshots. diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java index d7b90c0a8c867..43414b4ff65d7 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java @@ -320,6 +320,25 @@ public void itExecutesDartEntrypointProvidedByHost() { verify(mockFlutterEngine.getDartExecutor(), times(1)).executeDartEntrypoint(eq(dartEntrypoint)); } + @Test + public void itExecutesDartLibraryUriProvidedByHost() { + when(mockHost.getAppBundlePath()).thenReturn("/my/bundle/path"); + when(mockHost.getDartEntrypointFunctionName()).thenReturn("myEntrypoint"); + when(mockHost.getDartEntrypointLibraryUri()).thenReturn("package:foo/bar.dart"); + + DartExecutor.DartEntrypoint expectedEntrypoint = + new DartExecutor.DartEntrypoint("/my/bundle/path", "package:foo/bar.dart", "myEntrypoint"); + + FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost); + + delegate.onAttach(RuntimeEnvironment.application); + delegate.onCreateView(null, null, null, 0, true); + delegate.onStart(); + + verify(mockFlutterEngine.getDartExecutor(), times(1)) + .executeDartEntrypoint(eq(expectedEntrypoint)); + } + @Test public void itUsesDefaultFlutterLoaderAppBundlePathWhenUnspecified() { // ---- Test setup ---- diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java index f78c5e93ab89f..265cdc17a28bb 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java @@ -95,6 +95,7 @@ public void itCreatesDefaultIntentWithExpectedDefaults() { flutterActivity.setDelegate(new FlutterActivityAndFragmentDelegate(flutterActivity)); assertEquals("main", flutterActivity.getDartEntrypointFunctionName()); + assertNull(flutterActivity.getDartEntrypointLibraryUri()); assertEquals("/", flutterActivity.getInitialRoute()); assertArrayEquals(new String[] {}, flutterActivity.getFlutterShellArgs().toArray()); assertTrue(flutterActivity.shouldAttachEngineToActivity()); diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterAndroidComponentTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterAndroidComponentTest.java index 331906e473f43..3586339d8b99e 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/FlutterAndroidComponentTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterAndroidComponentTest.java @@ -305,6 +305,12 @@ public String getDartEntrypointFunctionName() { return "main"; } + @Nullable + @Override + public String getDartEntrypointLibraryUri() { + return null; + } + @NonNull @Override public String getAppBundlePath() { diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentTest.java index 2f3454ff76fe4..ea8b9d0acb29f 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentTest.java @@ -38,6 +38,7 @@ public void itCreatesDefaultFragmentWithExpectedDefaults() { fragment.setDelegate(new FlutterActivityAndFragmentDelegate(fragment)); assertEquals("main", fragment.getDartEntrypointFunctionName()); + assertNull(fragment.getDartEntrypointLibraryUri()); assertEquals("/", fragment.getInitialRoute()); assertArrayEquals(new String[] {}, fragment.getFlutterShellArgs().toArray()); assertTrue(fragment.shouldAttachEngineToActivity()); @@ -54,6 +55,7 @@ public void itCreatesNewEngineFragmentWithRequestedSettings() { FlutterFragment fragment = FlutterFragment.withNewEngine() .dartEntrypoint("custom_entrypoint") + .dartLibraryUri("package:foo/bar.dart") .initialRoute("/custom/route") .shouldAttachEngineToActivity(false) .handleDeeplinking(true) @@ -63,6 +65,7 @@ public void itCreatesNewEngineFragmentWithRequestedSettings() { fragment.setDelegate(new FlutterActivityAndFragmentDelegate(fragment)); assertEquals("custom_entrypoint", fragment.getDartEntrypointFunctionName()); + assertEquals("package:foo/bar.dart", fragment.getDartEntrypointLibraryUri()); assertEquals("/custom/route", fragment.getInitialRoute()); assertArrayEquals(new String[] {}, fragment.getFlutterShellArgs().toArray()); assertFalse(fragment.shouldAttachEngineToActivity()); diff --git a/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h b/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h index ea10495c614c2..5867ea36aa6c6 100644 --- a/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h +++ b/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h @@ -219,8 +219,9 @@ FLUTTER_DARWIN_EXPORT * FlutterDefaultDartEntrypoint (or nil); this will default to `main()`. If it is not the app's * main() function, that function must be decorated with `@pragma(vm:entry-point)` to ensure the * method is not tree-shaken by the Dart compiler. - * @param uri The URI of the Dart library which contains the entrypoint method. IF nil, - * this will default to the same library as the `main()` function in the Dart program. + * @param uri The URI of the Dart library which contains the entrypoint method + * (example "package:foo_package/main.dart"). If nil, this will default to + * the same library as the `main()` function in the Dart program. * @return YES if the call succeeds in creating and running a Flutter Engine instance; NO otherwise. */ - (BOOL)runWithEntrypoint:(nullable NSString*)entrypoint libraryURI:(nullable NSString*)uri; @@ -236,8 +237,9 @@ FLUTTER_DARWIN_EXPORT * FlutterDefaultDartEntrypoint (or nil); this will default to `main()`. If it is not the app's * main() function, that function must be decorated with `@pragma(vm:entry-point)` to ensure the * method is not tree-shaken by the Dart compiler. - * @param libraryURI The URI of the Dart library which contains the entrypoint method. IF nil, - * this will default to the same library as the `main()` function in the Dart program. + * @param libraryURI The URI of the Dart library which contains the entrypoint + * method (example "package:foo_package/main.dart"). If nil, this will + * default to the same library as the `main()` function in the Dart program. * @param initialRoute The name of the initial Flutter `Navigator` `Route` to load. If this is * FlutterDefaultInitialRoute (or nil), it will default to the "/" route. * @return YES if the call succeeds in creating and running a Flutter Engine instance; NO otherwise. @@ -257,8 +259,9 @@ FLUTTER_DARWIN_EXPORT * FlutterDefaultDartEntrypoint (or nil); this will default to `main()`. If it is not the app's * main() function, that function must be decorated with `@pragma(vm:entry-point)` to ensure the * method is not tree-shaken by the Dart compiler. - * @param libraryURI The URI of the Dart library which contains the entrypoint method. IF nil, - * this will default to the same library as the `main()` function in the Dart program. + * @param libraryURI The URI of the Dart library which contains the entrypoint + * method (example "package:foo_package/main.dart"). If nil, this will + * default to the same library as the `main()` function in the Dart program. * @param initialRoute The name of the initial Flutter `Navigator` `Route` to load. If this is * FlutterDefaultInitialRoute (or nil), it will default to the "/" route. * @param entrypointArgs Arguments passed as a list of string to Dart's entrypoint function.