Call a method inside custom widget - flutter

I have created a custom widget. It comprises of read only TextFormField with suffixed IconButton, API, Alert Dialog and callback function
The widget can be in 2 states, set or reset.
One put the widget in set condition by IconButton on TextFormField, this will execute an API call and the returned data is displayed on TextFormField.
The widget is reset from the parent screens depending on some application requirement.
I have imported and used this custom widget in my various activities (screens).
Their
In my screen I wish clear my custom widget and I have created clear method.
I wish to know who will I call this clearWidget method.
If required I can clearWidget method to class GetTimeWidget extends StatefulWidget
enum TimeWidgetEvent { Start, Stop }
class GetTimeWidget extends StatefulWidget {
Ref<String> time;
final TimeWidgetEvent mode;
final String label;
const GetTimeWidget({
required this.time,
required this.mode,
required this.label,
Key? key,
}) : super(key: key);
#override
State<GetTimeWidget> createState() => _GetTimeWidgetState();
}
class _GetTimeWidgetState extends State<GetTimeWidget> {
final TextEditingController controller;
#override
Widget build(BuildContext context) {
return TextFormField(
controller: controller,
readOnly: true,
//initialValue: ,
decoration: InputDecoration(
label: Text(widget.label),
hintText: 'Please Get ${widget.label} from sever',
suffixIcon: TextButton.icon(
onPressed: () {
//Execute API to get time
},
icon: (widget.mode == TimeWidgetEvent.Start)
? const Icon(Icons.play_circle)
: const Icon(Icons.stop_circle),
label: (widget.mode == TimeWidgetEvent.Start)
? const Text('Start')
: const Text('Stop'),
),
border: const OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please Get ${widget.label} from server'; //Validation error
}
return null; //Validation Success
},
);
}
void clearWidget()
{
controller.clear();
//Execute API
}
}

I think you can't. because the state class is private, and every method in that class (_GetTimeWidgetState) cannot called externally.
If I correctly understand what you want to do, is to change the internal state of _GetTimeWidgetState outside from this widget.
I think you can't. My suggest is to use one of the state managers that you can find for flutter, like Riverpod (my choice), or Cubit, Get/Getx, etc...
In that manner you can read/change the internal state using the global state managed by the state manager.
For example, with Riverpod you can define a StateClass that handles your data:
final myProvider = StateNotifierProvider<MyStateNotifier, MyState>((ref) {
return MyStateNotifier("someInitialDataInfo");
});
class MyStateNotifier extends StateNotifier<MyState> {
MyStateNotifier("someInitialDataInfo") : super( MyState("someInitialDataInfo"));
void clear(String someDataInfo) { state = MyState( someDataInfo) ;}
}
#immutable
class MyState {
..... }
Then in your ComsumerState ( in Riverpod you should use ConsumerStatefulWidget and ConsumerState) you can watch the notifier as here:
class _GetTimeWidgetState extends ConsumerState<GetTimeWidget> {
final TextEditingController controller;
#override
Widget build(BuildContext context, WidgetRef ref) {
final myState = ref.watch(myProvider );
if ( myState.someDataInfo == 'Clicked Reset!!!!' ) {
controller.clear();
}
return TextFormField( .... );
}
.... } ...}
Now , observe that the build method will be called when the state inside the Notifier class would change. Thus you will be notified once per change.
Inside the StateNotifier class (the class you use to extend and to define your MyStateNotifier class) will do the following match to put your widget in the dirty-state:
state != oldState
That means that every time you change the internal state field, it will put your widget to the the dirty state , and thus it will be re builded.
the MyState class is defined as #immutable, so every state change cannot not be done with something like :
state.setMyField ( ' my value ' );
but will be done changing the state object itself:
state = MyState ( ... );
or with its copy method:
state = state.copyWith( .... ) ;
In this manner you avoid some side-effects ( the state should always be immutable )

Related

How do I set my state when the widget builds with riverpod?

To preface, I am completely new to riverpod so my apologies if I get some terminology wrong.
I have an edit feature that I'm implementing with riverpod that I'm migrating over to from flutter_blocs. Basically, with the bloc implementation, I am fetching my data from my server and when the widget builds, the BlocConsumer will emit an event to set my data from the server into my bloc state so that I can display and edit it in a TextInput.
Bloc implementation:
BlocConsumer<JournalBloc, JournalState>(
bloc: BlocProvider.of<JournalBloc>(context)
..add(
SetModelState(
title: journalEntry.title,
rating: journalEntry.rating.toDouble(),
),
),
builder: (context, state) {
return Column(
children: [
TextInput(
labelText: 'label',
value: state.editEntryTitle.value,
onChanged: (title) => context.read<JournalBloc>().add(EditJournalEntryTitleChanged(title: title))
)
],
);
}
Now with Riverpod, where I'm stuck on is I don't know how to set my values from my server into my state when the widget renders. I have my controller, and I have my providers.
My controller:
class EditJournalEntryController extends StateNotifier<EditJournalEntryState> {
final JournalService journalService;
EditJournalEntryController({required this.journalService})
: super(EditJournalEntryState());
void setSelectedJournalType(JournalType journalType) {
state = state.copyWith(selectedJournalType: journalType);
}
void setRating(int rating) {
state = state.copyWith(rating: rating);
}
}
final editJournalEntryController =
StateNotifierProvider<EditJournalEntryController, EditJournalEntryState>((ref) {
final journalService = ref.watch(journalServiceProvider);
return EditJournalEntryController(journalService: journalService);
});
My state:
class EditJournalEntryState implements Equatable {
final AsyncValue<void> value;
final JournalType? selectedJournalType;
final int? rating;
bool get isLoading => value.isLoading;
EditJournalEntryState({
this.selectedJournalType,
this.rating,
this.value = const AsyncValue.data(null),
});
EditJournalEntryState copyWith({
MealType? selectedJournalType,
int? rating,
AsyncValue? value,
}) {
return EditJournalEntryState(
selectedJournalType: selectedJournalType ?? this.selectedJournalType,
rating: rating ?? this.rating,
value: value ?? this.value,
);
}
#override
List<Object?> get props => [selectedJournalType, rating, value];
#override
bool? get stringify => true;
}
What I have tried
In my UI, I am referencing my controller and then using my notifier to set my state:
class EditJournal extends StatelessWidget {
const EditFoodJournal({
Key? key,
required this.journalEntry, // coming from server in a few parents above
}) : super(key: key);
final JournalEntry journalEntry;
#override
Widget build(BuildContext context) {
return Consumer(
builder: ((context, ref, child) {
final state = ref.watch(editJournalEntryController);
final notifier = ref.read(editJournalEntryController.notifier);
notifier.setSelectedJournalType(journalEntry.type)
notifier.setRating(journalEntry.rating)
But for obvious reasons I get this error:
At least listener of the StateNotifier Instance of 'EditFoodJournalEntryController' threw an exception
when the notifier tried to update its state.
The exceptions thrown are:
Tried to modify a provider while the widget tree was building.
If you are encountering this error, chances are you tried to modify a provider
in a widget life-cycle, such as but not limited to:
- build
- initState
- dispose
- didUpdateWidget
- didChangeDepedencies
Modifying a provider inside those life-cycles is not allowed, as it could
lead to an inconsistent UI state. For example, two widgets could listen to the
same provider, but incorrectly receive different states.
To fix this problem, you have one of two solutions:
- (preferred) Move the logic for modifying your provider outside of a widget
life-cycle. For example, maybe you could update your provider inside a button's
onPressed instead.
- Delay your modification, such as by encasuplating the modification
in a `Future(() {...})`.
This will perform your upddate after the widget tree is done building.
Ideally I want to render that value from state and not from the variable I have.
I feel like I've ran into a wall. Is there an ideal way of handling with this?
you are trying to modify a provider while the widget tree is building.
final notifier = ref.read(editJournalEntryController.notifier);
notifier.setSelectedJournalType(journalEntry.type)//here
notifier.setRating(journalEntry.rating)//here
this will cause the issue, you need to initialize the editJournalEntryController with an initial state you can do it by .family provider modifier
Edit:
Something like this
final editJournalEntryController = StateNotifierProvider.family<
EditJournalEntryController,
EditJournalEntryState,
JournalEntry>((ref, journalEntry) {
final journalService = ref.watch(journalServiceProvider);
return EditJournalEntryController(journalService: journalService,journalentry: journalEntry);
});
EditJournalEntryController will be
class EditJournalEntryController extends StateNotifier<EditJournalEntryState> {
final JournalService journalService;
final JournalEntry journalentry;
EditJournalEntryController({required this.journalService,required this.journalentry})
: super(EditJournalEntryState(selectedJournalType: journalentry.type,rating: journalentry.rating));
void setSelectedJournalType(String journalType) {
state = state.copyWith(selectedJournalType: journalType);
}
void setRating(int rating) {
state = state.copyWith(rating: rating);
}
}
will be called like
class EditFoodJournal extends StatelessWidget {
const EditFoodJournal({
Key? key,
required this.journalEntry, // coming from server in a few parents above
}) : super(key: key);
final JournalEntry journalEntry;
#override
Widget build(BuildContext context) {
return Consumer(builder: (context, ref, child) {
final state = ref.watch(editJournalEntryController(journalEntry));
return Container();
});
}
}

Flutter : my custom Switch widget does not respond to state change

I made a custom SwitchTile widget to avoid code duplication, which is based on a StatefulWidget with a boolean value so this is quiet simple :
class SwitchTile extends StatefulWidget
{
final String title;
final bool value;
final ValueChanged<bool> onChanged;
const SwitchTile({Key? key, required this.title, required this.value, required this.onChanged}) : super(key: key);
#override
State<StatefulWidget> createState() => _SwitchTileState();
}
class _SwitchTileState extends State<SwitchTile>
{
late bool value;
#override
void initState()
{
super.initState();
this.value = widget.value;
}
#override
Widget build(BuildContext context)
{
return ListTile(
title: Text(widget.title),
trailing: Switch.adaptive(
value: this.value,
onChanged: (newValue) {
widget.onChanged(newValue);
this.setState(() => this.value = newValue);
}
)
);
}
#override
void dispose()
{
super.dispose();
}
}
Now I want to synchronize some SwitchTile widget with another boolean value, but when the value changes it does not rebuild as I expected. I use flutter_bloc, the bloc/events/states work very well. See what I tried to do :
// See my Widget build() method ; bloc variable is declared above
BlocBuilder<ChadsVascBloc, ChadsVascState>(
builder: (BuildContext context, ChadsVascState state)
{
return Column(
children: [
// this works very well
ListTile(
title: Text("Switch : ageOver65 is ${state.ageOver65.toString()}"),
trailing: Switch.adaptive(
value: state.ageOver65,
onChanged: (value) => bloc.add(AgeOver65Toggled(ageOver65: value))
)
),
// this is does not
SwitchTile(
// the change is well triggered in the title property !!!
title: "SwitchTile : ageOver65 is ${state.ageOver65.toString()}",
value: state.ageOver65, // but this does not change the button's status
onChanged: (value) => bloc.add(AgeOver65Toggled(ageOver65: value))
)
]
);
}
),
SwitchTile(
title: AppLocalizations.of(context)!.ageOver65,
value: bloc.state.ageOver65,
onChanged: (value) => bloc.add(AgeOver65Toggled(ageOver65: value)),
)
When I push the last button, the first one work very well ("base" Switch widget) but the second one (custom SwitchTile widget) does not : the title changes but not the button's position.
What did I do wrong ?
So basically, you use the same event twice, that's why the bloc doesn't recognize any changes because you add the same event even it has the different value.
And i think you use the Bloc in the wrong way. Unlike provider and getx, main objective of Bloc is deliver the context with state. It was never meant to save some object inside it for so long.
For your case,
I assume you are trying to edit some kind of object. Use Bloc event to transfer the related object into another screen, in edit screen, Bloc will listen the state and parsing it to the local variabel that was assigned to the Switch (use BlocConsumer, listener to set state)
if u want to change the value of the variabel, go for it, and confirm it by some trigger like button then add another event to finish the edit
The problem seems to be : how to update properly a custom Switch widget ?
It does not seem to be about Bloc, because when I do that :
BlocBuilder<ChadsVascBloc, ChadsVascState>(
builder: (context, state) => SwitchTile(
title: "AgeOver65 is ${state.ageOver65.toString()}",
value: state.ageOver65,
onChanged: null
),
)
If I trigger a state change, ${state.ageOver65.toString()} changes but not the value of the Switch.
Solved : my error was not using BLoC but with bad state management structure. As a Switch widget does not have to manage its own state, my custom Switch widget should not try to changes its own state.
So it can be a simple StatelessWidget but used inside a parent StatefulWidget, or - I my case - in a BlocBuilder. I finally use a simple context.watch() as the value of my custom widget.

How is the class variable being updated without calling setstate?

How am I able to get the updated value of the TextFormField without using setState((){})?
On TextFormField 's onChanged method, I am setting the class variable to the value, but normally we would need setState((){}) for the value to be updated. I am just wondering how is this working?
class ResetPasswordPage extends StatefulWidget {
const ResetPasswordPage({ Key? key }) : super(key: key);
#override
_ResetPasswordPageState createState() => _ResetPasswordPageState();
}
class _ResetPasswordPageState extends State<ResetPasswordPage> {
String _currentPassword = '';
void onChanged(){
print('current Password is: ');
print(_currentPassword); ----------->this is printing the updated value, without setstate?
}
#override
Widget build(BuildContext context) {
return Container(
TextFormField(
onChanged: (val) => {
_currentPassword = val,
onChanged()
},
),
)
}
}
The variable value does change without the need to use setState, setState is only needed to tell Flutter that the UI has to change to reflect changes in your data.
From Flutter docs:
Calling setState notifies the framework that the internal state of this object has changed in a way that might impact the user interface in this subtree, which causes the framework to schedule a build for this State object.
You can create Notifier -> listener, then set the listener to listen for the Notifier,so any update on the Notifier(controller)it will notify the listener(TextFormField) to update itself.
Like this:
final _controller = TextEditingController();
child: TextFormField(
controller: _controller,
decoration: //decoration,
),
when you call this _controller.text it will notify all the listeners of this TextEditingController that needs update, then you get the same result setState((){}) will give you.

BlocBuilder not updating after change

I have a bloc to manage all the quotations in the application. The quotation class, bloc, and events are given below:
I have a form in which on selecting the text field, I show a list view to the user, and the value of the selected list view is assigned to the bloc and displayed in the text field.
Everything works fine but when I assign the value to the bloc variable and return it back to the form the text field value does update BUT ONLY FOR SINGLE TIME. If I do select some other list option for the same or another field the field value doesn't update.
CAN ANYONE SUGGEST A FIX?
I have a custom textField created as shown below and I'm calling this inside a bloc builder:
BlocBuilder<QuoteBloc, QuoteState>(builder: (context, state) {
if (state is QuoteInitialized) {
return Column(
children: [
BookingFormField(
labelText: "Flying From",
onTap: () => Navigator.push(
context,
AirportCityPlaceSelection.route(
'tq-fb-flight-from',
),
),
controller: TextEditingController(
text: BlocProvider.of<QuoteBloc>(context)
.quote
.flight
.flightFrom,
),
),
BookingFormField(
labelText: "Flying To",
onTap: () {
Navigator.push(
context,
AirportCityPlaceSelection.route(
'tq-fb-flight-to',
),
);
},
controller: TextEditingController(
text: BlocProvider.of<QuoteBloc>(context)
.quote
.flight
.flightTo,
),
),
],
);
}
}),
class BookingFormField extends StatelessWidget {
final Function onTap;
final TextEditingController controller;
final String labelText;
BookingFormField({
#required this.onTap,
#required this.controller,
#required this.labelText,
});
#override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.only(
top: 10.0,
bottom: 10.0,
),
child: TextField(
controller: controller,
readOnly: true,
onTap: () => onTap(),
style: Theme.of(context).textTheme.bodyText2.copyWith(
fontSize: 13.0,
fontWeight: FontWeight.w600,
color: Theme.of(context).primaryColor,
),
);
}
}
And this is how I'm updating the value in the list view which is a new screen:
BlocProvider.of<QuoteBloc>(context).quote.flight.flightFrom = value;
BlocProvider.of<QuoteBloc>(context).add(QuoteUpdated());
Navigator.pop(context);
Quote Class:-
part 'flight.dart';
part 'car.dart';
part 'cruise.dart';
part 'hotel.dart';
part 'visa.dart';
part 'insurance.dart';
part 'transfer.dart';
class Quote {
String name;
String contactNumber;
String email;
Flight flight;
Car car;
Hotel hotel;
Cruise cruise;
Transfer transfer;
Visa visa;
Insurance insurance;
// Constructors & other functions
}
The events related to the quote bloc are:
abstract class QuoteEvent extends Equatable {
List<Object> get props => [];
}
class QuoteUpdated extends QuoteEvent {
List<Object> get props => [];
}
The quote State is
abstract class QuoteState extends Equatable {
List<Object> get props => [];
}
class QuoteInitialized extends QuoteState {
final Quote quote;
QuoteInitialized({
#required this.quote,
});
List<Object> get props => [this.quote];
}
class QuoteSubmissionInProgress extends QuoteState {}
class QuoteSubmissionSuccessful extends QuoteState {}
class QuoteSubmissionFailed extends QuoteState {}
Quote Bloc:
class QuoteBloc extends Bloc<QuoteEvent, QuoteState> {
final Quote quote;
QuoteBloc(Quote quote)
: assert(quote != null),
this.quote = quote,
super(QuoteInitialized(quote: quote));
#override
Stream<QuoteState> mapEventToState(QuoteEvent event) async* {
if (event is QuoteUpdated) {
yield QuoteInitialized(quote: this.quote);
}
}
}
Don't update state in UI LAYER (send event to bloc)
Try to remove equatable in QuoteState or Add Equatable to Quote class
A guess is that the state is considered to be the same, meaning that the following times you expect updated fields you actually didn't get a new state. Have you verified that you get a new yielded state in the BlocBuilder?
My guess is based on two things. Firstly, that symptom could manifest in that way. Secondly I don't see methods in the Quote class that allow for equals comparison (maybe you have it where you commented out code).
I had a similar problem which gave me a headache. I was using a cubit and it won't display a progress bar because the loading state was not set. Since bloc extends cubit you might have the same problem. I had to put a future.delayed before emitting the SearchLoading() state. After this change, the state was set and the progress bar was shown. I had this problem in the debug mode of an Android app as well as in the release build.
class SearchCubit extends Cubit<SearchState> {
final ClubRepository _clubRepository = ClubRepository();
final log = getLogger("SearchCubit");
SearchCubit() : super(SearchInitial());
Future<void> getClubs() async {
try {
log.d("Fetch clubs");
await Future.delayed(Duration(microseconds: 1));
emit(SearchLoading());
final List<Club> clubs = await _clubRepository.fetch();
await Future.delayed(Duration(seconds: 2));
emit(SearchLoaded(clubs));
} catch (err, stacktrace) {
emit(SearchError("Retrieving data from API failed!"));
}
}
}
I'm guessing this is happening because your Quote class does not extend Equatable. Please refer to the FAQs for more information 👍

How to Retrieve value from DropdownFormField?

I used dropdownformfield to get the gender of user in the sign up page, I created the widget in other class, I wanna know how can I control the field or retrieve the value when it changes because it doesnt have a controller unlike textformfield.
I would suggest adding an onChanged callback that can be passed into the constructor of the class containing the dropdown field.
class DropdownField extends StatelessWidget {
final Function(dynamic) onChanged;
DropdownField({#required this.onChanged});
#override
Widget build(BuildContext context) {
return DropdownButtonFormField(
onChanged: this.onChanged,
);
}
}
Then when you instantiate the class, have it manipulate something on callback.
class _OtherClassState extends State<OtherClass> {
var _selectedItem;
#override
Widget build(BuildContext context) {
return DropdownField(
onChanged: (newItem) => setState(() => this._selectedItem = newItem),
);
}
}