I am struggeling for the long time with handling correctly reauthentication of user in conjunction with storing the data in Provider.
During the first execution of the app on the device, the user is unauthenticated. Then user can register/login and re-build of the class below occure. Unfortunately, even throu the re-build occur, also when the document in Firestore changes, the change does not reflect in the Provider object or is reflected, but only when user does full reload of the app (depending on the scenario).
Here is my code:
class LandingFlowWidget extends StatefulWidget {
const LandingFlowWidget({Key? key}) : super(key: key);
#override
State<LandingFlowWidget> createState() => _LandingFlowWidgetState();
}
class _LandingFlowWidgetState extends State<LandingFlowWidget> {
late UserData? _userData;
#override
void initState() {
super.initState();
_userData = UserData();
}
#override
Widget build(BuildContext context) {
return StreamBuilder<User?>(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return ProgressIndicatorWidget(color: Color(0xFF3030D0));
} else if (snapshot.hasError) {
return ErrorScreen();
} else if (snapshot.hasData &&
(FirebaseAuth.instance.currentUser != null &&
FirebaseAuth.instance.currentUser!.isAnonymous == false))
return VerifyEmailScreen();
else {
if (FirebaseAuth.instance.currentUser == null)
return OnboardingScreen();
return ChangeNotifierProvider<UserData?>(
create: (context) => _userData,
builder: (context, _) {
return StreamBuilder<UserData>(
stream: FirebaseFirestore.instance
.collection('users')
.doc(FirebaseAuth.instance.currentUser?.uid)
.snapshots()
.map((snap) => UserData.fromJson(snap.data()!)),
builder: (BuildContext context,
AsyncSnapshot<UserData> snapshot) {
if (snapshot.hasError) {
return ErrorScreen();
} else if (snapshot.connectionState ==
ConnectionState.waiting) {
return ProgressIndicatorWidget(
color: Color(0xFF3030D0));
} else {
_userData = snapshot.data;
_userData?.updateState();
return OnboardingScreen();
}
});
});
}
});
}
}
I experimented with different approaches:
Changing Provider to ChangeNotifierProvider
StreamProvider insted of Provider + StreamBuilder in the function below
StreamProvider in the MultiProvider in main.dart with empty Stream or correct stream and adding new stream to StreamController when re-authentication occure.
I tried to look on the internet and did not find working solution of Provider + Change of Authentication. I'd appreciate some code snippets.
I found a very ugly workaround.
In main.dart, I created MultiProvider that contains StreamProvider:
MultiProvider(
providers: [
(...)
StreamProvider<UserData>.value(
value: FirebaseAuth.instance.currentUser == null
? Stream.empty()
: FirebaseClient.userStream,
initialData: UserData(),
),
(...)
],
(...)
The stream:
static Stream<UserData> userStream = FirebaseFirestore.instance
.collection('users')
.doc(FirebaseAuth.instance.currentUser?.uid)
.snapshots()
.map((snap) => UserData.fromJson(snap.data()!));
As mentioned in my initial question, this code does not work when re-authentication occurs but starts working again when the user does the full reload of the app. Having said that, when re-authentication occurs, I leverage Phoenix package to reload the entire app. Then the stream builds again with the correct user uid, and everything works as expected.
I would still appreciate it if someone could suggest a more elegant solution.
Related
So I'm currently using this nest of two streams, one to listen for AuthStateChanges, to know if the user is logged in, and another that listens to a firebase document snapshot request, to know if the user has already setup is account or not.
My problem is that the latter StreamBuilder(_userStream) only runs if the firts one runs, meaning that the only way for my _userStream to run is if the user either logs in or logs out(authStateChanges Stream).
This is inconvinient because after the user creates an account(moment where i run Auth().createUserWithPasswordAndEmail()), I need the user to go throw the process of seting up the account, and only after that the user can acess the mainPage. Only in the end of seting up the account theres a button to "Create Account", which changes the "HasSetupAccount" parameter in firebase to true. But because of the nested Streams problem, the app doesn't go to the mainPage until I force update it.
I hope my question is not as confusing as it looks :)
class _WidgetTreeState extends State<WidgetTree> {
#override
//construtor da class?
Widget build(BuildContext context) {
return StreamBuilder(
stream: Auth().authStateChanges,
builder: (context, snapshot) {
if (snapshot.hasData) {
return StreamBuilder(
stream: _userStream(),
builder:
((context, AsyncSnapshot<DocumentSnapshot> userSnapshot) {
if (userSnapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
} else {
Map<String, dynamic> userData =
userSnapshot.data!.data() as Map<String, dynamic>;
print(userSnapshot.data!.data().toString());
if (userData['HasSetupAccount'] == true) {
return MyHomePage();
} else {
return AccountSetup();
}
}
}));
} else {
return LoginPage();
}
},
);
}
Stream<DocumentSnapshot<Map<String, dynamic>>> _userStream() {
return FirebaseFirestore.instance
.collection('Users')
.doc(Auth().currentUser!.uid)
.snapshots();
}
}
Well, I want to check if the profile is complete after creating the account so I added a bool to the firestore. When the user fills in all the data and clicks "complete" at the end, then bool "complete" will be true and I did it, but now I want to check before the user starts filling in the data if bool is true or false. If this is true, the user will be redirected to the dashboard, if it is false, he will have to complete all the data after logging in. User login details are stored in firebase and the rest of the information is stored in firestore.
If any more information is needed, I will try to specify it
I would like to check if the value is true or false before redirecting to "CreateProfile1 ();", if it's possible
class MainPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: StreamBuilder<User?>(
stream: FirebaseAuth.instance.authStateChanges(),
builder: ((context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(
child: CircularProgressIndicator(),
);
} else if (snapshot.hasError) {
return Center(child: Text('Something went wrong!'));
} else if (snapshot.hasData) {
return CreateProfile1();
} else {
return AuthPage();
}
}),
));
}
}
I was trying to save bool value into variable, but i've got this error
external static Never _throw(Object error, StackTrace stackTrace);
Here is this var, final actually
final complete = FirebaseFirestore.instance
.collection('usersdData')
.doc(FirebaseAuth.instance.currentUser!.uid)
.get()
.then((value) {
if ((value.data() as dynamic)['complete'] == true) {
return true;
} else {
return false;
}
});
i have this problem, when try to get user from firebase auth using streambuilder, and then get the user data from firestore depending on the user id, always this:
userDoc.data()
return a null?
this is the code :
StreamBuilder<User?>(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (context, authSnapshot) {
// If the snapshot has user data, then they're already signed in. So Navigating to the Dashboard.
if (authSnapshot.hasData && authSnapshot.data != null) {
//return const TeacherDashboard();
return StreamBuilder<DocumentSnapshot>(
stream: FirebaseFirestore.instance
.collection("users")
.doc(authSnapshot.data?.uid)
.snapshots(),
builder: (context,
AsyncSnapshot<DocumentSnapshot> userSnapshot) {
if (userSnapshot.hasData && userSnapshot.data != null) {
final userDoc = userSnapshot.data;
print(userDoc!.get('isTeacher'));
final user = (userDoc != null
? userDoc.data()
: {"isTeacher": 0}) as Map<String, dynamic>;
if (user['isTeacher'] == 1) {
return const TeacherDashboard();
} else {
return const StudentsScreen();
}
} else {
return const Center(
child: CircularProgressIndicator(),
);
}
});
I assume You want to know the user is a teacher or a student. if teacher, go to teacher page, if student go to student page. and you are using a value to detect the user is a teacher or student. the value is 1.
so, if user value is == 1 go to teacher page. or go to student page.
if you want this function only you do not need to create a streambuilder here. you just need to get the user value. That you can achieve like this:
// Here I created one HomePage to decide which Screen to visit.
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
#override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
int? _value;
#override
void initState() {
super.initState();
getUserValue();
}
void getUserValue() async {
DocumentSnapshot snap = await FirebaseFirestore.instance
.collection('users')
.doc(FirebaseAuth.instance.currentUser!.uid)
.get();
setState(() {
_value = (snap.data() as Map<String, dynamic>)['isTeacher'];
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: _value == null
? const Center(
child: CircularProgressIndicator(),
)
: (_value == 1)
? const TeacherDashboard()
: const StudentsScreen(),
);
}
}
sidenote: I think you getting the error because You using Stateless widget. It's very important to use a Stateful widget and initially keep the value null. and if value is null show something like CircularProgressIndicator(). once value is available go to different Screen. in Stateless widget once the widget is built already it will get the value but will not rebuilt anything. so null value will decide your widget what gives you the error. and You must setState() Once you get the value.
Hope this will solve your problem.
So, I am using a stream to track the user's authentication state. Here is my setup, which works fine so far.
class Root extends ConsumerWidget {
final Widget _loadingView = Container(color: Colors.white, alignment: Alignment.center, child: UiHelper.circularProgress);
#override
Widget build(BuildContext context, ScopedReader watch) {
return watch(userStreamProvider).when(
loading: () => _loadingView,
error: (error, stackTrace) => _loadingView,
data: (user) => user?.emailVerified == true ? Products() : Login(),
);
}
}
The problem is, stream builds the UI multiple times. And I have a welcome dialog inside of my products page, which opens multiple times and as soon as I start the app it becomes a mess.
What should I do to avoid this scenario?
** Here I am using riverpod package
I personally recommend wrapping your widget with a StreamBuilder using the onAuthStateChanged stream. This stream automatically updates when the user change its state (logged in or out). Here is an example that may help you!
Stream<FirebaseUser> authStateChanges() {
FirebaseAuth _firebaseInstance = FirebaseAuth.instance;
return _firebaseInstance.onAuthStateChanged;
}
return StreamBuilder(
stream: authStateChanges(),
builder: (context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
// isLoggedIn
} else if (snapshot.hasData == false &&
snapshot.connectionState == ConnectionState.active) {
// isLoggedOut
} else {
// loadingView
}
},
);
My requirement is to make that StreamBuilder connection state to waiting.
I'm using publish subject, whenever I want to load data in stream builder I'm just adding data to the sink by calling postStudentsToAssign() method, here this method making an API call which takes some time, in that time I to want make that streamBuilder connection state to waiting
Stream Builder:
StreamBuilder(
stream: studentsBloc.studentsToAssign,
// initialData: [],
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
// While waiting for the data to load, show a loading spinner.
return getLoader();
default:
if (snapshot.hasError)
return Center(child: Text('Error: ${snapshot.error}'));
else
return _getDrawer(snapshot.data);
}
}),
Initializing Observable:
final _assignStudentSetter = PublishSubject<dynamic>();
Observable<List<AssignMilestoneModel>> get studentsToAssign =>
_studentsToAssignFetcher.stream;
Method that add's data to Stream:
postStudentsToAssign(int studyingClass, String milestoneId, String subject,
List studentList) async {
var response = await provider.postAssignedStudents(
studyingClass, milestoneId, subject, studentList);
_assignStudentSetter.sink.add(response);
}
You can send null to the stream, so the snapshot.connectionState changes to active. I don't know why and whether it's official solution, but it works (at least now). I found this accidentally.
I would like the Flutter team to explain how to set snapshot's connectionState. It's not clear from StreamBuilder documentation. It seems you should replace the stream with a new one to have snapshot in waiting state. But it's agains the logic you want to implement.
I checked StreamBuilder source to find out that the AsyncSnapshot.connectionState starts as waiting (after stream is connected), after receiving data changes to active. snapshot.hasData returns true if snapshot.data != null. That's how following code works.
class SearchScreen extends StatelessWidget {
final StreamController<SearchResult> _searchStreamController = StreamController<SearchResult>();
final SearchService _service = SearchService();
void _doSearch(String text) async {
if (text?.isNotEmpty ?? false) {
_searchStreamController.add(null);
_searchService.search(text)
.then((SearchResult result) => _searchStreamController.add(result))
.catchError((e) => _searchStreamController.addError(e));
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(children: <Widget>[
SearchBar(
onChanged: (text) => _doSearch(text),
),
StreamBuilder<SearchResult>(
stream: _searchStreamController.stream,
builder: (BuildContext context, AsyncSnapshot<SearchResult> snapshot) {
Widget widget;
if (snapshot.hasData) {
widget = Expanded(
// show search result
);
}
else if (snapshot.hasError) {
widget = Expanded(
// show error
);
}
else if(snapshot.connectionState == ConnectionState.active){
widget = Expanded(
// show loading
);
}
else {
// empty
widget = Container();
}
return widget;
},
),
]),
);
}
}