I am trying to get data from Database, but my widget is built before I can get them...
class CategoriesWidget extends StatefulWidget {
#override
_CategoriesWidgetState createState() => _CategoriesWidgetState();
}
class _CategoriesWidgetState extends State<CategoriesWidget> {
SharedPreferences prefs;
String token;
var _isInit = false;
#override
void initState() {
if (!_isInit) {
super.initState();
fetchCat();
_isInit = true;
}
}
var categories = {};
fetchCat() async {
final prefs = await SharedPreferences.getInstance();
setState(() {
token = prefs.getString('api_token');
});
await fetchCategories(token).then((result) {
categories = result[1];
print(categories);
print(result[1]);
});
}
#override
Widget build(BuildContext context) {
final deviceSize = MediaQuery.of(context).size;
print('2');
return Column(
// code here
);
}
}
you can see that I print 1 and 2 to see which one is getting the first and I got as result 2 then 1.
You should use a FutureBuilder.
#override
Widget build(BuildContext context) {
return FutureBuilder<String>(
future: _fetchCat(),
builder: (context, snapshot) => snapshot.hasData
? MyWidget(data: snapshot.data)
: Text('Loading...'),
);
}
And with a FutureBuilder, your Widget could probably stay Stateless.
Here is a Minimal Working Example:
Full source code:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'StackOverflow Answer',
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(child: CategoriesWidget()),
);
}
}
class CategoriesWidget extends StatefulWidget {
#override
_CategoriesWidgetState createState() => _CategoriesWidgetState();
}
class _CategoriesWidgetState extends State<CategoriesWidget> {
Future<String> _fetchCat() async {
await Future.delayed(Duration(seconds: 2));
return 'Category';
}
#override
Widget build(BuildContext context) {
return FutureBuilder<String>(
future: _fetchCat(),
builder: (context, snapshot) => Text(
snapshot.hasData ? snapshot.data ?? 'NO CATEGORY' : 'Loading...'),
);
}
}
Related
I am trying to set the home page of the Flutter app asynchronously, but that is not working because the build method cannot have async properties.
class _MyAppState extends State<MyApp> {
// Widget homeWidget;
// #override
// void initState() async {
// super.initState();
// homeWidget = (await AuthUser.getCurrentUser() != null)
// ? NavBarPage()
// : OnBoardingWidget();
// }
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'WizTkt',
theme: Theme.of(context).copyWith(
appBarTheme: Theme.of(context)
.appBarTheme
.copyWith(brightness: Brightness.dark),
primaryColor: Colors.blue),
home: (await AuthUser.getCurrentUser() != null)
? NavBarPage()
: OnBoardingWidget(),
);
}
}
As you can see in the code, I also tried to use initState to set the homepage widget but I cannot make initState an asynchronous function. I feel like there is a better way to choose your homepage in Flutter. What am I missing?
Do note that AuthUser.getCurrentUser() has to be an async function because I use the SharedPreferences library to obtain the login token stored in memory.
You can use FutureBuilder which allows you to build an Widget in a future time.
Here an example:
class OnBoardingWidget extends StatefulWidget {
const OnBoardingWidget({Key key}) : super(key: key);
#override
State<OnBoardingWidget> createState() => _OnBoardingWidgetState();
}
class _OnBoardingWidgetState extends State<OnBoardingWidget> {
final Future<String> _waiter = Future<String>.delayed(
const Duration(seconds: 2), () => 'Data Loaded',
);
#override
Widget build(BuildContext context) {
return Container(
child: FutureBuilder<String>(
future: _waiter,
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
Widget wdgt;
if (snapshot.hasData) {
wdgt = Text('Result: ${snapshot.data}');
} else if (snapshot.hasError) {
wdgt = Text('Ops ops ops');
} else {
wdgt = Text('Not ready yet');
}
return Center(child: wdgt);
},
),
);
}
}
I know I'm successfully fetching the data from the DB because I can print it out in fetchFavorites() but I'm not dealing with the List correctly in _getFavesState because my result is:
type List〈dynamic〉 is not a subtype of type 'FutureOr<List 〈String〉>'
So how do I actually create widgets from my data? Code:
Future<List<String>> fetchFavorites() async {
FirebaseFirestore firestore = FirebaseFirestore.instance;
final user = FirebaseAuth.instance.currentUser;
final userData = await firestore.collection('users').doc(user.uid).get();
var faves = userData.get("favorites");
return faves;
}
class GetFaves extends StatefulWidget {
#override
_GetFavesState createState() => _GetFavesState();
}
class _GetFavesState extends State<GetFaves> {
Widget build(BuildContext context) {
Future<List> favoritesList;
favoritesList = fetchFavorites();
return Column(children: [
favoritesList == null
? Text('No Favorites')
: FutureBuilder(
future: favoritesList,
builder: (context, snapshot) {
if (snapshot.hasData) {
print("has data");
return Container(
child: ListView.builder(
itemCount: snapshot.data.length,
scrollDirection: Axis.horizontal,
itemBuilder: (BuildContext context, int index) {
return Text('${snapshot.data[index].title}');
}));
} else if (snapshot.hasError) {
return Text('${snapshot.error.toString()}');
} else {
return CircularProgressIndicator();
}
}),
]);
}
}
EDIT:
I don't think it is a straight ahead list of strings. It is some kind of record that looks like a serialized JSON: {CoverUrl : https://...., Title: some title, cid: something} So the error makes sense. Not sure how that changes the solution.
A few remarks on your code:
You don't need a StatefulWidget since the data is managed by your FutureBuilder
favoritesList == null will always be false since it's a Future<List<String>>
In snapshot.data[index].title, snapshot.data[index] is a String, what is this title?
You can remove the Container, it just has a child and is therefore useless.
I think we can simplify a bit:
(I left out the Firestore part since you tell us it works fine. Though, are you sure userData.get("favorites") returns a String? If so, cast it to a String to match your Future<List<String>> signature)
Full source code
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
home: HomePage(),
),
);
}
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: GetFaves(),
);
}
}
class GetFaves extends StatefulWidget {
#override
_GetFavesState createState() => _GetFavesState();
}
class _GetFavesState extends State<GetFaves> {
Future<List<String>> _fetchFavorites() async {
return List.generate(10, (index) => 'Favorite $index');
}
#override
Widget build(BuildContext context) {
return FutureBuilder<List<String>>(
future: _fetchFavorites(),
builder: (context, snapshot) {
print(snapshot);
if (snapshot.hasData) {
print(snapshot.data);
return ListView(
scrollDirection: Axis.horizontal,
children: snapshot.data.map((favorite) => Text(favorite)).toList(),
);
} else if (snapshot.hasError) {
return Text(snapshot.error.toString());
} else {
return CircularProgressIndicator();
}
},
);
}
}
Update : Album from JSON Data
Your Albums are retrieved as JSON Data from Firestore: Here is an example:
[
{'cid': '593312', 'title': 'porttitor', 'coverUrl': 'https://source.unsplash.com/640x480'},
{'cid': '910654', 'title': 'auctor', 'coverUrl': 'https://source.unsplash.com/640x480'},
{'cid': '276961', 'title': 'nullam', 'coverUrl': 'https://source.unsplash.com/640x480'},
{'cid': '413021', 'title': 'rhoncus', 'coverUrl': 'https://source.unsplash.com/640x480'},
{'cid': '299898', 'title': 'posuere', 'coverUrl': 'https://source.unsplash.com/640x480'}
]
Such data in Dart is usually defined as a List<Map<String, dynamic>>.
In this solution, we will use the freezed package (depending on json_serializable package) to generate the Domain Class Album. The code of this class is generate in your_file.freezed.dart for the Immutable Domain Class and your_file.g.dart for the from/toJson functionality. To generate these two files, you will need to also install the Dev Dependency build_runner and run the following command at the root of your project:
flutter pub run build_runner watch --delete-conflicting-outputs
Once everything is setup, your _fetchFavorites() will become:
Future<List<Album>> fetchFavorites() async {
FirebaseFirestore firestore = FirebaseFirestore.instance;
final user = FirebaseAuth.instance.currentUser;
final userData = await firestore.collection('users').doc(user.uid).get();
var faves = userData.get("favorites");
return faves.map((jsonData) => Album.fromJson(jsonData)).toList();
}
Note: This snippet has not been tested
Full source code using dummy Data
Note: the faker package is used to generate random dummy data.
import 'package:faker/faker.dart';
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part '66473551.future.freezed.dart';
part '66473551.future.g.dart';
void main() {
runApp(
MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
home: HomePage(),
),
);
}
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: FavoriteList(),
);
}
}
class FavoriteList extends StatelessWidget {
Future<List<Album>> _fetchFavorites() async {
return dummyData.map((jsonData) => Album.fromJson(jsonData)).toList();
}
#override
Widget build(BuildContext context) {
return FutureBuilder<List<Album>>(
future: _fetchFavorites(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return ListView(
children: snapshot.data
.map((favorite) => AlbumWidget(album: favorite))
.toList(),
);
} else if (snapshot.hasError) {
return Text(snapshot.error.toString());
} else {
return CircularProgressIndicator();
}
},
);
}
}
class AlbumWidget extends StatelessWidget {
final Album album;
const AlbumWidget({
Key key,
this.album,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return ListTile(
leading: Image.network(album.coverUrl),
title: Text(album.title),
subtitle: Text(album.cid),
);
}
}
#freezed
abstract class Album with _$Album {
const factory Album({String cid, String title, String coverUrl}) = _Album;
factory Album.fromJson(Map<String, dynamic> json) => _$AlbumFromJson(json);
}
final faker = new Faker();
final List<Map<String, dynamic>> dummyData = List.generate(
10,
(index) => {
'cid': faker.randomGenerator.integer(999999).toString(),
'title': faker.lorem.word(),
'coverUrl': faker.image.image(),
},
);
You do something like this
class GetFaves extends StatefulWidget {
#override
_GetFavesState createState() => _GetFavesState();
}
class _GetFavesState extends State<GetFaves> {
#override
void initState(){
super.initState();
fetchFavourites();
}
#override
Widget build(BuildContext context) {
List<Widget> widgetList = [SizedBox(height:50),CircularProgressIndicator()];
return Column(
children:widgetList,
)
}
void fetchFavorites() async {
widgetList = [SizedBox(height:50),CircularProgressIndicator()];
FirebaseFirestore firestore = FirebaseFirestore.instance;
final user = FirebaseAuth.instance.currentUser;
final userData = await firestore.collection('users').doc(user.uid).get();
var faves = userData.get("favorites");
widgetList = [];
//Build the list of Containers here with a for loop and store it in your variable widgetList. I could not understand what you were building so I left that part for you
setState((){});
}
}
What I am basically doing is showing a loading animation in the start and when the documents have been loaded using the function from initState and you have made the widgets, I rebuild the class with a list of new Widgets
What was happening was that I was fetching from the database a List of type dynamic, which in this case is some kind of deserialized JSON looking text. When I changed the function signature of my function fetchFavorites to this, the build method of my calling class started working:
Future<List<dynamic>> fetchFavorites() async {
...
}
The elements of the returned records are accessed as a 2-dimensional array, so:
record[index]["title"]
and so on.
I have a Stream Provider (connected to firebase) that is not working. I am guessing that the problem lies in the fact that I am using a named navigator [Navigator.pushNamed(context, '/route',)]. I guess this makes the 'route' widget to not be the son of the widget that calls it. Let me show it better below.
My app structure is as follows:
My main widget which handles routing and receives the Stream with user authentication (there is no problem here):
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return StreamProvider<User>.value(
value: AuthService().user,
child: MaterialApp(
debugShowCheckedModeBanner: false,
home: Wrapper(),
routes: {
'/home': (context) => Wrapper(),
'/edit_profile': (context) => UserProfile() //This is where I am having trouble.
}
),
);
}
}
The Wrapper that validates if the user is authenticated and acts accordingly:
class Wrapper extends StatelessWidget {
#override
Widget build(BuildContext context) {
final user = Provider.of<User>(context);
// return either the Home or Authenticate widget
if (user == null){
return Authenticate();
} else {
return HomeWrapper();
}
}
}
The HomeWrapper which receives the second stream and redirects to the widget I am having trouble with:
class HomeWrapper extends StatefulWidget {
#override
_HomeWrapperState createState() => _HomeWrapperState();
}
class _HomeWrapperState extends State<HomeWrapper> {
String currentBodyName = 'home';
Widget currentBodyWidget = Home();
#override
Widget build(BuildContext context) {
Widget _drawerOptions = Row(
children: [
FlatButton(child: someChild, onPressed: () {Navigator.pushNamed(context, '/edit_profile',);},), //This is the actual call to the navigator.
],
);
return StreamProvider<Map>.value( //This is the problematic Stream!
value: DatabaseService().userDetail,
child: Scaffold(
//Body
body: currentBodyWidget,
//I am simplifying this to show the most important parts
bottomNavigationBar: myBottomNavigationBar(
buttons: <Widget>[
FlatButton(
icon: someIcon,
onPressed: () => _onItemTapped('home'),
),
FlatButton(
icon: otherIcon,
onPressed: () => _onItemTapped('second_screen'),
),
],)
//Drawer
drawer: Drawer(child: _drawerOptions,), //This one has the call to the problematic edit_profile route.
);
}
void _onItemTapped(String newBodyName) {
if (newBodyName != currentBodyName){
setState(() {
currentBodyName = newBodyName;
switch(newBodyName) {
case 'home': {
currentBodyWidget = Home();
}
break;
case 'second_screen': {
currentBodyWidget = SecondScreen();
}
break;
default: {
currentBodyWidget = Home();
}
break;
}
});
}
}
}
Finally the edit_profile route calls the UserProfile Widget which looks like this:
class UserProfile extends StatefulWidget {
#override
_UserProfileState createState() => _UserProfileState();
}
class _UserProfileState extends State<UserProfile> {
#override
Widget build(BuildContext context) {
//This is where the error occurs!!
final userDocument = Provider.of<Map>(context) ?? [];
print(userDocument);
return Scaffold(body: Container());
}
}
This is the error that it throws:
The following ProviderNotFoundError was thrown building UserProfile(dirty, state: _UserProfileState#09125):
Error: Could not find the correct Provider<Map<dynamic, dynamic>> above this UserProfile Widget
Thank you very much!!
Turns out my approach was wrong.
Instead of wrapping the HomeWrapper with the StreamProvider, hoping that it would pass the data to the next route (UserProfile ), what I did was to wrap the UserProfile widget with a StreamProvider, as follows:
(Note: I changed the Map StreamProvider for a UserData StreamProvider.)
class UserProfile extends StatefulWidget {
#override
_UserProfileState createState() => _UserProfileState();
}
class _UserProfileState extends State<UserProfile> {
#override
Widget build(BuildContext context) {
final user = Provider.of<User>(context);
return StreamBuilder<UserData>(
stream: DatabaseService(uid: user.uid).userData,
builder: (context, snapshot) {
if (snapshot.hasData) {
UserData userData = snapshot.data;
return Scaffold(
body: Container(
//My Widget here
);
} else
return Loading();
});
}
}
This series was very helpful: https://www.youtube.com/playlist?list=PL4cUxeGkcC9j--TKIdkb3ISfRbJeJYQwC
Below code always show OnboardingScreen a little time (maybe miliseconds), after that display MyHomePage. I am sure that you all understand what i try to do. I am using FutureBuilder to check getString method has data. Whats my fault ? Or any other best way for this ?
saveString() async {
final prefs = await SharedPreferences.getInstance();
prefs.setString('firstOpen', '1');
}
getString() method always return string.
getString() async {
final prefs = await SharedPreferences.getInstance();
String txt = prefs.getString('firstOpen');
return txt;
}
main.dart
home: new FutureBuilder(
future: getString(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return MyHomePage();
} else {
return OnboardingScreen();
}
})
Usually I'm using another route, rather than FutureBuilder. Because futurebuilder every hot reload will reset the futureBuilder.
There always will be some delay before the data loads, so you need to show something before the data will load.
Snapshot.hasData is showing only the return data of the resolved future.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: SplashScreen(),
);
}
}
class SplashScreen extends StatefulWidget {
#override
_SplashScreenState createState() => _SplashScreenState();
}
const isOnboardingFinished = 'isOnboardingFinished';
class _SplashScreenState extends State<SplashScreen> {
Timer timer;
bool isLoading = true;
#override
void initState() {
_checkIfFirstOpen();
super.initState();
}
Future<void> _checkIfFirstOpen() async {
final prefs = await SharedPreferences.getInstance();
var hasOpened = prefs.getBool(isOnboardingFinished) ?? false;
if (hasOpened) {
_changePage();
} else {
setState(() {
isLoading = false;
});
}
}
_changePage() {
Navigator.of(context).pushReplacement(
// this is route builder without any animation
PageRouteBuilder(
pageBuilder: (context, animation1, animation2) => HomePage(),
),
);
}
#override
Widget build(BuildContext context) {
return isLoading ? Container() : OnBoarding();
}
}
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(child: Text('homePage'));
}
}
class OnBoarding extends StatelessWidget {
Future<void> handleClose(BuildContext context) async {
final prefs = await SharedPreferences.getInstance();
prefs.setBool(isOnboardingFinished, true);
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (_) => HomePage(),
),
);
}
#override
Widget build(BuildContext context) {
return Container(
child: Center(
child: RaisedButton(
onPressed: () => handleClose(context),
child: Text('finish on bording and never show again'),
),
),
);
}
}
From the FutureBuilder class documentation:
The future must have been obtained earlier, e.g. during State.initState, State.didUpdateConfig, 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.
So you need to create a new Stateful widget to store this Future's as a State. With this state you can check which page to show. As suggested, you can start the future in the initState method:
class FirstPage extends StatefulWidget {
_FirstPageState createState() => _FirstPageState();
}
class _FirstPageState extends State<FirstPage> {
final Future<String> storedFuture;
#override
void initState() {
super.initState();
storedFuture = getString();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: storedFuture,
builder: (context, snapshot) {
if (snapshot.hasData) {
return MyHomePage();
} else {
return OnboardingScreen();
}
});
}
}
So in your home property you can call it FirstPage:
home: FirstPage(),
Your mistake was calling getString() from within the build method, which would restart the async call everytime the screen gets rebuilt.
I have created following 2 streams.
Stream 1 - Periodic Stream which is calling an async function at a particular time.
Stream 2 - Function which listens on stream 1 and then create stream 2
I wanted to add logic such that if stream 1 length is more that 0 then only create stream 2. But I am getting length of stream 1 as always 0.
My Code is as follows. (It runs after hot reload)
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
int _counter = 0;
#override
Widget build(BuildContext context) {
return MaterialApp(
home: MyPage(),
);
}
}
class MyPage extends StatefulWidget {
#override
_MyPageState createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> {
StreamController<List<String>> _myStreamController =
StreamController<List<String>>();
int _counter = 0;
Stream<List<String>> _stream() {
Duration interval = Duration(milliseconds: 100);
Stream<List<String>> stream =
Stream<List<String>>.periodic(interval, temp2);
return stream;
}
Future temp() async {
List<String> a = [];
_counter++;
a.add(_counter.toString());
return a;
}
List<String> temp2(int value) {
List<String> _localMsg = List();
temp().then((a) {
print(a);
_localMsg = a;
});
return _localMsg;
}
#override
void initState() {
super.initState();
_stream().listen((ondata) {
print(ondata);
if (ondata.length > 0) _myStreamController.sink.add(ondata);
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
child: StreamBuilder(
stream: _myStreamController.stream,
builder: (context, snapshot) {
if (snapshot.data != null) {
return Text(snapshot.data.toString());
} else {
return Text("waiting...");
}
},
),
),
),
);
}
}