"No Firebase App '[DEFAULT]' has been created" despite "initializeApp()" has been called (Flutter, Firebase) - flutter

I have the following workpiece of a starting widget:
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'firebase_options.dart';
import 'apps/auth_app.dart';
import 'apps/main_app.dart';
class StartingWidget extends StatelessWidget {
const StartingWidget({super.key});
void _initFirebase() async {
//WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
}
void _addAuthStatusListener() {
try {
FirebaseAuth.instance.authStateChanges().listen((User? user) {
if (user != null) {
//runApp(const MainApp());
} else {
//runApp(const AuthApp());
}
});
} catch (e) {
print(e.toString());
}
}
#override
Widget build(BuildContext context) {
_initFirebase();
_addAuthStatusListener();
return const Scaffold(
body: CircularProgressIndicator(),
);
}
}
When I start it on an Android emulator, I get the "No Firebase App '[DEFAULT]' has been created" error at line
FirebaseAuth.instance.authStateChanges().listen((User? user) {
despite Firebase.initializeApp() was called before. Uncommenting
WidgetsFlutterBinding.ensureInitialized();
doesn't change anything.

The reason is simple:
You have not waited for your _initFirebase() to complete, before you call _addAuthStatusListener(), which uses the Firebase app!
You also can't wait inside your build method (it has to render immediately and therefore can't be async), so I suggest you call _initFirebase() from inside _addAuthStatusListener() instead:
#override
Widget build(BuildContext context) {
// _initFirebase(); // <- Remove this here!
_addAuthStatusListener();
return const Scaffold(
body: CircularProgressIndicator(),
);
}
...
void _addAuthStatusListener() async { // Make async!
await _initFirebase(); // <- Add _initFirebase() here, with "await"!
try {
FirebaseAuth.instance.authStateChanges().listen((User? user) {
if (user != null) {
//runApp(const MainApp());
} else {
//runApp(const AuthApp());
}
});
} catch (e) {
print(e.toString());
}
}
That should do it! 🙂
Explanation
What you have to understand about async functions is that calling them without the await keyword only STARTS them. Then, the reader continues, without waiting for them to finish. Meanwhile, the async function also continues with its own stuff, in parallell with the main reader, until it's done.
And so, in your case above, you started the _initFirebase(), and inside there, a reader sat down and waited for Firebase.initializeApp() to complete. But while that was going on, the main reader continued to the next line in the build() method, which was _addAuthStatusListener(). And this function had plenty of time to make it to where it would have needed your completely initialized Firebase app (the line where you got the error), before said initialization had a chance to complete.
Later on, the Firebase.initializeApp() command would have completed, soon after which the entire _initFirebase() function would have completed, but by then, your app had already crashed.
Adding "await" before _initFirebase() makes sure, however, that the reader can't continue to the next line until this function has completed, even though it is async.

Related

Flutter RepositoryProvider and Hive LateInitializationError

I have app where I am using Bloc and Hive.
main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final appDocumentDirectory =
await path_provider.getApplicationDocumentsDirectory();
Hive.init(appDocumentDirectory.path);
runApp(
const MyApp(),
);
}
On MyApp widget registered MultiRepositoryProvider
return MultiRepositoryProvider(
providers: [
RepositoryProvider(create: (context) => AccountService()),
],
child: MultiBlocProvider(
providers: [
BlocProvider<AccountBloc>(
create: (context) => AccountBloc(context.read<AccountService>()),
),
],
child: MaterialApp(
home: const AppPage(),
),
),
);
AppPage Contains bottomNavigationBar and some pages
account.dart
class AccountService {
late Box<Account> _accounts;
AccountService() {
init();
}
Future<void> init() async {
Hive.registerAdapter(AccountAdapter());
_accounts = await Hive.openBox<Account>('accounts');
}
On appPage have BlocBuilder
BlocBuilder<AccountBloc, AccountState>(
builder: (context, state) {
if (state.accountStatus == AccountStatus.loading) {
return const CircularProgressIndicator();
} else if (state.accountStatus == AccountStatus.error) {
Future.delayed(Duration.zero, () {
errorDialog(context, state.error);
});
}
return SingleChildScrollView(....
When app first loaded I receive LateInitializationError that late Box <Account> _accounts from account Repository not initialized. But as soon as I navigate to another page and go back, the Box <Account> _accounts are initialized and the data appears.
How can I avoid this error and initialize the Hive box on application load?
Can you try this? I think you need to await Hive init function
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final appDocumentDirectory =
await path_provider.getApplicationDocumentsDirectory();
await Hive.init(appDocumentDirectory.path);
runApp(
const MyApp(),
);
}
It's been like 7 months, but if you are still looking for an answer, not sure if it's optimal but below should work.
My understanding on the issue you are having is that the reason why there is that "LateInitializationError" is because that your init function call in your constructor is asynchronously invoked without await for its result. As a result, there is a possibility that when you are calling functions on the box, the initialisation is not yet finished. When you navigate to another page and go back, the function init run happened to be finished. Hence, the error is gone. The complexity here is that constructor can not be marked as async for you to use that await keyword. Since you are using bloc, one possible workaround is to call the init function of your repo when bloc is in init state.
For demo purpose I defined below bloc states and events,
you can absolutely change them based on your needs.
// bloc states
abstract class AccountState{}
class InitState extends AccountState{}
class LoadedState extends AccountState{
LoadedState(this.accounts);
final List<Account> accounts;
}
class LoadingErrorState extends AccountState{}
//bloc events
abstract class AccountEvent {}
class InitEvent extends AccountEvent {}
... // other events
in your bloc logic you can call the init function from you repo on InitEvent
class AccountBloc extends Bloc<AccountEvent, AccountState> {
AccountBloc(this.repo) : super(InitState()) {
on<InitEvent>((event, emit) async {
await repo.init();
emit(LoadedState(account: repo.getAccounts()));
});
...// define handlers for other events
}
final AccountRepository repo;
}
in your service class you can remove the init from the constructor like:
class AccountService {
late Box<Account> _accounts;
AccountService();
Future<void> init() async {
Hive.registerAdapter(AccountAdapter());
_accounts = await Hive.openBox<Account>('accounts');
}
List<Account> getAccounts(){
return _accounts.values.toList();
}
}
Then in your bloc builder, you can add init event to your bloc when the state is InitState as below:
BlocBuilder<AccountBloc, AccountState>(
builder: (context, state) {
if (state is InitState) {
context.read<AccountBloc>.add(InitEvent());
return const CircularProgressIndicator();
} else if (state is LoadingErrorState) {
Future.delayed(Duration.zero, () {
errorDialog(context, state.error);
});
}
else if (state is LoadedState){
return SingleChildScrollView(....
}
Also, FYI, you can if you want the init to be called when the object of your account service is instantiated, you can take a look at below answer:
https://stackoverflow.com/a/59304510/16584569
However, you still going to need to await for the initialisation of your service. One possible way is just do it in your main function and pass down to your app, but it makes the structure of your code messy and when you want to swap to another repo, you need to remember to change code in main function as well.

How to catch async exception in one place (like main) and show it in AlertDialog?

Trouble
I build Flutter app + Dart.
Now i am trying to catch all future exceptions in ONE place (class) AND showAlertDialog.
Flutter Docs proposes 3 solutions to catch async errors:
runZonedGuarded
... async{ await future() }catch(e){ ... }
Future.onError
But no one can achieve all of the goals (in its purest form):
First: can't run in widget's build (need to return Widget, but returns Widget?.
Second: works in build, but don't catch async errors, which were throwed by unawaited futures, and is"dirty" (forces to use WidgetBinding.instance.addPostFrameCallback. I can ensure awaiting futures (which adds to the hassle), but I can't check does ensures it third-part libraries. Thus, it is bad case.
Third: is similar to second. And looks monstrous.
My (bearable) solution
I get first solution and added some details. So,
I created ZonedCatcher, which shows AlertDialog with exception or accumulates exceptions if it doesn't know where to show AlertDialog (BuildContext has not been provided).
AlertDialog requires MaterialLocalizations, so BuildContext is taken from MaterialApp's child MaterialChild.
void main() {
ZonedCatcher().runZonedApp();
}
...
class ZonedCatcher {
BuildContext? _materialContext;
set materialContext(BuildContext context) {
_materialContext = context;
if (_exceptionsStack.isNotEmpty) _showStacked();
}
final List<Object> _exceptionsStack = [];
void runZonedApp() {
runZonedGuarded<void>(
() => runApp(
Application(
MaterialChild(this),
),
),
_onError,
);
}
void _onError(Object exception, _) {
if (_materialContext == null) {
_exceptionsStack.add(exception);
} else {
_showException(exception);
}
}
void _showException(Object exception) {
print(exception);
showDialog(
context: _materialContext!,
builder: (newContext) => ExceptionAlertDialog(newContext),
);
}
void _showStacked() {
for (var exception in _exceptionsStack) {
_showException(exception);
}
}
}
...
class MaterialChild extends StatelessWidget {
MaterialChild(this.zonedCatcher);
final ZonedCatcher zonedCatcher;
#override
Widget build(BuildContext context) {
zonedCatcher.materialContext = context; //!
...
}
}
flaws
At this moment I don't know how organize app with several pages. materialContext can be taken only from MaterialApp childs, but pages are set already at the MaterialApp widget. Maybe, I will inject ZonedCatcher in all pages and building pages will re-set materialContext. But I probably will face with GlobalKey's problems, like reseting materialContext by some pages at the same time on gestures.
It is not common pattern, I have to thoroughly document this moment and this solution makes project harder to understand by others programmists.
This solution is not foreseen by Flutter creators and it can break on new packages with breaking-changes.
Any ideas?
By default, if there is an uncaught exception in a Flutter application, it is passed to FlutterError.onError. This can be overridden with a void Function(FlutterErrorDetails) to provide custom error handling behaviour:
void main() {
WidgetsFlutterBinding.ensureInitialized();
FlutterError.onError = (details) {
print(details.exception); // the uncaught exception
print(details.stack) // the stack trace at the time
}
runApp(MyApp());
}
If you want to show a dialog in this code, you will need access to a BuildContext (or some equivalent mechanism to hook into the element tree).
The standard way of doing this is with a GlobalKey. Here, for convenience (because you want to show a dialog) you can use a GlobalKey<NavigatorState>:
void main() {
WidgetsFlutterBinding.ensureInitialized();
final navigator = GlobalKey<NavigatorState>();
FlutterError.onError = (details) {
navigator.currentState!.push(MaterialPageRoute(builder: (context) {
// standard build method, return your dialog widget
return SimpleDialog(children: [Text(details.exception.toString())]);
}));
}
runApp(MyApp());
}
Note that if you need a BuildContext inside your onError callback, you can also use navigator.currentContext!.
You then need to pass your GlobalKey<NavigatorState> to MaterialApp (or Navigator if you create it manually):
#override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: navigatorKey, // pass in your navigator key
// other fields
);
}

How to update a list in Flutter with Bloc/Cubit?

my UI consists of a table of objects Apples.
Every cell in the table:
has an ADD button, if the apple is NOT present for that cell
it shows the apple and has a DELETE button, if the apple is present
The main page of my application is calling the following widget to LOAD the LIST of apples from an API. And also the ADD and DELETE functions communicate with the same API for adding and deleting the apple.
class ApplesLoader extends StatefulWidget {
#override
_ApplesLoaderState createState() => _ApplesLoaderState();
}
class _ApplesLoaderState extends State<ApplesLoader> {
#override
void initState() {
super.initState();
BlocProvider.of<ApplesCubit>(context).getAll();
}
#override
Widget build(BuildContext context) {
return BlocConsumer<ApplesCubit, ApplesState>(
listener: ...,
builder: (ctx, state) {
if (!(state is ApplesLoaded)) {
return circularProgressIndicator;
}
return ApplesViewer(state.Apples);
},
);
}
}
So after that ApplesViewerhas the list of apples and can correctly display the grid.
But now I have two problems:
When I press the ADD button or the DELETE button, all the application is rebuilt, while I could actually just re-paint the cell. But I don't know how to avoid this, because I also need to communicate to the application that the list of apples is actually changed. I don't want to rebuild the entire UI because it seems inefficient since what is actually changing on my page is only the selected cell.
When I press the ADD button or the DELETE button, I would like to show a circular progress indicator that replaces the button (while waiting for the API to actually creating/deleting the apple). But when the list changes, all the application is rebuilt instead. So, I am not sure how to achieve this desired behavior.
Could you help me in solving those two issues?
Or advise me for a change of infrastructure in case I am making some mistake in dealing with this?
If needed, this is the code for ApplesCubit
class ApplesCubit extends Cubit<ApplesState> {
final ApplesRepository _repository;
ApplesCubit(this._repository) : super(ApplesLoading());
Future<void> getAll() async {
try {
final apples = await _repository.getAll();
emit(ApplesLoaded(List<Apple>.from(apples)));
} on DataError catch (e) {
emit(ApplesError(e));
}
}
Future<void> create(List<Apple> apples, Apple appleToAdd) async {
try {
final newApple = await _repository.create(appleToAdd);
final updatedApples = List<Apple>.from(apples)..add(newApple);
emit(ApplesLoaded(updatedApples));
} on DataError catch (e) {
emit(ApplesError(e));
}
}
Future<void> delete(List<Apple> Appless, Apple appleToDelete) async {
try {
await _repository.deleteApples(appleToDelete);
final updatedApples = List<Apple>.from(Appless)..remove(appleToDelete);
emit(ApplesLoaded(updatedApples));
} on DataError catch (e) {
emit(ApplesError(e));
}
}
}

How do I cancel a StreamSubscription inside a Cubit?

I have a cubit that listens to a stream of messages, and emits a state which holds the messages.
In the screen, I am using a BlocProvider to access the cubit, and a BlocBuilder to display
the messages.
In instances like below, do I need to close the StreamSubscription created on listen()? Is there a clean way to do it?
class MessageCubit extends Cubit<MessageState> {
final GetMessagesUseCase getMessagesUseCase;
MessageCubit({this.getMessagesUseCase}) : super(MessageInitial());
Future<void> getMessages({String senderId, String recipientId}) async {
emit(MessageLoading());
try {
final messagesStreamData = getMessagesUseCase.call();
//This is where I listen to a stream
messagesStreamData.listen((messages) {
emit(MessageLoaded(messages: messages));
});
} on SocketException catch (_) {
emit(MessageFailure());
} catch (_) {
emit(MessageFailure());
}
}
}
You don't need to close the subscription, but you should as good practice to avoid potential memory leaks. Since it is so straightforward it's not any sacrifice.
Create a class variable of type StreamSubscription<your type>. Let's say it's named sub.
In getMessages before listen: await sub?.cancel()
Then sub = messagesStreamData.listen(...
Override the Cubit's close method and run the same command as in bullet 2.
Full code:
class MessageCubit extends Cubit<MessageState> {
final GetMessagesUseCase getMessagesUseCase;
// Added
StreamSubscription<YOUR_MESSAGES_TYPE> sub;
MessageCubit({this.getMessagesUseCase}) : super(MessageInitial());
Future<void> getMessages({String senderId, String recipientId}) async {
emit(MessageLoading());
try {
final messagesStreamData = getMessagesUseCase.call();
// Added
await sub?.cancel();
//This is where I listen to a stream
sub = messagesStreamData.listen((messages) {
emit(MessageLoaded(messages: messages));
});
} on SocketException catch (_) {
emit(MessageFailure());
} catch (_) {
emit(MessageFailure());
}
}
// Added
#override
Future<void> close() async {
await sub?.cancel();
return super.close();
}
}

Flutter: Hot restart crashes the app but cold restart doesn't (because a recorder is already initialized)

I am using the flutter_sound package to record some audio and as soon as the app starts up I initialise a recorder.
When I hot restart the app another recorder is initialised and the app crashes because on iOS there can only be one recorder.
When I cold restart the app I don't run into this problem, probably because all the resources are freed.
How can I make sure that the code that releases the recorder is called whenever I hot restart the app?
This is the relevant code in the UI.
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<RecorderService>(
create: (_) => RecorderService(),
),
],
child: MaterialApp(
home: ScreenToShow(),
),
);
}
And this is the relevant code in the Recorder Service class:
class RecorderService with ChangeNotifier {
Recording recording;
RecordingStatus status = RecordingStatus.uninitialized;
static const String RECORDING_FORMAT = ".aac";
static const String LISTENING_FORMAT = ".mp3";
static const Duration UPDATE_DURATION_OF_STREAM = Duration(milliseconds: 100);
RecorderService() {
_initialize();
}
/// Private properties
FlutterSoundRecorder _recorder;
Directory _tempDir;
FileConverterService _fileConverterService = FileConverterService();
/// This is the file path in which the [_recorder] writes its data. From the moment it gets assigned in [_initialize()] it stays fixed
String _pathToCurrentRecording;
/// This is the file path to which the [recording] will be saved to. It changes with every call of [_startWithoutReset()]
String _pathToSavedRecording;
/// This function can only be executed once per session else it crashes on iOS (because there is already an initialized recorder)
/// So when we hot restart the app this makes it crash
_initialize() async {
try {
/// The arguments for [openAudioSession] are explained here: https://github.com/dooboolab/flutter_sound/blob/master/doc/player.md#openaudiosession-and-closeaudiosession
_recorder = await FlutterSoundRecorder().openAudioSession(
focus: AudioFocus.requestFocusAndKeepOthers,
category: SessionCategory.playAndRecord,
mode: SessionMode.modeDefault,
audioFlags: outputToSpeaker);
await _recorder.setSubscriptionDuration(UPDATE_DURATION_OF_STREAM);
_tempDir = await getTemporaryDirectory();
_pathToSavedRecording =
"${_tempDir.path}/saved_recording$LISTENING_FORMAT";
status = RecordingStatus.initialized;
notifyListeners();
} catch (e) {
print("Recorder service could not be initialized because of error = $e");
}
}
#override
dispose() async {
try {
await _recorder?.closeAudioSession();
super.dispose();
} catch (e) {
print("Recorder service could not be disposed because of error = $e");
}
}
}
Are you properly closing the session. Read the documentation from here
I realized that this is a month past your initial post but I came across this problem today.
I've found that the fix is to not call the following within the initState() of the page:
_recorder = await FlutterSoundRecorder().openAudioSession(...)
Instead, I created the following:
Future<void> startAudioSession() async {recorderModule.openAudioSession(...);}
And called it at the beginning of the startRecorder function, and then used the closeAudioSession() in the stopRecorder function.