Flutter StreamProvider not updating after pushReplacement called - flutter

I have a flutter app that implements firebase authentication. After following this tutorial my main.dart file looks like this:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return StreamProvider<User>.value(
catchError: (_, err) => null,
value: AuthService().user,
child: MaterialApp(
home: Wrapper(),
),
);
}
}
Where the value: AuthService().user is returned as a stream by the following get function:
// auth change user stream
Stream<User> get user {
return _auth.onAuthStateChanged.map(
(FirebaseUser user) => _userFromFirebaseUser(user),
);
}
User _userFromFirebaseUser(FirebaseUser user) {
return user != null ? User(uid: user.uid) : null;
}
And the Wrapper() widget looks like this:
class Wrapper extends StatelessWidget {
#override
Widget build(BuildContext context) {
final user = Provider.of<User>(context);
if (user == null) {
print("user is null, should show Authenticate()");
return Authenticate();
} else {
return Home();
}
}
}
This code functions when:
Logging in from the Authenticate() widget
logging out from the Home() screen.
However, from the Home() screen navigation is laid out as such:
Home()
SecondPage()
ThirdPage()
I use Navigator.push() when going forward (i.e. from "SecondPage()" to "ThirdPage()"). I then wrapped both Secondary and Third page scaffolds in a WillPopScope, where the onWillPop calls Navigator.pushReplacement() to navigate backwards.
(Note: I did this because these pages read and write data from firebase firestore, and Navigator.pop() does not cause a rerender. This means the user may update something on the ThirdPage() that changes a value in firestore but when I then call Navigator.pop() it doesn't update the SecondPage() with the new data.)
My issue is, something about the Navigator.pushReplacement() when navigating from the SecondPage() to the Home() page is causing an issue so that when the user then logs out, the screen does not show the Authenticate() page.
I have placed a print("user is null, should show Authenticate()") call in the Wrapper() to verify that if (user == null) is true when the user clicks the logout button. It does print however it still doesn't show the Authenticate() screen.
I know it has something to do with the pushReplacement(), as logging out directly from Home() without having navigated anywhere functions correctly (i.e. Authenticate() page is shown). Any help is appreciated.

I got the same issue while following the same tutorial. In my case, it turned out signUp button works fine but for both log-out and sign-in I need to refresh to see changes. After debugging a file I came to this conclusion.
Even though the stream always listens to the incoming data, it is inside the Widget build method and needs to be called to get another stream to. So I just needed to call the
Wrapper() method once I detect a non-null user value.
So I did this.
Navigator.pushAndRemoveUntil(context, MaterialPageRoute(builder: (context)=>
Wrapper()), (_) => false );
I made an if-else statement in the signIn page, and when user returned non-null I called the Wrapper() once again and it did the trick.

I ran into the exact same problem and I chose to explicitely navigate back to the Wrapper when user logs out with something like that :
void signOut(BuildContext context) {
Auth().handleSignOut();
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => Wrapper()),
(_) => false
);
}
I still listen to the stream in a Provider to void user data for security reasons but I don't rely on this listener to get back to the initial screen.
void initAuthListener(){
userStream = Auth().user;
userStream.listen((data) {
if(data==null){
voidUser();
}else{
setUser(data);
}
}, onDone: () {
print("Task Done");
}, onError: (error) {
print("Some Error");
});
}
Having said that I'd be happy if anyone could shed some light on why the Wrapper is not rebuilt in our initial case.
Another thing, you mention "I did this because these pages read and write data from firebase firestore, and Navigator.pop() does not cause a rerender."
I would suggest you to use Streams to retrieve data from firestore and Streambuilders to display them as explained in get to know firebase playlist. This will simplify and should improve offline experience.

Related

BlocBuilder not updating after cubit emit

UPDATE After finding the onChange override method it appears that the updated state is not being emitted #confused
UPDATE 2 Further debugging revealed that the StreamController appears to be closed when the updated state attempts to emit.
For some reason 1 of my BlocBuilders in my app refuses to redraw after a Cubit emit and for the life of me I cannot figure out why, there are no errors when running or debugging, the state data is being updated and passed into the emit.
Currently, it is a hook widget, but I have tried making it Stateless, Stateful, calling the cubit method in an effect, on context.bloc.
I have tried making it a consumer, nothing makes it into the listen, even tried messing with listenWhen and buildWhen and nothing is giving any indication as to why this is not building.
It renders the loading state and that is where it ends.
Widget:
class LandingView extends HookWidget {
#override
Widget build(BuildContext context) {
final hasError = useState<bool>(false);
return Scaffold(
key: const Key(LANDING_VIEW_KEY),
body: BlocProvider<CoreCubit>(
create: (_) => sl<CoreCubit>()..fetchDomainOptions(),
child: BlocBuilder<CoreCubit, CoreState>(
builder: (context, state) {
switch (state.status) {
case CoreStatus.loaded:
return LandingViewLoaded();
case CoreStatus.error:
return LandingViewError();
case CoreStatus.loading:
default:
return AppIcon();
}
},
),
),
);
}
}
Cubit method:
Future<void> fetchDomainOptions() async {
final inputEither = await getDomainOptions();
return inputEither.fold(
_handleFailure,
(options) {
emit(state.copyWith(
domainOptions: options,
status: CoreStatus.loaded,
));
},
);
}
I have a few other widgets that work off freezed data classes and work of the same status key logic without any issues, I even went as far as trying to add a lastUpdated timestamp key onto it to make even more data change, but in the initial state domainOptions is null and status is CoreStatus.loading, which should already be enough to trigger a UI update.
TIA
I haven't figured out yet why exactly this happens but I believe we're dealing with some kind of race condition here.
Also, I don't know the proper solution to work around that but I have found that delaying calling emit() for a short amount of time prevents this issue from occurring.
void load() async {
try {
emit(LoadingState());
final vats = await provider.all();
await Future<void>.delayed(const Duration(milliseconds: 50));
emit(LoadedState(vats));
} catch (e) {
print(e.toString());
emit(ErrorState());
}
}
Add await Future<void>.delayed(const Duration(milliseconds: [whatever is needed])); before emitting your new state.
I came to this conclusion after reading about this issue on cubit's GitHub: Emitted state not received by CubitBuilder
i resolved my problem, i was using getIt.
i aded the parameter bloc.
my problem was not recibing the events (emit) of the cubit (bloc)
child: BlocBuilder<CoreCubit, CoreState>(
bloc: getIt<CoreCubit>(), // <- this
builder: (context, state) {
},
),

Flutter what is the best approach to use navigator and ChangeNotifierProvider together

I'm new to flutter, this question may be a very basic one.
I have a firebase phone auth login page to implement this,
if the user is logged in, then navigate to home page
else if the user is a new user, then navigate to the sign-up page
The problem is, whenever the values are changed at the provider, the consumer will get notified and rebuild the build method. I won't be able to listen to them within the build method and return a Navigator.of(context).pushNamed(). Any idea what is the right way to use ChangeNotifierProvider along with listeners and corresponding page navigation?
I have Login class and provider class as below,
class LoginPage extends StatefulWidget {
#override
_LoginPageState createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => LoginProvider(),
child: Consumer<LoginProvider>(builder: (context, loginState, child) {
return Scaffold(
...
body: RaisedButton(
onPressed: **loginState.doLogin(_textController.text, context);**
...
)
}),
);
}
}
class LoginProvider with ChangeNotifier {
bool _navigateToSignup = false;
bool get navigateToSignup => _navigateToSignup;
Future doLogin(String mobile, BuildContext context) async {
FirebaseAuth _auth = FirebaseAuth.instance;
_auth.verifyPhoneNumber(
...
verificationCompleted: (AuthCredential credential) async {
UserCredential result = await _auth.signInWithCredential(credential);
User user = result.user;
// if user is new user navigate to signup
// do not want to use Navigator.of(context).pushNamed('/signupPage'); here, instead would like to notify listeners at login page view and then use navigator.
if (user.metadata.creationTime == user.metadata.lastSignInTime) {
_navigateToSignup = true;
} else {
if (result.user != null) {
_navigateToHome = true;
//Navigator.of(context).pushNamedAndRemoveUntil('/homePage', ModalRoute.withName('/'));
}
}
notifyListeners();
},
...
);
}
}
Thanks in advance.
There are several approaches, you choose the one that suits you best.
Pass the context to the ChangeNotifier as you are already doing. I don't like this as well, but some people do it.
Pass a callback to your ChangeNotifier that will get called when you need to navigate. This callback will be executed by your UI code.
Same as 2, but instead of a callback export a Stream and emit an event indicating you need to Navigate. Then you just listen to that Stream on your UI and navigate from there.
Use a GlobalKey for your Navigator and pass it to your MaterialApp, than you can use this key everywhere. More details here.

Redirect user from named route

So I have a similar issue as the person who asked this older question, except with different requirements that none of the answers there help with.
When a user opens the app, I want them to be greeted with the login page if they haven't logged in or the home page (a bottom nav bar view) if they did. I can define this in the MaterialApp as follows:
MaterialApp(
initialRoute: authProvider.isAuthenticated
? '/home'
: '/login',
routes: {
'/home': (_) =>
ChangeNotifierProvider<BottomNavigationBarProvider>(
child: AppBottomNavigationBar(),
create: (_) => BottomNavigationBarProvider()),
'/login': (_) => LoginView()
},
)
So far so good. Except I want this to work on the web, and now even though the default screen when a user first opens myapp.com is myapp.com/#/login, any user can bypass the login screen by simply accessing myapp.com/#/home.
Now I tried to redirect the user to the login page in the initState() of the bottom navigation bar (and setting the initialRoute to be /home), but on mobile this has undesirable behaviour.
If I try this:
void initState() {
super.initState();
if (!Provider.of<AuthProvider>(context, listen: false).isAuthenticated) {
SchedulerBinding.instance.addPostFrameCallback((_) {
Navigator.of(context).pushNamed('/login');
});
}
}
then simply pressing back will return the user to the home page, again bypassing the login. If I try to use popAndPushNamed instead of just pushing, pressing back will open a blank screen (instead of closing the app).
Is there any way to do this correctly so it works on both web and mobile?
If you use the RouteAware mixin on your widget classes, they will be notified when they are navigated to (or away from). You can use this to check if the user is supposed to be there and to navigate them away if they are not:
To use it, first you need some global instance of RouteObserver that all your widget classes can access:
final routeObserver = RouteObserver<PageRoute>();
Then you need to register it with your MaterialApp:
MaterialApp(
routeObservers: [routeObserver],
)
Then register your widget to the route observer:
class HomeViewState extends State<HomeView> with RouteAware {
#override
void didChangeDependencies() {
super.didChangeDependencies();
routeObserver.subscribe(this, ModalRoute.of(context));
}
#override
void dispose() {
routeObserver.unsubscribe(this);
super.dispose();
}
void didPop() {
// This gets called when this widget gets popped
}
void didPopNext() {
// This gets called when another route gets popped making this widget visible
}
void didPush() {
// This gets called when this widget gets pushed
}
void didPushNext() {
// This gets called with another widget gets pushed making this widget hidden
}
...
}
In your case, you can use the didPush route to navigate the user to the login page if they get to that page in error:
void didPush() {
if (checkLoginStateSomehow() == notLoggedIn) {
Navigator.of(context).pushReplacementNamed('login');
}
}

How to open the last page from which user terminated the app in flutter?

I am noob in flutter and i have made the app of about 6-8 pages.All i want is to continue from last screen from which the user leave or terminated the app completely.
Also is it possible using mobx??
You can persist the route name every time you open a new route then look for the last rout every time you open the app:
String lastRouteKey = 'last_route';
void main() async {
SharedPreferences preferences = await SharedPreferences.getInstance();
String lastRoute = preferences.getString(lastRouteKey);
runApp(MyApp(lastRoute));
}
class MyApp extends StatelessWidget {
final String lastRoute;
MyApp(this.lastRoute);
#override
Widget build(BuildContext context) {
bool hasLastRoute = getWidgetByRouteName(lastRoute) != null;
return MaterialApp(
home: Foo(),
initialRoute: hasLastRoute ? lastRoute : '/',
onGenerateRoute: (RouteSettings route) {
persistLastRoute(route.name);
return MaterialPageRoute(
builder: (context) => getWidgetByRouteName(route.name),
);
},
);
}
Widget getWidgetByRouteName(String routeName) {
switch (routeName) {
case '/': return MainWidget();
// Put all your routes here.
default: return null;
}
}
void persistLastRoute(String routeName) async {
SharedPreferences preferences = await SharedPreferences.getInstance();
preferences.setString(lastRouteKey, routeName);
}
}
Note that this is not 100% precise, since persisting is asynchronous and the user may close the app before it finishes. However, it usually happens really fast and should work almost all the times.
Declare a global variable gv.strCurPage.
In the initstate of each page, set gv.strCurPage equal to current page name, e.g. 'page3'. Then, store this value into SharePreferences.
At the very beginning of main() inside main.dart, retrieve that value from SharePreference, store it in gv.strCurPage, if this value is empty, set gv.strCurPage = 'page1'.
In runApp(), set the first page, using a switch statement, according to the value of gv.strCurPage.
i.e. when user open the app for the first time, the first page is page1. Then, suppose the user naviagate to page5, a value 'page5' will be stored in sharepreference, and retrieved next time the user open the app. so 'page5' will become the first page of the app in next open.

How to route to a different screen after a state change?

I'm using flutter_redux and google_sign_in and I want to route from the Login page to a different page after logging in.
I am handling the API call to Google using middleware that dispatches a LoggedInSuccessfully action. I know I can use Navigator.pushNamed(context, "/routeName") for the actual routing, but I am new to both Flutter and Redux and my problem is that I just don't know where to call this.
The code below is for the GoogleAuthButtonContainer, which is my guess as to where the routing should be. The GoogleAuthButton is just a plain widget with the actual button and layout.
Any help appreciated, thanks!
#override
Widget build(BuildContext context) {
return new StoreConnector<AppState, _ViewModel>(
converter: _ViewModel.fromStore,
builder: (BuildContext context, _ViewModel vm) {
return new GoogleAuthButton(
buttonText: vm.buttonText,
onPressedCallback: vm.onPressedCallback,
);
},
);
}
}
class _ViewModel {
final String buttonText;
final Function onPressedCallback;
_ViewModel({this.onPressedCallback, this.buttonText});
static _ViewModel fromStore(Store<AppState> store) {
return new _ViewModel(
buttonText:
store.state.currentUser != null ? 'Log Out' : 'Log in with Google',
onPressedCallback: () {
if (store.state.currentUser != null) {
store.dispatch(new LogOut());
} else {
store.dispatch(new LogIn());
}
});
}
}
You can achieve this by 3 different ways:
Call the navigator directly (without using the ViewModel), but this is not a clean solution.
Set the navigatorKey and use it in a Middleware as described here
And another solution is what I've explained here which is passing the Navigator to the Action class and use it in the Middleware
So to sum up, you probably want to use a Middleware to do the navigation.