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

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.

Related

Flutter: Is it possible to know if you're currently off stage?

I have a number of pages in my app wrapped in Offstage widgets. Each page makes use of the provider package to render based on state updates (e.g. the user does something, we make a network call and display the result).
As the pages are wrapped in Offstage widgets, the build() methods (and subsequent network calls) are called even if it's not the current page.
Is there a way inside the build() method to know if the widget is currently off stage (and if so, skip any expensive logic)?
I'm assuming I can work something with global state etc, but I was wondering if there was anything built-in in relation to the Offstage widget itself, similar to mounted
You can try finding the parent OffStage widget and see if the offstage property is true or false
#override
Widget build(BuildContext context) {
final offstageParent = context.findAncestorWidgetOfExactType<Offstage>();
if (offstageParent != null && offstageParent.offstage == false) {
// widget is currently offstage.
print('offstaged child');
} else {
// widget is not offstage
print('non-offstaged child');
}
return const Text('Example Widget');
}
I made a custom-made mechanism for the goal you wanna achieve:
First, I am declaring a new Map<String, bool> in a separate file alone that will hold the offStage bool value with the key of each class widget.
Map<String, bool> offStageMap = {};
then in the implementation of the StatefulWidget where the offstage widget is in:
class ExampleWidget extends StatefulWidget {
ExampleWidget({super.key}) {
widgetMapKey = runtimeType.toString();
}
late final String widgetMapKey;
#override
State<ExampleWidget> createState() => _ExampleWidgetState();
}
class _ExampleWidgetState extends State<ExampleWidget> {
final bool defaultIsOffStaged = false;
bool? localStateIsOffStages;
#override
void initState() {
offStageMap[widget.widgetMapKey] ??= defaultIsOffStaged;
super.initState();
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
bool previousIsOffStaged = offStageMap[widget.widgetMapKey]!;
setState(() {
localStateIsOffStages =
offStageMap[widget.widgetMapKey] = !previousIsOffStaged;
});
},
child: Offstage(
offstage: localStateIsOffStages ?? offStageMap[widget.widgetMapKey]!,
child: Container(),
),
);
}
} },
child: Offstage(
offstage: localStateIsOffStages ?? offStageMap[widget.widgetMapKey]!,
child: Container(),
),
);
}
}
let me explain what this is about.
first I declared a defaultIsOffStaged where it should be the initial offStage value when nothing is saved in that map.
when that widget is inserted in the widget tree (initState() called), the widget.widgetMapKey of the ExampleWidget widget will be saved in that map with the value of the default one which is defaultIsOffStaged.
offStageMap[widget.widgetMapKey] ??= defaultIsOffStaged;
in the offstage property o the OffStage widget, in this line:
offstage: localStateIsOffStages ?? offStageMap[widget.widgetMapKey]!,
the nullable localStateIsOffStages will be null for the first time since it has no value yet, so offStageMap[widget.widgetMapKey]! which equals to defaultIsOffStaged will be the bool value of offstage.
until now what we have, is a map containing the key that belongs only to the ExampleWidget which is its widget.widgetMapKey with its offStage value, right?
now from all places in your app, you can get the offStage value of that widget with its widgetMapKey like this:
print(offStageMap[ExampleWidget().widgetMapKey]); // true
now let's say you want to change the offstage property of that widget, in my code I used a simple example of GestureDetector, so when we tap in the Text("toggle offstage") area, it toggles offStage, here is what happens:
we got the existing value in the map:
bool previousIsOffStaged = offStageMap[widget.widgetMapKey]!;
then assign the opposite of it, to that widget key in the map, and the localStateIsOffStages bool variable which was nullable, now it has a value.
and as normal so the state updates I wrapped it in a SetState(() {})
now the widget's offstage will be toggled, and every time the widget key in the map will be updated with that new value.
the localStateIsOffStages I declared just to hold the local state when this is happening while the StatefulWidget state updates.
after the StatefulWidget is disposed of (when you pop the route as an example) and open that route again, the initState() will execute but since we have now an entry in the map, it's not null so nothing will happen inside initState().
the localStateIsOffStages will be null, so the offStage property of the Offstage widget will be the value from the map, which is the previous value before the widget is disposed.
that's it, from other places you can check for the offstage value of that specific widget like this:
print(offStageMap[ExampleWidget().widgetMapKey])
you can do it for all your widget pages, so you will have a map containing the offStage values of them all.
I take it one step up, and made those methods that I guess they will help:
this will return a List with the pages where the value is true.
List<String> offstagedPages() {
List<String> isOffStagedPages = [];
offStageMap.forEach((runtimeType, isOffStaged) {
if (isOffStaged) {
isOffStagedPages.add(runtimeType);
}
});
return isOffStagedPages;
}
this will return a true if a page is off staged and false if not:
bool isPageWidgetOffStaged(String runtimeType) {
if (offStageMap.containsKey(runtimeType)) {
return offStageMap[runtimeType]!;
}
return false;
}
Hope this helps a little.
Maybe it's not applicable to you, but you might be able to solve it by simply not using Offstage. Consider this app:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
MyApp({super.key});
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
bool showFirst = true;
void switchPage() {
setState(() {
showFirst = !showFirst;
});
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: Stack(children: [
Offstage(offstage: !showFirst,child: A("first", switchPage)),
Offstage(offstage: showFirst,child: A("second", switchPage)),
]))));
}
}
class A extends StatelessWidget {
final String t;
final Function onTap;
const A(this.t, this.onTap, {Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
print('$t is building');
return TextButton(onPressed: ()=> onTap(), child: Text(t));
}
}
You will notice by the prints that both pages are build. But if you rewrite it like this without Offstage, only the visible one is build:
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: Stack(children: [
if (showFirst) A("first", switchPage),
if (!showFirst) A("second", switchPage),
]))));
}
If you want to just keep state alive your pages , you can use https://api.flutter.dev/flutter/widgets/AutomaticKeepAliveClientMixin-mixin.html , you may check this blog for example usage, https://medium.com/manabie/flutter-simple-cheatsheet-4370a68f98b3
If you are using Navigator, you can just extends NavigatorObserver. Then you will get didpush and didpop, use state to manage elementlifecycle, you will get page onPause and onResume fun.

Is there any way to generate the same key for the "same" widget in Flutter without having any values to be based on

I have a widget which can have multiple input sections from the same type. When I delete the first child then it behaves weirdly like showing still the old value from that first widget. I figured out I need to use keys for the children but then my UX gets broken. Let me show you some code snippet, please:
class _ParentState extends State<ParentWidget> {
...
#override
Widget build(BuildContext context) {
return Column(
children: stateList
.mapIndexed((index, element) => ChildWidget(key: UniqueKey(), input: element, onChanged: (text) {
setState(() {
stateList[index].text = text;
});
}))
.toList(),
);
}
}
class _ChildState extends State<ChildWidget> {
final ctrl = TextEditingController();
#override
void initState() {
super.initState();
ctrl.text = widget.input.text;
}
#override
Widget build(BuildContext context) {
return Column(
children: [
TextField(
controller: ctrl,
onChanged: (value) {
widget.onChanged(value);
}
),
...
]
);
}
}
When I wrote the user experience is broken, I was meaning that whenever the user starts to type into the text field then the focus gets loosen character-by-character since the callback is invoked and then generates a new child widget from the state because it has now "different" key... How can I refactor this? I just need a key mechanism which results the same for a child while it is not disposed yet!?
TL;DR
Solution:
remove key
by default, flutter is smart enough to determine which widget is need to rebuild or not. Thats why, key is very rarely used.
ChildWidget(input: element,
onChanged: (text) {
setState(() {
....
When you set UniquieKey() you widget will have different key every time build method called.
which is: everytime you call SetState() your apps will build NEW widget.
if you want to set key, you can use index value
ChildWidget(
key: ValueKey('$index'),
input: element,
onChanged: (text) {
what caused your "user experience broken".
it because, in your onChange function, you call setState() and also you set UniqueKey to your widget. So.. when setState() has called, it will re-execute build method, and since your ChildWidget has Unique key, it will rebuild the widget.
every time user typing, ChildWidget is rebuild. that caused lost focus

Call a method inside custom widget

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 )

How to set state to another class ? - Flutter

I want to change the value of an integer in another class and rebuild this class so the bottom navigation bar items will change according to the integer
Here's the main class were the bottom navigation bar exists and the condition to hide or show the items :
class Home extends StatefulWidget {
static int showCard = 0;
#override
_HomeState createState() => _HomeState();
}
#override
Widget build(BuildContext context) {
...
//the items to show and hide according to int showcard
if (Home.showCard == 0)
BottomNavigationBarItem(
icon: Icon(
Icons.person_outlined,
),
title: Text(
'Profile',
),
),
if (Home.showCard != 0)
BottomNavigationBarItem(
icon: Icon(Icons.settings),
title: Text(
'Settings',
),
)
And here's the second class where i want to change the value of showcard to hide the profile section ad show the setting , i'm calling set state with on pressed in this class it's changing the value but not rebuilding the main class :
class Homehome extends StatefulWidget {
const Homehome({Key? key}) : super(key: key);
#override
_HomehomeState createState() => _HomehomeState();
}
class _HomehomeState extends State<Homehome> {
#override
Widget build(BuildContext context) {
...
child: RaisedButton(
onPressed: () {
setState(() {
Home.showCard = 1;
});
Navigator.of(context)
.push(MaterialPageRoute(
builder: (context) => Green(),
));
},
any suggestions ?
You can use callback functions:
have a look at this solution.
https://stackoverflow.com/a/59832932/16479524
upvote if helpful :)
You don't need to change the state. You should only update the state when you want to change something inside a widget, for instance a Text value or something like that.
Every time you change a value, it changes. If you want to change a 'visible value', then you have to change the state by setState method.
It sounds like you need to get a better understanding of state.
Widgets in Flutter live in a hierarchy. You should have a state in one widget, and then child widgets update based on this state. You can pass state changes down to child widgets using parameters.
Also, prefer to use proper data types for the situation. Don’t use a numerical data type like an int for showing or hiding a widget; use a bool (true or false).

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.