How to handle Firebase Authchanges with fetching user model in Riverpod - flutter

Hello everyone I'am working on Firebase Auth with Riverpod.
I have a StreamProvider which it is listening FirebaseAuthChanges. This works fine.
And I have a UserProvider which it is modeling user from Firebase. (This works fine too but not on first start).
final myAccountProvider = StateNotifierProvider<UserNotifier, UserModel>(
(ref) {
return UserNotifier();
},
);
class UserNotifier extends StateNotifier<UserModel> {
UserNotifier()
: super(
UserModel(
uid: '',
name: '',
email: '',
bildirim: 0,
kullaniciAdi: '',
pphoto: '',
sehir: '',
isBaker: null,
isBakeryEmpty: null,
bakeryID: '',
bakerySetupTaskLevel: null,
),
) {
getUserFields();
debugPrint('USERNOTİFİER İNİTİALİZED');
}
final FirebaseAuth _firebaseAuth = FirebaseAuth.instance;
final FirebaseService _firebaseService = FirebaseService();
getUserFields() async {
UserModel model =
await _firebaseService.fetchMyDatas(_firebaseAuth.currentUser!.uid);
state = model;
}
}
Here where the problem has begun:
My MaterialApp's home property is returning AuthWrapper widget which it is controlling user auth.
class AuthWrapper extends ConsumerWidget {
const AuthWrapper({
Key? key,
}) : super(key: key);
#override
Widget build(BuildContext context, WidgetRef ref) {
final authState = ref.watch(authStateProvider);
final isBaker = ref.watch(myAccountProvider).isBaker;
return authState.when(
data: (user) {
if (user != null) {
if (isBaker == true) {
return const BakerRootView();
} else {
return const UserRootView();
}
} else {
return const LoginView();
}
},
error: (e, s) => Container(),
loading: () => const Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
),
);
}
}
Let's take a look for this widget. When the app start first the UserNotifier (so myAccountProvider) returning error that Null check op. used on null value
Where is that error ? On this line:
await _firebaseService.fetchMyDatas(_firebaseAuth.currentUser!.uid);
I know the problem what is. When the app initializing, without any firebase.currentUser, the UserNotifier can't return getUserField function.
Then I was think about defining UserNotifier after (user != null) condition like this:
if (user != null) {
final isBaker= ref.watch(myAccountProvider).isBaker;
if (isBaker== true) {
print('Baker Root Builded')
return const BakerRootView();
} else {
print('User Root Builded')
return const UserRootView();
}
} else {
return const LoginView();
}
At the first look this work's fine. But if I hot restart my app, in console, I see print('Baker Root Builded') and print('User Root Builded') together. Why is this so bad?
Because if the internet connection is not fast enough, UserRootView will be shown to eachuser.
Is there a better solution for modeling user with Authentication or not ? Any document, any article to you can show me ? Or How can I fix this issue ?

Related

ChangeNotifierProvider does not update the model

i am quite new with flutter. I am trying to add a ChangeNotifierProvider into my app. I use flutter_azure_b2c to log in a user, in order to handle to login outcome I have the following code:
AzureB2C.registerCallback(B2COperationSource.POLICY_TRIGGER_INTERACTIVE,
(result) async {
if (result.reason == B2COperationState.SUCCESS) {
List<String>? subjects = await AzureB2C.getSubjects();
if (subjects != null && subjects.isNotEmpty) {
B2CAccessToken? token = await AzureB2C.getAccessToken(subjects[0]);
if (!mounted || token == null) return;
final encodedPayload = token.token.split('.')[1];
final payloadData =
utf8.fuse(base64).decode(base64.normalize(encodedPayload));
final claims = Claims.fromJson(jsonDecode(payloadData));
var m = Provider.of<LoginModel>(context);
m.logIn(claims);
}
}
});
The problem is that when it arrives to var m = Provider.of<LoginModel>(context); the execution stops with out errors without executing m.logIn(claims);, so the model is not changed and the consumer is not called.
Any idea?
This is my consumer:
class App extends StatelessWidget {
const App({super.key});
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => LoginModel(),
child: MaterialApp(
debugShowCheckedModeBanner: false,
theme: appTheme,
home: Consumer<LoginModel>(
builder: (context, value, child) =>
value.claims != null ? const Home() : const Login(),
)),
);
}
}
class LoginModel extends ChangeNotifier {
Claims? _claims;
logIn(Claims claims) {
_claims = claims;
notifyListeners();
}
logOut() {
_claims = null;
notifyListeners();
}
Claims? get claims => _claims;
}
My LoginWidget:
class Login extends StatefulWidget {
const Login({super.key});
#override
LoginState createState() => LoginState();
}
class LoginState extends State<Login> {
B2CConfiguration? _configuration;
checkLogin(BuildContext context) async {
List<String>? subjects = await AzureB2C.getSubjects();
if (subjects != null && subjects.isNotEmpty) {
B2CAccessToken? token = await AzureB2C.getAccessToken(subjects[0]);
if (!mounted || token == null) return;
final encodedData = token.token.split('.')[1];
final data =
utf8.fuse(base64).decode(base64.normalize(encodedData));
final claims = Claims.fromJson(jsonDecode(data));
var m = Provider.of<LoginModel>(context, listen: true);
m.logIn(claims); //<-- debugger never reaches this line
}
}
#override
Widget build(BuildContext context) {
// It is possible to register callbacks in order to handle return values
// from asynchronous calls to the plugin
AzureB2C.registerCallback(B2COperationSource.INIT, (result) async {
if (result.reason == B2COperationState.SUCCESS) {
_configuration = await AzureB2C.getConfiguration();
if (!mounted) return;
await checkLogin(context);
}
});
AzureB2C.registerCallback(B2COperationSource.POLICY_TRIGGER_INTERACTIVE,
(result) async {
if (result.reason == B2COperationState.SUCCESS) {
if (!mounted) return;
await checkLogin(context);
}
});
// Important: Remeber to handle redirect states (if you want to support
// the web platform with redirect method) and init the AzureB2C plugin
// before the material app starts.
AzureB2C.handleRedirectFuture().then((_) => AzureB2C.init("auth_config"));
const String assetName = 'assets/images/logo.svg';
final Widget logo = SvgPicture.asset(
assetName,
);
return SafeArea(
child: //omitted,
);
}
}
I opened an issue as well, but it did not help me.
Try this
var m = Provider.of<LoginModel>(context, listen: false)._claims;
You are using the Provider syntax but not doing anything really with it. You need to set it like this Provider.of<LoginModel>(context, listen: false).login(claims) and call it like this Provider.of<LoginModel>(context, listen: false)._claims;
I fixed it, moving the callback registrations from the build method to the initState method.

How to sync stream variable in BlocState with the TextFormField

I am not able to figure out the right architecture of getting this done using firestore streams and flutter_bloc.
My goal :
I have a logged in user and I want to store the user details in the collection with the document id same as logged in user id and want to listen to changes, Throughout the program.
Meanwhile i would want to even store user details (if doesn't exist) and update user details.
My approach:
I am having a usermodel in UserState and i am listening to userModel via state using BlocBuilder.
Problem:
As i am using BlocBuilder my setState isn't working and TextFormField isn't working as it says beginBatchEdit on inactive InputConnection
Code:
UserCubit.dart
class UserCubit extends Cubit<UserState> {
final _firestore = FirebaseFirestore.instance;
final User? _currentUser = FirebaseAuth.instance.currentUser;
UserCubit() : super(UserInitialState()) {
emit(UserMainLoadingState());
_firestore --> listening to stream and updating state
.collection("sample")
.doc(_currentUser?.uid)
.snapshots()
.listen((event) {
event.exists
? emit(UserExists(sample: SampleModel.fromjson(event.data()!, event.id)))
: {emit(UserNotExists())};
});
}
Future<void> addSampleUser({required SampleModel sample}) async {
emit(UserSideLoadingState());
_firestore
.collection('sample')
.doc(_currentUser?.uid)
.set(sample.toJson())
.then((value) => emit(UserSavedUpdatedState()));
}
}
UserState.dart
abstract class UserState extends Equatable {
final SampleModel? sampleModel;
const UserState({this.sampleModel});
#override
List<Object?> get props => [sampleModel];
}
class UserExists extends UserState {
const UserExists({required SampleModel sample}) : super(sampleModel: sample);
}
Widget.dart (Save/Update User Details)
class _MyWidgetState extends State<MyWidget> {
// #override
var fullNameKey;
TextEditingController? fullNameController;
bool _formChecked = false;
TextEditingController? phoneNumberController;
Avatar? selectedAvatar;
#override
void initState() {
fullNameController = TextEditingController();
fullNameKey = GlobalKey<FormState>();
super.initState();
}
#override
Widget build(BuildContext context) {
return SafeArea(
child: BlocConsumer<UserCubit, UserState>(listener: (context, state) {
if (state is UserSavedUpdatedState) {
print("Saved/Updated User");
context.goNamed(Routes.profileMain);
}
}, builder: (context, state) {
// Here i am trying to get details from the state
selectedAvatar = state.sampleModel?.avatar == "boy"
? Avatar.boy
: state.sampleModel?.avatar == "boy"
? Avatar.girl
: null;
fullNameController!.text = state.sampleModel?.name ?? "";
if (state is UserMainLoadingState) {
return const Center(child: CircularProgressIndicator());
}
return Scaffold(
body: Column(
children: [
Row(
children: [
IconButton(
onPressed: () {
setState(() {
selectedAvatar = Avatar.girl; -- > This doesn't work because of BlocBuilder
});
},
icon: const Icon(Icons.girl)),
IconButton(
onPressed: () {
setState(() {
selectedAvatar = Avatar.boy; -- > This doesn't work because of BlocBuilder
});
},
icon: const Icon(Icons.boy))
],
),
Form(
key: fullNameKey,
child: TextFormField(
// BlocBuilder even freezes TextFormField
autovalidateMode: _formChecked
? AutovalidateMode.always
: AutovalidateMode.disabled,
validator: (value) {
if (value == null ||
fullNameController?.text.trim() == "") {
return "Name cannot be empty";
}
if (value.length < 3) {
return "Username must be greater than 3 characters";
}
return null;
},
controller: fullNameController,
decoration: const InputDecoration(
labelText: "Full Name",
),
)).marginDown(),
FilledButton(
onPressed: () {
setState(() {
_formChecked = true;
});
if (fullNameKey.currentState!.validate() &&
selectedAvatar != null) {
SampleModel sample = SampleModel(
name: fullNameController!.text,
avatar: selectedAvatar == Avatar.boy ? "boy" : "girl");
BlocProvider.of<UserCubit>(context)
.addSampleUser(sample: sample);
}
},
child: const Text("Submit"),
)
],
));
}),
);
}
}
As soon as the submit button is clicked it erases the entire text and validator gets activated. Avatar selection doesn't work as well.
What is the best way to achieve the desired function using streams, flutter_bloc, Suggestions would be greatly appreciated
as far as I can see you pre-select the avatar based on the emitted state. However, I do not see that you return the selection via an event/function to the bloc/cubit. So this is needed in order to send the updated avatar with the next emit.
From what I can see, I would also possibly exchange the abstract class state with a class state implementing Equatable and the simply always copyWith the state for any updates. This way you always have the same UserState - no need for if and else if for state selection, however, the data of the state changes based on the situation. I think for a user bloc/cubit this makes the lifecycle a bit easier
UPDATE:
IconButton(
onPressed: () {
setState(() {
context.read<UserCubit>.updateUser(selectedAvatar: Avatar.boy);
selectedAvatar = Avatar.boy; -- > possibly no longer needed if returned from Cubit
});
},
icon: const Icon(Icons.boy))
As for the state management, a state can look like this:
class TaskListState extends Equatable {
const TaskListState({
this.status = DataTransStatus.initial,
this.taskList = const [],
this.filter,
this.error,
this.editThisTaskId,
});
final DataTransStatus status;
final List<TaskListViewmodel> taskList;
final TaskListFilter? filter;
final String? error;
final String? editThisTaskId;
TaskListState copyWith({
DataTransStatus Function()? status,
List<TaskListViewmodel> Function()? taskList,
TaskListFilter Function()? filter,
String Function()? error,
String? Function()? editThisTaskId,
}) {
return TaskListState(
status: status != null ? status() : this.status,
taskList: taskList != null ? taskList() : this.taskList,
filter: filter != null ? filter() : this.filter,
error: error != null ? error() : this.error,
editThisTaskId: editThisTaskId != null
? editThisTaskId() : this.editThisTaskId,
);
}
#override
List<Object?> get props => [
status,
taskList,
filter,
error,
editThisTaskId,
];
}
which you use - in this case with a Stream - like this:
await emit.forEach<dynamic>(
_propertiesRepository.streamTasks(event.propertyId),
onData: (tasks) {
return state.copyWith(
status: () => DataTransStatus.success,
taskList: () => List.from(
tasks.map((t) => TaskListViewmodel.fromDomain(t))),
);
},
onError: (_, __) {
return state.copyWith(
status: () => DataTransStatus.failure,
);
},
);

Flutter Custom State Management

What I am trying to achieve is a small custom state management solution that I believe is powerful enough to run small and large apps. The core is based on the ValueNotifier and ValueListenable concepts in flutter. The data can be accessed anywhere in the app with out context since I am storing the data like this:
class UserData {
static ValueNotifier<DataLoader<User>> userData =
ValueNotifier(DataLoader<User>());
static Future<User> loadUserData() async {
await Future.delayed(const Duration(seconds: 3));
User user = User();
user.age = 23;
user.family = 'Naoushy';
user.name = 'Anass';
return user;
}
}
So by using UserData.userData you can use the data of the user whenever you want. Everything works fine until I encountered a problem of providing a child to my custom data consumer that rebuilds the widget when there is a new event fired. The DataLoader class looks like this:
enum Status { none, hasError, loading, loaded }
class DataLoader<T> {
Status status = Status.none;
T? data;
Object? error;
bool get hasError => error != null;
bool get hasData => data != null;
}
which is very simple. Now the class for consuming the data and rebuilding looks like this:
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:testing/utils/dataLoader/data_loader.dart';
class DataLoaderUI<T> extends StatefulWidget {
final ValueNotifier<DataLoader<T>> valueNotifier;
final Widget noneStatusUI;
final Widget hasErrorUI;
final Widget loadingUI;
final Widget child;
final Future<T> future;
const DataLoaderUI(
{Key? key,
required this.valueNotifier,
this.noneStatusUI = const Text('Data initialization has not started'),
this.hasErrorUI = const Center(child: Text('Unable to fetch data')),
this.loadingUI = const Center(
child: CircularProgressIndicator(),
),
required this.child,
required this.future})
: super(key: key);
#override
State<DataLoaderUI> createState() => _DataLoaderUIState();
}
class _DataLoaderUIState extends State<DataLoaderUI> {
Future startLoading() async {
widget.valueNotifier.value.status = Status.loading;
widget.valueNotifier.notifyListeners();
try {
var data = await widget.future;
widget.valueNotifier.value.data = data;
widget.valueNotifier.value.status = Status.loaded;
widget.valueNotifier.notifyListeners();
} catch (e) {
log('future error', error: e.toString());
widget.valueNotifier.value.error = e;
widget.valueNotifier.value.status = Status.hasError;
widget.valueNotifier.notifyListeners();
}
}
#override
void initState() {
super.initState();
log('init state launched');
if (!widget.valueNotifier.value.hasData) {
log('reloading or first loading');
startLoading();
}
}
//AsyncSnapshot asyncSnapshot;
#override
Widget build(BuildContext context) {
return ValueListenableBuilder<DataLoader>(
valueListenable: widget.valueNotifier,
builder: (context, dataLoader, ui) {
if (dataLoader.status == Status.none) {
return widget.noneStatusUI;
} else if (dataLoader.status == Status.hasError) {
return widget.hasErrorUI;
} else if (dataLoader.status == Status.loading) {
return widget.loadingUI;
} else {
return widget.child;
}
});
}
}
which is also simple yet very effective. since even if the initState function is relaunched if the data is already fetched the Future will not relaunch.
I am using the class like this:
class TabOne extends StatefulWidget {
static Tab tab = const Tab(
icon: Icon(Icons.upload),
);
const TabOne({Key? key}) : super(key: key);
#override
State<TabOne> createState() => _TabOneState();
}
class _TabOneState extends State<TabOne> {
#override
Widget build(BuildContext context) {
return DataLoaderUI<User>(
valueNotifier: UserData.userData,
future: UserData.loadUserData(),
child: Text(UserData.userData.value.data!.name??'No name'));
}
}
The error is in this line:
Text(UserData.userData.value.data!.name??'No name'));
Null check operator used on a null value
Since I am passing the Text widget as an argument with the data inside it. Flutter is trying to pass it but not able to since there is no data yet so its accessing null values. I tried with a normal string and it works perfectly. I looked at the FutureBuilder widget and they use a kind of builder and also the ValueLisnableBuilder has a builder as an arguement. The problem is that I am not capable of creating something like it for my custom solution. How can I just pass the child that I want without having such an error and without moving the ValueLisnable widget into my direct UI widget?
I have found the solution.
Modify the DataLoaderUI class to this:
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:testing/utils/dataLoader/data_loader.dart';
class DataLoaderUI<T> extends StatefulWidget {
final ValueNotifier<DataLoader<T>> valueNotifier;
final Widget noneStatusUI;
final Widget hasErrorUI;
final Widget loadingUI;
final Widget Function(T? snapshotData) child;
final Future<T> future;
const DataLoaderUI(
{Key? key,
required this.valueNotifier,
this.noneStatusUI = const Text('Data initialization has not started'),
this.hasErrorUI = const Center(child: Text('Unable to fetch data')),
this.loadingUI = const Center(
child: CircularProgressIndicator(),
),
required this.child,
required this.future})
: super(key: key);
#override
State<DataLoaderUI<T>> createState() => _DataLoaderUIState<T>();
}
class _DataLoaderUIState<T> extends State<DataLoaderUI<T>> {
Future startLoading() async {
widget.valueNotifier.value.status = Status.loading;
widget.valueNotifier.notifyListeners();
try {
var data = await widget.future;
widget.valueNotifier.value.data = data;
widget.valueNotifier.value.status = Status.loaded;
widget.valueNotifier.notifyListeners();
} catch (e) {
log('future error', error: e.toString());
widget.valueNotifier.value.error = e;
widget.valueNotifier.value.status = Status.hasError;
widget.valueNotifier.notifyListeners();
}
}
#override
void initState() {
super.initState();
log('init state launched');
if (!widget.valueNotifier.value.hasData) {
log('reloading or first loading');
startLoading();
}
}
//AsyncSnapshot asyncSnapshot;
#override
Widget build(BuildContext context) {
return ValueListenableBuilder<DataLoader<T>>(
valueListenable: widget.valueNotifier,
builder: (context, dataLoader, ui) {
if (dataLoader.status == Status.none) {
return widget.noneStatusUI;
} else if (dataLoader.status == Status.hasError) {
return widget.hasErrorUI;
} else if (dataLoader.status == Status.loading) {
return widget.loadingUI;
} else {
return widget.child(dataLoader.data);
}
});
}
}
and use it like this:
DataLoaderUI<User>(
valueNotifier: UserData.userData,
future: UserData.loadUserData(),
child: (user) {
return Text(user!.name ?? 'kk');
});
Take a look at my version of the same sort of state management approach here: https://github.com/lukehutch/flutter_reactive_widget

I keep getting getters called on null when I try to update the user profile

Hello I'm new to flutter, and I'm trying to do a simple user profile screen for a user who logged in but I stumbled in some errors, the first one was for when I tried to use the StreamBuilder() where the stream didn't get any data from the getter in the UserProvider()(that's where I putted my BL) it kept saying getCurrentUserData() was called on null, so i just connected it directly to the UserService() and it worked, but then when I tried to edit the user info and have the TextFormField() be filled with the user data, via the initState() and have the fullNameController get the data from the UserModel() the error returned it keeps saying fullName was called on null! how do I resolve this can anyone point on where I'm going wrong about here?
P.S I'm following this tutorial to build this.
My StreamBuilder() connected to UserProvider:
return StreamBuilder<UserModel>(
stream: userProviderData.getCurrentUserData(),
builder: (context, snapshot) {})
My StreamBuilder() directly connected to UserService:
Directly connected to UserService
return StreamBuilder<UserModel>(
stream: userService.getCurrentUser(),
builder: (context, snapshot) {})
UserService() class:
// Get User
Stream<UserModel> getCurrentUser() {
return _db.collection('users').doc(_auth.currentUser.uid).snapshots().map(
(user) => UserModel.fromJson(user.data()),
);
}
// Add or Update User info
Future<void> saveUser(UserModel user) {
final _options = SetOptions(merge: true);
return _db.collection('users').doc(user.userId).set(user.toMap(), _options);
}
UserProvider() class:
final userProvider = ChangeNotifierProvider<UserProvider>((ref) {
return;
});
class UserProvider with ChangeNotifier {
final userService = UserService();
String _userId;
String _fullName;
// Getters
String get userId => _userId;
String get fullName => _fullName;
Stream<UserModel> get getCurrentUserData => userService.getCurrentUser();
// Setters
set changeFullName(String fullName) {
_fullName = fullName;
notifyListeners();
}
// Functions
void loadUser(UserModel userModel) {
_userId = userModel.userId;
_fullName = userModel.fullName;
}
void updateUser() {
final _currentUser = UserModel(
userId: _userId,
fullName: _fullName,
);
userService.saveUser(_currentUser);
}
}
EditProfileScreen():
class EditProfileScreen extends StatefulWidget {
const EditProfileScreen({this.userModel});
final UserModel userModel;
#override
_EditProfileScreenState createState() => _EditProfileScreenState();
}
class _EditProfileScreenState extends State<EditProfileScreen> {
final _fullNameController = TextEditingController();
final _validator = Validator();
#override
void dispose() {
_fullNameController.dispose();
super.dispose();
}
#override
void initState() {
super.initState();
final userStream = context.read(userProvider);
if (widget.userModel != null) {
// Edit
_fullNameController.text = widget.userModel.fullName;
userStream.loadUser(widget.userModel);
}
}
#override
Widget build(BuildContext context) {
final userData = context.read(userProvider);
return Scaffold(
body: Column(
children: [
TextFormFiled(
hintText: ‘Full name’,
keyboardType: TextInputType.name,
controller: _fullNameController,
validator: (value) => _validator.validateFullName(
value,
),
onChanged: (value) {
userData.changeFullName = value;
debugPrint(value);
}
),
ElevatedButton(
onPressed: () {
userData.updateUser();
Navigator.of(context).pop();
},
child: const Text(‘Save’),
),
],
),
);
}
}
Did you forget to return something?
final userProvider = ChangeNotifierProvider<UserProvider>((ref) {
return; //return a UserProvider()
});

Future Provider Stuck In loading state

I am using a future provider to display a login page on load and then a loading indicator on loading. Here is my future provider
final loginProvider = FutureProvider.family((ref, UserInput input) =>
ref.read(authRepositoryProvider).doLogin(input.email, input.password));
In my UI I have this....
class LoginScreen extends HookWidget {
final TextEditingController emailEditingController = TextEditingController();
final TextEditingController passwordEditingController =
TextEditingController();
#override
Widget build(BuildContext context) {
var userInput =
UserInput(emailEditingController.text, passwordEditingController.text);
final login = useProvider(loginProvider(userInput));
return login.when(
data: (user) => Login(emailEditingController, passwordEditingController),
loading: () => const ProgressIndication(),
error: (error, stack) {
if (error is DioError) {
return Login(emailEditingController, passwordEditingController);
} else {
return Login(emailEditingController, passwordEditingController);
}
},
);
}
}
here is my doLogin function.
#override
Future<dynamic> doLogin(String email, String password) async {
try {
final response = await _read(dioProvider)
.post('$baseUrl/login', data: {'email': email, 'password': password});
final data = Map<String, dynamic>.from(response.data);
return data;
} on DioError catch (e) {
return BadRequestException(e.error);
} on SocketException {
return 'No Internet Connection';
}
}
I would like to know why it's stuck in the loading state. Any help will be appreciated.
First off, family creates a new instance of the provider when given input. So in your implementation, any time your text fields change, you're generating a new provider and watching that new provider. This is bad.
In your case, keeping the UserInput around for the sake of accessing the login state doesn't make a lot of sense. That is to say, in this instance, a FamilyProvider isn't ideal.
The following is an example of how you could choose to write it. This is not the only way you could write it. It is probably easier to grasp than streaming without an API like Firebase that handles most of that for you.
First, a StateNotifierProvider:
enum LoginState { loggedOut, loading, loggedIn, error }
class LoginStateNotifier extends StateNotifier<LoginState> {
LoginStateNotifier(this._read) : super(LoginState.loggedOut);
final Reader _read;
late final Map<String, dynamic> _user;
static final provider =
StateNotifierProvider<LoginStateNotifier, LoginState>((ref) => LoginStateNotifier(ref.read));
Future<void> login(String email, String password) async {
state = LoginState.loading;
try {
_user = await _read(authRepositoryProvider).doLogin(email, password);
state = LoginState.loggedIn;
} catch (e) {
state = LoginState.error;
}
}
Map<String, dynamic> get user => _user;
}
This allows us to have manual control over the state of the login process. It's not the most elegant, but practically, it works.
Next, a login screen. This is as barebones as they get. Ignore the error parameter for now - it will be cleared up in a moment.
class LoginScreen extends HookWidget {
const LoginScreen({Key? key, this.error = false}) : super(key: key);
final bool error;
#override
Widget build(BuildContext context) {
final emailController = useTextEditingController();
final passwordController = useTextEditingController();
return Column(
children: [
TextField(
controller: emailController,
),
TextField(
controller: passwordController,
),
ElevatedButton(
onPressed: () async {
await context.read(LoginStateNotifier.provider.notifier).login(
emailController.text,
passwordController.text,
);
},
child: Text('Login'),
),
if (error) Text('Error signing in'),
],
);
}
}
You'll notice we can use the useTextEditingController hook which will handle disposing of those, as well. You can also see the call to login through the StateNotifier.
Last but not least, we need to do something with our fancy new state.
class AuthPage extends HookWidget {
const AuthPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final loginState = useProvider(LoginStateNotifier.provider);
switch (loginState) {
case LoginState.loggedOut:
return LoginScreen();
case LoginState.loading:
return LoadingPage();
case LoginState.loggedIn:
return HomePage();
case LoginState.error:
return LoginScreen(error: true);
}
}
}
In practice, you're going to want to wrap this in another widget with a Scaffold.
I know this isn't exactly what you asked, but thought it might be helpful to see another approach to the problem.