Flutter: get default context? or load assets without context? - flutter

I'm trying to load a json file in a class extending SearchDelegate to search through its content.
I have a method to load this file:
Future<void> loadCountryData() async {
try {
String data = await DefaultAssetBundle
.of(context)
.loadString("assets/data/countries.json");
_countries = json.decode(data);
} catch (e) {
print(e);
}
}
Unfortunately this requires a Buildcontext (context) that seems only to be available in the SearchDelegate build methods (like buildActions, buildLeadings, etc), but no outside like for example in the constructor.
https://docs.flutter.io/flutter/material/SearchDelegate-class.html
As the #override xy build methods in SearchDelegate are called with every change in the search field, I would load my file over and over again, which is of course not ideal.
I want to load my file once at the beginning only.
Is there a way to get some sort of get default context that I could use for example in the constructor of SearchDelegate. Like in android (if I remmeber correctly)?
Or can I load an assets file without .of(context)?

Description of function
/// The bundle from the closest instance of this class that encloses
/// the given context.
///
/// If there is no [DefaultAssetBundle] ancestor widget in the tree
/// at the given context, then this will return the [rootBundle].
///
/// Typical usage is as follows:
///
/// ```dart
/// AssetBundle bundle = DefaultAssetBundle.of(context);
/// ```
static AssetBundle of(BuildContext context) {
final DefaultAssetBundle result = context.dependOnInheritedWidgetOfExactType<DefaultAssetBundle>();
return result?.bundle ?? rootBundle;
}
So you may simply use rootBundle instead of DefaultAssetBundle.of(context) for working with assets without context.

There is an option to get builtin AssetBundle without specifying a reference to BuildContext. Here is an example of how it could be done:
import 'package:flutter/services.dart'; // is required
Future<void> loadCountryData() async {
try {
// we can access builtin asset bundle with rootBundle
final data = await rootBundle.loadString("assets/data/countries.json");
_countries = json.decode(data);
} catch (e) {
print(e);
}
}

As DefaultAssetBundle is based on an InheritedWidget you will always need to pass a context.
of just looks up the widget tree based on a BuildContext until it finds a DefaultAssetBundle widget. This means that you cannot retrieve a DefaultAssetBundle object without a BuildContext.
You will need to pass a BuildContext to your method. I could imagine a situation like the following:
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: loadCountryData(context: context),
builder: (BuildContext context, AsyncSnapshot<JSON> jsonData) {
if (!jsonData.hasData) {
return Text('not loaded');
}
return Text('loaded'); // here you want to process your data
},
);
}
/// I am not sure what your decode returns, so I just called it JSON
/// I thought it would make more sense to return the JSON to use it in build
Future<JSON> loadCountryData({BuildContext context}) async {
try {
String data = await DefaultAssetBundle
.of(context)
.loadString("assets/data/countries.json");
return json.decode(data);
} catch(e) {
print(e);
return JSON.empty(); // imagine this exists
}
}
As you can see I passed the BuildContext from the build method. The FutureBuilder also allows to process the data in the build tree directly.

You can give the BuildContext as a parameter through to loadCountryData(BuildContext context).

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
}

How can I access json data as variable inside my scaffold in flutter?

How can I access var apiData = jsonDecode(response.body);
inside Widget build(BuildContext context) => Scaffold()
I want to use something like
if (apiData["studentEmail"] == "") { return const SignIn(); } else { return const Dashboard(); }
First of all, I suggest that you do the service operations in a separate class. I suggest you look at the service-repository pattern for this.
Bring your data from the api by creating the getApi method inside the service class.
For example,
class MyApi{
final String _getApi =
"https://free.currconv.com/api/v7/convert?q=USD_TRY,EUR_TRY&compact=ultra&apiKey=26cb9ffd85f9bee9c208";
Future<StudentModel?> getDatas() async {
var response = await Dio().get(_getApi);
if (response.statusCode == 200) {
return StudentModel.fromJson(response.data);
} else {
debugPrint('${response.statusCode} : ${response.data.toString()}');
throw UnimplementedError();
}
}
}
After that, Using FutureBuilder, give the future property the method that brings the api to your api class. And now you can access your data with the builder's AsynSnapshot. You can easily access the data in the future method with the snapshot that FutureBuilder now gives you.
FutureBuilder<StudentModel>(
future: MyApi.getDatas,
builder: (context, AsynSnapshot asynSnapshot){
// You can easily access the data in the future method with the
// snapshot that FutureBuilder now gives you.
asynSnapshot.data.yourData;
}
)

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

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

Flutter error : The argument type 'List<Future<Widget>>' can't be assigned to the parameter type 'List<Widget>'

I'm trying to do a list of item from Firebase Firestore (this is done) and to get for each item a different image URL from Firebase Cloud Storage.
I use a function called getPhotoUrl to change the value of the variable photoUrl. The problem is that the return is executed before getPhotoUrl. If I add await in front of the function getPhotoUrl and async after _docs.map((document), I got an error saying that The argument type 'List<Future>' can't be assigned to the parameter type 'List'.
My code:
class PhotosList extends StatefulWidget {
#override
_PhotosListState createState() => _PhotosListState();
}
class _PhotosListState extends State<PhotosList> {
String photoUrl = 'lib/assets/default-image.png';
List<DocumentSnapshot> _docs;
getPhotoUrl(documentID) {
Reference ref = storage
.ref('Users')
.child(currentUser.uid)
.child('Photos')
.child(documentID)
.child('image_1.jpg');
ref.getDownloadURL().then((value) {
setState(() {
photoUrl = value.toString();
});
}).catchError((e) {
setState(() {
print(e.error);
});
});
}
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: firestore
.collection('Users')
.doc(currentUser.uid)
.collection('Photos')
.orderBy('date')
.snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (!snapshot.hasData) return CircularProgressIndicator();
_docs = snapshot.data.docs;
if (_docs.isEmpty)
return Center(
child: Text("The list is empty."));
return Container(
child: ResponsiveGridList(
desiredItemWidth: 100,
squareCells: true,
minSpacing: 5,
children: _docs.map((document) {
getPhotoUrl(document.id);
return PhotosListItem(photoUrl: photoUrl);
}).toList(),
),
);
},
);
}
}
I think you mix 2 different ways. In every build cicle you map your docs and request that photoUrl, but inside that method you call setState, which re-triggers your build method. That way you should end in infinite loop of getting photo url and building your widget.
You have three options:
Load your photoUrls and store them inside your widget -> call set state -> check inside your mapping function if your photo is loaded, if yes, take it, if no, call your getPhotoUrl function
Load your photoUrls synchronously and return url from your function and set it to your PhotosListItem
(I would prefer this) Add your documentId to your photosListItem in your mapping function and inside your item you load this photo url. In this PhotoListItem you have a variable with your imageUrl and in initState you call your getPhotoUrl function
Inside your PhotoItem:
String imageUrl;
#override
void initState() {
Future.delayed(Duration.zero, () {
setState(() {
// load your data and set it to your variable
imageUrl = ..
});
});
super.initState();
}
You might use a FutureBuilder because StreamBuilder seems to be synchronous :
How to convert Future<List> to List in flutter?
Thanks for your answers guys, actually I found an other solution which is to get and write the URL in Firestore directly after uploading the image on the Storage.
This is the article which helped me a lot : https://medium.com/swlh/uploading-images-to-cloud-storage-using-flutter-130ac41741b2
(PS: some of the Firebase names changed since this article but it's still helpful.)
Regards.

How to create a dependency for ChangeNotifierProvider and make it wait to complete?

I have ChangeNotifierProvider object that uses data stored sqflite asset database which need to be loaded at the beginning as future. The problem is that ChangeNotifierProvider doesn't wait for future operation to complete. I tried to add a mechanism to make ChangeNotifierProvider wait but couldn't succeed. (tried FutureBuilder, FutureProvider, using all together etc...)
Note : FutureProvider solves waiting problem but it doesn't listen the object as ChangeNotifierProvider does. When I use them in multiprovider I had two different object instances...
All solutions that I found in StackOverflow or other sites don't give a general solution or approach for this particular problem. (or I couldn't find) I believe there must be a very basic solution/approach and decided to ask for your help. How can I implement a future to this code or how can I make ChangeNotifierProvider wait for future?
Here is my summary code;
class DataSource with ChangeNotifier {
int _myId;
List _myList;
int get myId => _myId;
List get myList => _myList;
void setMyId(int changeMyId) {
_myId = changeMyId;
notifyListeners();
}
.... same setter code for myList object.
DataSource(){initDatabase();}
Future<bool> initDatabase() {
.... fetching data from asset database. (this code works properly)
return true;
}
}
main.dart
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider<DataSource>(
create: (context) => DataSource(),
child: MaterialApp(
home: HomePage(),
),
);
}
}
Following code and widgets has this code part (it works fine)
return Consumer<DataSource>(
builder: (context, myDataSource, child) {.......
There are multiple ways that you can achieve. The main point of it is that you should stick to reactive principle rather than trying to await the change. Say for example, you could change the state of boolean value inside the DataSource class when the ajax request changes
class DataSource extends ChangeNotifier{
bool isDone = false;
Future<bool> initDatabase(){
//Do Whatever
isDone = true;
notifyListeners();
}
}
Then you could listen to this change in the build method like so
Widget build(BuildContext ctx){
bool isDone = Provider.of<DataSource>(context).isDone;
if(isDone){
// render result
}else{
// maybe render loading
}
}