How can I load Flutter text assets from a build context without reloading each build? - flutter

I'm confused about the Flutter documentation on loading text assets.
It states:
Each Flutter app has a rootBundle object for easy access to the main asset bundle. It is possible to load assets directly using the rootBundle global static from package:flutter/services.dart.
However, it’s recommended to obtain the AssetBundle for the current BuildContext using DefaultAssetBundle, rather than the default asset bundle that was built with the app; this approach enables a parent widget to substitute a different AssetBundle at run time, which can be useful for localization or testing scenarios.
I don't understand how I can implement text asset loading the recommended way, while at the same time avoiding to load the assets each time the widget is built.
Consider the following naive example that uses a FutureBuilder to display some text:
#override
Widget build(BuildContext context) {
var greeting = DefaultAssetBundle.of(context).loadString('assets/greeting');
return FutureBuilder<String>(
future: greeting,
builder (BuildContext context, AsyncSnapshot<String> snapshot) {
if (snapshot.hasData) {
return Text(snapshot.requireData);
} else {
return Text('Loading greeting...');
}
}
);
}
In the example, loadString is called whenever the widget is built. This seems inefficient to me.
Also, it goes explicitly against the FutureBuilder documentation, which tells me that:
The future must have been obtained earlier, e.g. during State.initState, State.didUpdateWidget, or State.didChangeDependencies. It must not be created during the State.build or StatelessWidget.build method call when constructing the FutureBuilder. If the future is created at the same time as the FutureBuilder, then every time the FutureBuilder's parent is rebuilt, the asynchronous task will be restarted.
Now, I could go ahead and load my assets in any of the recommended methods, but none of them has a BuildContext. Meaning I'd have to use the rootBundle, which wasn't recommended.
Since I'm new to Flutter, I'm unsure if the documentation is contradicting itself or if there's some obvious thing I'm missing here. Any clarification would be much appreciated.

I came up with the following solution for loading assets the way it's recommended in the Flutter docs.
Load the assets in a Widget's State and assign the Future to a nullable instance variable. This works in conjunction with FutureBuilder. Here's an example:
class MyWidgetState extends State<MyWidget> {
Future<String>? _greeting;
#override
Widget build(BuildContext context) {
// Wrapping `loadString` in a condition such as the following ensures that
// the asset is loaded no more than once. Seems kinda crude, though.
if (_greeting == null) {
_greeting = DefaultAssetBundle.of(context).loadString('assets/greeting');
}
return FutureBuilder<String>(
future: greeting,
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
if (snapshot.hasData) {
return Text(snapshot.requireData);
} else {
return Text('Loading greeting...');
}
});
}
}
This approach ensures that both recommendations from the Flutter docs are honored:
Assets are loaded from the Bundle for the current BuildContext.
Assets are loaded only once, avoiding that FutureBuilder restarts each build.

I would say to you... Remove Future builder.
You can do something like this:
String greeting;
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
greeting =
await DefaultAssetBundle.of(context).loadString('assets/greeting');
setState(() {});
});
}
#override
Widget build(BuildContext context) {
if (greeting != null && greeting.isNotEmpty) {
return Text(greeting);
} else {
return Text('Loading greeting...');
}
}

Try this
String greeting;
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => loadString());
}
void loadString() async {
if (greeting != null && greeting.isNotEmpty) {
greeting = await DefaultAssetBundle.of(context).loadString('assets/greeting');
setState(() {});
}
}
#override
Widget build(BuildContext context) {
return Text(greeting ?? 'Loading greeting...');
}
If your project has Null Safety then change String greeting to String? greeting

Related

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);}); }

How to persist value from a Future when switching between pages in Flutter?

I am trying to add a widget to the screen when the future has data in Screen 1.
class UserChoiceBooks extends StatelessWidget {
final String title;
UserChoiceBooks({Key key, this.title}) : super(key: key);
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: Provider.of<Books>(context, listen: false)
.getRecommendedBooks("test"),
builder: (ctx, snapshot) {
// Checking if future is resolved
if (snapshot.connectionState == ConnectionState.done) {
// If we got an error
if (snapshot.hasError) {
return Center(
child: Text(
'${snapshot.error} occured',
style: TextStyle(fontSize: 18),
),
);
// if we got our data
} else if (snapshot.hasData) {
// Extracting data from snapshot object
final List<Book> recommendedBooksML = snapshot.data;
return BookList(title, recommendedBooksML);
} else {
return SizedBox.shrink();
}
} else {
return Container();
}
}
// ... some code here
);
}
}
I navigate to a different screen Screen 2.
And then back to Screen 1. I loose the future data and it starts Building again.
PS:I found a temporary workaround by storing the Future data in a Global Variable
List<Book> placeholder=[];
And then setting the value from future...placeholder=snapshot.data;
When Switching between Screen 1 and 2. I am checking
placeholder==[]?UserChoiceBooks (title):BookList(title,placeholder)
Is there a better way to keep data from snapshot so that switching between screen future value is not Lost?
You are making a call during your build:
Provider.of<Books>(context, listen: false).getRecommendedBooks("test")
Thus when the screen is rebuilt, it is invoked again. You are only too lucky that no other rebuilds are happening between those two navigations, because in theory they are possible.
A recommended mindset is that a rebuild may happen at each frame, and non-rebuilding frames should be treated as a framework optimization.
So you should make a call one time and save it. There are a few options:
Create Books somewhere up the tree and pass it to the widget. Then make your widget stateful and call widget.books.getRecommendedBooks in its initState().
If Books is a singleton, you may use a service locator like https://pub.dev/packages/get_it to instantiate it once and then use GetIt.instance.get to fetch this instance anywhere in your widget. A service locator pattern has some advantages over the provider pattern.
A nasty way is to call the future in a stateful widget's state and call the method on first build if _future == null.
For stateful widgets see this tutorial: https://flutter.dev/docs/development/ui/interactive#stateful-and-stateless-widgets
Also note that you should use a key for such a widget that would be different for different titles that you pass to the constructor. Otherwise, when you replace your widget with one with another title, the framework would not know that all that follows is for another book and will not create a new state, thus initState() will not be called.

setState vs StreamProvider

I'm a Flutter newbie and I have a basic understanding question:
I built a google_maps widget where I get the location of the user and also would like to show the current location in a TextWidget.
So I'm calling a function in initState querying the stream of the geolocator package:
class _SimpleMapState extends State<SimpleMap> {
Position userPosStreamOutput;
void initPos() async {
userPosStream = geoService.getStreamLocation();
userPosStream.listen((event) {
print('event:$event');
setState(() {
userPosStreamOutput = event;
});
});
setState(() {});
}
#override
void initState() {
initPos();
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold( //(very simplified)
body: Text(userPosStreamOutput.toString()),
This works just fine. But would it make sense to use a Streamprovider instead? Why?
Thanks
Joerg
An advantage to use StreamProvider is to optimize the process of re-rendering. As setState is called to update userPosStreamOutput value, the build method will be called each time your stream yields an event.
With a StreamProvider, you can apply the same logic of initializing a stream but then, you will use a Consumer which will listen to new events incoming and provide updated data to children widgets without triggering other build method calls.
Using this approach, the build method will be called once and it also makes code more readable in my opinion.
#override
Widget build(BuildContext context) {
return Scaffold(
body: StreamProvider(
create: (_) => geoService.getStreamLocation(),
initialData: null,
child: Consumer<Position>(
builder: (context, userPosStreamOutput, _) {
return Text(userPosStreamOutput.toString());
},
),
),
);
}

Sometimes widgets not rendering in production builds

Sometimes the widget doesn't render properly. This only happens on the release build irrespective of the platform OS. Not only texts, sometimes other widgets like container, images won't get rendered either. This "blank state" occurs irrespective of the app's life cycle, meaning this happens both when the app is in the background and comes foreground or opened freshly. I can work around in the app normally without any issues but can't see any widgets which I'm supposed to see. We found there are multiple rebuilds happening in the stateful widget which is responsible for the checking and routing the users according to their login status.
Anything wrong in this method?
//initializations (state variable)
Widget widgetToRender = Splash();
var _firestore;
bool isLoading = false;
StreamSubscription userProfileSubscription;
//initalize function
void _intialise(BuildContext context) async {
if (isLoading) {
return;
}
_firestore = Provider.of<FirestoreService>(context);
userProfileSubscription = _firestore.userProfile.listen((userProfile) {
this.setState(() {
isLoading = true;
widgetToRender = App();
});
});
}
//build method
#override
Widget build(BuildContext context) {
if (widget.userSnapshot.connectionState == ConnectionState.active) {
if (widget.userSnapshot.hasData) {
_intialise(context);
} else {
setState((){
widgetToRender = AuthLanding();
})
}
}
return widgetToRender;
}
Flutter is a declarative framework.
You're going against it when you declare widgets like this:
Widget widgetToRender = Splash();
Also, you shouldn't call methods that execute imperative code from your build method:
_intialise(context);
My suggestion is to completely remove this code:
Widget widgetToRender = Splash();
var _firestore;
And rewrite things in a declarative manner. One way to do that is to use a StreamBuilder, and use its builder to return the widgets you want.
Addressing this may solve your rendering problems.
EDIT: step 1 would probably be to remove all imperative code and state variables in the build method:
#override
Widget build(BuildContext context) {
if (widget.userSnapshot.connectionState == ConnectionState.active) {
if (widget.userSnapshot.hasData) {
return App();
} else {
return AuthLanding();
}
}
return Scaffold(body: CircularProgressIndicator());
}
The load lag is because you are only working with widget.userSnapshot.connectionState == ConnectionState.active you will need to add progress indicators as the response from firestore depends on the network status of the user. Please let me know if that helps.

Change state in one Widget from another widget

I'm programming a flutter application in which a user is presented with a PageView widget that allows him/her to navigate between 3 "pages" by swiping.
I'm following the setup used in https://medium.com/flutter-community/flutter-app-architecture-101-vanilla-scoped-model-bloc-7eff7b2baf7e, where I use a single class to load data into my model, which should reflect the corresponding state change (IsLoadingData/HasData).
I have a main page that holds all ViewPage widgets. The pages are constructed in the MainPageState object like this:
#override
void initState() {
_setBloc = SetBloc(widget._repository);
_notificationBloc = NotificationBloc(widget._repository);
leftWidget = NotificationPage(_notificationBloc);
middleWidget = SetPage(_setBloc);
currentPage = middleWidget;
super.initState();
}
If we go into the NotificationPage, then the first thing it does is attempt to load data:
NotificationPage(this._notificationBloc) {
_notificationBloc.loadNotificationData();
}
which should be reflected in the build function when a user directs the application to it:
//TODO: Consider if state management is correct
#override
Widget build(BuildContext context) {
return StreamBuilder<NotificationState>(
stream: _notificationBloc.notification.asBroadcastStream(),
//initialData might be problematic
initialData: NotificationLoadingState(),
builder: (context, snapshot) {
if (snapshot.data is NotificationLoadingState) {
return _buildLoading();
}
if (snapshot.data is NotificationDataState) {
NotificationDataState state = snapshot.data;
return buildBody(context, state.notification);
} else {
return Container();
}
},
);
}
What happens is that the screen will always hit "NotificationLoadingState" even when data has been loaded, which happens in the repository:
void loadNotificationData() {
_setStreamController.sink.add(NotificationState._notificationLoading());
_repository.getNotificationTime().then((notification) {
_setStreamController.sink
.add(NotificationState._notificationData(notification));
print(notification);
});
}
The notification is printed whilst on another page that is not the notification page.
What am i doing wrong?
//....
class _SomeState extends State<SomeWidget> {
//....
Stream<int> notificationStream;
//....
#override
void initState() {
//....
notificationStream = _notificationBloc.notification.asBroadcastStream()
super.initState();
}
//....
Widget build(BuildContext context) {
return StreamBuilder<NotificationState>(
stream: notificationStream,
//....
Save your Stream somewhere and stop initialising it every time.
I suspect that the build method is called multiple times and therefore you create a new stream (initState is called once).
Please try let me know if this helped.