I'm new in Flutter development, i'm making an app with Firebase Auth, in where I'm using an Authentication Wrapper class that, if user is logged in, goes to Home Screen, else goes to SignIn Screen.
The problem is that, when I want to navigate to AuthWrapper, I get this error message in a red screen:
Error: Could not find the correct Provider<UserFirebaseModel> above this Builder Widget
This happens because you used a `BuildContext` that does not include the provider
of your choice. There are a few common scenarios:
- You added a new provider in your `main.dart` and performed a hot-reload.
To fix, perform a hot-restart.
- The provider you are trying to read is in a different route.
Providers are "scoped". So if you insert of provider inside a route, then
other routes will not be able to access that provider.
- You used a `BuildContext` that is an ancestor of the provider you are trying to read.
Make sure that Builder is under your MultiProvider/Provider<UserFirebaseModel>.
This usually happens when you are creating a provider and trying to read it immediately.
Here there are the most important classes of my code, where I think the problem is.
main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return StreamProvider.value(
value: Authentication().user,
initialData: UserFirebaseModel.initialData(),
child: MaterialApp(
title: 'AccessCity',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
fontFamily: 'Montserrat',
),
routes: getAppRoutes(),
initialRoute: '/',
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute(
builder: (BuildContext context) => HomeTempPage(),
);
},
),
);
}
}
authWrapper.dart
class AuthWrapper extends StatelessWidget {
#override
Widget build(BuildContext context) {
final user = Provider.of<UserFirebaseModel>(context);
print(user);
// ignore: unnecessary_null_comparison
if (user == null) {
return LoginPage();
} else {
return Home();
}
}
}
routes.dart
Map<String, WidgetBuilder> getAppRoutes() {
return <String, WidgetBuilder>{
// Home Temporal Page
'/': (BuildContext context) => HomeTempPage(),
// Components Pages
'generalBigButton': (BuildContext context) => GeneralBigButtonPage(),
'textEntryField': (BuildContext context) => TextEntryFieldPage(),
'secureTextEntryField': (BuildContext context) =>
SecureTextEntryFieldPage(),
'underlinedButton': (BuildContext context) => UnderlinedButtonPage(),
// Modules
'login': (BuildContext context) => LoginPage(),
'authWrapper': (BuildContext context) => AuthWrapper(),
};
}
homeTempPage.dart
const String _title = 'Home Temporal';
const String _goAccessCityButton = 'Ir a AccessCity';
const String _goAccessCityLabel = 'Ir a pantalla Login de la app';
class HomeTempPage extends StatefulWidget {
#override
_HomeTempPageState createState() => _HomeTempPageState();
}
class _HomeTempPageState extends State<HomeTempPage> {
final model = HomeTempModel();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_title),
backgroundColor: mainBlue,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
SizedBox(height: 10),
MaterialButton(
child: Text(_goAccessCityButton),
color: Colors.blue,
textColor: Colors.white,
onPressed: () {
model.navigateToStart(context);
},
),
Text(_goAccessCityLabel),
SizedBox(height: 30),
Expanded(
child: Container(
child: ListView(
children: model.getComponents(context),
),
),
),
],
),
),
);
}
}
In this last class, the line
navigateToStart() method
goes to route 'authWrapper'.
UserFirebaseModel
class UserFirebaseModel {
final String id;
final String email;
UserFirebaseModel(this.id, this.email);
factory UserFirebaseModel.initialData() {
return UserFirebaseModel('', '');
}
}
Authentication class
class Authentication {
final FirebaseAuth _firebaseAuth = FirebaseAuth.instance;
// Create user based on User (ex FirebaseUser)
UserFirebaseModel? _userFromFirebaseUser(User? user) {
final _mail;
if (user != null) {
_mail = user.email;
if (_mail != null) {
return UserFirebaseModel(user.uid, _mail);
}
} else {
return null;
}
}
// Auth change user stream
Stream<UserFirebaseModel?> get user {
return _firebaseAuth.authStateChanges().map(_userFromFirebaseUser);
}
// Sign in with email and password
Future<String?> signIn({
required String email,
required String password,
}) async {
try {
await _firebaseAuth.signInWithEmailAndPassword(
email: email,
password: password,
);
print("Signed in");
return "";
} on FirebaseAuthException catch (e) {
return e.message;
}
}
User? getUser() {
try {
return _firebaseAuth.currentUser;
} on FirebaseAuthException {
return null;
}
}
}
Thanks for your help, i need to solve it!
Related
When my user signs up I direct them to a page to inform them that they need to verify their email before continuing:
Here is my verification screen code:
class VerifyScreen extends StatefulWidget {
#override
_VerifyScreenState createState() => _VerifyScreenState();
}
class _VerifyScreenState extends State<VerifyScreen> {
final auth = FirebaseAuth.instance;
User user;
Timer timer;
#override
void initState() {
user = auth.currentUser;
user.sendEmailVerification();
timer = Timer.periodic(
Duration(seconds: 5),
(timer) {
checkEmailVerified();
},
);
super.initState();
}
#override
void dispose() {
timer.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
height: MediaQuery.of(context).size.height * 0.8,
width: MediaQuery.of(context).size.width * 0.8,
child: Text(
"An email has been sent to ${user.email} please verify before proceeding"),
),
),
);
}
Future<void> checkEmailVerified() async {
user = auth.currentUser;
await user.reload();
if (user.emailVerified) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => OurHomePage(),
),
);
timer.cancel();
}
}
}
Problem Statement: When I press the back arrow on my chrome browser:
I get returned to my homepage with the user signed in which I don't want. I would like my user to verify their email before being able to continue. Here's the drawer on my homepage after I press the back button without verifying the email:
I use provider to pass my user object around my app. Here is my main.dart:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(
MultiProvider(
providers: [
Provider(
create: (_) => FirebaseAuthService(),
),
StreamProvider<OurUser>(
create: (context) =>
context.read<FirebaseAuthService>().onAuthStateChanged),
],
child: MaterialApp(theme: OurTheme().buildTheme(), home: OurHomePage()),
),
);
}
I then user Consumer to consume that provider on my Homepage:
class OurHomePage extends StatefulWidget {
#override
_OurHomePageState createState() => _OurHomePageState();
}
class _OurHomePageState extends State<OurHomePage> {
#override
Widget build(BuildContext context) {
return Consumer<OurUser>(
builder: (_, user, __) {
return ChangeNotifierProvider<SignInViewModel>(
create: (_) => SignInViewModel(context.read),
builder: (_, child) {
return Scaffold(appBar: AppBar(title: Text("My Homepage")));
},
);
},
);
}
}
Can anyone help me resolve the issue I'm facing? Thanks in advance.
On homepage check if user is logged in and when is, check if he has verified email. If he has, let him in, otherwise show him some message.
On Flutter Web, the userChanges stream from a FirebaseAuth instance never emits the signed in user after a page reload. Instead, it only emits null. With the example below, the app gets stuck on the loading page.
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(App());
}
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/loading',
onGenerateRoute: (settings) {
switch (settings.name) {
case '/error':
return MaterialPageRoute(builder: (_) => ErrorScreen());
case '/loading':
return MaterialPageRoute(builder: (_) => LoadingScreen());
case '/signin':
return MaterialPageRoute(builder: (_) => SignInScreen());
case '/welcome':
return MaterialPageRoute(builder: (_) => WelcomeScreen());
default:
return MaterialPageRoute(
builder: (_) => Center(child: Text('Unknown route')));
}
},
);
}
}
class ErrorScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Center(
child: Text('An error occurred.'),
),
);
}
}
class LoadingScreen extends StatefulWidget {
#override
_LoadingScreenState createState() => _LoadingScreenState();
}
class _LoadingScreenState extends State<LoadingScreen> {
#override
void initState() {
init();
super.initState();
}
init() async {
try {
await Firebase.initializeApp();
if (kDebugMode) {
await FirebaseAuth.instance.useEmulator('http://localhost:1001');
}
final preferences = await SharedPreferences.getInstance();
bool signedIn = preferences.getBool('IS_SIGNED_IN') ?? false;
String landingPath = '/signin';
if (signedIn) {
landingPath = '/welcome';
// Wait for the userChanges to emit a non-null element.
await FirebaseAuth.instance
.userChanges()
.firstWhere((user) => user != null);
}
Navigator.of(context).pushReplacementNamed(landingPath);
} catch (error) {
print(error);
Navigator.of(context).pushReplacementNamed('/error');
}
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Center(
child: Text('Loading...'),
),
);
}
}
class SignInScreen extends StatelessWidget {
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
children: [
TextField(
controller: _emailController,
decoration: InputDecoration(labelText: 'Email address'),
),
TextField(
decoration: InputDecoration(labelText: 'Password'),
obscureText: true,
controller: _passwordController,
),
ElevatedButton(
onPressed: () => _submit(context),
child: Text('SIGN IN'),
),
],
),
),
);
}
void _submit(BuildContext context) async {
final email = _emailController.text;
final password = _passwordController.text;
FirebaseAuth.instance
.signInWithEmailAndPassword(email: email, password: password)
.then((credential) async {
final preferences = await SharedPreferences.getInstance();
await preferences.setBool('IS_SIGNED_IN', true);
Navigator.of(context).pushReplacementNamed('/welcome');
});
}
}
class WelcomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
children: [
Text('Welcome!'),
ElevatedButton(
onPressed: () => _signOut(context),
child: Text('SIGN OUT'),
)
],
),
),
);
}
void _signOut(BuildContext context) {
FirebaseAuth.instance.signOut().then((_) async {
final preferences = await SharedPreferences.getInstance();
await preferences.setBool('IS_SIGNED_IN', false);
Navigator.of(context).pushReplacementNamed('/signin');
});
}
}
If it helps, I'm using version 8.3.0 of the Firebase Javascript libraries in my index.html file.
Am I missing something here?
It turns out the code above is fine. The issue is the emulator. Commenting out these lines of code makes the app behave as expected:
if (kDebugMode) {
FirebaseAuth.instance.useEmulator('http://localhost:1001');
}
I've filed a bug report on FlutterFire's repo about this emulator issue.
my code:
void initState() {
super.initState();
FirebaseAuth.instance.currentUser().then((user) => Navigator.push(context, MaterialPageRoute(builder: (context) => HomeScreen())));
}
Here it is my initState it shows a login screen for a second and i dont want it..
Solutions are welcome.
Usually for the applications i build, i maintain a boolean variable called "auth" in the user object and change that to true once the user signs in.
And ideally there is a "Splash Screen" with this code.
if(user.auth)
{
//User Authenticated
Navigator.push(context, MaterialPageRoute(builder: (context) => HomeScreen())));
}else{
//User not Authenticated
Navigator.push(context, MaterialPageRoute(builder: (context) => LoginScreen())));
}
After authenticating, do not forget to store the user object or the auth variable in local storage / Shared Preferences so that the data is not lost once the user exits the app.
Code for saving user object :
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setString("current_user",json.encode(user.toJson()));
User class :
class User{
final String username;
final String email;
//is user signed in?
bool isAuth;
User({this.username, this.email,this.isAuth});
User.fromData(Map<String,dynamic> data)
: username = data['Username'],
email = data['email'],
isAuth = data['isAuth'] ?? false,
Map<String,dynamic> toJson() {
return {
"Username" : username,
"email" : email,
"isAuth" : isAuth ?? false,
};
}
Let me know if you have any further queries.
You can use Firebase authentication to check if the user is logged in or not. First create a splash screen:
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Meet Up',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: IntroScreen(),);
}
}
In the IntroScreen() StatefulWidget you can check if user is logged in or not and navigate to the specific page:
class IntroScreen extends StatefulWidget{
#override
_IntroScreenState createState() => _IntroScreenState();
}
class _IntroScreenState extends State<IntroScreen> {
#override
void initState() {
super.initState();
FirebaseAuth.instance.currentUser().then((res) {
print(res);
if (res != null) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => Home()),
);
}
else
{
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SignUp()),
);
}
});
}
#override
Widget build(BuildContext context) {
return new SplashScreen(
seconds: 5,
title: new Text('Welcome To Meet up!',
style: new TextStyle(
fontWeight: FontWeight.bold,
fontSize: 20.0
),),
image: Image.asset('assets/images/dart.png',fit:BoxFit.scaleDown),
backgroundColor: Colors.white,
styleTextUnderTheLoader: new TextStyle(),
photoSize: 100.0,
onClick: ()=>print("flutter"),
loaderColor: Colors.red
);
}
}
https://pub.dev/packages/splashscreen
Here, i am trying to implement Firebase login and signup system. i am trying to change screen base on user login or not.
Basically, i want to show feed screen when user is login and when user is not login i want to show login scree. if i do login in login screen it is working fine, so i did not added that code here. but issue come when i navigate from login screen to sign up scree and even if i successfully sign up it is not showing me feed screen. When i hot reload it show me feed screen.
Moreover, i also make sure that it is reaching where i am changing screen by print in console.
Note: i know i can using function to change between login screen and signup screen, so i don't need Navigator, which will again work for me. but i want to know why after navigating using navigator it is not working.
class DeleteWidget extends StatefulWidget {
#override
_DeleteWidgetState createState() => _DeleteWidgetState();
}
class _DeleteWidgetState extends State<DeleteWidget> {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: StreamBuilder<FirebaseUser>(
stream: FirebaseAuth.instance.onAuthStateChanged,
builder: (BuildContext context, snapshot) {
print(snapshot.hasData);
print(snapshot.connectionState);
if (ConnectionState.active == snapshot.connectionState) {
print("object 1");
if (snapshot.hasData) {
print("object 2");
return Feed();
} else {
print("object 3");
return LoginScreen();
}
} else {
return LoginScreen();
}
}),
);
}
}
class LoginScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
child: RaisedButton(
child: Text("login"),
onPressed: () async {
Navigator.push(
context, MaterialPageRoute(builder: (context) => SignUp()));
},
),
),
),
);
}
}
class SignUp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
child: RaisedButton(
child: Text("Sign up"),
onPressed: () async {
await FirebaseAuth.instance.signInAnonymously();
},
),
),
),
);
}
}
class Feed extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
child: RaisedButton(
child: Text("feed"),
onPressed: () async {
await FirebaseAuth.instance.signOut();
},
),
),
),
);
}
}
You can use the Provider Package to Listen if user is logged in and use a Wrapper to direct the user to the correct screen. If the user logs out at any stage, they will be automatically redirected to the Login Screen.
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
User _userFromFirebaseUser(FirebaseUser user) {
return user != null ? User(uid: user.uid) : null;
}
#override
Widget build(BuildContext context) {
return StreamProvider<User>.value(
value: FirebaseAuth.instance.onAuthStateChanged.map(_userFromFirebaseUser),
child: MaterialApp(
home: Wrapper(),
),
);
}
}
Wrapper
class Wrapper extends StatelessWidget {
#override
Widget build(BuildContext context) {
final user = Provider.of<User>(context);
if(user == null) {
return LoginScreen();
} else {
return Feed();
}
}
}
What is the correct way to handle this, I have done a lot of searching and most samples which use future builders use them to draw lists so maybe I should be avoiding them all together here.
I want to submit a login form, perform the network request and draw a progress bar while the login is happening, and if successful navigate to a home page. If unsuccessful it should just kill the progress bar and redraw the home page. That part seems to be working, unsure if I am using the Navigator correctly.
The login call returns a user and access token object. The Homepage needs to retrieve the access token which was written to the db by the successful login response. From what I can tell the navigation is happening too quickly and the retrieval of the access token appears to happen before the navigation to the home page.
class LoginPage extends StatefulWidget {
LoginPage({Key key, this.title}) : super(key: key);
final String title;
#override
_LoginPageState createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
bool _isValidForm = true;
Future<LoginResponse> _user;
void _submitLogin() {
setState(() {
if (_isValidForm) {
_user = login().then((_) => Navigator.push(context, MaterialPageRoute(builder: (context) => HomePage())));
}
});
}
Widget _buildLoginForm(AsyncSnapshot<LoginResponse> snapshot) {
if (snapshot.connectionState != ConnectionState.none && !snapshot.hasData) {
return new Center(child: new CircularProgressIndicator());
} else {
return SafeArea(
child: Center(
child: new ListView(
children: <Widget>[
//..more views
Padding(
padding: EdgeInsets.fromLTRB(16.0, 0.0, 16.0, 16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
//..email and password fields
FlatButton(
child: new Text(
'SIGN IN',
),
onPressed: _submitLogin),
]),
)
],
),
),
);
}
}
#override
Widget build(BuildContext context) {
return new FutureBuilder(
future: _user,
builder: (context, AsyncSnapshot<LoginResponse> snapshot) {
return new Scaffold(
backgroundColor: kMyGreen,
body: _buildLoginForm(snapshot),
);
},
);
}
Future<LoginResponse> login() async {
final response = await http.post(...);
if (response.statusCode == 200) {
var loginResponse = LoginResponse.fromJson(json.decode(response.body));
//Write the user details to local db
DBProvider.db.newUser(loginResponse.user);
//Write the tokens to local db
DBProvider.db.newToken(loginResponse.tokens);
return loginResponse;
} else {
throw Exception('Failed to login');
}
}
}
Database methods:
newUser(User newUser) async {
final db = await database;
//get the biggest id in the table
var table = await db.rawQuery("SELECT MAX(id)+1 as id FROM User");
int id = table.first["id"];
//insert to the table using the new id
var raw = await db.rawInsert(
"INSERT Into User (id,first_name,last_name)"
" VALUES (?,?,?)",
[id, newUser.firstName, newUser.lastName]);
return raw;
}
newToken(Tokens newTokens) async {
final db = await database;
//await db.rawDelete("DELETE FROM Token");
//get the biggest id in the table
var table = await db.rawQuery("SELECT MAX(id)+1 as id FROM Token");
int id = table.first["id"];
//insert to the table using the new id
var raw = await db.rawInsert(
"INSERT Into Token (id,access_token,refresh_token)"
" VALUES (?,?,?)",
[id, newTokens.accessToken, newTokens.refreshToken]);
return raw;
}
Future<Tokens> getToken() async {
final db = await database;
var res = await db.query("Token", limit: 1);
return res.isNotEmpty ? Tokens.fromJson(res.first) : null;
}
Home page
class HomePage extends StatefulWidget {
HomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage>{
#override
void initState() {
super.initState();
getHomePageStuff();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Home Page"),
),
body: Center(
child: RaisedButton(
onPressed: () {},
child: Text('Go back!'),
),
),
);
}
}
Future<HomePageStuffResponse> getHomePageStuff() async {
Tokens token = await DBProvider.db.getToken();
//Accessing the token here throws an NPE
var accessToken = token.accessToken;
debugPrint("token = " + accessToken);
final response = await http.get(..);
if (response.statusCode == 200) {
debugPrint("FETCH SUCCESS");
return stuff;
} else {
throw Exception('Failed to fetch home page stuff');
}
}
You can simply wrap Scaffold's body in FutureBuilder like this
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Home Page"),
),
body: FutureBuilder<HomePageStuffResponse>(
future: getHomePageStuff(),
builder: (context, snap) {
if(snap.hasError) {
return ErrorWidget('Error occurred while fetching data');
}
if(snap.hasData) {
return Center(
child: RaisedButton(
onPressed: () {},
child: Text('Go back!'),
),
);
}
}
),
);
}
}
Future<HomePageStuffResponse> getHomePageStuff() async {
Tokens token = await DBProvider.db.getToken();
//Accessing the token here throws an NPE
var accessToken = token.accessToken;
debugPrint("token = " + accessToken);
final response = await http.get(..);
if (response.statusCode == 200) {
debugPrint("FETCH SUCCESS");
return stuff;
} else {
throw Exception('Failed to fetch home page stuff');
}
}
Okay I was pretty close. Navigation is fine the way it is, the issue was the writing to the db was not being awaited on so that would happen simultaneously to the navigation (the newUser and newToken calls). As I would navigate to the home screen and try and read the access token the call would fail because it did not exist yet.
This was made a little harder to figure out because the debugger is a little strange in Android Studio for flutter so I just had to log everything to the console to see the issue.
If you read my question thank you for your time :)