I have this app where I have an intro screen that I want to hide if the user has already skipped once.
I'm using bloc with a Provider.
My issue is the connection state changes twice when I hot restart the app and I've been spending hours without understanding the reason.
Here is my code:
my main class
void main() => runApp(StatsApp());
class StatsApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(
create: (context) => IntroProvider(),
)
],
child: Consumer<IntroProvider>(builder: (context, value, child) {
return MaterialApp(
home: FutureBuilder(
future: value.bloc.checkSkipped(),
builder: (BuildContext context, snapshot) {
print(snapshot.connectionState);
print(snapshot.data);
return SplashScreen();
},
));
}),
);
}
}
my bloc
enum IntroEvents {
ReadLocalStorage,
SetIntroSkipped,
}
class IntroBloc extends Bloc<IntroEvents, bool> {
PrefsManager _prefsManager = PrefsManager.instance;
Future<bool> checkSkipped() async {
this.add(IntroEvents.ReadLocalStorage);
final skipped =
await _prefsManager.getValue(PrefTypes.Bool, "skippedIntro");
return skipped;
}
#override
// TODO: implement initialState
bool get initialState => false;
#override
Stream<bool> mapEventToState(IntroEvents event) async* {
switch (event) {
case IntroEvents.SetIntroSkipped:
_prefsManager.setValue(PrefTypes.Bool, "skippedIntro", true);
yield true;
break;
case IntroEvents.ReadLocalStorage:
final skipped =
await _prefsManager.getValue(PrefTypes.Bool, "skippedIntro");
yield skipped;
break;
default:
print("wtffffff");
}
}
}
my provider
class IntroProvider with ChangeNotifier {
IntroBloc _bloc;
IntroProvider(){
print("called IntroProvider");
_bloc = IntroBloc();
}
IntroBloc get bloc => _bloc;
}
Any help would be highly appreciated.
When working with snapshots you're able to check whether the snapshot.hasData. Normally you'd wrap the functionality you want to run when the snapshot has data in an if statement and provide some kind of default Widget when it does not.
FutureBuilder(
future: value.bloc.checkSkipped(),
builder: (BuildContext context, snapshot) {
print(snapshot.connectionState);
if (snapshot.hasData) {
print(snapshot.data);
}
return SplashScreen();
},
);
Related
The entry point is _processError function. I expect to get a widdet there. And this _processError runs from a parent FutureBuilder.
Then another Future builder should be executed, at least I think it should... But it seems there is no result from there. Whats wrong with it?
FutureBuilder<List<ShortLetter>>(
future: fetchMessages(),
builder: (BuildContext context, AsyncSnapshot<List<ShortLetter>> snapshot) {
...
} else if (snapshot.hasError) {
return _processError(snapshot, context); // I want to get a widget when an error happens
...
},
);
Future<bool> checkConnection() async {
debugPrint('---checkConnection---');
var connectivityResult = await (Connectivity().checkConnectivity());
...
// and returs true or false
}
Widget _processError(AsyncSnapshot snapshot, BuildContext context) {
var errorType = snapshot.error.runtimeType;
debugPrint('AllMessagesView, snapshot error: $errorType');
debugPrint(snapshot.error.toString());
if (errorType == TimeoutException) {
debugPrint('0000000000000000');
//////////////////////////////////////////////////////
// there is any output in console from the FutureBuilder below
// but checkConnection() was executed
FutureBuilder<bool>(
future: checkConnection(),
builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
if (snapshot.hasData) {
debugPrint('11111111111111 snapshot data: ${snapshot.data}');
if (snapshot.data == true) {
...
}
...
} else if (snapshot.hasError) {
debugPrint('2222222222222');
...
} else {
debugPrint('Error. This should not happen.');
...
}
},
);
...
}
...
}
here is a sample console output and any result from the second FutureBuilder
I/flutter (10556): AllMessagesView, snapshot error: TimeoutException
I/flutter (10556): TimeoutException after 0:00:10.000000: Future not completed
I/flutter (10556): 0000000000000000
I/flutter (10556): ---checkConnection---
Parent FutureBuilder is already been processed, I think we don't need to pass Async data.
This demo widget may help.
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
Future<int> parentF() async {
return await Future.delayed(Duration(seconds: 2), () => 4);
}
Future<String> childF(int sec) async {
return await Future.delayed(Duration(seconds: sec), () => "got the child");
}
Widget childFB(final data) {
print(data.runtimeType);
return FutureBuilder(
future: childF(4),
builder: (context, snapshot) => snapshot.hasData
? Text("${snapshot.data!} parent data: $data ")
: const Text("loading second child"));
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
FutureBuilder(
future: parentF(),
builder: (context, parentSnapshot) {
return parentSnapshot.hasData
? FutureBuilder<String>(
future: childF(3),
builder: (context, snapshot) {
return snapshot.hasData
? Text(
"${snapshot.data!} parent data: ${parentSnapshot.data} ")
: const Text("loading child");
},
)
: const Text("loading parent");
},
),
FutureBuilder(
future: parentF(),
builder: (context, parentSnapshot) {
return parentSnapshot.hasData
? childFB(parentSnapshot
.data) // it already have normal data, not async
: const Text("loading parent");
},
),
],
));
}
}
I would like to check if the user has already filled in the registration form:
Here is my code for the connectionState:
class LandingPage extends StatelessWidget {
// final Geolocator _geolocator = Geolocator()..forceAndroidLocationManager;
#override
Widget build(BuildContext context) {
final auth = Provider.of<AuthBase>(context, listen: false);
return StreamBuilder<User>(
stream: auth.onAuthStateChanged,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.active) {
User user = snapshot.data;
if (user == null) {
return SignInPage();
} else {
// _geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.best)
MatchEngine.instance.initialise(user.uid);
return Chat();
}
} else {
return Scaffold(
body: MyAppsCircularProgressIndicator(title: "MyApp",),
);
}
},
);
}
}
this code works fine for connectionstate.
I would like to add in the first code:
if (not signed in) {
show sign in page
} else {
if (not registered)
show register page
else
show home page
}
or
StreamBuilder(
stream: auth.authStateChanges()
builder: (_, snapshot) {
// check connectionState too
if (snapshot.hasData) {
StreamBuilder(
stream: database.userData() // this is a stream you create that reads from `userData/$uid` or similar
builder: (_, snapshot) {
if (snapshot.hasData) {
return HomePage()
} else {
return RegisterPage()
}
}
)
} else {
return SignInPage()
}
}
)
I would like to add the last code to the previous one to have my connectionstate + my redirection to RegisterPage.
I tried everything but to no avail ... could someone help me? Thank you
You could use the provider package and then create a seperate file which has the following code. I personally use this and it works well.
class Wrapper extends StatelessWidget {
#override
Widget build(BuildContext context) {
final user = Provider.of<User>(context);
if (user == null) {
return SignIn();
} else {
return Dashboard();
}
}
}
and in your main.dart file where you are building the material app. Put the wrapper (or whatever you name it) widget instead such as the following.
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return FutureBuilder(
// Initialize FlutterFire
future: Firebase.initializeApp(),
builder: (context, snapshot) {
// Check for errors
if (snapshot.hasError) {
return ErrorPage();
}
// Show Application
if (snapshot.connectionState == ConnectionState.done) {
return StreamProvider<Help4YouUser>.value(
value: AuthService().user,
child: MaterialApp(
debugShowCheckedModeBanner: false,
home: Wrapper(),
),
);
}
// Initialization
return LoadingWidget();
},
);
}
}
Any clarification needed please comment
I am trying to add distance from user to the Location object, but this requires using an asynchronous call that I can't figure out where and how to do exactly. From there I will sort Locations by distance from user. I tried the code below bc it's where the sorted locations would be used, but I get an error saying "await" can only be used in "async" or "async*" methods even though it is being used with an async function. How do I add distance from user to a Location object given it requires an asynchronous call?
class MapWidget extends StatefulWidget {
...
#override
_MapWidgetState createState() => _MapWidgetState();
}
class _MapWidgetState extends
State<MapWidget> {
Future <List<Location>> sortLocations() async {
return null;//function not done
}
#override
Widget build(BuildContext context) {
final List<Location> sortedLocations = await sortLocations();
...
You cannot use await functions in build method because it cannot be async.To use async operations in build you must use FutureBuilder or StreamBuilder.
Future<List<Location>> sortLocations() {
...
return <Location>[];
}
#override
Widget build(BuildContext context) {
return FutureBuilder<List<Location>>(
future: sortLocations(),
builder: (context, snapshot) {
if(snapshot.hasError) {
return Center(child: Text(snapshot.error.toString()));
}
if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator()));
}
return ListView(...);
},
);
}
Future<List<Location>> sortLocations() {
...
return <Location>[];
}
#override
Widget build(BuildContext context) {
return StreamBuilder<List<Location>>(
stream: sortLocations().asStream(),
builder: (context, snapshot) {
if(snapshot.hasError) {
return Center(child: Text(snapshot.error.toString()));
}
if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator()));
}
return ListView(...);
},
);
}
In Flutter there is a widget call FutureBuilder, that helps you build UI after the data is returned from an async function. You can use it as:
#override
Widget build(BuildContext context) {
return FutureBuilder<List<Location>>(
future: sortLocations(),
builder: (context, snapshot) {
if (!snapshot.hasData) return Container(child: Center(child: CircularProgressIndicator()));
var sortedLocations = snapshot.data;
// Build your UI here
return ...
}
);
you cannot use await in build method instead use it in initState
final List<Location> sortedLocations= new List();
#override
void initState(){
super.initState();
getdata();
}
getdata()async{
sortedLocations.clear();
sortedLocations = await sortLocations();
setState((){});
}
I am using Provider and the stream FirebaseAuth.instance.onAuthStateChanged in the app to decide where to redirect on startup, but although the user is already logged in (from a previous startup) the app starts on the login screen and almost 1 second later redirects to the home page, from which it should have started from the first moment. This happens even in airplane mode.
I would like to know if there is any approach to solve this, even if it is not possible to show the home screen at once, I don't know how to differentiate between the not logged user (null->login screen) and loading user (null->loading screen).
Some of the code:
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
final FirebaseAuth _auth = FirebaseAuth.instance;
final DatabaseService db = DatabaseService();
#override
Widget build(BuildContext context) {
return StreamProvider<FirebaseUser>.value(
value: _auth.onAuthStateChanged,
child: Consumer<FirebaseUser>(
builder: (context, firebaseUser, child) {
return MultiProvider(
providers: [
if (firebaseUser != null)
ChangeNotifierProvider(create: (ctx) => CollectionState(firebaseUser)),
StreamProvider<List<Collection>>.value(value: db.streamCollections(firebaseUser)),
],
child: MaterialApp(
title: 'My App',
routes: {
'/': (ctx) => LandingPage(),
'/login': (ctx) => LoginPage(),
'/emailSignIn': (ctx) => EmailSignInPage(),
'/emailSignUp': (ctx) => EmailSignUpPage(),
'/emailUnverified': (ctx) => EmailUnverifiedPage(),
'/home': (ctx) => HomePage(),
'/settings': (ctx) => Settings(),
},
),
);
},
),
);
}
}
class LandingPage extends StatelessWidget {
final DatabaseService _db = DatabaseService();
#override
Widget build(BuildContext context) {
final user = Provider.of<FirebaseUser>(context);
final userCondition =
user == null ? 'null' : user.isEmailVerified ? 'verifiedUser' : 'unverifiedUser';
switch (userCondition) {
case 'null':
return LoginPage();
break;
case 'unverifiedUser':
return EmailUnverifiedPage();
break;
case 'verifiedUser':
return HomePage();
break;
}
}
}
The code is a bit simplified, I use a service for the authentication instance instead, just that.
I know I'm very late, but I've had the same problem for weeks and I finally figured it out.
#ChinkySight is right when he says it's best to use a StreamBuilder, mostly because you have access to the connectionState property.
The reason why lag exists is because the connection to the stream is not fully established. So during ConnectionState.waiting, return a widget like a splash screen or just a container.
class Home extends StatelessWidget {
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: FirebaseAuth.instance.onAuthStateChanged,
builder: (_, snapshot) {
// Added this line
if (snapshot.connectionState == ConnectionState.waiting) {
return Container();
}
if (snapshot.data is FirebaseUser && snapshot.data != null) {
return HomePage();
}
return LoginPage();
});
}
}
You can even give your return statements fancy animations with the Animated Switcher
return StreamBuilder(
stream: FirebaseAuth.instance.onAuthStateChanged,
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
Widget widget;
if (snapshot.connectionState == ConnectionState.waiting) {
return Container();
}
switch (snapshot.hasData) {
case (true):
widget = HomePage();
break;
case (false):
widget = LoginPage();
}
return Stack(
children: <Widget>[
Scaffold(
backgroundColor: Colors.grey.shade200,
),
AnimatedSwitcher(
duration: Duration(milliseconds: 700),
child: FadeTransition(
opacity: animation,
child: widget,
),
);
},
)
],
);
},
);
This works for FlutterFire.
Firebase Auth enables you to subscribe in realtime to this state via a
Stream. Once called, the stream provides an immediate event of the
user's current authentication state, and then provides subsequent
events whenever the authentication state changes. To subscribe to
these changes, call the authStateChanges() method on your FirebaseAuth
instance:
import 'package:firebase_auth/firebase_auth.dart' as auth;
import 'package:flutter/material.dart';
import 'menu.dart';
import 'login.dart';
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:firebase_core/firebase_core.dart';
Future main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(
MyApp()
);
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp, DeviceOrientation.portraitUp]);
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'TestApp',
theme: ThemeData(primarySwatch: Colors.blue),
home:
StreamBuilder<auth.User>(
stream: auth.FirebaseAuth.instance.authStateChanges(),
builder: (BuildContext context, AsyncSnapshot<auth.User> snapshot) {
if(snapshot.hasData) {
print("data exists");
return HomePage();
}
else {
return LoginPage();
}
},
)
);
}
}
I have a StreamBuilder object to render a list from a FireStore collection:
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: Firestore.instance.collection('posts').snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError) return new Text('Error: ${snapshot.error}');
switch (snapshot.connectionState) {
default:
return new ListView(
children:
snapshot.data.documents.map((DocumentSnapshot document) {
return Post(document: document);
}).toList());
}
});
}
I'm trying to make it so that if the snapshot.hasError, the StreamBuilder tries again. How can i do that?
Generally, you should always combine StreamBuilder with a stateful widget. Otherwise the stream would be recreated every time the build method is called.
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
Stream<QuerySnapshot> postsStream;
#override
void initState() {
super.initState();
postsStream = Firestore.instance.collection('posts').snapshots();
}
void retryLoad() {
setState(() {
postsStream = Firestore.instance.collection('posts').snapshots();
})
}
#override
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: postsStream,
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError) {
return RaisedButton(
child: Text('Retry'),
onPressed: retryLoad,
);
}
// ...
},
);
}
}