How to wait on getter in flutter? - flutter

Below is the code of a provider class. Whenever the app starts i want to get the forms which were saved in the shared preferences. However it is taking sometime to load the from sharedpreferences. So When i access the forms for the first time it is initially empty, im getting an empty list. Is there anyway to delay the getter until it has the objects of form model.
class FormProvider with ChangeNotifier {
FormProvider() {
loadformPreferences();
}
List<FormModel> _forms = [];
List<FormModel> get forms => _forms;
Future<void> saveForm(FormModel form) async {
_forms.add(form);
await saveformPreferences();
notifyListeners();
}
Future<void> saveformPreferences() async {
List<String> myforms = _forms.map((f) => json.encode(f.toJson())).toList();
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setStringList('forms', myforms);
}
Future<void> loadformPreferences() async {
// WidgetsFlutterBinding.ensureInitialized();
SharedPreferences prefs = await SharedPreferences.getInstance();
var result = prefs.getStringList('forms');
if (result != null) {
_forms = result.map((f) => FormModel.fromJson(json.decode(f))).toList();
}
}
}

In Flutter, all actions related to building the UI must not be asynchronous (or expensive) in order to main a high frame-rate.
Thus, even if you could hypothetically find a way to "wait" for the results from SharedPreferences, this would not be desirable, since whatever waiting was done would block the UI from making progress.
Thus, there are a couple approaches to this common problem:
Handle initial state explicitly in the UI
The simplest solution is to explicitly handle the case where the Provider has not yet fetched its data, by representing some sort of initial state in your Provider. One easy way to do this is to initialize _forms to null instead of []. Then in your Widget.build method, you could do something specific (like show a loading spinner) when the result is null:
Widget build(BuildContext context) {
final provider = Provider.of<FormProvider>(context);
if (provider.forms == null) {
return CircularProgressIndicator();
}
// Otherwise, Do something useful with provider.forms
}
Construct your provider with the resolved data
Let's say that you don't use the FormProvider until the user performs an action, like click a button, at which point you push a new view onto the navigator with the FormProvider.
If you wish to guarantee that the FormProvider will always be initialized with the SharedPreferences values, then you can delay the construction of the new view until SharedPreferences has finished:
class MyButton extends StatelessWidget {
Widget build(BuildContext context) {
return Button(onClick: () async {
final forms = await _fetchFormsFromSharedPrefs();
Navigator.push(context, MaterialPageView(builder: (context) =>
Provider(create: (_) => FormProvider(forms))));
});
}
}

Related

How to get data from bloc stream from other page in flutter

I have a problem like this :
In Splash Page , i check in sharedpreference to get saved token when login successfully .If i have token , i request Api to get account information and move to next page like this:
Future check() async {
String _getToken = await splashBloc.getTokenFormSharedPref();
if (_getToken=='0') {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => LoginMain()));
} else {
splashBloc.getAccountInfo();
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => HomeScreenMain()));
}
}
and this is BLoC class:
class SplashBloc extends BlocBase{
String _getToken = '';
Future<String> getTokenFormSharedPref() async{
SharedPreferences prefs = await SharedPreferences.getInstance();
_getToken = (prefs.getString('token') ?? '0');
return _getToken;
}
final accountInfoController = new StreamController<Account>();
Sink<Account> get accountInfoSink => accountInfoController.sink;
Stream<Account> get accountInfoStream => accountInfoController.stream;
Future getAccountInfo() async{
Account account = await NetworkService().getAccountInfo2(_getToken);
accountInfoSink.add(account);
print('from splash: '+account.fullName);
}
#override
void dispose() {
accountInfoController.close();
}
}
When i check log , it totally request successfully and the problem is how can i acesss data in streambuilder in next page that is HomeScreenMain()?
Thanks for help!!
You can declare a variable in HomeScreenMain() and send the data you received before to the class constructor like this:
HomeScreenMain() {
final data;
HomeScreenMain(this.data)
//....
}
and when you want to call this widget you can pass that data from block to this widget
You appear to use a very basic approach with BLoC. Not sure if my answer helps there.
But if you use the library flutter_bloc, then you can use on the next page
ˋfinal bloc = context.read;
This looks for a provider of this bloc type upstream in the Widget tree and assigns it to ˋbloc

how to make more than one asynchronous calls when open a page using Riverpod?

when the user of my app open the Home page, I need to make two asynchronous calls to the server
first, I need to get current user data.
and then based on the current user data, I need fetch his favorite restaurants.
I have two separate methods to get those data from server
class MyAPI {
Future<User> getUserData() async {}
Future<List<Restaurant>> getUserData() async {}
}
then how do I construct those 2 asynchronous methods in my HomePage using Riverpod?
show circular loading indicator
make those 2 asynchronous calls
hide circular loading indicator and load lisView
I know about FutureProvider from Riverpod, but FutureProvider is only for one asynchronous service right?
do I need to somehow combine those two into a single method first and then use FutureBuilder? or is it another way that more common to use? I am not sure
how to solve this issue. sorry I am a beginner in Flutter
This look like a use case of a stateNotifier:
first in your data model define a UserData class :
class UserData{
final User user;
final List<Restaurant> restaurants;
UserData(this.user, this.restaurants)
}
next define a state and it's associated stateNotifierProvider :
final userDataProvider = StateNotifierProvider<UserDataNotifier, AsyncValue<UserData>>((ref) => UserDataNotifier());
class UserDataNotifier extends StateNotifier<AsyncValue<UserData>> {
UserDataNotifier() : super(AsyncValue.loading()){
init();
}
final _api = MyAPI();
void init() async {
state = AsyncValue.loading();
try {
final user = await _api.getUser;
final List<Restaurant> restaurants = await _api.getFavoriteRestaurant(user);
state = AsyncValue.data(UserData(user,restaurants));
} catch (e) {
state = AsyncValue.error(e);
}
}
}
finally in you UI use a consumer :
Consumer(builder: (context, watch, child) {
return watch(userDataProvider).when(
loading: ()=> CircularProgressIndicator(),
error: (error,_) =>
Center(child: Text(error.toString())),
data: (data) => Center(child: Text(data.toString())));
})

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.

I have a question about navigating to the next page conditionally in initstate

I want to implement Auto Login with Shared preferences.
What I want to implement is that as soon as 'LoginPage' starts, it goes to the next page without rendering LoginPage according to the Flag value stored in Shared preferences.
However, there is a problem in not becoming Navigate even though implementing these functions and calling them from initstate. What is the problem?
//Login Page
void autoLogIn() async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
final String userId = prefs.getString('username');
print("ddddddddddddddd");
SocketProvider provider = Provider.of<SocketProvider>(context);
Future.delayed(Duration(milliseconds: 100)).then((_) {**//I tried giving Delay but it still didn't work.**
Navigator.of(context).pushNamedAndRemoveUntil("/MainPage", (route) => false);
});
}
#override
void initState() {
// TODO: implement initState
loginBloc = BlocProvider.of<LoginBloc>(context);
if(!kReleaseMode){
_idController.text = "TESTTEST";
_passwordController.text = "1234123";
}
initBadgeList();
autoLogIn();**//This is the function in question.**
super.initState();
print("1111111111111111");
}
I don't think you should show LoginPage widget if user is already logged in and then navigate to main page.
I suggest you to use FutureBuilder and show either splash screen or loader while performing await SharedPreferences.getInstance(). In this case your App widget should look like this:
class App extends MaterialApp {
App()
: super(
title: 'MyApp',
...
home: FutureBuilder(
future: SharedPreferences.getInstance(),
builder: (context, snapshot) {
if (snapshot.data != null) {
final SharedPreferences prefs = snapshot.data;
final userId = prefs.getString('username');
...
return userId == null ?? LoginPage() : MainPage();
} else {
return SplashScreenOrLoader();
}
}));
}
But if you still want to show LoginPage first, just replace SplashScreenOrLoader() with LoginPage() in code above.