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

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.

Related

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

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.

Flutter: Async function in Getx Controller takes no effect when initialized

Updates:
2021/06/11 After hours of debugging yesterday, I confirmed that the problem is caused by aws amplify configuration: _configureAmplify(). Because the location of the amplify server was set wrong, so _configureAmplify() takes several seconds to work... and therefore, the readPost() function did not work on initialization, as it must run after _configureAmplify()...
2021/06/10I made changes to my code according to S. M. JAHANGIR's advice, and updated the question. The issue still presists. The value of posts is not updated when called in initialization and the data only shows up after reload. (if I commented out the _controller.readPost() in UI, the value of posts is always empty.
I have this page that loads information from aws amplify with getx implemented. However, I found out the readPost() async funtion in getx controller dart file is not reading from database, when the controller instance is initialized. I have to add a _controller.readPost() in UI file to make it work. And the data only shows up after a reload of that UI page...
Getx Controller dart file:
class ReadPostController extends GetxController {
var isLoading = true.obs;
var posts = <Posty>[].obs;
#override
void onInit() {
_configureAmplify();
await readPost();
super.onInit();
// print('show post return value: $posts');
}
void _configureAmplify() {
final provider = ModelProvider();
final dataStorePlugin = AmplifyDataStore(modelProvider: provider);
AmplifyStorageS3 storage = new AmplifyStorageS3();
AmplifyAuthCognito auth = new AmplifyAuthCognito();
AmplifyAPI apiRest = AmplifyAPI();
// Amplify.addPlugin(dataStorePlugin);
Amplify..addPlugins([dataStorePlugin, storage, auth, apiRest]);
Amplify.configure(amplifyconfig);
print('Amplify configured');
}
// read all posts from databases
Future readPost() async {
try {
isLoading(true);
var result = await Amplify.DataStore.query(Posty.classType);
print('finish loading request');
result = result.sublist(1);
posts.assignAll(result);
// print(the value of posts is $posts');
} finally {
isLoading(false);
}
}
#override
void onClose() {
// called just before the Controller is deleted from memory
super.onClose();
}
}
And in the UI part:
class TabBody extends StatelessWidget {
TabBody({Key? key}) : super(key: key);
final ReadPostController _controller = Get.put(ReadPostController());
#override
Widget build(BuildContext context) {
_controller.readPost();//if commented out, _controller.post is empty
return Container(
child: Obx(
() => Text('showing:${_controller.posts[1].title}'),
));
}
}
In my understanding, the readPost() function should be called when the ReadPost_controller is initiallized. And the UI will update when the posts = <Posty>[].obs changes. Guys, what am I doing wrong here?
First, when you are calling readPost on onInit you are not awaiting. So change it to:
onInit() async{
...
await readPost();
...
}
Secondly, posts is a RxList so you need to use the assignAll method to update it.
Therefore, in your readPost method, instead of posts.value = reault you need to use posts.assignAll(result)
Calling from the UI works because readPost every time the build method is called by the Flutter framework and actually the UI shows the data from every previous call.
I think try with GetBuilder instead of Obx.
GetBuilder<ReadPostController>(
builder: (value) => Text('showing:${value.posts[1].title}'),
)
and also use update(). in readPost() method.

Flutter Riverpod: Refresh Page Offscreen using State Notifier

I'm using StateNotifier and Riverpod.
I have a notification page, which contains a list of notification. When a notification arrives, I trigger a refresh for notification. However, when I navigate to notification page, it still using the old (cached?) list.
How do I refresh a page offscreen?
Foreground Message
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
_localNotification.showLocalNotification(message);
ProviderContainer().read(notificationProvider).getNotification();
});
Background Message
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp();
final LocalNotification localNotification = LocalNotification();
await localNotification.init();
localNotification.showLocalNotification(message);
ProviderContainer().read(notificationProvider).getNotification();
}
NotificationProvider
Future<void> getNotification() async {
try {
state = const NotificationLoadingState();
_notificationList = await _notificationRepository.getNotification();
state = NotificationLoadedState(_notificationList); // ==> I get a new list here
} catch (e, s) {
state = const NotificationErrorState('error fetching notification');
}
}
UI
final state = watch(notificationProvider.state);
if (state is NotificationLoadingState) {
return _buildLoading();
} else if (state is NotificationLoadedState) {
return _buildLoaded(state.notificationList); // ==> I still get the old list here
} else if (state is NotificationErrorState) {
return _buildError(state.message);
}
Edit:
I managed to solve the foreground message handler by using navigatorKey.currentContext.
However, I still haven't solved background message handler.
I've tried changing my main.dart to use UncontrolledProviderScope with a global ProviderContainer, which then was called from background message handler. It's still not refreshing.
With this line you are creating a new instance for your providers :
ProviderContainer().read(notificationProvider).getNotification();
you need to use a context to get the existing instance of ProviderContainer:
context.read(notificationProvider).getNotification();
or if you are not inside ui make dependencies between your providers

Flutter/Dart : How to wait for asynchronous task before app starts?

I am working on a dart application where I want to fetch the data present in cache (SharedPreferences) and then show it on UI (home screen) of the app.
Problem : Since SharedPreferences is an await call, my home page loads, tries to read the data and app crashes because data fetch has not yet happened from SharedPreferences, and app loads before that.
How can I not start the app until cache read from SharedPreferences is done?
This is required because I have to display data from SharedPreferences on home page of the app.
Various view files of my project call static function : MyService.getValue(key) which crashes as cacheResponseJson has not populated yet. I want to wait for SharedPreferences to complete before my app starts.
Class MyService {
String _cacheString;
static Map < String, dynamic > cacheResponseJson;
MyService() {
asyncInit();
}
Future < void > asyncInit() async {
SharedPreferences sharedPreferences = await SharedPreferences.getInstance();
_cacheString = sharedPreferences.getString(“ConfigCache”);
cacheResponseJson = jsonDecode(ecsCacheString);
}
static String getValue(String key) {
return cacheResponseJson[key];
}
}
void main() {
MyService s = MyService();
}
Any help would be highly appreciated!
You can run code in your main() method, before the call to runApp() that kicks off your application.
For example:
void main() async {
WidgetsFlutterBinding.ensureInitialized(); // makes sure plugins are initialized
final sharedPreferences = MySharedPreferencesService(); // however you create your service
final config = await sharedPreferences.get('config');
runApp(MyApp(config: config));
}
Can you try wrapping the function asyncInit() in initstate then in the function then setstate the values
_cacheString = sharedPreferences.getString(“ConfigCache”);
cacheResponseJson = jsonDecode(ecsCacheString);
I hope it works.
avoid using initialization etc outside the runApp() function, you can create a singleton
class MyService{
MyService._oneTime();
static final _instance = MyService._oneTime();
factory MyService(){
return _instance;
}
Future <bool> asyncInit() async {
//do stuff
return true;
}
}
and incorporate that in the UI like this
runApp(
FutureBuilder(
future: MyService().asyncInit(),
builder: (_,snap){
if(snap.hasData){
//here you can use the MyService singleton and its members
return MaterialApp();
}
return CircularProgressIndicator();
},
)
);
if you take this approach you can do any UI related feedback for the user while the data loads

where to load model from file in flutter apps?

Suppose I store my data in a dedicated repo class like so:
class UrlEntry {
final String url;
final String title;
UrlEntry({#required this.url, this.title});
}
class UrlRepository with ChangeNotifier {
List<UrlEntry> urlEntries = new List<UrlEntry>();
// Returns the urls as a separate list. Modifyable, but doesnt change state.
List<UrlEntry> getUrls() => new List<UrlEntry>.from(urlEntries);
add(UrlEntry url) {
this.urlEntries.add(url);
print(
"url entry ${url.url} added. Now having ${urlEntries.length} entries ");
notifyListeners();
}
removeByUrl(String url) {
var beforeCount = this.urlEntries.length;
this.urlEntries.removeWhere((entry) => entry.url == url);
var afterCount = this.urlEntries.length;
if (beforeCount != afterCount) notifyListeners();
print("removed: ${beforeCount != afterCount}");
}
save() async {
final storageFile = await composeStorageFile();
print("storage file is '${storageFile.path}");
if (await storageFile.exists()) {
print("deleting existing file");
await storageFile.delete();
}
if (urlEntries == null || urlEntries.length < 1) {
print("no entries to save");
return false;
}
print(
"saving ${urlEntries.length} url entries to file $storageFile} ...");
for (var entry in urlEntries) {
await storageFile.writeAsString('${entry.url} ${entry.title}',
mode: FileMode.append);
}
}
Future<File> composeStorageFile() async {
Directory storageDir = await getApplicationDocumentsDirectory();
return File('${storageDir.path}/url_collection.lst');
}
void dispose() async {
super.dispose();
print("disposing ...");
urlEntries.clear();
this.urlEntries = null;
}
load() async {
final storageFile = await composeStorageFile();
if (!await storageFile.exists()) {
print("storage file ${storageFile.path} not existing - not loading");
return false;
}
print("loading file ${storageFile.path}");
urlEntries = List <UrlEntry> () ;
final fileLines = storageFile.readAsLinesSync() ;
for (var line in fileLines) {
var separatorIndex = line.indexOf(' ') ;
final url = line.substring(0, separatorIndex) ;
var title = line.substring(separatorIndex+1) ;
if (title == 'null') title = null ;
urlEntries.add(new UrlEntry(url: url, title: title)) ;
}
notifyListeners() ;
}
}
Above code has several issues I unfortunately donnot know how to circumvent:
most of the methods of UrlRepository are async. This is because of getApplicationDocumentsDirectory() being async. I think former is an absolute flaw but introducing semaphores here to create an artificial bottleneck would pollute the code, so I still stick to async; but call me old-fashioned - I dont like the idea having save and load operations being theoretically able to overlap each other. I mean, with getApplicationDocumentsDirectory, we're talking about a simple configurational detail that will not need much computational power to compute, nor to store, nor will it change that often and it pollutes the code with otherwise unnessecary stuff. So, Is there another way to get the results of getApplicationDocumentsDirectory() without await / async / then ?
If this is not the case - where should I put the call to save()? My first idea was to save data not every model change, but instead at the latest possible executional place, which is one of the dispose-related methods, like so:
class MyAppState extends State<MyApp> {
UrlRepository urlRepository;
...
#override
void deactivate() async {
await urlRepository.save() ;
super.deactivate();
}
Unfortunately this results in urlRepository.save() being executed only the half, no matter whether I call it in a unit test, on a avd or on a real device. Right in the middle its terminated - I checked that with printouts. I think this is because, being forced again to make a completely unrelated method async (here deactivate()), I have to accept that execution is not granted to terminate at the return command, but earlier (?). I tried to put the call to MyState.dispose() as well as to urlRepository.dispose() with the same result except I cannot make the dispose methods async and hence just call save() async and hope everything has been saved before super.dispose() kicks in,...
I thought it natural to load the repositotry state inside of initState(), but I want to make sure that either the load has completed before creating widgets (ie calling the builder), or will be loaded after all widgets have already been in place so the model change will trigger rebuild. Since load() has to be async for known reasons and initState is not, I cannot assure even one of above cases and stick with urlRepository.load() and hope the best. So, where to put the call to urlRepository.load() ?
First: You have to use async/await methods because you don't know what the user's device may be doing while running your application, so even though the device might be performing a process that "doesn't need much power computational" that process may take a little longer than expected.
Second: Do not trust in deactivate() and dispose() functions, the user could kill your app and it would never do the save process. I'm not really sure how to automate this process. I suggest you do it manually somewhere in your code.
Third: Don't use initState() to load your data, you should use a FutureBuilder in which the future parameter is your urlRepository.load() function, while its loading you can show a CircularProgressIndicator() and if it has data you show your widget.
Example:
#override
Widget build() {
return FutureBuilder(
future: urlRepository.load() // without await keyword
builder: (context, snapshot) {
if(!snapshot.hasData)
return CircularProgressIndicator();
return YourWidget(); // if snapshot.hasData is true the process has finished
}
);
}
Psdt: It might be useful if urlRepository.load() would return something like a bool. Doing this you could show a widget if snapshot.data is true or another widget if snapshot.data is false.