I want to do a login app. I have a class user, which has an id and a username that I want to keep to display it later in the app, and I have a user_api class, where I do the http request.
I wanted to use Singleton to store the user once the user logins in, but I find out that inherited widget was a better idea. So now I'm struggling with them because I can't store the user object. After I login, my user becomes null and I can't figure out how it works. Here's my code: basically we have a root page that manages the cases in which the user is logged in or not:
void main() {
runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Model(
user: User(),
child: MaterialApp(
routes: {
'/login': (context) => new LoginView(),
'/homepage_view': (context) => new HomepageView(),
},
title: 'Flutter login demo',
home: RootPage(),
),
);
}
}
In the rootPage:
enum UserStatus {
notDetermined,
notSignedIn,
signedIn,
}
class RootPage extends StatefulWidget {
#override
_RootPageState createState() => _RootPageState();
}
class _RootPageState extends State<RootPage> {
UserStatus userStatus = UserStatus.notDetermined;
#override
void didChangeDependencies() {
// TODO: implement didChangeDependencies
super.didChangeDependencies();
var user = Model.of(context).user;
setState(() {
userStatus = user.id == null? UserStatus.notSignedIn : UserStatus.signedIn;
print((userStatus));
});
}
void _signedIn() {
setState(() {
userStatus = UserStatus.signedIn;
});
}
void _signedOut() {
setState(() {
userStatus = UserStatus.notSignedIn;
});
}
#override
Widget build(BuildContext context) {
switch (userStatus) {
case UserStatus.notDetermined:
return _buildWaitingScreen();
case UserStatus.notSignedIn:
return LoginView(
onSignedIn: _signedIn,
);
case UserStatus.signedIn:
return HomepageView(
onSignedOut: _signedOut,
);
}
return Container(
child: Text(("CHILD")),
);
}
}
Widget _buildWaitingScreen() {
return Scaffold(
body: Container(
alignment: Alignment.center,
child: CircularProgressIndicator(),
),
);
}
the most important stuff of the login page::
var user = Model.of(context).user;
user = await getUserByIdClient()
if (user.loginError == false){
print (user);
widget.onSignedIn();
}
Here's my inherited widget:
class Model extends InheritedWidget {
Model({Key key, Widget child, this.user}) : super(key: key, child: child);
final User user;
#override
bool updateShouldNotify(InheritedWidget oldWidget) => true;
static Model of(BuildContext context) {
return (context.inheritFromWidgetOfExactType(Model) as Model);
}
}
From what I understand, it seems that you're looking in to storing user session in your app. One way of doing this is by storing user credentials in shared_preferences (i.e. userId). Depending on your use case, your backend might require users to re-authenticate, so keep an eye on that.
Store user credentials after login.
// Obtain shared preferences.
final prefs = await SharedPreferences.getInstance();
// Save user details on userId
String userId = ...;
await prefs.setString('userId', userId);
When has been signed out, you can remove the data.
await prefs.remove('userId');
For verifying user session, you can then check the stored value. If it's empty, logout the user.
final String? userId = prefs.getString('userId');
if(userId != null){
// User is logged-in
} else {
// User is signed-out
}
Related
There is very easy way to use Hive key-value database on StatefulWidgets, for example:
class HookDemo extends StatefulWidget {
#override
_HookDemoState createState() => _HookDemoState();
}
class _HookDemoState extends State<HookDemo> {
Box user;
#override
void initState() {
super.initState();
user = Hive.box<User>('user');
}
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
final _u = User()
..nameFamily = 'myname'
..mobileNumber = '123456789';
_user!.add(_u);
_u.save();
},
child: Icon(Icons.add),
),
...
);
}
}
here we defined Box user property and inside initState we implemented what's user such as user = Hive.box<User>('user');
after that we can use user without any problem and getting already opened error
now in this current application we used HookWidget and when we want to use Hive we get error as box already opened
main.dart:
Future<void> initHiveDriver() async {
final appDocumentDirectory = await path_provider.getApplicationDocumentsDirectory();
await Hive.initFlutter(appDocumentDirectory.path);
await Hive.openBox<UserAdapter>('user');
}
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
///...
initHiveDriver();
runApp(
ProviderScope(observers: [
Logger()
],
child: MyApp()),
);
}
how can i create a provider for Hive with Riverpod and use it into HookWidget?
I am using Hive with Riverpod like this.
I am using a named constructor so I can await the openBox call.
final hiveProvider = FutureProvider<HiveDB>((_) => HiveDB.create());
class HiveDB {
var _userBox;
HiveDB._create() {}
static Future<HiveDB> create() async {
final component = HiveDB._create();
await component._init();
return component;
}
_init() async {
Hive.registerAdapter(UserAdapter());
this._userBox = await Hive.openBox<User>('user');
}
storeUser(User user) {
this._userBox.put('user', user);
}
User getUser() {
return this._userBox.get('user');
}
}
Use in a ConsumerWidget:
class SomeWidget extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final provider = ref.watch(hiveProvider).data?.value;
...
}
}
I've integrated an Apple sign in for Flutter.
Everything is working fine until the point when I have to logout from the Stores() page.
The app landing page (Home) shows a series of buttons to login with different apps:
Google
Anonymous
Apple
Email & Password
All of them are able to logout by using a logout button, but not Apple.
Here is my code
main.dart
class Main extends StatelessWidget {
#override
Widget build(BuildContext context) {
return StreamProvider<User>.value(
value: AuthService().user,
child: MaterialApp(
home: Wrapper(),
routes: {
"/stores": (_) => Stores()
},
));
}
}
Wrapper.dart
class Wrapper extends StatelessWidget {
#override
Widget build(BuildContext context) {
final user = Provider.of<User>(context);
if (user == null) {
return Home(); <-- Landing page before login
} else {
return Stores(); <-- Landing page after login, where the logout button is
}
}
}
Home.dart
class _HomeState extends State<Home> {
final AuthService _auth = AuthService();
bool loading = false;
final welcomeText = 'Welcome';
final subtitle = 'Make grocery chores easier';
final anonymousButtonText = 'Skip';
#override
Widget build(BuildContext context) {
return Stack(children: [
AuthLayout(),
Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
title: Center(child: Text(welcomeText)),
backgroundColor: Colors.transparent,
),
body: SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget> [
// Sign In with Apple
Padding(
padding: EdgeInsets.all(8.0),
child: FutureBuilder<Object>(
future: _auth.appleSignInAvailable,
builder: (context, snapshot) {
if (snapshot.data == true) {
return AppleSignInButton(
onPressed: () async {
FirebaseUser user =
await _auth.appleSignIn();
if (user != null) {
Navigator.pushReplacementNamed(context, "/stores");
}
},
);
} else {
return Container();
}
})) ....
AuthService.dart
class AuthService {
final FirebaseAuth _auth = FirebaseAuth.instance;
final GoogleSignIn _googleSignIn = new GoogleSignIn();
// Create user object based on FirebaseUser
User _userFromFirebaseUser(FirebaseUser user) {
return user != null ? User(uid: user.uid) : null;
}
// Auth change user stream
Stream<User> get user {
return _auth.onAuthStateChanged.map(
_userFromFirebaseUser);
}
// SignIn with Google
Future signInGoogle() async {
GoogleSignInAccount googleSignInAccount = await _googleSignIn.signIn();
GoogleSignInAuthentication googleSignInAuthentication =
await googleSignInAccount.authentication;
AuthCredential credential = GoogleAuthProvider.getCredential(
idToken: googleSignInAuthentication.idToken,
accessToken: googleSignInAuthentication.accessToken);
try {
AuthResult result = (await _auth.signInWithCredential(credential));
FirebaseUser user = result.user;
return _userFromFirebaseUser(user);
} catch (e) {
print(e.toString());
return null;
}
}
// SignIn with Apple
// Determine if Apple Signin is available on device
Future<bool> get appleSignInAvailable => AppleSignIn.isAvailable();
Future appleSignIn() async {
try {
final AuthorizationResult appleResult =
await AppleSignIn.performRequests([
AppleIdRequest(requestedScopes: [Scope.email, Scope.fullName])
]);
if (appleResult.error != null) {
// handle error from Apple
}
final AuthCredential credential = OAuthProvider(providerId: 'apple.com')
.getCredential(
accessToken: String.fromCharCodes(
appleResult.credential.authorizationCode),
idToken:
String.fromCharCodes(appleResult.credential.identityToken));
AuthResult result = (await _auth.signInWithCredential(credential));
FirebaseUser user = result.user;
return user;
} catch (error) {
print(error);
return null;
}
}
// SignOut
Future signOut() async {
try {
return await _auth.signOut(); <-- Should I do something different here for Apple?
} catch (e) {
print(e.toString());
return null;
}
}
}
All the other apps logout correctly, but Apple doesnt. Should I do something different in the signout since it's using /routes?
Any help is much appreciated!!
Many thanks
Joe
await _firebaseAuth.signOut();
Use the above code for signout
and check firebase IOS configuration
Should be fine, make sure that you have followed the steps described here:
https://firebase.google.com/docs/auth/ios/apple
Which means your application needs to fulfill the following requirements to make apple sign in work properly:
Be a member of the Apple Developer Program
Enable Sign In with Apple for your app on the Certificates, Identifiers & Profiles page of Apple's developer site.
Enable Apple as a sign-in provider on firebase
i want to be able to call an empty variable from a class, assign a value to it and make it persistent, anything aside provider e.t.c would be help, i don't want to overhaul the entire app again to do some bloc, provider e.t.c
NB: all screens are stateful widgets
i have tried creating a class with an empty string and passing a value to it from another screen, but this doesn't seem to work
import 'package:cloud_firestore/cloud_firestore.dart';
import 'dart:async';
import 'package:firebase_auth/firebase_auth.dart';
class MethodA {
// id(user, context){
// var name =user.email;
// }
String identity;
MethodA({this.iD});
bool isLoggedIn() {
if (FirebaseAuth.instance.currentUser() != null) {
return true;
} else {
return false;
}
}
Future<void> addUserA( userinfo) async {
//this.iD=id;
Firestore.instance
.collection('user')
.document('furtherinfo').collection(identity).document('Personal Info')
.setData(userdoc)
.catchError((e) {
print(e);
});
}
each time i pass the argument to i.e foo='bar';
and i import that class in another screen, i.e screen 9, foo is automatically set to null, but i would want foo to be bar
I would suggest that you use the Provider since it is the easiest way for me to manage state throughout the app. Flutter starts with one component on top of the widget tree so i would place my provider here.
Example
void main() {runApp(MyApp());}
class MyApp extends StatelessWidget {
MyApp();
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
StreamProvider<FirebaseUser>.value(
stream: FirebaseAuth.instance.onAuthStateChanged, // Provider to manage user throughout the app.
),
],
child: MaterialApp(
title: 'My App',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primaryColor: Colors.green,
primarySwatch: Colors.green,
accentColor: Colors.yellow,
),
home: MainPage(),
),
);
}
}
Then in your class you can do the following
class MethodAService with ChangeNotifier {
String _identity = null;
FirebaseUser _user = null;
// constructor with the (new changes )
MethodAService(FirebaseUser user){
this._user = user;
}
get identity => _identity ;
setIdentity(String identity) {
_identity = identity ;
notifyListeners(); // required to notify the widgets of your change
}
}
Then when you want to use it anywhere in your app just do the following in the build method
#override
Widget build(BuildContext context) {
final user = Provider.of<FirebaseUser>(context); // to get the current user
final methodA = Provider.of<MethodAService>(context); // get your service with identity
// now you can set the string using
methodA.setIdentity('new identity');
// or just use it like this
if(methodA.identity.isNotEmpty()){
print(methodA.identity);
}else{
print('Identity is empty');
}
return ChangeNotifierProvider<MethodAService>(
builder: (context) => MethodAService(user), // Your provider to manage your object, sending the Firebase user in
child: loggedIn ? HomePage() : LoginPage(), );
}
References
Provider Package
Fireship 185 Provider
Great Youtube video explaining the code
Update for comment
For getting the user uid you can just do user.uid
Changed code above to fit the
I'm not sure put the whole app in a StreamProvider is the best choice. That means the app will be rebuilt on each stream value.
To make a Widget available on all screens, you need a TransitionBuilder in your MaterialApp.
To avoid the external dependency you can also use an InheritedWidget
signed_user.dart
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
class SignedUser extends InheritedWidget {
final FirebaseUser user;
SignedUser({#required this.user, #required Widget child})
: super(child: child);
#override
bool updateShouldNotify(SignedUser oldWidget) => true;
static SignedUser of(BuildContext context) =>
context.inheritFromWidgetOfExactType(SignedUser);
}
my_transition_builder.dart
class MyTransitionBuilder extends StatefulWidget {
final Widget child;
const MyTransitionBuilder({Key key, this.child}) : super(key: key);
#override
_MyTransitionBuilderState createState() => _MyTransitionBuilderState();
}
class _MyTransitionBuilderState extends State<MyTransitionBuilder> {
StreamBuilder<FirebaseUser> _builder;
#override
void initState() {
super.initState();
_builder = StreamBuilder<FirebaseUser>(
stream: FirebaseAuth.instance.onAuthStateChanged,
builder: (context, snapshot) {
return SignedUser(
child: widget.child,
user: snapshot.data,
);
});
}
#override
Widget build(BuildContext context) {
return _builder;
}
}
main.dart
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
// this will make your inherited widget available on all screens of your app
builder: (context, child) {
return MyTransitionBuilder(child: child);
},
routes: {
'/editAccount': (context) => new EditAccountPage(),
},
theme: ThemeData(
primarySwatch: Colors.green,
),
home: MyHomePage(),
);
}
}
usage in edit_account_page.dart
#override
Widget build(BuildContext context) {
var user = SignedUser.of(context).user;
return Scaffold(
body: FutureBuilder<DocumentSnapshot>(
future: Firestore.instance.document('users/${user.uid}').get(),
I've been trying without success to load different pages according to my Shared Preference settings.
Based on several posts found in stackoverflow, i end up with the following solution:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:testing/screens/login.dart';
import 'package:testing/screens/home.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
Widget page = Login();
Future getSharedPrefs() async {
String user = Preferences.local.getString('user');
if (user != null) {
print(user);
this.page = Home();
}
}
#override
void initState() {
super.initState();
this.getSharedPrefs();
}
#override
Widget build(BuildContext context) {
return MaterialApp(home: this.page);
}
}
class Preferences {
static SharedPreferences local;
/// Initializes the Shared Preferences and sets the info towards a global variable
static Future init() async {
local = await SharedPreferences.getInstance();
}
}
The variable user is not null because the print(user) returns a value as expected, but the login screen is always being opened.
Your problem is that your build method returns before your getSharedPrefs future is complete. The getSharedPrefs returns instantly as soon as it's called because it's async and you're treating it as a "Fire and Forget" by not awaiting. Seeing that you can't await in your initState function that makes sense.
This is where you want to use the FutureBuilder widget. Create a Future that returns a boolean (or enum if you want more states) and use a future builder as your home child to return the correct widget.
Create your future
Future<bool> showLoginPage() async {
var sharedPreferences = await SharedPreferences.getInstance();
// sharedPreferences.setString('user', 'hasuser');
String user = sharedPreferences.getString('user');
return user == null;
}
When user is null this will return true. Use this future in a Future builder to listen to the value changes and respond accordingly.
#override
Widget build(BuildContext context) {
return MaterialApp(home: FutureBuilder<bool>(
future: showLoginPage(),
builder: (buildContext, snapshot) {
if(snapshot.hasData) {
if(snapshot.data){
// Return your login here
return Container(color: Colors.blue);
}
// Return your home here
return Container(color: Colors.red);
} else {
// Return loading screen while reading preferences
return Center(child: CircularProgressIndicator());
}
},
));
}
I ran this code and it works fine. You should see a blue screen when login is required and a red screen when there's a user present. Uncomment the line in showLoginPage to test.
There is a much pretty way of doing this.
Assuming that you have some routes and a boolean SharedPreference key called initialized.
You need to use the WidgetsFlutterBinding.ensureInitialized() function before calling runApp() method.
void main() async {
var mapp;
var routes = <String, WidgetBuilder>{
'/initialize': (BuildContext context) => Initialize(),
'/register': (BuildContext context) => Register(),
'/home': (BuildContext context) => Home(),
};
print("Initializing.");
WidgetsFlutterBinding.ensureInitialized();
await SharedPreferencesClass.restore("initialized").then((value) {
if (value) {
mapp = MaterialApp(
debugShowCheckedModeBanner: false,
title: 'AppName',
theme: ThemeData(
primarySwatch: Colors.blue,
),
routes: routes,
home: Home(),
);
} else {
mapp = MaterialApp(
debugShowCheckedModeBanner: false,
title: 'AppName',
theme: ThemeData(
primarySwatch: Colors.blue,
),
routes: routes,
home: Initialize(),
);
}
});
print("Done.");
runApp(mapp);
}
The SharedPreference Class Code :
class SharedPreferencesClass {
static Future restore(String key) async {
final SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
return (sharedPrefs.get(key) ?? false);
}
static save(String key, dynamic value) async {
final SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
if (value is bool) {
sharedPrefs.setBool(key, value);
} else if (value is String) {
sharedPrefs.setString(key, value);
} else if (value is int) {
sharedPrefs.setInt(key, value);
} else if (value is double) {
sharedPrefs.setDouble(key, value);
} else if (value is List<String>) {
sharedPrefs.setStringList(key, value);
}
}
}
At the HompePage, am navigating to Settings page with;
Navigator.push(
context,
new MaterialPageRoute(
builder: (BuildContext context) => Settings()));
the Settings() page contains an int input to allow user specify the number of posts they want to see at the HomePage. When users input the number and form.save, the value is stored in SharedPreferences. But when the user go back to the HomePage, the initial number of post still there. I want the HomePagestate to refresh so that the number of post the user specify at the Settings Page will take effect immediately the form is saved.
Below is some snippets of my code;
This is the form _submit on Settings() page,
_submit() async {
final form = _formKey.currentState;
SharedPreferences prefs = await SharedPreferences.getInstance();
if (form.validate()) {
prefs.setInt('defaultField', newva);
form.save();
final mysb = SnackBar(
duration: Duration(seconds: 1),
content: new Text(
'Data Saved Successfully',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
backgroundColor: Colors.red,
);
_scaffoldKey.currentState?.showSnackBar(mysb);
myHomePageState.setState(() {
newSULength = newva;
});
print('Done for $newva');
}
}
This is my MyHomePage()
MyHomePageState myHomePageState = new MyHomePageState();
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
State<MyHomePage> createState() => new MyHomePageState();
static MyHomePageState of(BuildContext context){
final MyHomePageState navigator = context.ancestorStateOfType(const TypeMatcher<MyHomePageState>());
assert(() {
if(navigator == null) {
throw new FlutterError('Error occoured');
}
return true;
}());
return navigator;
}
}
class MyHomePageState extends State<MyHomePage> {
int newSULength = 0;
void initState() {
// TODO: implement initState
super.initState();
loadDF();
}
set newle(String value) => setState(() => _string = value);
loadDF() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
setState(() {
newSULength = (prefs.getInt('defaultField') ?? 5);
for (int i = 0; i < newSULength; i++) {
\\todos
}
});
print('Done');
}
}
You can use callbacks to indicate the HomePage that the Settings page changed some value in shared preference. Refer this