Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 6fa1fcd

Browse files
author
Michael Klimushyn
authored
Register plugins at the right time, once (#15979)
Currently we're automatically registering plugins both when the FlutterEngine is constructed and in the `flutter create` template, when FlutterActivity#configureFlutterEngine is called. The initial registration is too early to contain a reference to the activity and the second registration can cause problems in some plugins. This alters the flow so automatic registration happens in two discrete places, and contains the `activity` in its first and only call for most apps. 1. We're no longer automatically registering plugins on `FlutterEngine` in any of our activities/fragments at construction time. But since the FlutterEngine default constructor still automatically registers plugins, anyone constructing the engine themselves (for example, in a service) is still going to get automatic registration at `FlutterEngine` instantiation time. 2. We now automatically register plugins in the base `FlutterActivity`'s `configureFlutterEngine` hook. Anyone using `FlutterActivity` (or `FlutterFragment`) should be automatically registered once that hook is called. Right now the `flutter create` template overrides the base class method with a subclass that registers everything manually in the same spot. But with this in place we can safely recommend to remove the subclass and rely on this hook to automatically register going forward. Registering at this time means `activity` is set correctly.
1 parent f30ff4f commit 6fa1fcd

File tree

7 files changed

+170
-16
lines changed

7 files changed

+170
-16
lines changed

shell/platform/android/BUILD.gn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,7 @@ action("robolectric_tests") {
435435
"test/io/flutter/plugin/common/StandardMessageCodecTest.java",
436436
"test/io/flutter/plugin/editing/TextInputPluginTest.java",
437437
"test/io/flutter/plugin/platform/SingleViewPresentationTest.java",
438+
"test/io/flutter/plugins/GeneratedPluginRegistrant.java",
438439
"test/io/flutter/util/PreconditionsTest.java",
439440
]
440441

shell/platform/android/io/flutter/embedding/android/FlutterActivity.java

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@
3333
import io.flutter.plugin.platform.PlatformPlugin;
3434
import io.flutter.view.FlutterMain;
3535

36+
import java.lang.reflect.InvocationTargetException;
37+
import java.lang.reflect.Method;
38+
3639
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.DART_ENTRYPOINT_META_DATA_KEY;
3740
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.DEFAULT_BACKGROUND_MODE;
3841
import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.DEFAULT_DART_ENTRYPOINT;
@@ -873,14 +876,18 @@ public PlatformPlugin providePlatformPlugin(@Nullable Activity activity, @NonNul
873876
}
874877

875878
/**
876-
* Hook for subclasses to easily configure a {@code FlutterEngine}, e.g., register
877-
* plugins.
878-
* <p>
879-
* This method is called after {@link #provideFlutterEngine(Context)}.
879+
* Hook for subclasses to easily configure a {@code FlutterEngine}.
880+
*
881+
* <p>This method is called after {@link #provideFlutterEngine(Context)}.
882+
*
883+
* <p>All plugins listed in the app's pubspec are registered in the base implementation of this
884+
* method. To avoid automatic plugin registration, override this method without invoking super().
885+
* To keep automatic plugin registration and further configure the flutterEngine, override this
886+
* method, invoke super(), and then configure the flutterEngine as desired.
880887
*/
881888
@Override
882889
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
883-
// No-op. Hook for subclasses.
890+
registerPlugins(flutterEngine);
884891
}
885892

886893
/**
@@ -950,4 +957,28 @@ public void onFlutterUiNoLongerDisplayed() {
950957
// no-op
951958
}
952959

960+
/**
961+
* Registers all plugins that an app lists in its pubspec.yaml.
962+
* <p>
963+
* The Flutter tool generates a class called GeneratedPluginRegistrant, which includes the code
964+
* necessary to register every plugin in the pubspec.yaml with a given {@code FlutterEngine}.
965+
* The GeneratedPluginRegistrant must be generated per app, because each app uses different sets
966+
* of plugins. Therefore, the Android embedding cannot place a compile-time dependency on this
967+
* generated class. This method uses reflection to attempt to locate the generated file and then
968+
* use it at runtime.
969+
* <p>
970+
* This method fizzles if the GeneratedPluginRegistrant cannot be found or invoked. This situation
971+
* should never occur, but if any eventuality comes up that prevents an app from using this
972+
* behavior, that app can still write code that explicitly registers plugins.
973+
*/
974+
private static void registerPlugins(@NonNull FlutterEngine flutterEngine) {
975+
try {
976+
Class<?> generatedPluginRegistrant = Class.forName("io.flutter.plugins.GeneratedPluginRegistrant");
977+
Method registrationMethod = generatedPluginRegistrant.getDeclaredMethod("registerWith", FlutterEngine.class);
978+
registrationMethod.invoke(null, flutterEngine);
979+
} catch (Exception e) {
980+
Log.w(TAG, "Tried to automatically register plugins with FlutterEngine ("
981+
+ flutterEngine + ") but could not find and invoke the GeneratedPluginRegistrant.");
982+
}
983+
}
953984
}

shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
import io.flutter.embedding.engine.FlutterShellArgs;
2828
import io.flutter.embedding.engine.dart.DartExecutor;
2929
import io.flutter.embedding.engine.renderer.FlutterUiDisplayListener;
30+
import io.flutter.embedding.engine.FlutterJNI;
31+
import io.flutter.embedding.engine.loader.FlutterLoader;
3032
import io.flutter.plugin.platform.PlatformPlugin;
3133

3234
import static android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW;
@@ -223,7 +225,7 @@ void onAttach(@NonNull Context context) {
223225
// FlutterView.
224226
Log.v(TAG, "No preferred FlutterEngine was provided. Creating a new FlutterEngine for"
225227
+ " this FlutterFragment.");
226-
flutterEngine = new FlutterEngine(host.getContext(), host.getFlutterShellArgs().toArray());
228+
flutterEngine = new FlutterEngine(host.getContext(), host.getFlutterShellArgs().toArray(), /*automaticallyRegisterPlugins=*/false);
227229
isFlutterEngineFromHost = false;
228230
}
229231

shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,7 @@ public void onPreEngineRestart() {
126126
* {@link RenderSurface} is registered. See
127127
* {@link #getRenderer()} and {@link FlutterRenderer#startRenderingToSurface(RenderSurface)}.
128128
* <p>
129-
* A new {@code FlutterEngine} does not come with any Flutter plugins attached. To attach plugins,
130-
* see {@link #getPlugins()}.
129+
* A new {@code FlutterEngine} automatically attaches all plugins. See {@link #getPlugins()}.
131130
* <p>
132131
* A new {@code FlutterEngine} does come with all default system channels attached.
133132
* <p>
@@ -152,6 +151,19 @@ public FlutterEngine(@NonNull Context context, @Nullable String[] dartVmArgs) {
152151
this(context, FlutterLoader.getInstance(), new FlutterJNI(), dartVmArgs, true);
153152
}
154153

154+
/**
155+
* Same as {@link #FlutterEngine(Context)} with added support for passing Dart
156+
* VM arguments and avoiding automatic plugin registration.
157+
* <p>
158+
* If the Dart VM has already started, the given arguments will have no effect.
159+
*/
160+
public FlutterEngine(
161+
@NonNull Context context,
162+
@Nullable String[] dartVmArgs,
163+
boolean automaticallyRegisterPlugins) {
164+
this(context, FlutterLoader.getInstance(), new FlutterJNI(), dartVmArgs, automaticallyRegisterPlugins);
165+
}
166+
155167
/**
156168
* Same as {@link #FlutterEngine(Context, FlutterLoader, FlutterJNI, String[])} but with no Dart
157169
* VM flags.

shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
import android.support.annotation.NonNull;
77
import android.support.annotation.Nullable;
88

9+
import io.flutter.plugins.GeneratedPluginRegistrant;
10+
import java.util.List;
11+
import org.junit.After;
12+
import org.junit.Before;
913
import org.junit.Test;
1014
import org.junit.runner.RunWith;
1115
import org.robolectric.Robolectric;
@@ -30,6 +34,16 @@
3034
@Config(manifest=Config.NONE)
3135
@RunWith(RobolectricTestRunner.class)
3236
public class FlutterActivityTest {
37+
@Before
38+
public void setUp() {
39+
GeneratedPluginRegistrant.clearRegisteredEngines();
40+
}
41+
42+
@After
43+
public void tearDown() {
44+
GeneratedPluginRegistrant.clearRegisteredEngines();
45+
}
46+
3347
@Test
3448
public void itCreatesDefaultIntentWithExpectedDefaults() {
3549
Intent intent = FlutterActivity.createDefaultIntent(RuntimeEnvironment.application);
@@ -122,6 +136,19 @@ public void itCreatesCachedEngineIntentThatDestroysTheEngine() {
122136
assertTrue(flutterActivity.shouldDestroyEngineWithHost());
123137
}
124138

139+
@Test
140+
public void itRegistersPluginsAtConfigurationTime() {
141+
FlutterActivity activity = Robolectric.buildActivity(FlutterActivityWithProvidedEngine.class).get();
142+
activity.onCreate(null);
143+
144+
assertTrue(GeneratedPluginRegistrant.getRegisteredEngines().isEmpty());
145+
activity.configureFlutterEngine(activity.getFlutterEngine());
146+
147+
List<FlutterEngine> registeredEngines = GeneratedPluginRegistrant.getRegisteredEngines();
148+
assertEquals(1, registeredEngines.size());
149+
assertEquals(activity.getFlutterEngine(), registeredEngines.get(0));
150+
}
151+
125152
static class FlutterActivityWithProvidedEngine extends FlutterActivity {
126153
@Override
127154
protected void onCreate(@Nullable Bundle savedInstanceState) {
@@ -140,7 +167,7 @@ public FlutterEngine provideFlutterEngine(@NonNull Context context) {
140167
mock(FlutterLoader.class),
141168
flutterJNI,
142169
new String[]{},
143-
true
170+
false
144171
);
145172
}
146173
}
Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
package test.io.flutter.embedding.engine;
22

3+
import io.flutter.plugins.GeneratedPluginRegistrant;
4+
import java.util.List;
5+
import org.junit.After;
6+
import org.junit.Before;
37
import org.junit.Test;
48
import org.junit.runner.RunWith;
9+
import org.mockito.Mock;
10+
import org.mockito.MockitoAnnotations;
511
import org.robolectric.RobolectricTestRunner;
612
import org.robolectric.RuntimeEnvironment;
713
import org.robolectric.annotation.Config;
@@ -10,25 +16,52 @@
1016
import io.flutter.embedding.engine.FlutterJNI;
1117
import io.flutter.embedding.engine.loader.FlutterLoader;
1218

19+
import static org.junit.Assert.assertEquals;
20+
import static org.junit.Assert.assertTrue;
1321
import static org.mockito.Mockito.mock;
1422
import static org.mockito.Mockito.when;
1523

1624
@Config(manifest=Config.NONE)
1725
@RunWith(RobolectricTestRunner.class)
1826
public class FlutterEngineTest {
19-
@Test
20-
public void itDoesNotCrashIfGeneratedPluginRegistrantIsUnavailable() {
21-
FlutterJNI flutterJNI = mock(FlutterJNI.class);
27+
@Mock FlutterJNI flutterJNI;
28+
29+
@Before
30+
public void setUp() {
31+
MockitoAnnotations.initMocks(this);
2232
when(flutterJNI.isAttached()).thenReturn(true);
33+
GeneratedPluginRegistrant.clearRegisteredEngines();
34+
}
2335

36+
@After
37+
public void tearDown() {
38+
GeneratedPluginRegistrant.clearRegisteredEngines();
39+
}
40+
41+
@Test
42+
public void itAutomaticallyRegistersPluginsByDefault() {
43+
assertTrue(GeneratedPluginRegistrant.getRegisteredEngines().isEmpty());
2444
FlutterEngine flutterEngine = new FlutterEngine(
45+
RuntimeEnvironment.application,
46+
mock(FlutterLoader.class),
47+
flutterJNI
48+
);
49+
50+
List<FlutterEngine> registeredEngines = GeneratedPluginRegistrant.getRegisteredEngines();
51+
assertEquals(1, registeredEngines.size());
52+
assertEquals(flutterEngine, registeredEngines.get(0));
53+
}
54+
55+
@Test
56+
public void itCanBeConfiguredToNotAutomaticallyRegisterPlugins() {
57+
new FlutterEngine(
2558
RuntimeEnvironment.application,
2659
mock(FlutterLoader.class),
2760
flutterJNI,
28-
new String[] {},
29-
true
61+
/*dartVmArgs=*/new String[] {},
62+
/*automaticallyRegisterPlugins=*/false
3063
);
31-
// The fact that the above constructor executed without error means that
32-
// it dealt with a non-existent GeneratedPluginRegistrant.
64+
65+
assertTrue(GeneratedPluginRegistrant.getRegisteredEngines().isEmpty());
3366
}
3467
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package io.flutter.plugins;
2+
3+
import android.support.annotation.VisibleForTesting;
4+
import io.flutter.embedding.engine.FlutterEngine;
5+
import java.util.List;
6+
import java.util.ArrayList;
7+
8+
/**
9+
* A fake of the {@code GeneratedPluginRegistrant} normally built by the tool into Flutter apps.
10+
*
11+
* <p>Used to test engine logic which interacts with the generated class.
12+
*/
13+
@VisibleForTesting
14+
public class GeneratedPluginRegistrant {
15+
private static final List<FlutterEngine> registeredEngines = new ArrayList<>();
16+
17+
/**
18+
* The one and only method currently generated by the tool.
19+
*
20+
* <p>Normally it registers all plugins in an app with the given {@code engine}. This fake tracks
21+
* all registered engines instead.
22+
*/
23+
public static void registerWith(FlutterEngine engine) {
24+
registeredEngines.add(engine);
25+
}
26+
27+
/**
28+
* Clears the mutable static state regrettably stored in this class.
29+
*
30+
* <p>{@link #registerWith} is a static call with no visible side effects. In order to verify when
31+
* it's been called we also unfortunately need to store the state statically. This should be
32+
* called before and after each test run accessing this class to make sure the state is clear both
33+
* before and after the run.
34+
*/
35+
public static void clearRegisteredEngines() {
36+
registeredEngines.clear();
37+
}
38+
39+
/**
40+
* Returns a list of all the engines registered so far.
41+
*
42+
* <p>CAUTION: This list is static and must be manually wiped in between test runs. See
43+
* {@link #clearRegisteredEngines()}.
44+
*/
45+
public static List<FlutterEngine> getRegisteredEngines() {
46+
return new ArrayList<>(registeredEngines);
47+
}
48+
}

0 commit comments

Comments
 (0)