Complete scoped model async call in initstate before widget builds - flutter

I have a requirement which is to make an API call when the page is opened and use the result of the API call to populate a widget. I have to cater for cases like when the call is being made, show a loader, if an error occurs, show a widget that allows the user to reload the page which calls that method again if the call is successful use the result to populate the widget.
This looks like a perfect use case for a FutureBuilder, however, should the call fail, there won't be a way to reload as FutureBuilder is executed once.
An alternative is to use a StreamBuilder where I just add to the stream the error or response, this allows me to reload the page if the call fails. The streambuidler approach works fine.
Initially, I've tried to use a ScopedModel approach by making the async API call in the initstate method, however, as expected the api call takes some time and the widget Build method is called so the page is built before the api call completes and when it's done, even if I call notifylisteners() the widget tree doesn't rebuild with the data from the API call. Is there a way to make this work using a ScopedModel approach?
DashboardService.dart
Future<string> getDashboardState() async{
try{
var response = await client.get('/getdashboardstate');
return response;
}catch(error){
return error;
}
}
DashboardModel.dart
Future<BaseDataResponse<MambuLoan>> getDashboardState() async {
try {
setState(ViewState.Busy);
var info = await _dashboardService.getDashboardState();
if (info.isSuccessful) {
return info;
} else {
setState(ViewState.Error);
return info;
}
} catch (error) {
setState(ViewState.Error);
return error;
}
}
DashboardView.dart
#override
void initState() {
// TODO: implement initState
super.initState();
getDashboard() // result of this set some data in the scopedmodel
}
Future<String> getDashboard() async{
var response = await dashboardModel.getDashboardState();
return response;
}
Widget Build(BuildContext context){
//gets data in scoped model to build widget
return Container() //actual widget here
}
UPDATE
I Was approaching this the wrong way. I ended up not needing a stateful widget but a stateless one in which I called my Future in the build method before returning a widget.

Related

Flutter RiverPod: Is it ok to return another provider from build method of Notifier?

I want to keep my return value as AsyncValue rather than Stream so I am returning StreamProvider from build method of Notifier. After reading the codebase of riverpod I can't see any drawback of this, but I have never come across any project doing something like this. Is this fine, or is there any straight forward way to convert Stream to AsyncValue.
final _userProvider = StreamProvider.autoDispose<User?>((ref) {
final repository = ref.watch(repositoryProvider);
return repository.getUser(); //returns Stream<User?>
});
class AuthNotifier extends AutoDisposeNotifier<AsyncValue<User?>> {
#override
AsyncValue<User?> build() {
return ref.watch(_userProvider);
}
Future<void> singOut() {
return ref.read(repositoryProvider).signOut();
}
}
final authProvider =
AutoDisposeNotifierProvider<AuthNotifier, AsyncValue<User?>>(
AuthNotifier.new);
This is fine, yes.
Being able to do such a thing is the goal of the build method & ref.watch
As long as you don't return the provider itself but the value exposed by the provider, there is no problem:
build() {
return ref.watch(provider); // OK
}
build() {
return provider // KO
}

Flutter jsonDecode FlutterSession value is not loading in widget initially. but works on hotload

i am initializing a variable with value from session. but could not print it in the widget. but it is showing after hot load. here is my code :
class _dashboardState extends State<dashboard> {
var logindata;
#override
initState() {
super.initState();
_getSession() async {
logindata = jsonDecode(await FlutterSession().get("login_data"));
}
_getSession();
}
#override
Widget build(BuildContext context) {
print(logindata); // prints null
}
}
Instead of jsonDecode(await FlutterSession().get("login_data"))
if i add any random string or number like
logindata = "Session value";
it prints normally. otherwise on hot load
only i am getting the session value.
what will be the reason?
please do help :(. i am new to flutter.
After following ideas from the comments i have updated the code as follows:
class _dashboardState extends State<dashboard> {
var logindata;
#override
void initState() {
getSessionValue().then((logindata) {
setState(() {
logindata = logindata;
});
});
super.initState();
}
Future<void> getSessionValue() async {
logindata = jsonDecode(await FlutterSession().get("login_data"));
return logindata;
}
#override
Widget build(BuildContext context) {
print(logindata); // first prints null then correct array without hotload.
}
}
here i got first null, then the correct value. but in my case i need the value of an object in the array logindata, that is
logindata["shop_name"] . so in that case i am getting error The method '[]' was called on null. Receiver: null Tried calling: []("shop_name") . What do i do now ? i am really stuck here. :(
Let me explain this first,
lifecycle of State goes like this createState -> initState ->........-> build
so you're right about the order of execution
you're calling getSessionValue() from initState and expecting widget to build right after it, but since getSessionValue() returns a Future after awaiting,
the execution continues and builds the widget not waiting for the returned Future value from getSessionValue(), so it prints null initially, and then when the Future is available youre calling setState and it prints the actual value
there is no notable delay here but the execution flow causes it to behave like this
so what's the solution?... Here comes FutureBuilder to the rescue
it is possible to get different states of a Future using FutureBuilder and you can make changes in the UI accordingly
so in your case, inside build, you can add a FutureBuilder like this,
FutureBuilder(
future: getSessionValue(),
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none: return Text("none");
case ConnectionState.waiting: return Text("waiting");
case ConnectionState.active: return Text("active");
case ConnectionState.done:
print(logindata); // this will print your result
return Text("${logindata}");
}
})
keep in mind that the builder should always return a widget
as the async operation is running, you can show the progress to the user by
showing the appropriate UI for different states
for eg: when in ConnectionState.waiting, you can show/return a progress bar
Hope this helps, Thank you
That is a normal behaviour since you are having an async function to get the login data (so it will take some time to be there) while the widget will be building , then build method will get executed first which will make the print executed with no data and then when you hot reload it will be executed perfectly , so if you you want to print it right after you get the data you can make the function this way :
_getSession() async {
logindata = jsonDecode(await FlutterSession().get("login_data")).then((value) {print(value);}); }

decide the route based on the initialization phase of flutter_native_splash

I'm writing a flutter application using flutter 2.10 and I'm debugging it using an Android Emulator
I included the flutter_native_splash plugin from https://pub.dev/packages/flutter_native_splash and I use version 2.0.1+1
the problem that I'm having is that I decide what's the first screen that the user will see based on the initialization phase. I check the stored user token, see his premissions, verify them with the server, and forward him to him relevant route.
since the runApp() function executes in the background while the initialization phase is running I cannot choose the page that will be shown. and if I try to nativgate to a route in the initialization function I get an exception.
as a workaround for now I created an init_home route with FutureBuilder that awaits for a global variable called GeneralService.defaultRoute to be set and then changes the route.
class _InitHomeState extends State<InitHome> {
#override
Widget build(BuildContext context) {
return FutureBuilder<dynamic>(
future: () async {
var waitCount=0;
while (GeneralService.defaultRoute == "") {
waitCount++;
await Future.delayed(const Duration(milliseconds: 100));
if (waitCount>20) {
break;
}
}
if (GeneralService.defaultRoute == "") {
return Future.error("initialization failed");
}
Navigator.of(context).pushReplacementNamed(GeneralService.defaultRoute);
...
any ideas how to resolve this issue properly ?
I use a Stateful widget as Splash Screen.
In the build method, you just return the 'loading' view such as Container with a background color etc. (with texts or whatever you like but just consider it as the loading screen).
In the initState(), you call a function that we can name redirect(). This should be an async function that performs the queries/checks and at the end, calls the Navigator.of(context).pushReplacementNamed etc.
class _SplashState extends State<Splash> {
#override
void initState() {
super.initState();
redirect();
}
#override
Widget build(BuildContext context) {
return Container(color: Colors.blue);
}
Future<void> redirect() async {
var name = 'LOGIN';
... // make db calls, checks etc
Navigator.of(context).pushReplacementNamed(name);
}
}
Your build function just creates the loading UI, and the redirect function in the initState is the one running in the background and when it has finished computing, calls the Navigator.push to your desired page.

How to force initState every time the page is rendered in flutter?

I am adding some data into the SharedPreferenceson page2 of my app and I am trying to retrieve the data on the homepage. I have used an init function on page 1 as follows:
#override
void initState() {
super.initState();
_getrecent();
}
void _getrecent() async {
final prefs = await SharedPreferences.getInstance();
// prefs.clear();
String b = prefs.getString("recent").toString();
Map<String, dynamic> p = json.decode(b);
if (b.isNotEmpty) {
print("Shared pref:" + b);
setState(() {
c = Drug.fromJson(p);
});
cond = true;
} else {
print("none in shared prefs");
cond = false;
}
}
Since the initState() loads only once, I was wondering if there was a way to load it every time page1 is rendered. Or perhaps there is a better way to do this. I am new to flutter so I don't have a lot of idea in State Management.
you can override didChangeDependencies method. Called when a dependency of the [State] object changes as you use the setState,
#override
void didChangeDependencies() {
// your codes
}
Also, you should know that using setState updates the whole widget, which has an overhead. To avoid that you should const, declaring a widget const will only render once so it's efficient.
First thing is you can't force initState to rebuild your widget.
For that you have setState to rebuild your widget. As far as I can
understand you want to recall initState just to call _getrecent()
again.
Here's what you should ideally do :
A simple solution would be to use FutureBuilder. You just need to use _getrecent() in FutureBuilder as parent where you want to use the data you get from _getrecent(). This way everytime your Widget rebuilds it will call _getrecent().
You simply use setState() methode if you want to update widget. Here's documentation for this https://api.flutter.dev/flutter/widgets/State/setState.html
init function will render it only once initially, to avoid that -
You can use setState( ) method to re-render your whole widget.

How to dispose rxsubject correctly in flutter?

I am trying to dispose rxsubject on the bloc. But when I call the dispose method on UI part, it throws error saying :
Bad state: Cannot add new events after calling close
Here's my bloc.
class EventBloc {
final EventRepository _repository = EventRepository();
final BehaviorSubject<EventResponse> _subject =
BehaviorSubject<EventResponse>();
getEvents() async {
EventResponse response = await _repository.getEvents();
_subject.sink.add(response);
}
dispose() {
_subject?.close();
}
BehaviorSubject<EventResponse> get subject => _subject;
}
final eventBloc = EventBloc();
dispose method in UI:
void dispose() {
super.dispose();
eventBloc.dispose();
}
When I am not calling dispose method on the UI, it works. Should I not call dispose method at all? If not, how should I dispose it?
Solution
moving final eventBloc=EventBloc(); from bloc and initializing bloc in UI.
Previously, without closing the subject, my UI would retain the data and when I navigated to the events page, the data would already be displayed there until api call succeed and when it did, it would simply rebuild listview with new data. But now, every time I navigate to the events page, all the progress is lost. Streambuilder starts from the very beginning of api call and until snapshot, it shows progress indicator and then data. It is like using PublishSubject.
Also, I heard an argument about not having to dispose stream when bloc is initialized within the bloc since the widget doesn't create the subject. How much is that true? Doesn't that lead to memory leaks?
To prevent your data from getting lost, you can make use of provider.
You'll need to create your bloc a level higher than the widget that uses it, ideally at the top level of the app. Like this:
runApp(
Provider<EventBloc>(
create: (_) => EventBloc(),
dispose: (context, value) => value.dispose(),
child: MyApp(),
),
);
Then your widget can access it when it needs it. Like:
#override
build(context) {
final EventBloc eventBloc = Provider.of<EventBloc>(context);
//Rest of your code
}