-
Notifications
You must be signed in to change notification settings - Fork 30.1k
Description
Steps to reproduce
- Execute
flutter run --releaseon the code sample with an Android phone.
Expected results: The app displays a centered Text widget with "Locale: en_US"
Actual results: The app displays a centered Text widget with "Locale: und"
The issue is a race condition that occurs when using FlutterEngineGroup to provide a FlutterEngine to a FlutterActivity.
RootCause
The root cause of issue seems to be that FlutterEngineGroup requires an entrypoint to be specified when the engine is created and it executes the entrypoint immediately. As a result, the engine returned by FlutterActivity.provideFlutterEngine() is already running. However, the FlutterActivityAndFragmentDelegate.onAttach() method, which indirectly called provideFutterEngine(), then sets up the PlatformPlugin using host.providePlatformPlugin() and calls host.configureFlutterEngine().
Since the engine is already running when it's returned from FlutterActivity.provideFlutterEngine(), there is a race between when the flutter code tries to access Platform.localeName and when the native code calls host.providePlatformPlugin()
Code Sample
A flutter project containing the following code that illustrates the issue can be found at: https://github.com/champeauxr/engine_group_race_condition
The Flutter UI just centers a Text widget that displays the current locale name:
import 'dart:io';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: Text('Locale: ${Platform.localeName}'),
),
),
);
}
}The native Android code simply overrides the provideFlutterEngine() and shouldDestroyEngineWithHost() methods of FlutterActivity to provide an engine created using the FlutterEngineGroup.
package com.example.engine_group_race_condition
import android.content.Context
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.FlutterEngineGroup
var engineGroup: FlutterEngineGroup? = null
class MainActivity: FlutterActivity() {
override fun shouldDestroyEngineWithHost(): Boolean {
return true;
}
override fun provideFlutterEngine(context: Context): FlutterEngine {
if (engineGroup == null) {
engineGroup = FlutterEngineGroup(this)
}
val engine = engineGroup!!.createAndRunDefaultEngine(this)
// This sleep() ensures that the flutter code started by createAndRunDefaultEngine() wins
// the race with FlutterActivity's call to either host.providePlatformPlugin() or
// perhaps host.configureFlutterEngine()
Thread.sleep(500);
return engine;
}
}A sleep() was added after creating the engine to ensure that the Flutter code wins the race to consistently demonstrate the issue. Without this sleep(), the problem is intermittent and happens more often than not on a release build, and with much less frequency on a debug build. Also, when running a debug build, the Text widget displays "Locale: und" for a fraction of a second before refreshing and displaying "Locale: en_US". However with a release build, the "Locale: und" remains in the Text widget.
Additional Investigation Notes
FlutterActivity.onCreate() calls FlutterActivityAndFragmentDelegate.onAttach()
void onAttach(@NonNull Context context) {
ensureAlive();
// When "retain instance" is true, the FlutterEngine will survive configuration
// changes. Therefore, we create a new one only if one does not already exist.
if (flutterEngine == null) {
setupFlutterEngine();
}
if (host.shouldAttachEngineToActivity()) {
// Notify any plugins that are currently attached to our FlutterEngine that they
// are now attached to an Activity.
//
// Passing this Fragment's Lifecycle should be sufficient because as long as this Fragment
// is attached to its Activity, the lifecycles should be in sync. Once this Fragment is
// detached from its Activity, that Activity will be detached from the FlutterEngine, too,
// which means there shouldn't be any possibility for the Fragment Lifecycle to get out of
// sync with the Activity. We use the Fragment's Lifecycle because it is possible that the
// attached Activity is not a LifecycleOwner.
Log.v(TAG, "Attaching FlutterEngine to the Activity that owns this delegate.");
flutterEngine.getActivityControlSurface().attachToActivity(this, host.getLifecycle());
}
// Regardless of whether or not a FlutterEngine already existed, the PlatformPlugin
// is bound to a specific Activity. Therefore, it needs to be created and configured
// every time this Fragment attaches to a new Activity.
// TODO(mattcarroll): the PlatformPlugin needs to be reimagined because it implicitly takes
// control of the entire window. This is unacceptable for non-fullscreen
// use-cases.
platformPlugin = host.providePlatformPlugin(host.getActivity(), flutterEngine);
host.configureFlutterEngine(flutterEngine);
isAttached = true;
}The call to setupFlutterEngine() is what calls FlutterActivity.provideFlutterEngine() and when setupFlutterEngine() returns, the engine is already running the Flutter code.
At the end of onAttach(), it runs the following two lines:
platformPlugin = host.providePlatformPlugin(host.getActivity(), flutterEngine);
host.configureFlutterEngine(flutterEngine);Presumably, one of these two lines connects up the PlatformPlugin to the Flutter engine and prior to these being called, the Flutter code would not be able to retrieve the locale name from Platform.localeName. However the FlutterEngine produced by the FlutterEngineGroup is already running Flutter code before we get here and may have already attempted to retrieve the locale name.
In normal scenarios, where the FlutterEngineGroup is not used, the entrypoint on the Flutter engine isn't executed until FlutterActivityAndFragmentDelegate.onStart() is called by FlutterActivity.onStart()
void onStart() {
Log.v(TAG, "onStart()");
ensureAlive();
doInitialFlutterViewRun();
}It would seem that FlutterEngineGroup should provide a method that creates an engine without executing an entrypoint. This would allow other initialization, such as that performed by FlutterActivityAndFragmentDelegate to be performed before the entrypoint is executed.
FlutterDoctor output
[√] Flutter (Channel stable, 2.10.0, on Microsoft Windows [Version 10.0.19043.1526], locale en-US)
• Flutter version 2.10.0 at E:\flutter
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision 5f105a6ca7 (4 weeks ago), 2022-02-01 14:15:42 -0800
• Engine revision 776efd2034
• Dart version 2.16.0
• DevTools version 2.9.2
[√] Android toolchain - develop for Android devices (Android SDK version 31.0.0)
• Android SDK at E:\Android\Sdk
• Platform android-31, build-tools 31.0.0
• ANDROID_SDK_ROOT = E:\Android\Sdk
• Java binary at: C:\Program Files\Android\Android Studio\jre\bin\java
• Java version OpenJDK Runtime Environment (build 11.0.11+9-b60-7590822)
• All Android licenses accepted.
[√] Chrome - develop for the web
• Chrome at C:\Program Files (x86)\Google\Chrome\Application\chrome.exe
[√] Visual Studio - develop for Windows (Visual Studio Professional 2022 17.0.6)
• Visual Studio at C:\Program Files\Microsoft Visual Studio\2022\Professional
• Visual Studio Professional 2022 version 17.0.32126.317
• Windows 10 SDK version 10.0.19041.0
[√] Android Studio (version 2021.1)
• Android Studio at C:\Program Files\Android\Android Studio
• Flutter plugin can be installed from:
https://plugins.jetbrains.com/plugin/9212-flutter
• Dart plugin can be installed from:
https://plugins.jetbrains.com/plugin/6351-dart
• Java version OpenJDK Runtime Environment (build 11.0.11+9-b60-7590822)
[√] VS Code, 64-bit edition (version 1.61.0)
• VS Code at C:\Program Files\Microsoft VS Code
• Flutter extension version 3.29.0
[√] Connected device (4 available)
• SM G781U1 (mobile) • RFCR11D2E3V • android-arm64 • Android 11 (API 30)
• Windows (desktop) • windows • windows-x64 • Microsoft Windows [Version 10.0.19043.1526]
• Chrome (web) • chrome • web-javascript • Google Chrome 99.0.4844.51
• Edge (web) • edge • web-javascript • Microsoft Edge 96.0.1054.62
[√] HTTP Host Availability
• All required HTTP hosts are available
• No issues found!