Maintain state between rebuilds - flutter

I have a StatefulWidget which is built from a list of fields.
It looks something like this:
class MultiFieldForm extends StatefulWidget {
/// constructor
const MultiFieldForm(this.fields);
/// Fields
final List<Field> fields;
#override
_MultiFieldFormState createState() => _MultiFieldForm();
}
class _MultiFieldFormState extends State<MultiFieldForm> {
/// constructor
_MultiFieldFormState();
/// Fields the user has minimized
List<Field> collapsedFields = [];
/// Fields the user has filled with the values
Map<Field,String> filledFields = {};
#override
Widget build(BuildContext context) {
// build code here
}
}
The state takes this list of fields and creates a form for each field with a text box to fill out, and a button to submit the whole thing.
Now this works, except if a new field is added to the form while the user is filling it out, and entirely new widget is built in the place of the old one. For the user this appear like their form has just been completely wiped. And indeed they have to start over again. This is not ideal, because I actually have new fields appearing all the time.
What I would like to do is have access to the old state when building the new state, so I can copy what the user has already filled and adjust it so that it fits the new form.
This is sort of what didUpdateWidget does, but as far as I can tell, I only get access to the old widget and not its state.
How can I transfer the state of the old widget to the new widget?

Related

Changes in Object from Second Screen is also Changing the Value in First Screen in flutter dart

A class Object passing from First Screen to the second Screen While on the second screen when the object changed its value, it's also changing the value on first screen.
Code of First Screen. (widget.templateModel is an object class that i am passing to the second screen)
Navigator.push(context,MaterialPageRoute(builder: (context) =>
EditEmojiTextTemplateScreen(templateModel: widget.templateModel,));
Code of Second Screen (On the second screen i am receiving the object and when i am changing the value of widget.templateModel it also changing the value on the first screen for a simple understandable code below i changed the value in initState while in the gif i am changing value in TextFormField)
class EditEmojiTextTemplateScreen extends StatefulWidget {
final TemplateModel templateModel;
EditEmojiTextTemplateScreen({
Key? key,
required this.templateModel,
}) : super(key: key);
#override
State<EditEmojiTextTemplateScreen> createState() =>
_EditEmojiTextTemplateScreenState();
}
class _EditEmojiTextTemplateScreenState
extends State<EditEmojiTextTemplateScreen> {
final SharedPreferences sharedPreferences = sl();
var txtNameController = TextEditingController();
var txtColorController = TextEditingController();
_EditEmojiTextTemplateScreenState();
#override
void initState() {
widget.templateModel.emoji[0].titleTwo = "kdfff"; //here i am changing the value and it also changing the value on first screen and i dont want this behavior of this object
super.initState();
}
Note: This is happening because of widget variable as mentioned in the documentation but i don't know how to prevent this behavior.
package:flutter/src/widgets/framework.dart
The current configuration.
A [State] object's configuration is the corresponding [StatefulWidget]
instance. This property is initialized by the framework before calling
[initState]. If the parent updates this location in the tree to a new
widget with the same [runtimeType] and [Widget.key] as the current
configuration, the framework will update this property to refer to the
new widget and then call [didUpdateWidget], passing the old
configuration as an argument.
Now I see what you are trying to do.
You could initialize a NEW istance of TemplateModel in the InitState of the second screen.
Then, set the new object's properties like this (or write a cleaner method to do that):
newObject.property1 = oldObject.property1;
newObject.property2 = oldObject.property2;
...
Once the user presses the save button, change oldObject's properties again, so that the first page updates.
You might want to take a look at state management to better understand how to approach this kind of problems.
As the other answer suggests, take a look at state management solutions.
Also keep the models immutable by creating them with final fields. Then to modify, create new instances via copyWith()
Please update you code after navigation.then method
template = snapshot.data;

How to get the State<> instance inside of its StatefulWidget?

I have an unusual use case where I'd like to add a getter to a StatefulWidget class that accesses its State instance. Something like this:
class Foo extends StatefulWidget {
Foo({super.key});
int get bar => SomethingThatGetsFooState.bar;
#override
State<Foo> createState() => _FooState();
}
class _FooState extends State<Foo> {
int bar = 42;
#override
Widget build(BuildContext context) {
return Container();
}
}
Does SomethingThatGetsFooState exist?
I wonder, if your approach is the right way.
Flutter's way isn't 'Ask something about its state'
Flutter is much more like this: 'The consumer of a Widget passes something to another Widget, which the other Widget e.g. calls in case of certain situations (e.g. value change).'
Approach 1
You map pass a Callback Function to Foo and pass that along to _FooState.
If something special happens inside _FooState, you may call the callback and thus pass some value back to the provider of the Callback.
Approach 2
Probably use a state management solution like Flutter Redux. Using Flutter Redux, you establish a state store somewhere at the top of the widget tree, e.g. in MaterialApp.
Then you subscribe to the store at certain other locations, where dependent widgets exist, which need to update accordingly.
In one project I created certain Action classes, which I send to certain so called reducers of those states, to perform a change on the state:
StoreProvider.of<EModel>(context).dispatch( SaveToFileAction())
This call finds the relevant EModel state and asks it to perform the SaveToFileAction().
This way, a calling Widget not even needs to know, who is responsible for the Action.
The responsible Widget can even be moved around the widget tree - and the application still works. The initiator of SaveToFileAction() and the receiver are decoupled. The receiver you told a coordination 'Tell me, if someone tried to ask me for something.'
Could your provide some further details? Describe the usage pattern?
#SteAp is correct for suggesting there's a code smell in my OP. Typically there's no need to access State thru its StatefulWidget. But as I responded to his post, I'm fleshing out the first pass at a state management package, so it's an unusual case. Once I get it working, I'll revisit.
Below worked without Flutter complaining.
class _IntHolder {
int value;
}
class Foo extends StatefulWidget {
Foo({super.key});
final _intHolder = _IntHolder();
int get bar => _intHolder.value;
#override
State<Foo> createState() => _FooState();
}
class _FooState extends State<Foo> {
int value = 42;
#override
Widget build(BuildContext context) {
widget._intHolder.value = value;
return Container();
}
}

reusable classes in flutter

I have a page like the following. This page has two buttons. One of them is sending the form to the relevant place. In my case to the instructor. But, this one is not important for now. The second button is the important one. Anyway, the second one is saving the form as a draft. So, I know I need to save the data that is entered, in SQLite. Then when he/she want to edit the form again I need to pull the data from SQLite then fill the relevant places. Here is the question. When I click to edit button. The page below will show up with the content that is filled before. However, for this task, it does not make sense to code again the screen below for the edit button. So I need to reuse the page below. How can I do it? Any idea. Is there any content that could be helpful? Please attach a link, video, etc. It doesn't matter. I just want to learn how to do it. I hope I could explain the situation. If any more information is required to understand the situation feel free to ask for it.
There are many ways to achieve what you need. But basically, you need to check whether there is saved data or not? If there is saved data then show it otherwise show an empty page. Simple example:
class MyPageData {
String firstField;
String secondField;
// ... fields with page info
}
class MyFormPage extends StatefulWidget {
#override
State<StatefulWidget> createState() => _MyForPageState();
}
class _MyForPageState extends State<MyFormPage>{
MyPageData _savedData;
#override
void initState() {
super.initState();
_savedData = loadDataFromDb();
}
#override
Widget build(BuildContext context) {
String myFirstField = "";
String mySecondField = "";
// other fields
if (_savedData != null){
myFirstField = _savedData.firstField;
mySecondField = _savedData.secondField;
// other fields
}
// render page with fields values above
}
}
Here MyPageData is a model of all the data on your page. So it's easier to work with. Also, this data is saved to db and restored from db in the future.
MyFormPage is a stateful widget for your form page. There is a loadDataFromDb method that is used to load saved data. Then in the build method, we check whether there is saved data or not. If there is data we filled initial values for all fields on our page and use those fields as initial values for the widgets from which our page is constructed. So if there is no data saved then _savedData will be null and all widgets will have empty initial values.
If you put your page in a widget like this
class MyPage extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
body: // your page here
)
}
}
you can open it in different locations.
_onButton1Click(BuildContext context){
// do something button 1 specific
final route = MaterialPageRoute(builder: (context) => MyPage());
Navigator.of(context).push(route);
}
_onButton2Click(BuildContext context){
// do something button 2 specific
final route = MaterialPageRoute(builder: (context) => MyPage());
Navigator.of(context).push(route);
}

Can a StatelessWidget contain member variables?

I have a StatelessWidget that uses a ScopedModel to store its data. The widget basically presents a list of checkboxes and some buttons to save the state of the checked boxes.
Now I want to keep track of if the user has altered any of the checkboxes, i.e. checked/unchecked them since the widget was displayed. So I added something like this:
class MyCheckboxScreen extends StatelessWidget{
bool _hasBeenModified = false;
void _itemCheckedChange(bool checked, MyModel model){
_hasBeenModified = true;
// Code to change the model here
}
void _onCloseButton(){
if( _hasBeenModified ){
// Show a warning that there are modifications that will not be be saved
}
}
void _onSaveButton(Context context, MyModel model){
model.save();
Navigator.of(context).pop();
}
}
This seems to work, but my StatelessWidget now contains its own state.
The state isn't used to update the UI and redraw anything, it's only used to check if there are modifications to any checkbox when pressing the "Close" button.
Is it safe for a StatelessWidget to have this kind of internal state? Or could it be a problem? For example, could the widget be recreated unexpectedly, and the internal state lost?
I don't find the documentation to be very clear on this, it just says
For compositions that can change dynamically, e.g. due to having an internal clock-driven state, or depending on some system state, consider using StatefulWidget.
But this sounds like it only applies to state that affects the UI and requires rebuilding the widget.
Yes, a StatelessWidget can have mutable variables (your code compiles) but that's wrong.
A widget that does not require mutable state
This is taken from the documentation. Even if you have a single non-final variable, it means that something can actually be changed in your widget. It's not an immutable class. Ideally, you should use StatelessWidgets like this:
class MyWidget extends StatelessWidget {
final int a;
final String b;
const MyWidget(this.a, this.b);
}
Or something similar such as
class MyWidget extends StatelessWidget {
const MyWidget();
}
When you have non final variables, use a StatefulWidget. Your class has to clearly be a StatefulWidget:
// This is not final. It can be changed (and you will change it)
// so using the stateless way is wrong
bool _hasBeenModified = false;
void _itemCheckedChange(bool checked, MyModel model){
_hasBeenModified = true;
}
The UI doesn't matter actually because here's a matter of variables and mutability. Something is changing (bool _hasBeenModified) so it cannot be a StatelessWidget because it has to be used in all those cases where the state is immutable.
Are there any reasons why you don't use a stateful widget? Stateless widgets aren't intended to be used that way.. And without any benefits, I don’t understand why you overcomplicate things..

Nested Flutter Futurebuilder is not updating the view

I have a Stateful widget that I am displaying inside a stateless widget. I have reused this Widget in several parts of my application, where it worked.
The widget looks a somethin like this
class InnerWidget extends StatefulWidget {
// private keys for objects in the remote dataahase
List<int> privateKeys;
const InnerWidgt({
Key key,
this.privateKeys,
}) : super(key: key);
#override
_InnerWidgetState createState() => _InnerWidgetState();
}
class _InnerWidgetState extends State<InnerWidget> {
// data retrieved from the database
Future<List<SomeClass>> data;
#override
void initState() {
this.data = this.fetchData(widget.privateKeys);
super.initState();
}
#override
Widget build(BuildContext context) {
return FutureBuilder<List<SomeClass>> (
future: this.data,
builder: (BuildContext contest, AsyncSnapshot<List<SomeClass>> snapshot) {
if(snapshot.hasData){
// return a widget (a list of names in this case)
}
return Text('Loading...');
}
);
}
Future<List<SomeClass>> fetchData(List<int> pKeys){
// fetches the JSON objects from the server and returns them as instances
}
}
The widget is created and given a list of keys, it makes a call to the remote API to get some objects and displays them in a list. I thought it might be nice that this widget is kind of handling itself, but maybe it's bad design?
What happens, is that the fetchData() method is never called and the "Loading..." is displayed forever. However snapshot.hasData does return true, so I'm thinking this is some kind of repainting issue.
The view I am talking about, where this is used is a detail view, which you navigate to from a list (ListView). I am using this exact same InnerWidget in the the elements inside the ListView, where it works perfectly fine. But when I click on the item to navigate to the detail page, the same InnerWidget does not work. (I know fetching the same data twice is not good, I have that in the back of my head to change it, and there's already a Cache underneath). Maybe this has something to do with the preserved state in the ElementTree? That because I am using the same InnerWidgit, it's preserving its state and not rerendering?
I hope I phrased my question well enough, I did not want to throw hundreds of lines of code at you, that's why created this minimal example of the widget at least. I can add the list and navigation part too if needed, but maybe this is something, that somebody that has worked more with Flutter, knows right away, I'm only 3 weeks in.