Skip to content

Commit cfa524f

Browse files
New Plugin API PR4: Adds Lifecycle support to the new plugin system. (flutter#9049)
1 parent 6b8ac18 commit cfa524f

File tree

10 files changed

+256
-18
lines changed

10 files changed

+256
-18
lines changed

ci/licenses_golden/licenses_flutter

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/Flutt
543543
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterTextureView.java
544544
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterView.java
545545
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java
546+
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngineAndroidLifecycle.java
546547
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEnginePluginRegistry.java
547548
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java
548549
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterShellArgs.java

shell/platform/android/BUILD.gn

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ action("flutter_shell_java") {
128128
"io/flutter/embedding/android/FlutterTextureView.java",
129129
"io/flutter/embedding/android/FlutterView.java",
130130
"io/flutter/embedding/engine/FlutterEngine.java",
131+
"io/flutter/embedding/engine/FlutterEngineAndroidLifecycle.java",
131132
"io/flutter/embedding/engine/FlutterEnginePluginRegistry.java",
132133
"io/flutter/embedding/engine/FlutterJNI.java",
133134
"io/flutter/embedding/engine/FlutterShellArgs.java",
@@ -216,6 +217,8 @@ action("flutter_shell_java") {
216217
"//third_party/android_support/android_support_annotations.jar",
217218
"//third_party/android_support/android_support_fragment.jar",
218219
"//third_party/android_support/android_arch_lifecycle_common.jar",
220+
"//third_party/android_support/android_arch_lifecycle_common_java8.jar",
221+
"//third_party/android_support/android_arch_lifecycle_runtime.jar",
219222
"//third_party/android_support/android_arch_lifecycle_viewmodel.jar",
220223
]
221224

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,9 +274,26 @@ protected FlutterFragment createFlutterFragment() {
274274
.flutterShellArgs(FlutterShellArgs.fromIntent(getIntent()))
275275
.renderMode(FlutterView.RenderMode.surface)
276276
.transparencyMode(FlutterView.TransparencyMode.opaque)
277+
.shouldAttachEngineToActivity(shouldAttachEngineToActivity())
277278
.build();
278279
}
279280

281+
/**
282+
* Hook for subclasses to control whether or not the {@link FlutterFragment} within this
283+
* {@code Activity} automatically attaches its {@link FlutterEngine} to this {@code Activity}.
284+
* <p>
285+
* For an explanation of why this control exists, see {@link FlutterFragment.Builder#shouldAttachEngineToActivity()}.
286+
* <p>
287+
* This property is controlled with a protected method instead of an {@code Intent} argument because
288+
* the only situation where changing this value would help, is a situation in which
289+
* {@code FlutterActivity} is being subclassed to utilize a custom and/or cached {@link FlutterEngine}.
290+
* <p>
291+
* Defaults to {@code true}.
292+
*/
293+
protected boolean shouldAttachEngineToActivity() {
294+
return true;
295+
}
296+
280297
@Override
281298
public void onPostResume() {
282299
super.onPostResume();

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

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ public class FlutterFragment extends Fragment {
6666
protected static final String ARG_FLUTTER_INITIALIZATION_ARGS = "initialization_args";
6767
protected static final String ARG_FLUTTERVIEW_RENDER_MODE = "flutterview_render_mode";
6868
protected static final String ARG_FLUTTERVIEW_TRANSPARENCY_MODE = "flutterview_transparency_mode";
69+
protected static final String ARG_SHOULD_ATTACH_ENGINE_TO_ACTIVITY = "should_attach_engine_to_activity";
6970

7071
/**
7172
* Builder that creates a new {@code FlutterFragment} with {@code arguments} that correspond
@@ -118,6 +119,7 @@ public static class Builder {
118119
private FlutterShellArgs shellArgs = null;
119120
private FlutterView.RenderMode renderMode = FlutterView.RenderMode.surface;
120121
private FlutterView.TransparencyMode transparencyMode = FlutterView.TransparencyMode.transparent;
122+
private boolean shouldAttachEngineToActivity = true;
121123

122124
/**
123125
* Constructs a {@code Builder} that is configured to construct an instance of
@@ -199,6 +201,46 @@ public Builder transparencyMode(@NonNull FlutterView.TransparencyMode transparen
199201
return this;
200202
}
201203

204+
/**
205+
* Whether or not this {@code FlutterFragment} should automatically attach its
206+
* {@code Activity} as a control surface for its {@link FlutterEngine}.
207+
* <p>
208+
* Control surfaces are used to provide Android resources and lifecycle events to
209+
* plugins that are attached to the {@link FlutterEngine}. If {@code shouldAttachEngineToActivity}
210+
* is true then this {@code FlutterFragment} will connect its {@link FlutterEngine} to the
211+
* surrounding {@code Activity}, along with any plugins that are registered with that
212+
* {@link FlutterEngine}. This allows plugins to access the {@code Activity}, as well as
213+
* receive {@code Activity}-specific calls, e.g., {@link android.app.Activity#onNewIntent(Intent)}.
214+
* If {@code shouldAttachEngineToActivity} is false, then this {@code FlutterFragment} will not
215+
* automatically manage the connection between its {@link FlutterEngine} and the surrounding
216+
* {@code Activity}. The {@code Activity} will need to be manually connected to this
217+
* {@code FlutterFragment}'s {@link FlutterEngine} by the app developer. See
218+
* {@link FlutterEngine#getActivityControlSurface()}.
219+
* <p>
220+
* One reason that a developer might choose to manually manage the relationship between the
221+
* {@code Activity} and {@link FlutterEngine} is if the developer wants to move the
222+
* {@link FlutterEngine} somewhere else. For example, a developer might want the
223+
* {@link FlutterEngine} to outlive the surrounding {@code Activity} so that it can be used
224+
* later in a different {@code Activity}. To accomplish this, the {@link FlutterEngine} will
225+
* need to be disconnected from the surrounding {@code Activity} at an unusual time, preventing
226+
* this {@code FlutterFragment} from correctly managing the relationship between the
227+
* {@link FlutterEngine} and the surrounding {@code Activity}.
228+
* <p>
229+
* Another reason that a developer might choose to manually manage the relationship between the
230+
* {@code Activity} and {@link FlutterEngine} is if the developer wants to prevent, or explicitly
231+
* control when the {@link FlutterEngine}'s plugins have access to the surrounding {@code Activity}.
232+
* For example, imagine that this {@code FlutterFragment} only takes up part of the screen and
233+
* the app developer wants to ensure that none of the Flutter plugins are able to manipulate
234+
* the surrounding {@code Activity}. In this case, the developer would not want the
235+
* {@link FlutterEngine} to have access to the {@code Activity}, which can be accomplished by
236+
* setting {@code shouldAttachEngineToActivity} to {@code false}.
237+
*/
238+
@NonNull
239+
public Builder shouldAttachEngineToActivity(boolean shouldAttachEngineToActivity) {
240+
this.shouldAttachEngineToActivity = shouldAttachEngineToActivity;
241+
return this;
242+
}
243+
202244
/**
203245
* Creates a {@link Bundle} of arguments that are assigned to the new {@code FlutterFragment}.
204246
* <p>
@@ -217,6 +259,7 @@ protected Bundle createArgs() {
217259
}
218260
args.putString(ARG_FLUTTERVIEW_RENDER_MODE, renderMode != null ? renderMode.name() : FlutterView.RenderMode.surface.name());
219261
args.putString(ARG_FLUTTERVIEW_TRANSPARENCY_MODE, transparencyMode != null ? transparencyMode.name() : FlutterView.TransparencyMode.transparent.name());
262+
args.putBoolean(ARG_SHOULD_ATTACH_ENGINE_TO_ACTIVITY, shouldAttachEngineToActivity);
220263
return args;
221264
}
222265

@@ -303,10 +346,12 @@ public void onAttach(Context context) {
303346
// use-cases.
304347
platformPlugin = new PlatformPlugin(getActivity(), flutterEngine.getPlatformChannel());
305348

306-
// Notify any plugins that are currently attached to our FlutterEngine that they
307-
// are now attached to an Activity.
308-
// TODO(mattcarroll): send in a real lifecycle.
309-
flutterEngine.getActivityControlSurface().attachToActivity(getActivity(), null);
349+
if (shouldAttachEngineToActivity()) {
350+
// Notify any plugins that are currently attached to our FlutterEngine that they
351+
// are now attached to an Activity.
352+
// TODO(mattcarroll): send in a real lifecycle.
353+
flutterEngine.getActivityControlSurface().attachToActivity(getActivity(), null);
354+
}
310355
}
311356

312357
private void initializeFlutter(@NonNull Context context) {
@@ -543,9 +588,11 @@ public void onDetach() {
543588
super.onDetach();
544589
Log.d(TAG, "onDetach()");
545590

546-
// Notify plugins that they are no longer attached to an Activity.
547-
// TODO(mattcarroll): differentiate between detaching for config changes and otherwise.
548-
flutterEngine.getActivityControlSurface().detachFromActivity();
591+
if (shouldAttachEngineToActivity()) {
592+
// Notify plugins that they are no longer attached to an Activity.
593+
// TODO(mattcarroll): differentiate between detaching for config changes and otherwise.
594+
flutterEngine.getActivityControlSurface().detachFromActivity();
595+
}
549596

550597
// Null out the platformPlugin to avoid a possible retain cycle between the plugin, this Fragment,
551598
// and this Fragment's Activity.
@@ -572,6 +619,10 @@ protected boolean retainFlutterEngineAfterFragmentDestruction() {
572619
return false;
573620
}
574621

622+
protected boolean shouldAttachEngineToActivity() {
623+
return getArguments().getBoolean(ARG_SHOULD_ATTACH_ENGINE_TO_ACTIVITY);
624+
}
625+
575626
/**
576627
* The hardware back button was pressed.
577628
*

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

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
package io.flutter.embedding.engine;
66

7+
import android.arch.lifecycle.Lifecycle;
8+
import android.arch.lifecycle.LifecycleOwner;
79
import android.content.Context;
810
import android.support.annotation.NonNull;
911

@@ -48,7 +50,7 @@
4850
* a {@link io.flutter.embedding.android.FlutterView} as a {@link FlutterRenderer.RenderSurface}.
4951
*/
5052
// TODO(mattcarroll): re-evaluate system channel APIs - some are not well named or differentiated
51-
public class FlutterEngine {
53+
public class FlutterEngine implements LifecycleOwner {
5254
private static final String TAG = "FlutterEngine";
5355

5456
@NonNull
@@ -59,6 +61,8 @@ public class FlutterEngine {
5961
private final DartExecutor dartExecutor;
6062
@NonNull
6163
private final FlutterEnginePluginRegistry pluginRegistry;
64+
@NonNull
65+
private final FlutterEngineAndroidLifecycle androidLifecycle;
6266

6367
// System channels.
6468
@NonNull
@@ -125,11 +129,11 @@ public FlutterEngine(@NonNull Context context) {
125129
systemChannel = new SystemChannel(dartExecutor);
126130
textInputChannel = new TextInputChannel(dartExecutor);
127131

128-
// TODO(mattcarroll): bring in Lifecycle.
132+
androidLifecycle = new FlutterEngineAndroidLifecycle(this);
129133
this.pluginRegistry = new FlutterEnginePluginRegistry(
130134
context.getApplicationContext(),
131135
this,
132-
null
136+
androidLifecycle
133137
);
134138
}
135139

@@ -154,7 +158,8 @@ private boolean isAttachedToJni() {
154158
* This {@code FlutterEngine} instance should be discarded after invoking this method.
155159
*/
156160
public void destroy() {
157-
pluginRegistry.removeAll();
161+
// The order that these things are destroyed is important.
162+
pluginRegistry.destroy();
158163
dartExecutor.onDetachedFromJNI();
159164
flutterJNI.removeEngineLifecycleListener(engineLifecycleListener);
160165
flutterJNI.detachFromNativeAndReleaseResources();
@@ -288,6 +293,13 @@ public ContentProviderControlSurface getContentProviderControlSurface() {
288293
return pluginRegistry;
289294
}
290295

296+
// TODO(mattcarroll): determine if we really need to expose this from FlutterEngine vs making PluginBinding a LifecycleOwner
297+
@NonNull
298+
@Override
299+
public Lifecycle getLifecycle() {
300+
return androidLifecycle;
301+
}
302+
291303
/**
292304
* Lifecycle callbacks for Flutter engine lifecycle events.
293305
*/
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package io.flutter.embedding.engine;
2+
3+
import android.arch.lifecycle.DefaultLifecycleObserver;
4+
import android.arch.lifecycle.Lifecycle;
5+
import android.arch.lifecycle.LifecycleObserver;
6+
import android.arch.lifecycle.LifecycleOwner;
7+
import android.arch.lifecycle.LifecycleRegistry;
8+
import android.support.annotation.NonNull;
9+
import android.support.annotation.Nullable;
10+
11+
/**
12+
* Android {@link Lifecycle} that is owned by a {@link FlutterEngine}.
13+
* <p>
14+
* {@code FlutterEngineAndroidLifecycle} exists so that {@code FlutterPlugin}s can monitor Android
15+
* lifecycle events. When the associated {@link FlutterEngine} is running in an {@code Activity},
16+
* that {@code Activity}'s {@link Lifecycle} can be set as the {@code backingLifecycle} of this
17+
* class, allowing all Flutter plugins to receive the {@code Activity}'s lifecycle events. Likewise,
18+
* when the associated {@link FlutterEngine} is running in a {@code Service}, that {@code Service}'s
19+
* {@link Lifecycle} can be set as the {@code backingLifecycle}.
20+
* <p>
21+
* Sometimes a {@link FlutterEngine} exists in a non-lifecycle location, e.g., an {@code Application},
22+
* {@code ContentProvider}, or {@code BroadcastReceiver}. In these cases, this lifecycle reports
23+
* itself in the {@link Lifecycle.State#CREATED} state.
24+
* <p>
25+
* Regardless of what happens to a backing {@code Activity} or @{code Service}, this lifecycle
26+
* will only report itself as {@link Lifecycle.State#DESTROYED} when the associated {@link FlutterEngine}
27+
* itself is destroyed. This is because a {@link Lifecycle} is not allowed to emit any events after
28+
* going to the {@link Lifecycle.State#DESTROYED} state. Thus, this lifecycle cannot emit such an
29+
* event until its associated {@link FlutterEngine} is destroyed. This then begs the question, what
30+
* happens when the backing {@code Activity} or {@code Service} is destroyed? This lifecycle will
31+
* report the process up to the {@link Lifecycle.Event#ON_STOP} event, but will ignore the
32+
* {@link Lifecycle.Event#ON_DESTROY} event. At that point, this lifecycle will be back in its
33+
* default {@link Lifecycle.State#CREATED} state until some other backing {@link Lifecycle} is
34+
* registered.
35+
*/
36+
final class FlutterEngineAndroidLifecycle extends LifecycleRegistry {
37+
private static final String TAG = "FlutterEngineAndroidLifecycle";
38+
39+
@Nullable
40+
private Lifecycle backingLifecycle;
41+
private boolean isDestroyed = false;
42+
43+
private final LifecycleObserver forwardingObserver = new DefaultLifecycleObserver() {
44+
@Override
45+
public void onCreate(@NonNull LifecycleOwner owner) {
46+
// No-op. The FlutterEngine's Lifecycle is always at least Created
47+
// until it is Destroyed, so we ignore onCreate() events from
48+
// backing Lifecycles.
49+
}
50+
51+
@Override
52+
public void onStart(@NonNull LifecycleOwner owner) {
53+
handleLifecycleEvent(Event.ON_START);
54+
}
55+
56+
@Override
57+
public void onResume(@NonNull LifecycleOwner owner) {
58+
handleLifecycleEvent(Event.ON_RESUME);
59+
}
60+
61+
@Override
62+
public void onPause(@NonNull LifecycleOwner owner) {
63+
handleLifecycleEvent(Event.ON_PAUSE);
64+
}
65+
66+
@Override
67+
public void onStop(@NonNull LifecycleOwner owner) {
68+
handleLifecycleEvent(Event.ON_STOP);
69+
}
70+
71+
@Override
72+
public void onDestroy(@NonNull LifecycleOwner owner) {
73+
// No-op. We don't allow FlutterEngine's Lifecycle to report destruction
74+
// until the FlutterEngine itself is destroyed. This is because a Lifecycle
75+
// is contractually obligated to send no more event once it gets to the
76+
// Destroyed state, which would prevent FlutterEngine from switching to
77+
// the next Lifecycle that is attached.
78+
}
79+
};
80+
81+
FlutterEngineAndroidLifecycle(@NonNull LifecycleOwner provider) {
82+
super(provider);
83+
}
84+
85+
public void setBackingLifecycle(@Nullable Lifecycle lifecycle) {
86+
ensureNotDestroyed();
87+
88+
// We no longer want to propagate events from the old Lifecycle. Deregister our forwarding observer.
89+
if (backingLifecycle != null) {
90+
backingLifecycle.removeObserver(forwardingObserver);
91+
}
92+
93+
// Manually move us to the Stopped state before we switch out the underlying Lifecycle.
94+
handleLifecycleEvent(Event.ON_STOP);
95+
96+
// Switch out the underlying lifecycle.
97+
backingLifecycle = lifecycle;
98+
99+
if (backingLifecycle != null) {
100+
// Add our forwardingObserver to the new backing Lifecycle so that this PluginRegistry is
101+
// controlled by that backing lifecycle. Adding our forwarding observer will automatically
102+
// result in invocations of the necessary Lifecycle events to bring us up to speed with the
103+
// new backingLifecycle, e.g., onStart(), onResume().
104+
lifecycle.addObserver(forwardingObserver);
105+
}
106+
}
107+
108+
@Override
109+
public void handleLifecycleEvent(@NonNull Event event) {
110+
ensureNotDestroyed();
111+
super.handleLifecycleEvent(event);
112+
}
113+
114+
public void destroy() {
115+
ensureNotDestroyed();
116+
setBackingLifecycle(null);
117+
markState(State.DESTROYED);
118+
isDestroyed = true;
119+
}
120+
121+
private void ensureNotDestroyed() {
122+
if (isDestroyed) {
123+
throw new IllegalStateException("Tried to invoke a method on a destroyed FlutterEngineAndroidLifecycle.");
124+
}
125+
}
126+
}

0 commit comments

Comments
 (0)