Reusing widgets with Bloc - flutter

What do I want to achieve?
I'm using the Flutter BLoc library for my authentication component. Inside login page I have some text fields like username/password that I'm going to share with other pages as well, such as register page, forgot password, change password etc. Basically, following the DRY principle.
Each page (Register, Login, Forgot Password etc.) has it's own BLoc component.
My problem
I couldn't find a way to decouple the widget from the BLoc. I want to be able pass in the stateful widget any BLoc component depending on the page it is used in.
To make sense out of the written above let's take a look at my code.
login.dart A of piece of code from the build method from the Login page.
Widget _loginForm(){
return BlocListener<LoginBloc, LoginState>(
listener: (context, state) {
final status = state.formStatus;
if (status is SubmissionFailed) {
...
}
},
child: Form(
key: _formKey,
child: Column(
children: [
// The reusable widget: Email input field.
BlocBuilder<LoginBloc, LoginState>(
builder:(context, state){
return emailInputField(context, state);
}
),
...
Now let's take a look in the emailInputField widget
Widget emailInputField(BuildContext context, dynamic state) {
return TextFormField(
validator: (value) =>
state.isValidEmail ? null : 'Please input a valid email',
onChanged: (value) =>
// HERE - I want to decouple this widget from "LoginBloc".
context.read<LoginBloc>().add(LoginUsernameChanged(username: value)),
labelText: 'Email',
...
);
}
login_bloc.dart The Login BLoc
class LoginBloc extends Bloc<BaseEvent, LoginState>{
LoginBloc() : super(LoginState());
#override
Stream<LoginState> mapEventToState(BaseEvent event) async*{
yield* event.handleEvent(state);
}
}
And the login events class, for having a full picture login_event.dart
abstract class LoginEvent extends BaseEvent {
AuthenticationService authService = GetIt.I.get<AuthenticationService>();
}
// Event 1
class LoginUsernameChanged extends LoginEvent {
final String username;
LoginUsernameChanged({this.username});
#override
Stream<LoginState> handleEvent(BaseState state) async* {
// Dart style down-casting...
LoginState loginState = state;
yield loginState.copyWith(username: username);
}
}
base_event.dart
abstract class BaseEvent {
Stream<BaseState> handleEvent(BaseState state);
}
And again, is there a way to decouple that view from the BLoc?
I hope my question makes sense after reading this.
P.S
To make things even simpler, one idea I though of is to keep one single Bloc component that will handle a group of related input fields (password/username) and then just pass in different states as I did in login.dart page when calling the emailInput widget.

What I would do is decouple the widget from bloc entirely. Instead of taking the bloc state and using bloc.add, create dependencies that you can populate with any parameter.
In you example you would have:
Widget emailInputField({
required BuildContext context,
required bool isValidEmail,
required void Function(String?) onChange,
}) {
return TextFormField(
validator: (value) => isValidEmail ? null : 'Please input a valid email',
onChanged: onChange,
labelText: 'Email',
...
);
}
Then you can use emailInputField with any bloc you want. Or any state management library for that matter.
For your example this would give:
Widget _loginForm(){
return BlocListener<LoginBloc, LoginState>(
listener: (context, state) {
final status = state.formStatus;
if (status is SubmissionFailed) {
...
}
},
child: Form(
key: _formKey,
child: Column(
children: [
// The reusable widget: Email input field.
BlocBuilder<LoginBloc, LoginState>(
builder:(context, state){
return emailInputField(
context: context,
isValidEmail: state.isValidEmail,
onChanged: (value) => context.read<LoginBloc>().add(LoginUsernameChanged(username: value))
);
}
),
...

Related

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 to listen to the changes and rebuild the widget in flutter using provider packge?

The problem i am facing is,
I am connecting my ui to backend with websocket using subscribe method(graphql client). That means there is a connection between my ui and backend. I am storing the data i got from backend in the local storage
From the local storage, i am getting that data,
Whenever i receive the data from backend it should be reflected in the ui automatically. For reflecting change in ui, i am using state management provider package.What should i do to make my widget rebuild on listening to the changes i had made using provider package;
class MyNotifier extends ChangeNotifier {
bool _listenableValue = false;
bool get listenableValue => _listenableValue
MyNotifier.instance();
void setValue(){
_listenableValue = !_listenableValue;
notifyListeners();
}
}
...
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider<MyNotifier>(
create: (context) => MyNotifier.instance(),
builder: (context, child){
return Column(
children: [
StaticWidget(),
//This text widget will rebuild when value is changed
Selector<MyNotifier, bool>(
selector: (_, notifier) => notifier.listenableValue,
builder: (_, value, __) => Text('$value');
),
//Change value with button
IconButton(
icon: //icon
onPressed: (){
context.read<MyNotifier>().setValue();
},
);
]
);
}
);
}
Don' t use Consumer. Consumer will rebuild all widgets when a data changed. This is the bad situation for performance.
Selector is the best in my opinion.
If you have your state in a ChangeNotifier like:
class MyState extends ChangeNotifier{
addStorage(Map<String, String> data) {...}
getAllDataFromStorage() {...}
}
Then you can make your UI rebuild by just wrapping the desired widgets in a Consumer.
Consumer<MyState>(builder: (context, state) {
return Container(
padding: EdgeInsets.only(top: 10),
child: LayoutBuilder(builder: (context, constraints) {
if (screenLayout >= 1024) {
return desktopWidget(context, visitsList);
} else if (screenLayout >= 768 && screenLayout <= 1023) {
return tabletWidget(context, visitsList);
} else {
return mobileWidget(context, visitingsList, screenLayout);
}
})},
);
Note that somewhere above this snippet you should have a ChangeNotifierProvider injecting your MyState in the widget tree.
For a really thorough and complete guide take a look at Simple state management
**best way to listen changes in provider its to make getter and setter i will show you example below**
class ContactProvider with ChangeNotifier {
bool isBusy = true;
bool get IsBusy => isBusy;
set IsBusy(bool data) {
this.isBusy = data;
notifyListeners();
}
void maketru(){
IsBusy=false;
}
}
and now you can use this context.read<ContactProvider>().Isbusy;

Trigger a validate/submit form method from the AppBar in flutter

I have 3 widgets.
The HomeApp - A stateless Widget that connects everything.
A custom AppBarWidget - A Stateful Widget which have a submit button.
A SurveyForm Widget - A Stateful widget which have 50+ inputs including TextFormField, radio buttons, and a whole bunch of custom input types.
When the user press the submit button in the appbar, it needs to do some custom validations in the survey form, create a new model class. which translate into a JSON object, and get POST to an API.
I've read a few options,
Initiate all the variables and the validation/submit logics in the HomeApp, pass it down to the SurveyForm, and use callback to trigger the function. But it involves me passing down 50+ parameters to the SurveyForm.
Using a GlobalKey. Not really sure how it works, When I try to do final key = new GlobalKey<SurveyForm>();, It said 'SurveyForm' doesn't extend 'State<StatefulWidget>'..
Pass the parent instance to the child widget, suggested by this link.
Calling a method of child widget, but this link said it's discouraged.
Write a custom controller.
What would be the preferable way in Flutter?
You can use a Form widget Tutorial.
Basically you wrap all of your TextFields and checkbox and other stuff with a Form widget and set it's key to a GlobalKey<FormState>(). now you can use another type of fields in it(for example instead of TextaField you use a TextFormField) now you can set a property called onSaved on them(We're gonna use it in a bit). then whenever you want to submit the forms data, you call a function that you've written in your state class(by adding it to an onPressed value of a button). It's something like this:
void _submitForm() {
final form = _formKey.currentState; // this is your GlobalKey<FormState> that you specified and set to the key of your Form Widget.
if (form.validate()) { // form.validate() will call all the validator functions that you've passed to your inputfields
form.save(); // This will call all the onSaved functions that you passed to your forms.
}
}
It gets your Form widget, calls all the validator functions of the inputs, if everything fits together, calls all the onSaved functions of the inputs.
You can create a dictionary and in onSaved of the inputfields set the inputs values in it.
Now you can use json.encode(your_data_dict) to turn it to a JSON string or use your object. then post the json.
So I managed to solved this.
My problem should actual be break down into 3 problems.
Validate textformfield, radio buttons, all custom inputs.
Create a new model class that can be passed translate to JSON.
Submit the form from a button within another widget (Appbar).
In order to solve the first one, I wrapped everything in SurveyForm into a Form widget. This part is simple, there are many tutorials online. In order to do validation on custom inputs, I wrapped the input in a FormField widget, and trigger the didChange method of FormFieldState. This allows me to use validator and onSaved method of FormField using methods callback. Below is my working code for a datepicker.
class Q1<T> extends StatefulWidget {
final FormFieldValidator<DateTime> validator;
final FormFieldSetter<DateTime> onSaved;
const Q1({
Key key,
#required this.onSaved,
#required this.validator,
}) : assert(validator != null),
assert(onSaved != null),
super(
key: key,
);
#override
_Q1State createState() => _Q1State();
}
class _Q1State extends State<Q1> {
DateTime _completionDate;
#override
Widget build(BuildContext context) {
return FormField(
validator: (val) {
return widget.validator(val);
},
onSaved: (val) {
widget.onSaved(val);
},
builder: (FormFieldState<dynamic> field) {
return Row(
children: [
Expanded(
flex: 1,
child: Text('1. Completion Date'),
),
Expanded(
flex: 2,
child: Row(
children: [
Container(
padding: EdgeInsets.all(
SizeConfig().getProportionateScreenWidth(
kDatePickerIconTextBoxPadding),
),
decoration: BoxDecoration(
border: Border.all(
color: kDatePickerIconTextBoxBorderColor,
),
borderRadius: BorderRadius.all(Radius.circular(
SizeConfig().getProportionateScreenWidth(
kDatePickerIconTextBoxRadius),
) // <--- border radius here
),
),
child: Text(_completionDate == null
? 'Pick a date'
: DateFormat(kDateFormat).format(_completionDate)),
),
IconButton(
onPressed: () {
showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime(2000),
lastDate: DateTime(2100),
).then((date) {
setState(() {
field.didChange(date);
_completionDate = date;
});
});
},
icon: FaIcon(FontAwesomeIcons.calendarAlt)),
],
),
),
],
);
},
);
}
}
And I just called the widget with
Q1(
validator: (val) {
if (val == null) {
return 'Completion Date is missing';
}
return null;
},
onSaved: (value) {
setState(() {
widget.questionnaire.completion_date = DateFormat(kDateFormat).format(value);
});
},
),
In order to solve the 2nd one. I used json_serializable, where the object can also be passed around between HomeApp and SurveyForm widget, so I don't need to pass around 50+ variables.
And for the 3rd problem, I initiated final _formKey = GlobalKey<FormState>(); in the HomeApp, and passed it down to the SurveyForm widget, and use onPressed callback in the Appbar to trigger validation/submit in the SurveyForm.
Bloc will be used for submitting JSON and load animation, but that's beyond this.
I'm new to flutter (Been using this for a month), please let me know if anyone have a better solution.

BlocBuilder partially update ListView

Project
Hi, I'm trying to use a bloc pattern to create a list view that can be filtered by a TextField
Here is my code
bloc:
class HomeBloc extends Bloc<HomeEvent, HomeState> {
List<Book> displayList = [];
....
#override
HomeState get initialState => UnfilteredState();
#override
Stream<HomeState> mapEventToState(
HomeEvent event,
) async* {
....
//handle filter by input
if(event is FilterListByTextEvent) {
displayList = displayList.where((book){
return book.title.toLowerCase().contains(event.filterString.toLowerCase());
}).toList();
yield FilteredState();
}
}
}
view
class BookList extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocBuilder<HomeBloc, HomeState>(
builder: (context, state) {
final HomeBloc bloc = Provider.of<HomeBloc>(context);
print(bloc.displayList);
return ListView.builder(
shrinkWrap: true,
itemCount: bloc.displayList.length,
itemBuilder: (context, index) {
return Dismissible(
key: UniqueKey(),
background: Container(
color: selectedColor,
),
child: Container(
height: 120,
padding: const EdgeInsets.fromLTRB(20, 4, 20, 4),
child: BookCard(
book: bloc.displayList[index],
),
),
onDismissed: (DismissDirection direction) {
},
);
},
);
},
);
}
}
Problem
I've read some other discussion about bloc pattern and List view but the issue I'm facing here is different.
Every time my Filter event is called, bloc correctly generate a new displayList but, when BlocBuilder rebuild my UI, listview is not correctly updated.
The new filtered list is rendered but old results do not disappear. It seems like the new list is simply stacked on top of the old one.
In order to understand what was happening I tried printing the list that has to be rendered, inside the BlocBuilder before the build method is called.
The printed result was correct. In the console I see only the new filtered elements while in the UI I see both the new one and the old one, one below the other.
What am I missing here?
Thanks
Keep an intermediate event, eg. a ListInit for which you will display a CircularProgressIndicator. BlocBuilder will be stuck on previous state unless you update it over again.
So in your bloc, first yield the ListInit state and then perform filtering operations, and then yield your FilteredState.
Make new state for loading and yield before displayList.
if(event is FilterListByTextEvent) {
yield LoadFilterList();
displayList = displayList.where((book)
{
return
book.title.toLowerCase().contains(event.filterString.toLowerCase());
}).toList();
yield FilteredState();
}

How to run a function inside a child stateful widget from parent widget?

I am trying to run a function(with arguments) inside two-levels down StateFul widget, by clicking a button in the parent of the parent of that child(after having all widgets built, so not inside the constructor). just like in the image below:
More details is that I created a Carousal which has Cards inside, published here.
I created it with StreamBuilder in mind(this was the only use case scenario that I used it for so far), so once the stream send an update, the builder re-create the whole Carousal, so I can pass the SELECTED_CARD_ID to it.
But now I need to trigger the selection of the carousal's Cards programmatically, or in another word no need for two construction based on the snapshot's data like this:
return StreamBuilder(
stream: userProfileBloc.userFaviourateStream,
builder: (context, snapshot) {
if (snapshot.hasData) {
return SelectableCarousal(
selectedId: snapshot.data.toInt(),
onTap: //Update the stream
//some props...,
);
} else {
return SelectableCarousalLoading(
selectedId: null,
onTap: //Update the stream
//some props...,
);
}
},
);
But instead, I'm trying to have something like this so I can use it for others use cases:
Widget myCarousal = SelectableCarousal(
selectedId: null,
onTap: //Update the stream
//some props...,
);
return StreamBuilder(
stream: userProfileBloc.userFaviourateStream,
builder: (context, snapshot) {
// Then when data ready I can update
// the selection by calling two-level down function
if (snapshot.hasData) {
myCarousal.selectById(3);
}
// Build same carousal in all cases.
return myCarousal;
},
);
so this led me to my original question "How to run a function(with arguments) inside two-levels down StateFul widget?".
I appreciate any help. thanks a lot.
I was able to solve that challenge using the BLoC & Stream & StreamSubscription, see the image below:
Inside the Homepage screen:
///...Inside Homepage screen level-0
RaisedButton(
child: Text('Update value in the BLoC'),
onPressed: () {
bloc.changeSelectedState(isSel);
},
),
//...
inside the BLoC:
class Bloc {
final BehaviorSubject<bool> _isSelectedStreamController = BehaviorSubject<bool>();
// Retrieve data from stream
Stream<bool> get isSelectedStream => _isSelectedStreamController.stream;
// Add data to stream
Function(bool) get changeSelectedState => _isSelectedStreamController.sink.add;
void dispose() {
_isSelectedStreamController.close();
}
}
final bloc = Bloc();
Inside any widget in any level as long as it can reach the bloc:
// This inside the two-levels down stateful widget..
StreamSubscription isSelectedSubscription;
Stream isSelectedStream = bloc.isSelectedStream;
isSelectedSubscription = isSelectedStream.listen((value) {
// Set flag then setState so can show the border.
setState(() {
isSelected = value;
});
});
//...other code
#override
Widget build(BuildContext context) {
return Container(
decoration: isSelected
? BoxDecoration(
color: Colors.deepOrangeAccent,
border: Border.all(
width: 2,
color: Colors.amber,
),
)
: null,
//...other code
);
}
so the new design of my widget includes the BLoC as a main part of it, see the image:
and...works like a charm with flexible and clean code and architecture ^^