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

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.

Related

Proper use of cubit

I am still learning how to use cubits and blocs, and I am trying to use a cubit in my project, but I got a little bit confused about how to use it.
There is a screen that requires a phone number and I use the lib "intl_phone_number_input" to format, validate and select the country. When I click the button to the next page it needs to check if the phone is valid, but I need to have a variable that stores this info. The widget InternationalPhoneNumberInput has a property onInputValidated that returns true if the phone number is valid, so where should I create this variable? Should I create it in my widget class or inside the cubit? I created it inside cubit but I am not sure if it is the correct way, so I got this:
onInputValidated: (bool value) {
BlocProvider.of<LoginCubit>(context).isValid =
value;
},
I've studied and seen some examples about cubits and how to use'em but I still didn't get it at all, because in the examples the cubit never used a variable, all variables became a state, but in my case, I need the value as a variable.
I am confused too about how to show a dialog using cubit, I've done it this way:
#override
Widget build(BuildContext context) {
return BlocConsumer<LoginCubit, LoginState>(
listenWhen: (previous, current) => current is ShowDialogErrorLoginState || current is NavigateFromLoginStateToHomePageState,
listener: (context, state) {
if (state is ShowDialogErrorLoginState) {
showErrorDialog(context, state.titleMessage, state.bodyMessage);
}
if (state is NavigateFromLoginStateToHomePageState) {
Navigator.pushReplacement(context,
MaterialPageRoute(builder: (context) => const MyHomePage()));
}
},
builder: (context, state) {
if (state is ShowLoginState) {
return buildPhoneForm(context);
}
if (state is SendingCodeLoginState) {
return ProgressView(message: 'Sending SMS code',);
}
if (state is ShowCodeLoginState) {
return buildCodeForm(context);
}
return const ErrorView('Unknown error');
},
);
}
and in my cubit I did the following:
void goToCodeVerification(String phoneNumber) async {
if (!isValid){
String titleMessage = "Phone number invalid";
String bodyMessage = "The given phone number is invalid";
emit(ShowDialogErrorLoginState(titleMessage, bodyMessage));
emit(ShowLoginState());
} else {
emit(SendingCodeLoginState());
// TO DO
// use API to send a code
emit(ShowCodeLoginState());
}
}
Is this the correct way to show a dialog with a cubit?
Ok, so you have a value you want to use, the variable doesn't affect state, and you need to access it inside your cubit.
for something like this, I think storing the variable on the cubit makes the most sense, but keep in mind either approach is acceptable for such a simple case.
I also don't really like how the below code looks:
onInputValidated: (bool value) {
BlocProvider.of<LoginCubit>(context).isValid =
value;
},
it is a bit clunky, I would prefer to move the whole callback into the cubit:
void onInputValidated(bool value) => isValid = value;
that way:
final cubit = BlocProvider.of<LoginCubit>(context);
...
onInputValidated: cubit.onInputValidated,

Flutter - how to correctly create a button widget that has a spinner inside of it to isolate the setState call?

I have a reuable stateful widget that returns a button layout. The button text changes to a loading spinner when the network call is in progress and back to text when network request is completed.
I can pass a parameter showSpinner from outside the widget, but that requires to call setState outside of the widget, what leads to rebuilding of other widgets.
So I need to call setState from inside the button widget.
I am also passing a callback as a parameter into the button widget. Is there any way to isolate the spinner change state setting to inside of such a widget, so that it still is reusable?
The simplest and most concise solution does not require an additional library. Just use a ValueNotifier and a ValueListenableBuilder. This will also allow you to make the reusable button widget stateless and only rebuild the button's child (loading indicator/text).
In the buttons' parent instantiate the isLoading ValueNotifier and pass to your button widget's constructor.
final isLoading = ValueNotifier(false);
Then in your button widget, use a ValueListenableBuilder.
// disable the button while waiting for the network request
onPressed: isLoading.value
? null
: () async {
// updating the state is super easy!!
isLoading.value = true;
// TODO: make network request here
isLoading.value = false;
},
#override
Widget build(BuildContext context) {
return ValueListenableBuilder<bool>(
valueListenable: isLoading,
builder: (context, value, child) {
if (value) {
return CircularProgressIndicator();
} else {
return Text('Load Data');
}
},
);
}
You can use StreamBuilder to solve this problem.
First, we need to create a stream. Create a new file to store it, we'll name it banana_stream.dart, for example ;).
class BananaStream{
final _streamController = StreamController<bool>();
Stream<bool> get stream => _streamController.stream;
void dispose(){
_streamController.close();
}
void add(bool isLoading){
_streamController.sink.add(isLoading);
}
}
To access this, you should use Provider, so add a Provider as parent of the Widget that contain your reusable button.
Provider<BananaStream>(
create: (context) => BananaStream(),
dispose: (context, bloc) => bloc.dispose(),
child: YourWidget(),
),
Then add the StreamBuilder to your button widget:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return StreamBuilder<bool>(
stream: Provider.of<BananaStream>(context, listen:false),
initialData: false,
builder: (context, snapshot){
final isLoading = snapshot.data;
if(isLoading == false){
return YourButtonWithNoSpinner();
} else{
return YourButtonWithSpinner();
}
}
);
}
}
And to change isLoading outside, you can use this code:
final provider = Provider.of<BananaStream>(context, listen:false);
provider.add(true); //here is where you change the isLoading value
That's it!
Alternatively, you can use ValueNotifier or ChangeNotifier but i find it hard to implement.
I found the perfect solution for this and it is using the bloc pattern. With this package https://pub.dev/packages/flutter_bloc
The idea is that you create a BLOC or a CUBIT class. Cubit is just a simplified version of BLOC. (BLOC = business logic component).
Then you use the bloc class with BlocBuilder that streams out a Widget depending on what input you pass into it. And that leads to rebuilding only the needed button widget and not the all tree.
simplified examples in the flutter counter app:
// input is done like this
onPressed: () {
context.read<CounterCubit>().decrement();
}
// the widget that builds a widget depending on input
_counterTextBuilder() {
return BlocBuilder<CounterCubit, CounterState>(
builder: (context, state) {
if (state.counterValue < 0){
return Text("negative value!",);
} else if (state.counterValue < 5){
return Text("OK: ${state.counterValue}",
);
} else {
return ElevatedButton(onPressed: (){}, child: const Text("RESET NOW!!!"));
}
},
);
}

How do you do Rx.CombineLatestStream using the flutter_bloc package

I put together a sample app that implements an username and password field using streams with verification using validation transforms. I used RxDart for this and I hooked up the "Login" button to enable/disable based on 2 streams results being true.
bloc.dart
Stream<bool> get submitValidWithCombineLatestStream =>
CombineLatestStream([email, password], (list) => true);
login_page.dart
Widget submitButton(Bloc bloc) {
return StreamBuilder(
stream: bloc.submitValidWithCombineLatestStream,
builder: (_, snapshot) {
return RaisedButton(
color: Colors.deepPurpleAccent,
child: Text(
'Submit',
style: TextStyle(color: Colors.white),
),
onPressed: !snapshot.hasData ? null : bloc.submit,
);
},
);
}
I have coded the same sample app using flutter_bloc but I'm trying to figure out how to enable/disable the "Login" button using whatever bloc has for CombineLatestStream. Does anyone know how to do this?
I realize that this is total overkill but I'm trying to figure this out using this simple example and I'm not sure how to access all the cool RxDart functionality once you convert to bloc (flutter_bloc). I'd like to have the best of both worlds WITHOUT importing/using RxDart.
Is that possible?
An example of what I’m looking for would be:
I’m building a form that requires 3 different backend calls. I’d prefer to show the progress indicator while all 3 calls are working and block until all 3 calls return. I’d like to do that via CombineLatestStreams since all 3 return Steam Futures.
Problem in your main bloc class,check out this github code,
https://github.com/hoc081098/flutter_validation_login_form_BLoC_pattern_RxDart
In short like this,you can use CombineLatestStream
you can set in your main bloc class
like this,
return LoginBloc._(
emailChanged: emailS.add,
passwordChanged: passwordS.add,
submitLogin: () => submitLoginS.add(null),
emailError$: emailError$,
passwordError$: passwordError$,
isLoading$: isLoadingS.stream,
message$: message$,
dispose: DisposeBag([...subjects, message$.connect()]).dispose,
);
This is how you combine streams with bloc using RxDart, you have emit new values if there is something new in listener, that's all.
You might create listener whenever you want to do calls to your backend.
import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:rxdart/rxdart.dart';
class TestState {
final bool isValid;
TestState(this.isValid);
}
class TestCubit extends Cubit<TestState> {
final List<StreamSubscription> _ss = [];
final _emailSubject = BehaviorSubject<String>.seeded(null);
final _passwordSubject = BehaviorSubject<String>.seeded(null);
TestCubit() : super(TestState(false)) {
_ss.add(_subscribeToEmailAndPassword());
}
StreamSubscription _subscribeToEmailAndPassword() {
return Rx.combineLatest([_emailSubject, _passwordSubject], (list) {
if (list[0] != null && list[1] != null) {
return true;
}
return false;
}).listen((value) {
emit(TestState(value));
});
}
#override
Future<void> close() async {
_ss.forEach((e) async => await e.cancel());
await super.close();
}
void emailFieldChange(String email) {
_emailSubject.add(email);
}
void passwordFieldChange(String password) {
_passwordSubject.add(password);
}
}

Flutter StreamProvider not updating after pushReplacement called

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.

Global snackbar/dialog utils class in flutter

I want to create a GlobalMessageUtils class that would open a material snackbar or dialog without having to pass the build context. The idea is that whenever there's any error (no network, bad request, etc) I am able to pop open a snackbar and relay the message to the user.Is there a concept of global context?
I was playing with the idea of making my GlobalMessageUtils class a singleton that takes in a build context and instantiate it at the MaterialApp level, but I haven't gotten this to work. Any body have any ideas? Is this even a good pattern in flutter? If not, how do you guys deal with error handling at a global level?
Using the BLOC pattern and Rxdart, I created a UiErrorUtils class
class UiErrorUtils {
// opens snackbar
void openSnackBar(BuildContext context, String message) async {
await Scaffold.of(context).showSnackBar(
SnackBar(
content: Text(message),
),
);
}
// subscribes to stream that triggers open snackbar
void subscribeToSnackBarStream(BuildContext context, PublishSubject<String> stream){
stream.listen((String message){
openSnackBar(context, message);
});
}
}
In your StatefulWidget, you can use the context provided in the initState hook:
class WidgetThatUsesUIErrorUtils extends StatefulWidget {
final UiErrorUtils uiErrorUtils;
final Bloc bloc;
WidgetThatUsesUIErrorUtils({this.uiErrorUtils, this.bloc});
WidgetThatUsesUIErrorUtils createState() => WidgetThatUsesUIErrorUtilsState(
uiErrorUtils: uiErrorUtils,
bloc: bloc,
);
}
class WidgetThatUsesUIErrorUtilsState extends State<WidgetThatUsesUIErrorUtils> {
final Bloc _bloc;
final UiErrorUtils _uiErrorUtils;
WidgetThatUsesUIErrorUtilsState({Bloc bloc, UiErrorUtils uiErrorUtils})
: _bloc = bloc ?? Bloc(),
_uiErrorUtils = uiErrorUtils ?? UiErrorUtils();
#override
void initState() {
super.initState();
// Subscribe to UI feedback streams from provided _bloc
_uiErrorUtils.subscribeToSnackBarStream(context, _bloc.snackBarSubject);
}
}
BLOC
class Bloc extends BlocBase {
// UI Feedback Subjects
final PublishSubject<String> snackBarSubject = PublishSubject<String>();
// some function that gets data from network
Future<bool> getDataRequest() async {
try {
// get request code here
} catch(error) {
this.snackBarSubject.add(error);
}
}
#override
void dispose() {
snackBarSubject?.close();
}
}
Now your widget has subscribed to the bloc's snackBarStream.
So in your bloc whenever a request fails you can add the message to the snackBarStream and since your widget has subscribed via UiErrorUtils the snackbar will trigger with the message.
define global variable e.g. inside main.dart
final GlobalKey<ScaffoldMessengerState> rootScaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
MaterialApp widget has scaffoldMessengerKey property so set this key to property
return MaterialApp(
debugShowCheckedModeBanner: false,
scaffoldMessengerKey: rootScaffoldMessengerKey,
home: Scaffold(),
);
now you're able to show SnackBar from any place in app
rootScaffoldMessengerKey.currentState?.showSnackBar(SnackBar(content: Text('some text')));
I wrote a Package that supports application wide message display.
EZ Flutter supports displaying a message to the user from anywhere inside the app with just one line of code. Global Messaging is handled with a BLOC and widget added as the body of a Scaffold.
Github : https://github.com/Ephenodrom/EZ-Flutter
dependencies:
ez_flutter: ^0.2.0
Add the message wrapper
Add the EzGlobalMessageWrapper as the body to a Scaffold.
Scaffold{
appBar: ...
body: EzGlobalMessageWrapper(
MyWidget(
...
)
)
}
Add message to the bloc
Load the EzMessageBloc via the EzBlocProvider using the get method.
Add a EzMessage to the bloc. The supported EzMessageTypes are :
SUCCESS (default color : Colors.green)
INFO (default color : Colors.blue)
WARNING (default color : Colors.orange)
ERROR (default color : Colors.red)
EzBlocProvider.of<EzGlobalBloc>(context)
.get<EzMessageBloc>(EzMessageBloc)
.addition
.add(EzMessage("This is a success message", EzMessageType.SUCCESS));
https://github.com/Ephenodrom/EZ-Flutter/blob/master/documentation/GLOBAL_MESSAGE.md
For a Provider solution if you are using BaseWidget, then you can just create a base class method and use it everywhere you are using provider.
class BaseWidget<T extends ChangeNotifier> extends StatefulWidget {
final Widget Function(BuildContext context, T model, Widget child) builder;
.
.
.
showToast(BuildContext context, String message, {int durationSeconds = 5}) {
final ScaffoldMessengerState scaffoldMessenger =
ScaffoldMessenger.of(context);
scaffoldMessenger.showSnackBar(SnackBar(
content: Text(message),
duration: Duration(seconds: durationSeconds),
action: SnackBarAction(
label: 'HIDE',
onPressed: () {
scaffoldMessenger.hideCurrentSnackBar();
},
)));
}
Then just call it from your views.
.
.
.
showToast(context, "Factor Model Created Successfully",
durationSeconds: 30);
Navigator.pop(context, 'save');