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
}
},
);
Related
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.
I'm trying to wrap my head around external data handling in Flutter application.
The idea is following:
An invitation comes as deep link of form https://host/join?id=<id>
Router handles such deep link by opening invites page and calling acceptExternalInvitation():
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
final notificationBloc = NotificationCubit();
final dataSharingBloc = IncomingDataBloc(notificationBloc);
return MultiBlocProvider(
...
child: MaterialApp(
...
home: SafeArea(child: _MyHomePage()),
onGenerateRoute: (settings) => _getRoute(dataSharingBloc, settings)));
}
Route? _getRoute(IncomingDataBloc incomingDataBloc, RouteSettings settings) {
if (settings.name == null) {
return null;
}
final route = settings.name!;
if (route.startsWith('/join')) {
final match = RegExp(r"/join\?id=(.+)$").firstMatch(route);
if (match != null) {
incomingDataBloc.acceptExternalInvitation(match.group(1)!);
return MaterialPageRoute(
builder: (Navcontext) => InvitesPage(), settings: settings);
}
}
return null;
}
}
class _MyHomePage extends StatelessWidget {
#override
Widget build(BuildContext context) =>
BlocConsumer<NotificationCubit, NotificationState>(
listener: (context, state) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(state.message)));
},
builder: (context, state) => BlocBuilder<IncomingDataBloc, IncomingDataState>(
builder: (context, state) => Scaffold(
appBar: AppBar(),
drawer: _getDrawer(context),
body: OverviewPage())));
...
}
IncomingDataBloc is responsible for handling all events that come from outside of the application such as deep links here. IncomingDataBloc.acceptExternalInvitation is defined as following:
void acceptExternalInvitation(String invitationEncodedString) {
final invitation = MemberInvitation.fromBase64(invitationEncodedString);
emit(NewAcceptedInviteState(invitation));
_notificationCubit.notify("Got invitation from ${invitation.from}");
}
Main point here is emitting the NewAcceptedInviteState(invitation), which should be handled by the invitations page.
Invitations page in general contains list of invitations and allows to accept or reject them. Invitations coming from deep links should trigger acceptance action:
class InvitesPage extends StatelessWidget {
final MemberInvitation? invitation;
InvitesPage({this.invitation});
#override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: Text("Invitations")),
body: BlocListener<IncomingDataBloc, IncomingDataState>(
listener: (context, state) async {
Logger.root.finest("--------Accepting invitation");
await _acceptInvite(context, (state as NewAcceptedInviteState).invitation);
Navigator.pop(context);
},
listenWhen: (previous, current) => current is NewAcceptedInviteState,
child: ...
)
);
The problem I have is with the listener in InvitesPage. When I send a link to the application using adb shell am start -a android.intent.action.VIEW -c android.intent.category.BROWSABLE -d "https://host/join?id=MY_ID" my.app the handling of the link is performed without problems, the notification from p.3 is shown and the InvitesPage is opened. However the code of listener is not executed.
If however I set a break point in listener code it's hit and code is executed.
How can I fix that? Can it be caused by using BlocListener<IncomingDataBloc, IncomingDataState> in home page, which is higher in widgets hierarchy?
And may be more generic question - is such approach of handling external event right one?
Ok, I've figured that out.
Indeed the problem was consuming states in two places. So instead of calling a Bloc method in route, which in my opinion wasn't conceptually correct I just open an invites pages with an invitation provided and let all invitation related logic remain contained in invites page, bloc and state:
Route? _getRoute(RouteSettings settings) {
if (settings.name == null) {
return null;
}
final route = settings.name!;
if (route == '/settings') {
return MaterialPageRoute(builder: (context) => SettingsPage());
}
if (route.startsWith('/join')) {
final match = RegExp(r"/join\?invitation=(.+)$").firstMatch(route);
if (match != null) {
return MaterialPageRoute(
builder: (Navcontext) =>
InvitesPage(invitation: MemberInvitation.fromBase64(match.group(1)!)),
settings: settings);
}
}
return null;
}
}
only thing I don't really like is handling invitation upon InvitesPage creation. For that I had to make it a stateful widget and implement initState():
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (invitation != null) {
context.read<InviteListCubit>().acceptExternalInvitation(invitation!);
}
});
}
But I think it makes sense as state of the page can change with new invitation coming from outside.
My application have different routes and I would like to know how to call my api with cubit just once when the user come for the first time on the screen and also not to re-call the api every time he returns to the screen already initialized.
my structure use bloC
and this is my profile page initialization class
#override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final user = context.read<AuthCubit>().state;
final bloc = context.read<ProfileCubit>();
return Scaffold(
body: FutureBuilder(
future: bloc.updateProfilePicture(user!.id),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return BlocBuilder<ProfileCubit, ProfilePicture?>(
buildWhen: (prev, curr) => prev != curr,
builder: (context, picture) {
return picture != null
? Profil(profilePicture: picture, updateIndex: updateIndex)
: Profil(updateIndex: updateIndex);
},
);
}
return Center(
child: CircularProgressIndicator(
color: Colors.orange,
),
);
},
),
);
}
There are many ways to solve this problem
1- easy (but not clean code) is to use boolean global varibal
like isApiReqursted with default value (false) and when call the api set it to true
2- you can cache the response in the repoistory or bloc and make the api method frst check if there are data if there isit does not need to make http request
I'm trying to write a simple Flutter app using the Google Maps plugin. I need to use multiple BLoC/ChangeNotifier objects in order to manage the object shown on-screen.
The issue comes out when I call notifyListeners() on a ChangeNotifier. The method which calls notifyListeners() completes its execution, and then the app freezes completely (no widget update, unable to interact with existing widgets).
I've tried to understand where's the problem: the only thing I understood is that it works fine while CompaniesData (which is the ChangeNotifier that causes the problem) is empty.
class CompaniesData extends ChangeNotifier {
Map<MarkerId, Company> _companiesMap;
set companies(Set<Company> companies) {
_companiesMap = companies != null
? Map.fromIterable(
companies,
key: (company) => MarkerId(company.id.toString()),
value: (company) => company,
)
: null;
notifyListeners();
;
}
bool get available => _companiesMap != null;
Company companyWithId(MarkerId id) => available ? _companiesMap[id] : null;
Map<MarkerId, Company> get companiesIfAvailable =>
available ? _companiesMap : Map();
Iterable<Company> companiesFromIds(BuildContext context, Set<int> ids) {
Set<int> idsCopy = Set.from(ids);
return companiesIfAvailable.entries
.where((entry) => idsCopy.remove(entry.value.id))
.map<Company>((entry) => entry.value);
}
}
#override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
Consumer<CompaniesData>(
builder: (context, data, child) {
return BlocBuilder(
bloc: BlocProvider.of<ShownCompaniesBloc>(context),
builder: (context, shownCompaniesState) {
return BlocBuilder(
bloc: BlocProvider.of<FavoriteCompaniesBloc>(context),
builder: (context, favoriteCompaniesState) {
return BlocBuilder(
bloc: BlocProvider.of<MapPropertiesBloc>(context),
builder: (context, mapPropertiesState) {
CompaniesData data =
Provider.of<CompaniesData>(context, listen: false);
// ...
As you can see, the build method contains multiple nested BLoC/Consumer objects.
#override
void initState() {
_fetchCompanies();
super.initState();
}
void _fetchCompanies() {
findUser().then((location) {
Set<Company> companies = Set.from([Company.fake()]);
// CompaniesData.companies is a setter, which calls
// notifyListeners
_companiesData.companies = companies;
});
}
I don't get error messages, exception, my app simply dies after the end of the execution of the callback given to findUser().then().
EDIT:
I changed the code a little bit, and I figured out that the problem isn't notifyListeners (or at least it isn't now).
final Completer<Map<MarkerId, Company>> _companiesData = Completer();
_AeroMainViewState() {
findUser()
.then(_fetchCompanies)
.then((companies) => _companiesData.complete(Map.fromIterable(
companies,
key: (company) => MarkerId(company.id.toString()),
value: (company) => company,
)));
}
Future<Set<Company>> _fetchCompanies(LatLng location) async =>
Set.from([Company.fake()]);
// ...
child: FutureBuilder<Map<MarkerId, Company>>(
future: _companiesData.future,
builder: (context, snapshot) {
// this builder function isn't called at all
// when the Completer _companiesData is completed
if (snapshot.connectionState == ConnectionState.done) {
return Provider<Map<MarkerId, Company>>.value(
value: snapshot.data,
child: // ...
} else {
return Center(child: CircularProgressIndicator());
}
}),
// ...
Removing the ChangeNotifier doesn't fix the issue.
I post my error for future reference. I was doing this in a class:
static Stream<Obj1> stream() async* {
while (true) {
yield Obj1();
}
}
_subscription = Obj1.stream().listen((event) {
// do something...
}
Since the Stream contains potentially an infinite number of objects, the subscription to that stream was blocking the main (and only) thread.
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;
},
),
]),
);
}
}