Proper use of cubit - flutter

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,

Related

Why is Bloc skipping this emit command?

I have an bloc that receives an event called OpenTourStop that invokes a function whose first line of code invokes emit(Waiting()); and then proceeds to execute some logic before emitting a different state. In the UI, the BlocConsumer is supposed to print out a message to the console as soon as state equals Waiting, but it NEVER does. The bloc does not emit the Waiting state, but does emit the other states that result from completing the function. What am I doing wrong?
Below are the relevant sections of code for the bloc and UI
Bloc code:
class QuiztourManagerBloc
extends Bloc<QuiztourManagerEvent, QuiztourManagerState> {
final QuiztourRemoteData _repo;
QuiztourManagerBloc({QuiztourRemoteData repo})
: _repo = repo,
super(QuiztourManagerInitial()) {
on<OpenTourStop>(_openTourStop);
}
_openTourStop(event, emit) {
emit(Waiting()); // Why doesn't the Waiting state show up in the UI?
final _tourStopIndex = event.tourStopIndex;
// section of code removed for clarity
if (_quizPlayDoc.seenRules && tourStopGameResults.isEmpty) {
emit(ShowQuizQuestionViewManager(
quizPlayDoc: _quizPlayDoc, tourStopIndex: _tourStopIndex));
// emit(ShowQuizQuestions(quizPlayDoc: _quizPlayDoc, tourStopIndex: _tourStopIndex));
} else if (tourStopGameResults.length > 0) {
emit(ShowQuizTourStopScreen(
tour: event.tour,
tourStopIndex: event.tourStopIndex,
quizPlayDoc: _quizPlayDoc,
maxTourStopPoints: _maxTourStopPoints.toString(),
pointsEarned: _tourStopScore.toString(),
));
} else {
emit(ShowQuizRules(_quizPlayDoc));
}
}
}
UI code (from class QuizTourStopViewManager) :
#override
Widget build(BuildContext context) {
return BlocConsumer<QuiztourManagerBloc, QuiztourManagerState>(
builder: (context, state) {
if (state is Waiting) {
print('!!!!!!!!!!!!!!!!!!!!! Waiting '); // Why does this line never get executed?
return Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
else if (state is ShowQuizTourStopScreen) {
return QuizTourStop( );
}
},
listener: (_, state) {},
);
}
The UI that triggers the event is a button. The code associated with that button is below:
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
if (tourType == "quizTour") {
BlocProvider.of<QuiztourManagerBloc>(context)
.add(OpenTourStop(
tour: tour,
tourStopIndex: selectedTourStopIndex,
));
return QuizTourStopViewManager(
tour: tour,
// game: widget.game,
selectedTourStopIndex: selectedTourStopIndex,
);
I guess that when you send 'OpenTourStop' event at 'onTap' method, 'QuizTourStopViewManager' page is not builded.
So would you try to change event call position after 'OpenTourStop' page is builded?
At initState() method inside.
or
At 'bloc' parameter in BlocConsumer method.

Any way to have strongly typed use of Navigator?

I have a MaterialButton with the following onPressed field:
onPressed: () async {
final int intResult = await showMyDialog(context, provider.myArg) ?? 0;
//...
}
Here is the showMyDialog function:
Future<int?> showMyDialog(BuildContext context, Object someArg) async {
return showDialog<int>(
context: context,
builder: (BuildContext context) {
return ChangeNotifierProvider<MyProvider>(
create: (_) => MyProvider(someArg),
child: MyDialog(),
);
},
);
}
Now the problem I see is that in MyDialog (a StatelessWidget), I need to use Navigator.pop(...) to return a value for the awaited intResult. However, I can't seem to find a way to strongly type these calls, and so it's hard to be certain that no runtime type error will happen.
The best I have right now, which is admittedly a bit inconvenient, is to subclass StatelessWidget and wrap the Navigator functions in it:
abstract class TypedStatelessWidget<T extends Object> extends StatelessWidget {
void pop(BuildContext context, [T? result]) {
Navigator.pop(context, result);
}
}
Then in a TypedStatelessWidget<int> we can use pop(context, 0) normally, and pop(context, 'hi') will be marked as a type error by the editor. This still doesn't link the dialog return type and the navigator, but at least it avoids manually typing each navigator call.
Is there any better way to have this strongly typed ?
You don't need to wrap the Navigator.pop method as you can type its return value.
Navigator.pop<int>(context, 1);
I let you refer to the pop method documentation for more details.
So here's what I ended up doing. Essentially, this allows for a widget type and a provider type to be "tied together". This way, when the custom pop function is called in the dialog, there is a type contract (see PopType) for what the return value should be. There is also support for non-manual pop navigation by using WillPopScope, which gets the typed provider's return function and arguments (see the abstract functions in TypedProvider) to properly pass that value up the widget tree.
mixin TypedWidget<T, N extends TypedProvider<T>?> on Widget {
ChangeNotifierProvider<N> asTypedWidget(
BuildContext context, N Function(BuildContext) createProviderFunc) {
return ChangeNotifierProvider<N>(
create: createProviderFunc,
builder: (BuildContext innerContext, _) {
return WillPopScope(
child: this,
onWillPop: () {
final N provider = Provider.of<N>(innerContext, listen: false);
if (provider == null) {
return Future<bool>.value(true);
}
provider.onPopFunction(provider.getPopFunctionArgs());
return Future<bool>.value(true);
});
});
}
/// Builds this TypedWidget inside `showDialog`.
///
/// The `TypedProvider`'s `onPopFunction` is called if the
/// dialog is closed outside of a manual `Navigator.pop()`. This doesn't have
/// a return type; all returning actions should be done inside the defined
/// `onPopFunction` of the provider.
///
/// Example:
/// ```
/// MyTypedWidget().showTypedDialog(
/// context,
/// (BuildContext context) => MyTypedProvider(...)
/// );
/// ```
Future<void> showTypedDialog(
BuildContext context, N Function(BuildContext) createProviderFunc,
{bool barrierDismissible = true}) async {
await showDialog<void>(
context: context,
barrierDismissible: barrierDismissible,
builder: (_) => asTypedWidget(context, createProviderFunc),
);
}
}
abstract class TypedProvider<PopType> with ChangeNotifier {
TypedProvider(this.onPopFunction);
Function(PopType) onPopFunction;
PopType getPopFunctionArgs();
void pop(BuildContext context) {
onPopFunction(getPopFunctionArgs());
Navigator.pop(context);
}
}
There are most likely other ways to achieve that, and this solution certainly has some constraints, but it effectively provides strong typing to the dialog.

calling setState in flutter Bloc listener

I am calling setState in flutter BlocListener. is there any problem doing this?
....
return BlocListener<XBloc, XState>(
listener: (context, state) {
if (state is XLoadedState) {
setState(() {
name = state.name;
});
}....
....
It's not a problem but it's kinda useless and anti pattern. And using setState you are forcing everything to rebuild even if it's not necessary.
You could just wrap the widget that uses name into a BlocBuilder<XBloc,XState>, for example like this:
BlocBuilder<XBloc,XState>(
builder: (context, state){
if (state is XLoadedState){
return Text(state.name);
}else{
//return something for when state.name is null, I guess
}
}
)
You can check more about this here

How to inform FutureBuilder that database was updated?

I have a group profile page, where a user can change the description of a group. He clicks on the description, gets on a new screen and saves it to Firestore. He then get's back via Navigator.pop(context) to the group profile page which lists all elements via FutureBuilder.
First, I had the database request for my FutureBuilder inside the main build method (directly inside future builder 'future: request') which was working but I learnt it is wrong. But now I have to wait for a rebuild to see changes. How do I tell FutureBuilder that there is a data update?
I am loading Firestore data as follows within the group profile page:
Future<DocumentSnapshot> _future;
#override
void initState() {
super.initState();
_getFiretoreData();
}
Future<void> _getFiretoreData() async{
setState(() {
this._future = Firestore.instance
.collection('users')
.document(globals.userId.toString())
.get();});
}
The FutureBuilder is inside the main build method and gets the 'already loaded' future like this:
FutureBuilder(future: _future, ...)
Now I would like to tell him: a change happened to _future, please rebuild ;-).
Ok, I managed it like this (which took me only a few lines of code). Leave the code as it is and get a true callback from the navigator to know that there was a change on the second page:
// check if second page callback is true
bool _changed = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
ProfileUpdate(userId: globals.userId.toString())),
);
// if it's true, reload future data
_changed ? _getFiretoreData() : Container();
On the second page give the save button a Navigator.pop(context, true).
i would advice you not to use future builder in this situation and use future.then() in an async function and after you get your data update the build without using future builder..!
Future getData() async {
//here you can call the function and handle the output(return value) as result
getFiretoreData().then((result) {
// print(result);
setState(() {
//handle your result here.
//update build here.
});
});
}
How about this?
#override
Widget build(BuildContext context) {
if (_future == null) {
// show loading indicator while waiting for data
return Center(child: CircularProgressIndicator());
} else {
return YourWidget();
}
}
You do not need to set any state. You just need to return your collection of users in your GetFirestoreData method.
Future<TypeYouReturning> _getFirestoreData() async{
return Firestore.instance
.collection('users')
.document(globals.userId.toString())
.get();
}
Inside your FutureBuilder widget you can set it up something like Theo recommended, I would do something like this
return FutureBuilder(
future: _getFirestoreData(),
builder: (context, AsyncSnapshot<TypeYouReturning> snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
);
} else {
if (snapshot.data.length == 0)
return Text("No available data just yet");
return Container();//This should be the desire widget you want the user to see
}
},
);
Why don't you use Stream builder instead of Future builder?
StreamBuilder(stream: _future, ...)
You can change the variable name to _stream for clarity.

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.