I have a flutter widget which retrieves Json information from an API and displays a widget of that information. My initState is as follows:
class _BuildCardsWidgetState extends State<BuildCardsWidget> {
bool display = false;
late Resource _resource;
Future<Resource> fetchResource(Build build) async {
var url = Uri.parse(APIURL); <-actual url link hidden
final response = await http.get(url);
var resourceReturn = json.decode(response.body);
var resourceJson = resourceReturn[0]['resource'];
return Resource.fromJson(resourceJson, build);
}
#override
void initState() {
fetchResource(widget.build).then((value) =>
this.setState(() {
_resource = value;
display = true;
}));
super.initState();
}
#override
Widget build(BuildContext context) {
return ... <- _resource is used within build
However, whenever the program gets to the line
final response = await http.get(url);
This error is thrown:
The following LateError was thrown building BuildCardsWidget(dirty, state: _BuildCardsWidgetState#b3c7a):
LateInitializationError: Field '_resource' has not been initialized.
The thing that confuses me is this error flashes, but then right after the widget is correctly built and it runs smoothly. Any suggestions on how to fix this or on how to ignore this error since it does appear to work after the initial error?
Try to avoid using asynchronous functions inside initState, if it's not properly handled with await in a separate function, the widget won't load your data before build your widgets. A quick and simple way to solve this is to use a FutureBuilder().
class _BuildCardsWidgetState extends State<BuildCardsWidget> {
Future<Resource> fetchResource(Build build) async {
var url = Uri.parse(APIURL); <-actual url link hidden
var resourceReturn = json.decode(response.body);
var resourceJson = resourceReturn[0]['resource'];
return Resource.fromJson(resourceJson, build);
}
#override
Widget build(BuildContext context) {
return FutureBuilder<Resource>(
future: fetchResource(build),
builder: (context, snapshot) {
return SizedBox();
}
if(snapshot.hasError) {
/// Handle error
}
}
);
}
}
Related
This is what I'm trying to achieve using flutter GetX package but not working properly.
I have a Firestore document, if the document is changed I want to call an api and keep the data up to date as observable.
The code below seems to work but initial screen shows null error then it shows the data.
I don't know how I can make sure both fetchFirestoreUser() and fetchApiData() (async methods) returns data before I move to the home screen.
GetX StateMixin seems to help with async data load problem but then I don't know how I can refresh the api data when the firestore document is changed.
I'm not sure if any other state management would be best for my scenario but I find GetX easy compared to other state management package.
I would very much appreciate if someone would tell me how I can solve this problem, many thanks in advance.
Auth Controller.
class AuthController extends SuperController {
static AuthController instance = Get.find();
late Rx<User?> _user;
FirebaseAuth auth = FirebaseAuth.instance;
var _firestoreUser = FirestoreUser().obs;
var _apiData = ProfileUser().obs;
#override
void onReady() async {
super.onReady();
_user = Rx<User?>(auth.currentUser);
_user.bindStream(auth.userChanges());
//get firestore document
fetchFirestoreUser();
//fetch data from api
fetchApiData();
ever(_user, _initialScreen);
//Refresh api data if firestore document has changed.
_firestoreUser.listen((val) {
fetchApiData();
});
}
Rx<FirestoreUser?> get firestoreUser => _firestoreUser;
_initialScreen(User? user) {
if (user == null) {
Get.offAll(() => Login());
} else {
Get.offAll(() => Home());
}
}
ProfileUser get apiData => _apiData.value;
void fetchFirestoreUser() async {
Stream<FirestoreUser> firestoreUser =
FirestoreDB().getFirestoreUser(_user.value!.uid);
_firestoreUser.bindStream(firestoreUser);
}
fetchApiData() async {
var result = await RemoteService.getProfile(_user.value!.uid);
if (result != null) {
_apiData.value = result;
}
}
#override
void onDetached() {}
#override
void onInactive() {}
#override
void onPaused() {}
#override
void onResumed() {
fetchApiData();
}
}
Home screen
class Home extends StatelessWidget {
const Home({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
child: Obx(() =>
Text("username: " + AuthController.instance.apiData.username!))),
),
);
}
}
To be honest, I never used GetX so I'm not too familiar with that syntax.
But I can see from your code that you're setting some mutable state when you call this method:
fetchApiData() async {
var result = await RemoteService.getProfile(_user.value!.uid);
if (result != null) {
_apiData.value = result;
}
}
Instead, a more robust solution would be to make everything reactive and immutable. You could do this by combining providers if you use Riverpod:
final authStateChangesProvider = StreamProvider.autoDispose<User?>((ref) {
final authService = ref.watch(authRepositoryProvider);
return authService.authStateChanges();
});
final apiDataProvider = FutureProvider.autoDispose<APIData?>((ref) {
final userValue = ref.watch(authStateChangesProvider);
final user = userValue.value;
if (user != null) {
// note: this should also be turned into a provider, rather than using a static method
return RemoteService.getProfile(user.uid);
} else {
// decide if it makes sense to return null or throw and exception when the user is not signed in
return Future.value(null);
}
});
Then, you can just use a ConsumerWidget to watch the data:
#override
Widget build(BuildContext context, WidgetRef ref) {
// this will cause the widget to rebuild whenever the auth state changes
final apiData = ref.watch(apiDataProvider);
return apiData.when(
data: (data) => /* some widget */,
loading: () => /* some loading widget */,
error: (e, st) => /* some error widget */,
);
}
Note: Riverpod has a bit of a learning curve (worth it imho) so you'll have to learn it how to use it first, before you can understand how this code works.
Actually the reason behind this that you put your controller in the same page that you are calling so in the starting stage of your page Get.put() calls your controller and because you are fetching data from the API it takes a few seconds/milliseconds to get the data and for that time your Obx() renders the error. To prevent this you can apply some conditional logic to your code like below :
Obx(() => AuthController.instance.apiData != null ? Text("username: " + AuthController.instance.apiData.username!) : CircularProgressIndicator())) :
Here is my stateful widget and url is a property pass it to the widget from parent widget. I don't know where did I go wrong?? I created a future builder widget that has getData() as a future. But the print statement inside was not executed ever. Why is that and it returns me always null value, and this results me a red container appearing on screen and not the table widget.
class TimeTable extends StatefulWidget {
final url;
const TimeTable({Key? key,required this.url}) : super(key: key);
#override
_TimeTableState createState() => _TimeTableState();
}
class _TimeTableState extends State<TimeTable> {
Future<List<Train>> getData() async{
final list = await TrainClient(url: widget.url).getName();
print("this line not executed");
return list;
}
#override
Widget build(BuildContext context) {
return Scaffold(body: FutureBuilder(
future: getData(),
builder: (context,projectSnap){
if(projectSnap.connectionState == ConnectionState.none ||
projectSnap.data == null) {
return Container(color: Colors.red,);
}
return buildDataTable(trains: projectSnap.data);
}));
}
}
getData is a future method and it returns a list, The list gets printed when I call that object Train Client. I had my print statement inside TrainClient class to check whether the list is created successfully.
Here is the code of TrainClient
class TrainClient {
final String url;
TrainClient({required this.url});
Future<List<Train>> getName() async {
final uri = Uri.parse(url);
final response = await get(uri);
if (response.statusCode == 200) {
print("ulla");
final data = json.decode(response.body);
final result = data["RESULTS"]["directTrains"]["trainsList"];
final list = result.map((json) => Train.fromJson(json));
print(list);
return list;
}else{
throw Exception();
}
}
}
The TrainClient class has no error since it printed the list successfully as shown below
(Instance of 'Train', Instance of 'Train', Instance of 'Train', ..., Instance of 'Train', Instance of 'Train')
You should always obtain future earlier (in initState/didChangeDependencies).
Each time your build is executed, new future is created. So it never finishes, if your widget rebuilds often.
late final _dataFuture = getData();
...
FutureBuilder(
future: _dataFuture,
builder: (context,projectSnap){
...
}
);
I'm calling a API and saving its response using providers so that i can access from anywhere in my flutter app. The model used for storing response is
class Album with ChangeNotifier {
final String id;
final List<String> categoriesId;
final String title;
final String imageUrl;
Album({this.id, this.categoriesId, this.title, this.imageUrl});
factory Album.fromJson(Map<String, dynamic> json) {
List<dynamic> res = json['category_list'];
List<String> cats = [];
res.forEach((tx) {
cats.add(tx.toString());
});
return Album(
id: json['id'].toString(),
categoriesId: cats,
title: json['name'],
imageUrl: json["image"],
);
}
}
While the function calling endpoint is,
String albumUrl = "http://192.168.227.102:9000/static/album.json";
Future<List<Album>> fetchAnime(BuildContext context) async {
List<Album> resAlbum = [];
try {
final response = await http.get(Uri.parse(albumUrl));
if (response.statusCode == 200) {
List<dynamic> result = jsonDecode(response.body)['album'];
result.forEach((tx) {
Album test = Anime.fromJson(tx as Map<String, dynamic>);
resAlbum.add(test);
});
}
} catch (e) {
print(e);
}
return resAlbum;
}
The provider for model Album is,
class AlbumProvider with ChangeNotifier {
List<Album> _items = [];
void getPostData(context) async {
bool loading = true;
if (this._items.isNotEmpty) {
this._items = await fetchAlbum(context);
}
loading = false;
notifyListeners();
}
}
The view using this provider is,
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
void initState() {
Provider.of<AlbumProvider>(context, listen: false).getPostData(context);
super.initState();
}
#override
Widget build(BuildContext context) {
List<Album> albumData = Provider.of<AlbumProvider>(context).allitems;
return Scaffold(
appBar: AppBar(
title: Text(
'Home Screen',
),
),
body:albumData.isEmpty
?Text("we are fetching data")
:Text("fetching complete")
)
}
}
When i run the app i got following error,
The following assertion was thrown while dispatching notifications for AlbumProvider:
setState() or markNeedsBuild() called during build.
This _InheritedProviderScope widget cannot be marked as needing to build because the framework is already in the process of building widgets. A widget can be marked as needing to be built during the build phase only if one of its ancestors is currently building. This exception is allowed because the framework builds parent widgets before children, which means a dirty descendant will always be built. Otherwise, the framework might not visit this widget during this build phase.
The widget on which setState() or markNeedsBuild() was called was: _InheritedProviderScope
What are the reasons for getting the error and How can i remove this error?
This is because, when you are calling
Provider.of<AlbumProvider>(context, listen: false).getPostData(context);
from initState method of HomePage, which in turn invokes following method
void getPostData(context) async {
bool loading = true;
if (this._items.isNotEmpty) {
this._items = await fetchAlbum(context);
}
loading = false;
notifyListeners();
}
This function is calling notifiyListeners();
The issue is that you are listening for changes to AlbumProvider in build method of HomePage.
List<Album> albumData = Provider.of<AlbumProvider>(context).allitems;
So ultimately, initState is calling getPostData which is calling notifyListeners() which will try to rebuild the widget tree before it has finished building.
You should FutureBuilder instead of calling getPostData() in the initState method.
This ShakePlugin is not working with this piece of code,when im just using this code without these api calls and all its working fine.
class MyAppState extends State<MyApp> {
List data;
String _search = 'nature';
int index = 0;
File imageFile;
String imageData;
bool dataLoaded;
var path;
int count = 10;
FlutterShakePlugin _shakePlugin;
void initState() {
super.initState();
_shakePlugin = FlutterShakePlugin(
onPhoneShaken: () {
setState(() {
count=count+10;
});
},
},
)..startListening();
}
void dispose() {
super.dispose();
_shakePlugin.stopListening();
}
Future<String> getjsondata() async {
try {
var response = await http.get(
'https://api.unsplash.com/search/photos?per_page=${count}&client_id=TcAQEO3JoMG90U7Rl-YUiDo1x9XbZukzMOMQhxUVCV4&query=${_search}');
setState(() {
var converted = json.decode(response.body);
data = converted['results'];
});
} catch (e) {}
return 'success';
}
void saveImage(int i) async {
var url = data[i]['urls']['small'].toString();
var imageId = await ImageDownloader.downloadImage(url);
path = await ImageDownloader.findPath(imageId);
}
#override
Widget build(BuildContext context) {
getjsondata();
return GestureDetector(
child: SwipeDetector(
child: Container(
child: Image.network(
data[index]['urls']['small'],
I want to increase the count of images i recieve from api on shake of screen but this is not working even if i have installed all the libraries and all.
Calling your getjsondata method in the build method will cause the ui to render infinitely because you're calling setState in getjsondata. I think the shake plugin is working fine but its result is void because the screen is in an infinite render state.
If you move getjsondata to a FutureBuilder, remove the setState call from inside the getjsondata method and render your ui on the result of the Future your code should work.
I have 3 classes: Users, Posts and Comments. User has many Posts and
Posts has many Comments.
I want that all data to be fetched before the widget's build method is called.
I tryed to use initState() to do this:
class FetchDataExample extends StatefulWidget {
final User _user;
FetchDataExample(this._user);
#override
_State createState() => _State(_user);
}
class _State extends State<FetchDataExample> {
final User _user;
_State(this._user);
#override
void initState() {
_user.setPosts();
super.initState();
}
#override
Widget build(BuildContext context) {
print(this._user.posts[0]);
return Container(
);
}
}
In User class I have:
void setPosts() async {
String url = 'https://jsonplaceholder.typicode.com/posts?userId=' + this.id.toString();
var request = Requester.get(url); // Returns a Future<Response>
await request.then((value) => this.posts = Post.jsonToPosts(json.decode(value.body)));
this.posts.forEach((post) => post.setComments());
print(this.posts[0]);
}
The 'setComments()' has the same logic.
I have two prints:
Inside build that returns null;
Inside setPosts the returns Instance of 'Post';
So, by the time that Build method is called in the widget, the initState has not finished yet.
I need it be finished, does anyone know how can I do that?
You can use a FutureBuilder to build a widget by using latest result from a future.
And also you can combile multiple futures into a single one using Future.wait method.
Here is a sample code:
_getPageData() async {
var _combinedFutures = await Future.wait([setPosts, setComments]);
//do stuff with data
}
...
#override
Widget build(BuildContext context) {
return FutureBuilder(
future:_getPageData(),
builder: (context, snapshot) {
return Container();
}),
);
});