Yesterday I made a simple widget that fetches data to display some basic info, but I noticed that when I pop back to the list, the data is usually absent and I only get my error texts.
I figured this is due to these widgets originally being stateless, so I'm trying to convert them to stateful in order to reload the data when the page is loaded.
This is how I gather the data for my widget:
class BasicDogWidget extends StatefulWidget {
String URL;
BasicDogWidget(this.URL);
#override
createState() => new BasicDogWidgetState(URL);
}
class BasicDogWidgetState extends State<BasicDogWidget> {
String URL;
BasicDogWidgetState(this.URL);
var result;
var imageLink;
var dogName;
var dogType;
var dogColor;
var dogGender;
var dogAge;
#override
initState() {
fetchImageLink(URL).then((result) {
setState(imageLink = result);
});
fetchDogInfo(URL, 'datas-nev').then((result) {
setState(dogName = result);
});
fetchDogInfo(URL, 'datas-tipus').then((result) {
setState(dogType = result);
});
fetchDogInfo(URL, 'datas-szin').then((result) {
setState(dogColor = result);
});
fetchDogInfo(URL, 'datas-nem').then((result) {
setState(dogGender = result);
});
fetchDogInfo(URL, 'datas-kor').then((result) {
setState(dogAge = result);
});
super.initState();
}
#override
Widget build(BuildContext context) {
if (imageLink == null) {
return new Container();
}
if (dogName == null) {
return new Container();
}
if (dogType == null) {
return new Container();
}
if (dogColor == null) {
return new Container();
}
if (dogGender == null) {
return new Container();
}
if (dogAge == null) {
return new Container();
}
return buildBasicWidget(
imageLink, dogName, dogType, dogColor, dogGender, dogAge, URL);
}
}
However, it seems that the data collected by fetchDogInfo can't be passed in the setState method as it is a string.
E/flutter (12296): [ERROR:topaz/lib/tonic/logging/dart_error.cc(16)] Unhandled exception:
E/flutter (12296): type 'String' is not a subtype of type 'VoidCallback' of 'fn' where
E/flutter (12296): String is from dart:core
E/flutter (12296):
E/flutter (12296): #0 State.setState (package:flutter/src/widgets/framework.dart:1086)
E/flutter (12296): #1 BasicDogWidgetState.initState.<anonymous closure> (package:osszefogasaszanhuzokert/dog.dart:230)
Is there any way this issue can be bypassed?
You are executing async code
fetchImageLink(URL).then((result) {
which means fetchImageLink(URL) will eventually return a value and then then(...) is executed, but this call is async, which means it's added to the event queue for later execution and the code synchronically continues to execute until the end of initState and then build until this sync code is run to its completion, then the next "task" from the event queue is executed, which might be the then(...) part from your fetchImageLink() call if it already completed.
That shouldn't be a problem though.
You could just check if the value is available
#override
Widget build(BuildContext context) {
if(imageLink == null) {
return new Container(); // dummy widget until there is something real to render
}
... // your other build code
Flutter have FutureBuilder class. It provides a widget that does all the heavy lifting to support asynchronous Future objects. The nice thing about FutureBuilder is that is can be added directly to the widget tree, and based off the current status of the connection, the relevant UI can be displayed to indicate progress, a result or an error.
Example:
class JokePageState extends State<JokePage> {
Future<String> response;
initState() {
super.initState();
response = http.read(dadJokeApi, headers: httpHeaders);
}
Widget build(BuildContext context) {
return new Scaffold(
body: new Center(
child: new FutureBuilder<String>(
future: response,
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return const Icon(Icons.sync_problem);
case ConnectionState.waiting:
case ConnectionState.active:
return const CircularProgressIndicator();
case ConnectionState.done:
final decoded = json.decode(snapshot.data);
if (decoded['status'] == 200) {
return new Text(decoded['joke']);
} else {
return const Icon(Icons.error);
}
}
},
),
),
);
}
}
Related
I am struggling with RxDart (maybe just straight up Rx programming). I currently have a stateful widget that calls my bloc in it's didChangeDependencies(). That call goes out and gets data via http request and adds it to a stream. I'm using BehaviorSubject and this works fine. I have child widgets using StreamBuilders and they get data no problem. My issue comes in dealing with errors. If my http request fails, I hydrate the stream with addError('whatever error') but my child widget's StreamBuilder is not receiving that error. It doesn't get anything at all.
So I have a few questions.
Is that expected?
Should error handling not be done in StreamBuilder? Ideally, I want to show something in the UI if something goes wrong so not sure how else to do it.
I could make my child widget stateful and use stream.listen. I do receive the errors there but it seems like overkill to have that and the StreamBuilder.
Am I missing something fundamental here about streams and error handling?
Here is my bloc:
final _plans = BehaviorSubject<List<PlanModel>>();
Observable<List<PlanModel>> get plans => _plans.stream;
fetchPlans() async {
try {
final _plans = await _planRepository.getPlans();
_plans.add(_plans);
}
on AuthenticationException {
_plans.addError('authentication error');
}
on SocketException {
_plans.addError('no network connection');
}
catch(error) {
_plans.addError('fetch unsuccessful');
}
}
Simplified Parent Widget:
class PlanPage extends StatefulWidget {
#override
PlanPageState createState() {
return new PlanPageState();
}
}
class PlanPageState extends State<PlanPage> {
#override
void didChangeDependencies() async {
super.didChangeDependencies();
var planBloc = BaseProvider.of<PlanBloc>(context);
planBloc.fetchPlans();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar( title: const Text('Your Plan') ),
body: PlanWrapper()
);
}
}
Simplified Child Widget with StreamBuilder:
class PlanWrapper extends StatelessWidget {
#override
Widget build(BuildContext context) {
var planBloc = BaseProvider.of<PlanBloc>(context);
return StreamBuilder(
stream: planBloc.plans,
builder: (BuildContext context, AsyncSnapshot<List<PlanModel>> plans) {
if (plans.hasError) {
//ERROR NEVER COMES IN HERE
switch(plans.error) {
case 'authentication error':
return RyAuthErrorCard();
case 'no network connection':
return RyNetworkErrorCard();
default:
return RyGenericErrorCard(GeneralException().errorMessages()['message']);
}
}
if (plans.hasData && plans.data.isNotEmpty) {
return ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: _buildPlanTiles(context, plans.data)
);
}
return Center(child: const CircularProgressIndicator());
}
);
}
}
There was an issue about this in the RxDart GitHub (https://github.com/ReactiveX/rxdart/issues/227). BehaviorSubjects were not replaying errors to new listeners.
It was fixed in version 0.21.0. "Breaking Change: BehaviorSubject will now emit an Error, if the last event was also an Error. Before, when an Error occurred before a listen, the subscriber would not be notified of that Error."
I have a screen App in which i have onGenerateRoute property of MaterialApp. In the routes method i make an api call and once i get the response i want to let user navigate to login screen
I tried calling my widget Login inside .then() function
class App extends StatelessWidget {
Widget build(BuildContext context) {
return AppBlocProvider(
child: LoginBlocProvider(
child: MaterialApp(
onGenerateRoute: routes,
),
),
);
}
Route routes(RouteSettings settings) {
print(settings.name);
switch (settings.name) {
case '/':
return MaterialPageRoute(builder: (context) {
//HERE I AM MAKING API CALL
final appBloc = AppBlocProvider.of(context);
appBloc.verifyUser().then((response) {
//HERE ONCE I GET THE RESPONSE I WANT TO NAVIGATE USER TO
//lOGIN ACTIVITY
print('called');
return Login();
});
return AppBlocProvider(
child: Center(child: CircularProgressIndicator()),
);
});
break;
case '/Login':
return MaterialPageRoute(builder: (BuildContext context) {
return Login();
});
break;
case '/HomeScreen':
return MaterialPageRoute(builder: (BuildContext context) {
return Home();
});
break;
}
return MaterialPageRoute(builder: (context) {
print('returned null');
});
}
api call get successful and even .then() method executes but login screen doesn't appear
The reason return Login(); doesn't do anything was because another return has been executed already: return AppBlocProvider(child: Widget());
Similar to this sample, since a return has been already made, the other return won't do anything. The sample prints 'bar', and 'foo' was never printed using print(bar());.
void main() {
print(bar());
}
Future<String> foo() async{
await Future.delayed(Duration(seconds: 5));
return 'foo';
}
String bar(){
String txt = 'bar';
foo().then((String value){
print('Future finished: $value');
// Since print already got a String return,
// returning this value won't do anything
return value; // 'foo' won't be printed on main()
});
return txt;
}
You may want to consider moving the navigation inside Login and also display CircularProgressIndicator() there.
I have an flutter app in which I have to check the login status when the app is launched and call the relevant screen accordingly.
The code used to launch the app:
class MyApp extends StatefulWidget {
#override
MyAppState createState() {
return new MyAppState();
}
}
class MyAppState extends State<MyApp> {
bool isLoggedIn;
Future<bool> getLoginState() async{
SharedPreferences pf = await SharedPreferences.getInstance();
bool loginState = pf.getBool('loginState');
return loginState;
// return pf.commit();
}
#override
Widget build(BuildContext context) {
getLoginState().then((isAuth){
this.isLoggedIn = isAuth;
});
if(this.isLoggedIn) {return Container(child: Text('Logged In'));}
else {return Container(child: Text('Not Logged In));}
}
}
I am able to save the SharedPreference and retrieve it here, the issue is that as getLoginState() is an async function, this.isLoggedIn is null by the time the if condition is executed. The boolean assertion fails in the if statement and the app crashes.
How do I ensure that the bool variable isLoggedIn used in the if condition has a value when the if statement is executed?
Any help would be appreciated.
You can use FutureBuilder to solve this problem.
#override
Widget build(BuildContext context) {
new FutureBuilder<String>(
future: getLoginState(),
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.active:
case ConnectionState.waiting:
return new Text('Loading...');
case ConnectionState.done:
if (snapshot.hasData) {
loginState = snapshot.data;
if(loginState) {
return Container(child: Text('Logged In'));
}
else {
return Container(child: Text('Not Logged In));
}
} else {
return Container(child: Text('Error..));
}
}
},
)
}
Note: we don't need isLoggedIn state variable.
I have one question regarding how to reload a list after refresh indicator is called in Flutter, using Streams and RxDart.
Here is what I have , my model class:
class HomeState {
List<Event> result;
final bool hasError;
final bool isLoading;
HomeState({
this.result,
this.hasError = false,
this.isLoading = false,
});
factory HomeState.initial() =>
new HomeState(result: new List<Event>());
factory HomeState.loading() => new HomeState(isLoading: true);
factory HomeState.error() => new HomeState(hasError: true);
}
class HomeBloc {
Stream<HomeState> state;
final EventRepository repository;
HomeBloc(this.repository) {
state = new Observable.just(new HomeState.initial());
}
void loadEvents(){
state = new Observable.fromFuture(repository.getEventList(1)).map<HomeState>((List<Event> list){
return new HomeState(
result: list,
isLoading: false
);
}).onErrorReturn(new HomeState.error())
.startWith(new HomeState.loading());
}
}
My widget:
class HomePageRx extends StatefulWidget {
#override
_HomePageRxState createState() => _HomePageRxState();
}
class _HomePageRxState extends State<HomePageRx> {
HomeBloc bloc;
_HomePageRxState() {
bloc = new HomeBloc(new EventRest());
bloc.loadEvents();
}
Future<Null> _onRefresh() async {
bloc.loadEvents();
return null;
}
#override
Widget build(BuildContext context) {
return new StreamBuilder(
stream: bloc.state,
builder: (BuildContext context, AsyncSnapshot<HomeState> snapshot) {
var state = snapshot.data;
return new Scaffold(
body: new RefreshIndicator(
onRefresh: _onRefresh,
child: new LayoutBuilder(builder:
(BuildContext context, BoxConstraints boxConstraints) {
if (state.isLoading) {
return new Center(
child: new CircularProgressIndicator(
backgroundColor: Colors.deepOrangeAccent,
strokeWidth: 5.0,
),
);
} else {
if (state.result.length > 0) {
return new ListView.builder(
itemCount: snapshot.data.result.length,
itemBuilder: (BuildContext context, int index) {
return new Text(snapshot.data.result[index].title);
});
} else {
return new Center(
child: new Text("Empty data"),
);
}
}
}),
),
);
});
}
}
The problem is when I do the pull refresh from list, the UI doesn't refresh (the server is called, the animation of the refreshindicator also), I know that the issue is related to the stream but I don't know how to solve it.
Expected result : Display the CircularProgressIndicator until the data is loaded
Any help? Thanks
You are not supposed to change the instance of state.
You should instead submit a new value to the observable. So that StreamBuilder, which is listening to state will be notified of a new value.
Which means you can't just have an Observable instance internally, as Observable doesn't have any method for adding pushing new values. So you'll need a Subject.
Basically this changes your Bloc to the following :
class HomeBloc {
final Stream<HomeState> state;
final EventRepository repository;
final Subject<HomeState> _stateSubject;
factory HomeBloc(EventRepository respository) {
final subject = new BehaviorSubject(seedValue: new HomeState.initial());
return new HomeBloc._(
repository: respository,
stateSubject: subject,
state: subject.asBroadcastStream());
}
HomeBloc._({this.state, Subject<HomeState> stateSubject, this.repository})
: _stateSubject = stateSubject;
Future<void> loadEvents() async {
_stateSubject.add(new HomeState.loading());
try {
final list = await repository.getEventList(1);
_stateSubject.add(new HomeState(result: list, isLoading: false));
} catch (err) {
_stateSubject.addError(err);
}
}
}
Also, notice how loadEvent use addError with the exception. Instead of pushing a HomeState with a hasError: true.
In my app, I have a drawer with a UserAccountsDrawerHeader, which I feed its properties by simply getting the x property from FirebaseAuth.instance.currentUser.x
In the latest firebase_auth 0.2.0 version , where currentUser() is async.
I have been trying for several hours to store the information of the currently logged user and have not yet reached the correct way to do this.
I understand that I can access them by something like the following:
Future<String> _getCurrentUserName() async {
FirebaseUser user = await FirebaseAuth.instance.currentUser();
return user.displayName;
}
...
new UserAccountsDrawerHeader(accountName: new Text(_getCurrentUserName()))
I understand that these code snippets will give type mismatch, but I am just trying to illustrate what I am trying to do.
What am I missing exactly that is preventing me from reaching a solution?
Update
class _MyTabsState extends State<MyTabs> with TickerProviderStateMixin {
TabController controller;
Pages _page;
String _currentUserName;
String _currentUserEmail;
String _currentUserPhoto;
#override
void initState() {
super.initState();
_states();
controller = new TabController(length: 5, vsync: this);
controller.addListener(_select);
_page = pages[0];
}
My method
I just coupled the auth state with my previously implemented TabBar state
_states() async{
var user = await FirebaseAuth.instance.currentUser();
var name = user.displayName;
var email = user.email;
var photoUrl = user.photoUrl;
setState(() {
this._currentUserName=name;
this._currentUserEmail=email;
this._currentUserPhoto=photoUrl;
_page = pages[controller.index];
});
}
My Drawer
drawer: new Drawer(
child: new ListView(
children: <Widget>[
new UserAccountsDrawerHeader(accountName: new Text(_currentUserName) ,
accountEmail: new Text (_currentUserEmail),
currentAccountPicture: new CircleAvatar(
backgroundImage: new NetworkImage(_currentUserPhoto),
),
Here is the exception I get from the debug console
I/flutter (14926): ══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
I/flutter (14926): The following assertion was thrown building MyTabs(dirty, state: _MyTabsState#f49aa(tickers:
I/flutter (14926): tracking 1 ticker)):
I/flutter (14926): 'package:flutter/src/widgets/text.dart': Failed assertion: line 207 pos 15: 'data != null': is not
I/flutter (14926): true.
I/flutter (14926): Either the assertion indicates an error in the framework itself, or we should provide substantially
Update 2:
This is how I modified the google sign in function from the firebase examples:
Future <FirebaseUser> _testSignInWithGoogle() async {
final GoogleSignInAccount googleUser = await _googleSignIn.signIn();
final GoogleSignInAuthentication googleAuth =
await googleUser.authentication;
//checking if there is a current user
var check = await FirebaseAuth.instance.currentUser();
if (check!=null){
final FirebaseUser user = check;
return user;
}
else{
final FirebaseUser user = await _auth.signInWithGoogle(
accessToken: googleAuth.accessToken,
idToken: googleAuth.idToken,
);
assert(user.email != null);
assert(user.displayName != null);
assert(!user.isAnonymous);
assert(await user.getToken() != null);
return user;
}
}
Update 3:
My main function
void main() {
runApp(
new MaterialApp(
home: new SignIn(),
routes: <String, WidgetBuilder>{
"/SignUp":(BuildContext context)=> new SignUp(),
"/Login": (BuildContext context)=> new SignIn(),
"/MyTabs": (BuildContext context)=> new MyTabs()},
));
}
And then my SignIn contains a google button that when pressed:
onPressed: () { _testSignInWithGoogle(). //async returns FirebaseUser
whenComplete(()=>Navigator.of(context).pushNamed("/MyTabs")
);
}
and the Drawer from update 1 is included within MyTabs build.
There are several possibilities.
First : Use a stateful widget
Override the initState method like this :
class Test extends StatefulWidget {
#override
_TestState createState() => new _TestState();
}
class _TestState extends State<Test> {
String _currentUserName;
#override
initState() {
super.initState();
doAsyncStuff();
}
doAsyncStuff() async {
var name = await _getCurrentUserName();
setState(() {
this._currentUserName = name;
});
}
#override
Widget build(BuildContext context) {
if (_currentUserName == null)
return new Container();
return new Text(_currentUserName);
}
}
Second : Use the FutureBuilder widget
Basically, it's a wrapper for those who don't want to use a stateful widget. It does the same in the end.
But you won't be able to reuse your future somewhere else.
class Test extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new FutureBuilder(
future: _getCurrentUserName(),
builder: (context, AsyncSnapshot<int> snapshot) {
if (snapshot.hasData)
return new Text(snapshot.data.toString());
else
return new Container();
},
);
}
}
Explanation :
Your getCurrentUserName is asynchronous.
You can't just directly mix it with other synchronous functions.
Asynchronous functions are quite useful. But if you want to use them, just remember two things :
Inside another async function, you can var x = await myFuture, which will wait until myFuture finish to get it's result.
But you can't use await inside a sync function.
Instead, you can use
myFuture.then(myFunction) or myFuture.whenComplete(myFunction). myFunction will be called when the future is finished. And they both .then and .whenComplete will pass the result of your future as parameter to your myFunction.
"How to properly implement authentification" ?
You should definitely not do it this way. You'll have tons of code duplication.
The most ideal way to organise layers such as Authentification is like this :
runApp(new Configuration.fromFile("confs.json",
child: new Authentification(
child: new MaterialApp(
home: new Column(
children: <Widget>[
new Text("Hello"),
new AuthentifiedBuilder(
inRoles: [UserRole.admin],
builder: (context, user) {
return new Text(user.name);
}
),
],
),
),
),
));
And then, when you need a configuration or the current user inside a widget, you'd do this :
#override
Widget build(BuildContext context) {
var user = Authentification.of(context).user;
var host = Configuration.of(context).host;
// do stuff with host and the user
return new Container();
}
There are so many advantages about doing this, that there's no reason not to do it.
Such as "Code once, use everywhere". Or the ability to have a generic value and override it for a specific widget.
You'll realise that a lot of Flutter widgets are following this idea.
Such as Navigator, Scaffold, Theme, ...
But "How to do this ??"
It's all thanks to the BuildContext context parameter. Which provides a few helpers to do it.
For exemple, the code of Authentification.of(context) would be the following :
class Authentification extends StatefulWidget {
final Widget child;
static AuthentificationData of(BuildContext context) {
final AuthentificationData auth = context.inheritFromWidgetOfExactType(AuthentificationData);
assert(auth != null);
return auth;
}
Authentification({this.child});
#override
AuthentificationState createState() => new AuthentificationState();
}