Closed navaronbracke closed 1 month ago
The void method should only return after the body has completed, including any awaits in the implementation.
the initialize() method should have fully completed before runApp() is invoked
The only time that a method would wait to return until after the body has completed is when it is await
ed.
You can try this in dartpad:
void main() async {
initialize();
print('Main method done');
}
void initialize() async {
await Future.delayed(const Duration(seconds: 1));
print('Future done');
}
When you execute this, we get:
Main method done
[One second delay]
Future done
This is the expected behavior.
To make this more clear, the same thing will happen even if initialize
returns a Future<void>
but is not awaited.
void main() async {
initialize(); // Not awaited!
print('Main method done');
}
Future<void> initialize() async { // Now a Future<void>
await Future.delayed(const Duration(seconds: 1));
print('Future done');
}
Using the await
keyword tells the method to suspend execution of the method until the Future is complete. But we have not used await
here, making the code execute synchronously, allowing the Future to run on its own without being waited on. We are given the choice in our code whether or not to await that Future by adding await
.
Although when the initialize method returns void, we don't have that choice at all, which is bad. This is the reason for the existence of the avoid_void_async lint https://dart.dev/tools/linter-rules/avoid_void_async, which reminds us to make async methods return a Future so that any executor may choose to await the work.
There's a similar issue here, where we're not awaiting the initialize method when we probably want to. Let me introduce another lint, unawaited_futures: https://dart.dev/tools/linter-rules/unawaited_futures. This lint will warn us when we've called a method that returns a Future without await
ing it. In the rare case we actually want to "fire and forget", you can use unawaited(initialize());
to silence the lint.
To summarize, for a method to wait for a Future to complete, that method must await
it. From the main method's perspective, it doesn't know the implementation of the initialize
method, except that it's a Function that returns void. No matter how many await
s are in the initialize
method's implementation, it has no effect unless the main
method chooses to await that work.
I did not know about avoid_void_async
yet, TIL. I guess we should raise the awareness of avoid_void_async
?
And now that I see the example, it does make sense. Just feels weird that we could write void async
methods, considering the hidden pitfall.
This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of flutter doctor -v
and a minimal reproduction of the issue.
I filed the following issue here rather than in dart-lang/sdk, because I'm not sure if it is an issue with the root bundle or with awaits
Steps to reproduce
flutter create bug
main.dart
.env
file to the assets of the app. AddFLAG=test
to said file (the value doesn't matter)q
await Environment.initializeAsync();
and uncomment the lineEnvironment.initialize();
StateError('not initialized')
Expected results
I would expect the
void initialize() async {}
to be idempotent to the awaitedFuture<void> initializeAsync() async {}
.The void method should only return after the body has completed, including any awaits in the implementation.
Actual results
Only the method that returns a Future (that is awaited) results in correct behavior.
In the logs below, we can see that the log
flutter: initialize(): env = {FLAG: production}
happens after the framework tried building theMyApp
widget, even though theinitialize()
method should have fully completed beforerunApp()
is invoked.Code sample
Code sample
```dart import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; DotEnv dotenv = DotEnv(); void main() async { WidgetsFlutterBinding.ensureInitialized(); await Environment.initializeAsync(); //Environment.initialize(); runApp(const MyApp()); } class Environment { static FutureScreenshots or Video
Screenshots / Video demonstration
N/ALogs
Logs
### First run, using `await Environment.initializeAsync();` ```console navaronbracke@MacBook-Pro-van-Navaron flavor_sample % flutter run -d macos Launching lib/main.dart on macOS in debug mode... Building macOS application... ✓ Built build/macos/Build/Products/Debug/flavor_sample.app [IMPORTANT:flutter/shell/platform/darwin/graphics/FlutterDarwinContextMetalSkia.mm(66)] Using the Skia rendering backend (Metal). 2024-03-29 12:00:34.052 flavor_sample[12007:94433] WARNING: Secure coding is automatically enabled for restorable state! However, not on all supported macOS versions of this application. Opt-in to secure coding explicitly by implementing NSApplicationDelegate.applicationSupportsSecureRestorableState:. flutter: initializeAsync(): env = {FLAG: test} Syncing files to device macOS... 37ms Flutter run key commands. r Hot reload. 🔥🔥🔥 R Hot restart. h List all available interactive commands. d Detach (terminate "flutter run" but leave application running). c Clear the screen q Quit (terminate the application on the device). A Dart VM Service on macOS is available at: http://127.0.0.1:53702/Ko1pattNBpk=/ The Flutter DevTools debugger and profiler on macOS is available at: http://127.0.0.1:9101?uri=http://127.0.0.1:53702/Ko1pattNBpk=/ Application finished. ``` ### Second run, using `Environment.initialize();` ```console navaronbracke@MacBook-Pro-van-Navaron flavor_sample % flutter run -d macos Launching lib/main.dart on macOS in debug mode... Building macOS application... ✓ Built build/macos/Build/Products/Debug/flavor_sample.app [IMPORTANT:flutter/shell/platform/darwin/graphics/FlutterDarwinContextMetalSkia.mm(66)] Using the Skia rendering backend (Metal). 2024-03-29 12:03:15.937 flavor_sample[12357:96922] WARNING: Secure coding is automatically enabled for restorable state! However, not on all supported macOS versions of this application. Opt-in to secure coding explicitly by implementing NSApplicationDelegate.applicationSupportsSecureRestorableState:. Syncing files to device macOS... 57ms Flutter run key commands. r Hot reload. 🔥🔥🔥 R Hot restart. h List all available interactive commands. d Detach (terminate "flutter run" but leave application running). c Clear the screen q Quit (terminate the application on the device). A Dart VM Service on macOS is available at: http://127.0.0.1:53810/lF_xwYkKW3U=/ ══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════ The following StateError was thrown building MyApp(dirty): Bad state: not initialized The relevant error-causing widget was: MyApp MyApp:file:///Users/navaronbracke/Desktop/flavor_sample/lib/main.dart:12:16 When the exception was thrown, this was the stack: #0 DotEnv.env (package:flavor_sample/main.dart:33:7) #1 MyApp.build (package:flavor_sample/main.dart:72:39) #2 StatelessElement.build (package:flutter/src/widgets/framework.dart:5557:49) #3 ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5487:15) #4 Element.rebuild (package:flutter/src/widgets/framework.dart:5203:7) #5 ComponentElement._firstBuild (package:flutter/src/widgets/framework.dart:5469:5) #6 ComponentElement.mount (package:flutter/src/widgets/framework.dart:5463:5) ... Normal element mounting (27 frames) #33 Element.inflateWidget (package:flutter/src/widgets/framework.dart:4340:16) #34 Element.updateChild (package:flutter/src/widgets/framework.dart:3849:18) #35 _RawViewElement._updateChild (package:flutter/src/widgets/view.dart:291:16) #36 _RawViewElement.mount (package:flutter/src/widgets/view.dart:314:5) ... Normal element mounting (7 frames) #43 Element.inflateWidget (package:flutter/src/widgets/framework.dart:4340:16) #44 Element.updateChild (package:flutter/src/widgets/framework.dart:3849:18) #45 RootElement._rebuild (package:flutter/src/widgets/binding.dart:1581:16) #46 RootElement.mount (package:flutter/src/widgets/binding.dart:1550:5) #47 RootWidget.attach.Flutter Doctor output
Doctor output
```console [✓] Flutter (Channel master, 3.21.0-17.0.pre.24, on macOS 14.3.1 23D60 darwin-x64, locale en-BE) • Flutter version 3.21.0-17.0.pre.24 on channel master at /Users/navaronbracke/Documents/flutter • Upstream repository git@github.com:navaronbracke/flutter.git • FLUTTER_GIT_URL = git@github.com:navaronbracke/flutter.git • Framework revision 1a2f34ab5b (8 hours ago), 2024-03-28 19:39:17 -0700 • Engine revision 68aa9ba386 • Dart version 3.4.0 (build 3.4.0-282.0.dev) • DevTools version 2.34.1 [✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0) • Android SDK at /Users/navaronbracke/Library/Android/sdk • Platform android-34, build-tools 34.0.0 • ANDROID_HOME = /Users/navaronbracke/Library/Android/sdk • Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java • Java version OpenJDK Runtime Environment (build 17.0.9+0-17.0.9b1087.7-11185874) • All Android licenses accepted. [✓] Xcode - develop for iOS and macOS (Xcode 15.3) • Xcode at /Applications/Xcode.app/Contents/Developer • Build 15E204a • CocoaPods version 1.15.2 [✓] Chrome - develop for the web • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome [✓] Android Studio (version 2023.2) • Android Studio at /Applications/Android Studio.app/Contents • 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 17.0.9+0-17.0.9b1087.7-11185874) [✓] VS Code (version 1.87.2) • VS Code at /Applications/Visual Studio Code.app/Contents • Flutter extension version 3.84.0 [✓] Connected device (2 available) • macOS (desktop) • macos • darwin-x64 • macOS 14.3.1 23D60 darwin-x64 • Chrome (web) • chrome • web-javascript • Google Chrome 123.0.6312.87 [✓] Network resources • All expected network resources are available. • No issues found! ```