Let FlutterActivity/Fragment/FragmentActivity support dart entrypoint args (flutter/engine#30709)

This commit is contained in:
ColdPaleLight 2022-04-01 13:21:01 +08:00 committed by GitHub
parent 31e332ee32
commit 8a886fa8a5
9 changed files with 179 additions and 16 deletions

View File

@ -11,6 +11,7 @@ import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.DEFAULT_
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.DEFAULT_INITIAL_ROUTE;
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_BACKGROUND_MODE;
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_CACHED_ENGINE_ID;
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_DART_ENTRYPOINT_ARGS;
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_DESTROY_ENGINE_WITH_ACTIVITY;
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_ENABLE_STATE_RESTORATION;
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_INITIAL_ROUTE;
@ -49,6 +50,8 @@ import io.flutter.embedding.engine.plugins.activity.ActivityControlSurface;
import io.flutter.embedding.engine.plugins.util.GeneratedPluginRegister;
import io.flutter.plugin.platform.PlatformPlugin;
import io.flutter.util.ViewUtils;
import java.util.ArrayList;
import java.util.List;
/**
* {@code Activity} which displays a fullscreen Flutter UI.
@ -64,7 +67,7 @@ import io.flutter.util.ViewUtils;
* <li>Displays an Android launch screen.
* <li>Displays a Flutter splash screen.
* <li>Configures the status bar appearance.
* <li>Chooses the Dart execution app bundle path and entrypoint.
* <li>Chooses the Dart execution app bundle path, entrypoint and entrypoint arguments.
* <li>Chooses Flutter's initial route.
* <li>Renders {@code Activity} transparently, if desired.
* <li>Offers hooks for subclasses to provide and configure a {@link
@ -72,7 +75,7 @@ import io.flutter.util.ViewUtils;
* <li>Save and restore instance state, see {@code #shouldRestoreAndSaveState()};
* </ul>
*
* <p><strong>Dart entrypoint, initial route, and app bundle path</strong>
* <p><strong>Dart entrypoint, entrypoint arguments, initial route, and app bundle path</strong>
*
* <p>The Dart entrypoint executed within this {@code Activity} is "main()" by default. To change
* the entrypoint that a {@code FlutterActivity} executes, subclass {@code FlutterActivity} and
@ -80,6 +83,10 @@ import io.flutter.util.ViewUtils;
* tree-shaken away, you need to annotate those functions with {@code @pragma('vm:entry-point')} in
* Dart.
*
* <p>The Dart entrypoint arguments will be passed as a list of string to Dart's entrypoint
* function. It can be passed using a {@link NewEngineIntentBuilder} via {@link
* NewEngineIntentBuilder#dartEntrypointArgs}.
*
* <p>The Flutter route that is initially loaded within this {@code Activity} is "/". The initial
* route may be specified explicitly by passing the name of the route as a {@code String} in {@link
* FlutterActivityLaunchConfigs#EXTRA_INITIAL_ROUTE}, e.g., "my/deep/link".
@ -87,12 +94,13 @@ import io.flutter.util.ViewUtils;
* <p>The initial route can each be controlled using a {@link NewEngineIntentBuilder} via {@link
* NewEngineIntentBuilder#initialRoute}.
*
* <p>The app bundle path, Dart entrypoint, and initial route can also be controlled in a subclass
* of {@code FlutterActivity} by overriding their respective methods:
* <p>The app bundle path, Dart entrypoint, Dart entrypoint arguments, and initial route can also be
* controlled in a subclass of {@code FlutterActivity} by overriding their respective methods:
*
* <ul>
* <li>{@link #getAppBundlePath()}
* <li>{@link #getDartEntrypointFunctionName()}
* <li>{@link #getDartEntrypointArgs()}
* <li>{@link #getInitialRoute()}
* </ul>
*
@ -254,6 +262,7 @@ public class FlutterActivity extends Activity
private final Class<? extends FlutterActivity> activityClass;
private String initialRoute = DEFAULT_INITIAL_ROUTE;
private String backgroundMode = DEFAULT_BACKGROUND_MODE;
@Nullable private List<String> dartEntrypointArgs;
/**
* Constructor that allows this {@code NewEngineIntentBuilder} to be used by subclasses of
@ -307,6 +316,21 @@ public class FlutterActivity extends Activity
return this;
}
/**
* The Dart entrypoint arguments will be passed as a list of string to Dart's entrypoint
* function.
*
* <p>A value of null means do not pass any arguments to Dart's entrypoint function.
*
* @param dartEntrypointArgs The Dart entrypoint arguments.
* @return The engine intent builder.
*/
@NonNull
public NewEngineIntentBuilder dartEntrypointArgs(@Nullable List<String> dartEntrypointArgs) {
this.dartEntrypointArgs = dartEntrypointArgs;
return this;
}
/**
* Creates and returns an {@link Intent} that will launch a {@code FlutterActivity} with the
* desired configuration.
@ -316,10 +340,15 @@ public class FlutterActivity extends Activity
*/
@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);
Intent intent =
new Intent(context, activityClass)
.putExtra(EXTRA_INITIAL_ROUTE, initialRoute)
.putExtra(EXTRA_BACKGROUND_MODE, backgroundMode)
.putExtra(EXTRA_DESTROY_ENGINE_WITH_ACTIVITY, true);
if (dartEntrypointArgs != null) {
intent.putExtra(EXTRA_DART_ENTRYPOINT_ARGS, new ArrayList(dartEntrypointArgs));
}
return intent;
}
}
@ -834,6 +863,18 @@ public class FlutterActivity extends Activity
}
}
/**
* The Dart entrypoint arguments will be passed as a list of string to Dart's entrypoint function.
*
* <p>A value of null means do not pass any arguments to Dart's entrypoint function.
*
* <p>Subclasses may override this method to directly control the Dart entrypoint arguments.
*/
@Nullable
public List<String> getDartEntrypointArgs() {
return (List<String>) getIntent().getSerializableExtra(EXTRA_DART_ENTRYPOINT_ARGS);
}
/**
* The Dart library URI for the entrypoint that will be executed as soon as the Dart snapshot is
* loaded.

View File

@ -30,6 +30,7 @@ import io.flutter.embedding.engine.renderer.FlutterUiDisplayListener;
import io.flutter.plugin.platform.PlatformPlugin;
import io.flutter.util.ViewUtils;
import java.util.Arrays;
import java.util.List;
/**
* Delegate that implements all Flutter logic that is the same between a {@link FlutterActivity} and
@ -450,7 +451,7 @@ import java.util.Arrays;
appBundlePathOverride, host.getDartEntrypointFunctionName())
: new DartExecutor.DartEntrypoint(
appBundlePathOverride, libraryUri, host.getDartEntrypointFunctionName());
flutterEngine.getDartExecutor().executeDartEntrypoint(entrypoint);
flutterEngine.getDartExecutor().executeDartEntrypoint(entrypoint, host.getDartEntrypointArgs());
}
private String maybeGetInitialRouteFromIntent(Intent intent) {
@ -953,6 +954,10 @@ import java.util.Arrays;
@Nullable
String getDartEntrypointLibraryUri();
/** Returns arguments that passed as a list of string to Dart's entrypoint function. */
@Nullable
List<String> getDartEntrypointArgs();
/** Returns the path to the app bundle where the Dart code exists. */
@NonNull
String getAppBundlePath();

View File

@ -23,6 +23,7 @@ public class FlutterActivityLaunchConfigs {
/* package */ static final String EXTRA_INITIAL_ROUTE = "route";
/* package */ static final String EXTRA_BACKGROUND_MODE = "background_mode";
/* package */ static final String EXTRA_CACHED_ENGINE_ID = "cached_engine_id";
/* package */ static final String EXTRA_DART_ENTRYPOINT_ARGS = "dart_entrypoint_args";
/* package */ static final String EXTRA_DESTROY_ENGINE_WITH_ACTIVITY =
"destroy_engine_with_activity";
/* package */ static final String EXTRA_ENABLE_STATE_RESTORATION = "enable_state_restoration";

View File

@ -25,6 +25,8 @@ import io.flutter.embedding.engine.FlutterShellArgs;
import io.flutter.embedding.engine.renderer.FlutterUiDisplayListener;
import io.flutter.plugin.platform.PlatformPlugin;
import io.flutter.util.ViewUtils;
import java.util.ArrayList;
import java.util.List;
/**
* {@code Fragment} which displays a Flutter UI that takes up all available {@code Fragment} space.
@ -107,6 +109,8 @@ public class FlutterFragment extends Fragment
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";
/** The Dart entrypoint arguments that is executed upon initialization. */
protected static final String ARG_DART_ENTRYPOINT_ARGS = "dart_entrypoint_args";
/** 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. */
@ -226,6 +230,7 @@ public class FlutterFragment extends Fragment
private final Class<? extends FlutterFragment> fragmentClass;
private String dartEntrypoint = "main";
private String dartLibraryUri = null;
private List<String> dartEntrypointArgs;
private String initialRoute = "/";
private boolean handleDeeplinking = false;
private String appBundlePath = null;
@ -265,6 +270,13 @@ public class FlutterFragment extends Fragment
return this;
}
/** Arguments passed as a list of string to Dart's entrypoint function. */
@NonNull
public NewEngineFragmentBuilder dartEntrypointArgs(@NonNull List<String> dartEntrypointArgs) {
this.dartEntrypointArgs = dartEntrypointArgs;
return this;
}
/**
* The initial route that a Flutter app will render in this {@link FlutterFragment}, defaults to
* "/".
@ -420,6 +432,9 @@ public class FlutterFragment extends Fragment
args.putString(ARG_APP_BUNDLE_PATH, appBundlePath);
args.putString(ARG_DART_ENTRYPOINT, dartEntrypoint);
args.putString(ARG_DART_ENTRYPOINT_URI, dartLibraryUri);
args.putStringArrayList(
ARG_DART_ENTRYPOINT_ARGS,
dartEntrypointArgs != null ? new ArrayList(dartEntrypointArgs) : null);
// TODO(mattcarroll): determine if we should have an explicit FlutterTestFragment instead of
// conflating.
if (null != shellArgs) {
@ -1045,6 +1060,19 @@ public class FlutterFragment extends Fragment
return getArguments().getString(ARG_DART_ENTRYPOINT, "main");
}
/**
* The Dart entrypoint arguments will be passed as a list of string to Dart's entrypoint function.
*
* <p>A value of null means do not pass any arguments to Dart's entrypoint function.
*
* <p>Subclasses may override this method to directly control the Dart entrypoint arguments.
*/
@Override
@Nullable
public List<String> getDartEntrypointArgs() {
return getArguments().getStringArrayList(ARG_DART_ENTRYPOINT_ARGS);
}
/**
* Returns the library URI of the Dart method that this {@code FlutterFragment} should execute to
* start a Flutter app.

View File

@ -11,6 +11,7 @@ import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.DEFAULT_
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.DEFAULT_INITIAL_ROUTE;
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_BACKGROUND_MODE;
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_CACHED_ENGINE_ID;
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_DART_ENTRYPOINT_ARGS;
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_DESTROY_ENGINE_WITH_ACTIVITY;
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_INITIAL_ROUTE;
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.HANDLE_DEEPLINKING_META_DATA_KEY;
@ -47,6 +48,8 @@ import io.flutter.embedding.engine.FlutterShellArgs;
import io.flutter.embedding.engine.plugins.util.GeneratedPluginRegister;
import io.flutter.plugin.platform.PlatformPlugin;
import io.flutter.util.ViewUtils;
import java.util.ArrayList;
import java.util.List;
/**
* A Flutter {@code Activity} that is based upon {@link FragmentActivity}.
@ -97,6 +100,7 @@ public class FlutterFragmentActivity extends FragmentActivity
private final Class<? extends FlutterFragmentActivity> activityClass;
private String initialRoute = DEFAULT_INITIAL_ROUTE;
private String backgroundMode = DEFAULT_BACKGROUND_MODE;
@Nullable private List<String> dartEntrypointArgs;
/**
* Constructor that allows this {@code NewEngineIntentBuilder} to be used by subclasses of
@ -145,16 +149,36 @@ public class FlutterFragmentActivity extends FragmentActivity
return this;
}
/**
* The Dart entrypoint arguments will be passed as a list of string to Dart's entrypoint
* function.
*
* <p>A value of null means do not pass any arguments to Dart's entrypoint function.
*
* @param dartEntrypointArgs The Dart entrypoint arguments.
* @return The engine intent builder.
*/
@NonNull
public NewEngineIntentBuilder dartEntrypointArgs(@Nullable List<String> dartEntrypointArgs) {
this.dartEntrypointArgs = dartEntrypointArgs;
return this;
}
/**
* Creates and returns an {@link Intent} that will launch a {@code FlutterFragmentActivity} with
* the desired configuration.
*/
@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);
Intent intent =
new Intent(context, activityClass)
.putExtra(EXTRA_INITIAL_ROUTE, initialRoute)
.putExtra(EXTRA_BACKGROUND_MODE, backgroundMode)
.putExtra(EXTRA_DESTROY_ENGINE_WITH_ACTIVITY, true);
if (dartEntrypointArgs != null) {
intent.putExtra(EXTRA_DART_ENTRYPOINT_ARGS, new ArrayList(dartEntrypointArgs));
}
return intent;
}
}
@ -478,6 +502,7 @@ public class FlutterFragmentActivity extends FragmentActivity
return FlutterFragment.withNewEngine()
.dartEntrypoint(getDartEntrypointFunctionName())
.dartLibraryUri(getDartEntrypointLibraryUri())
.dartEntrypointArgs(getDartEntrypointArgs())
.initialRoute(getInitialRoute())
.appBundlePath(getAppBundlePath())
.flutterShellArgs(FlutterShellArgs.fromIntent(getIntent()))
@ -700,6 +725,18 @@ public class FlutterFragmentActivity extends FragmentActivity
}
}
/**
* The Dart entrypoint arguments will be passed as a list of string to Dart's entrypoint function.
*
* <p>A value of null means do not pass any arguments to Dart's entrypoint function.
*
* <p>Subclasses may override this method to directly control the Dart entrypoint arguments.
*/
@Nullable
public List<String> getDartEntrypointArgs() {
return (List<String>) getIntent().getSerializableExtra(EXTRA_DART_ENTRYPOINT_ARGS);
}
/**
* The Dart library URI for the entrypoint that will be executed as soon as the Dart snapshot is
* loaded.

View File

@ -46,6 +46,9 @@ import io.flutter.embedding.engine.systemchannels.SystemChannel;
import io.flutter.embedding.engine.systemchannels.TextInputChannel;
import io.flutter.plugin.localization.LocalizationPlugin;
import io.flutter.plugin.platform.PlatformViewsController;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -75,6 +78,7 @@ public class FlutterActivityAndFragmentDelegateTest {
when(mockHost.getLifecycle()).thenReturn(mock(Lifecycle.class));
when(mockHost.getFlutterShellArgs()).thenReturn(new FlutterShellArgs(new String[] {}));
when(mockHost.getDartEntrypointFunctionName()).thenReturn("main");
when(mockHost.getDartEntrypointArgs()).thenReturn(null);
when(mockHost.getAppBundlePath()).thenReturn("/fake/path");
when(mockHost.getInitialRoute()).thenReturn("/");
when(mockHost.getRenderMode()).thenReturn(RenderMode.surface);
@ -343,7 +347,35 @@ public class FlutterActivityAndFragmentDelegateTest {
delegate.onStart();
// Verify that the host's Dart entrypoint was used.
verify(mockFlutterEngine.getDartExecutor(), times(1)).executeDartEntrypoint(eq(dartEntrypoint));
verify(mockFlutterEngine.getDartExecutor(), times(1))
.executeDartEntrypoint(eq(dartEntrypoint), isNull());
}
@Test
public void itExecutesDartEntrypointWithArgsProvidedByHost() {
// ---- Test setup ----
// Set Dart entrypoint parameters on fake host.
when(mockHost.getAppBundlePath()).thenReturn("/my/bundle/path");
when(mockHost.getDartEntrypointFunctionName()).thenReturn("myEntrypoint");
List<String> dartEntrypointArgs = new ArrayList<String>(Arrays.asList("foo", "bar"));
when(mockHost.getDartEntrypointArgs()).thenReturn(dartEntrypointArgs);
// Create the DartEntrypoint that we expect to be executed
DartExecutor.DartEntrypoint dartEntrypoint =
new DartExecutor.DartEntrypoint("/my/bundle/path", "myEntrypoint");
// Create the real object that we're testing.
FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost);
// --- Execute the behavior under test ---
// Dart is executed in onStart().
delegate.onAttach(RuntimeEnvironment.application);
delegate.onCreateView(null, null, null, 0, true);
delegate.onStart();
// Verify that the host's Dart entrypoint was used.
verify(mockFlutterEngine.getDartExecutor(), times(1))
.executeDartEntrypoint(any(DartExecutor.DartEntrypoint.class), eq(dartEntrypointArgs));
}
@Test
@ -362,7 +394,7 @@ public class FlutterActivityAndFragmentDelegateTest {
delegate.onStart();
verify(mockFlutterEngine.getDartExecutor(), times(1))
.executeDartEntrypoint(eq(expectedEntrypoint));
.executeDartEntrypoint(eq(expectedEntrypoint), isNull());
}
@Test
@ -391,7 +423,8 @@ public class FlutterActivityAndFragmentDelegateTest {
delegate.onStart();
// Verify that the host's Dart entrypoint was used.
verify(mockFlutterEngine.getDartExecutor(), times(1)).executeDartEntrypoint(eq(dartEntrypoint));
verify(mockFlutterEngine.getDartExecutor(), times(1))
.executeDartEntrypoint(eq(dartEntrypoint), isNull());
}
// "Attaching" to the surrounding Activity refers to Flutter being able to control

View File

@ -40,6 +40,8 @@ import io.flutter.embedding.engine.plugins.activity.ActivityAware;
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding.OnSaveInstanceStateListener;
import io.flutter.plugins.GeneratedPluginRegistrant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.junit.After;
import org.junit.Before;
@ -95,6 +97,7 @@ public class FlutterActivityTest {
assertEquals("main", flutterActivity.getDartEntrypointFunctionName());
assertNull(flutterActivity.getDartEntrypointLibraryUri());
assertNull(flutterActivity.getDartEntrypointArgs());
assertEquals("/", flutterActivity.getInitialRoute());
assertArrayEquals(new String[] {}, flutterActivity.getFlutterShellArgs().toArray());
assertTrue(flutterActivity.shouldAttachEngineToActivity());
@ -139,6 +142,7 @@ public class FlutterActivityTest {
Intent intent =
FlutterActivity.withNewEngine()
.initialRoute("/custom/route")
.dartEntrypointArgs(new ArrayList<String>(Arrays.asList("foo", "bar")))
.backgroundMode(BackgroundMode.transparent)
.build(RuntimeEnvironment.application);
ActivityController<FlutterActivity> activityController =
@ -147,6 +151,8 @@ public class FlutterActivityTest {
flutterActivity.setDelegate(new FlutterActivityAndFragmentDelegate(flutterActivity));
assertEquals("/custom/route", flutterActivity.getInitialRoute());
assertArrayEquals(
new String[] {"foo", "bar"}, flutterActivity.getDartEntrypointArgs().toArray());
assertArrayEquals(new String[] {}, flutterActivity.getFlutterShellArgs().toArray());
assertTrue(flutterActivity.shouldAttachEngineToActivity());
assertNull(flutterActivity.getCachedEngineId());

View File

@ -32,6 +32,7 @@ import io.flutter.embedding.engine.plugins.activity.ActivityAware;
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
import io.flutter.embedding.engine.systemchannels.LifecycleChannel;
import io.flutter.plugin.platform.PlatformPlugin;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
@ -311,6 +312,12 @@ public class FlutterAndroidComponentTest {
return null;
}
@Nullable
@Override
public List<String> getDartEntrypointArgs() {
return null;
}
@NonNull
@Override
public String getAppBundlePath() {

View File

@ -20,6 +20,8 @@ import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.embedding.engine.FlutterEngineCache;
import io.flutter.embedding.engine.FlutterJNI;
import io.flutter.embedding.engine.loader.FlutterLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -39,6 +41,7 @@ public class FlutterFragmentTest {
assertEquals("main", fragment.getDartEntrypointFunctionName());
assertNull(fragment.getDartEntrypointLibraryUri());
assertNull(fragment.getDartEntrypointArgs());
assertEquals("/", fragment.getInitialRoute());
assertArrayEquals(new String[] {}, fragment.getFlutterShellArgs().toArray());
assertTrue(fragment.shouldAttachEngineToActivity());
@ -56,6 +59,7 @@ public class FlutterFragmentTest {
FlutterFragment.withNewEngine()
.dartEntrypoint("custom_entrypoint")
.dartLibraryUri("package:foo/bar.dart")
.dartEntrypointArgs(new ArrayList<String>(Arrays.asList("foo", "bar")))
.initialRoute("/custom/route")
.shouldAttachEngineToActivity(false)
.handleDeeplinking(true)
@ -67,6 +71,7 @@ public class FlutterFragmentTest {
assertEquals("custom_entrypoint", fragment.getDartEntrypointFunctionName());
assertEquals("package:foo/bar.dart", fragment.getDartEntrypointLibraryUri());
assertEquals("/custom/route", fragment.getInitialRoute());
assertArrayEquals(new String[] {"foo", "bar"}, fragment.getDartEntrypointArgs().toArray());
assertArrayEquals(new String[] {}, fragment.getFlutterShellArgs().toArray());
assertFalse(fragment.shouldAttachEngineToActivity());
assertTrue(fragment.shouldHandleDeeplinking());