Flutter BLoC being recreated - dart

I'm discovering Flutter and the bloc pattern and to practice I'm making an app about pizzas.
I am using a BlocProvider to access the blocks. It is from the generic_bloc_provider package. It is a basic implementation using an InheritedWidget combined with a StatelessWidget.
I have a page with two editable textfields, for the name and price of the pizza I want to create. It is backed by a bloc.
Here's the code :
AddPizzaPage.dart :
class AddPizzaPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
print("Building AddPizzaPage");
return Scaffold(
appBar: AppBar(
title: Text("Adding Pizza"),
),
body: BlocProvider(
bloc: AddPizzaBloc(),
child: ModifyPizzaWidget(),
),
);
}
}
ListPage.dart:
class ModifyPizzaWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
final addPizzaBloc = BlocProvider.of<AddPizzaBloc>(context);
return Container(
margin: EdgeInsets.all(16.0),
child: Column(
children: <Widget>[
TextField(
decoration: InputDecoration(hintText: "Nom de la pizza"),
onChanged: (name) {
addPizzaBloc.pizzaNameSink.add(name);
},
),
TextField(
decoration: InputDecoration(hintText: "Prix de la pizza"),
keyboardType: TextInputType.number,
onChanged: (price) {
addPizzaBloc.pizzaPriceSink.add(price);
},
),
IconButton(
icon: Icon(Icons.check),
iconSize: 40,
onPressed: () {
addPizzaBloc.evenSink.add(AddPizzaEvent.VALIDATE);
Navigator.of(context).pop();
},
)
],
),
);
}
}
AddPizzaBloc.dart :
enum AddPizzaEvent {
VALIDATE
}
class AddPizzaBloc extends Bloc {
final _pizza = Pizza.empty();
final _pizzaSubject = BehaviorSubject<Pizza>();
final _repository = PizzaRepository();
Sink<String> get pizzaNameSink => _pizzaNameController.sink;
final _pizzaNameController = StreamController<String>();
Sink<String> get pizzaPriceSink => _pizzaPriceController.sink;
final _pizzaPriceController = StreamController<String>();
Sink<AddPizzaEvent> get evenSink => _eventSink.sink;
final _eventSink = StreamController<AddPizzaEvent>();
AddPizzaBloc() {
print("Created");
_pizzaNameController.stream.listen(_addPizzaName);
_pizzaPriceController.stream.listen(_addPizzaPrice);
_eventSink.stream.listen(_onEventReceived);
}
dispose() {
print("Disposed");
_pizzaSubject.close();
_pizzaNameController.close();
_pizzaPriceController.close();
_eventSink.close();
}
void _addPizzaName(String pizzaName) {
_pizza.name = pizzaName;
print(_pizza);
}
void _addPizzaPrice(String price) {
var pizzaPrice = double.tryParse(price) ?? 0.0;
_pizza.price = pizzaPrice;
print(_pizza);
}
void _onEventReceived(AddPizzaEvent event) {
print("Received $event");
if (event == AddPizzaEvent.VALIDATE) {
print(_pizza);
_repository.addPizza(_pizza);
}
}
}
My issue is that I store the Pizza being built inside the block but the widget is rebuilt, and so the bloc is rebuilt and I lose the state.
The full code is available on gitlab
I don't know how to use the bloc to power the addPizza form.

This happens because you're creating the instance of your BLoC within the build method:
BlocProvider(
bloc: Bloc(),
child: ...
)
The consequence is that any rebuild would not reuse the previous instance (with some awful memory leaks too).
The solution would be to make a StatefulWidget and create that BLoC instance within initState, followed by a dispose override to clean things.
But since you're using a package already, you can use provider instead. It is a popular alternative that does everything listed above.
As such your BlocProvider usage becomes:
StatefulProvider(
valueBuilder: (_) => AddPizzaBloc(),
dispose: (_, bloc) => bloc.dispose(),
child: // ...
),
then obtained as such:
Provider.of<AddPizzaBloc>(context);

Related

Flutter - update form state from validator

I have a form with some inputs. I am using a GlobalKey<FormState> to handle submissions and validation and so on.
One of the fields is supposed to take a double input, so I validate that by trying to parse the input value to double like so :
return TextFormField(
decoration: InputDecoration(labelText: 'Price'),
keyboardType: TextInputType.number,
validator: (String value) {
double _parsedValue = double.tryParse(value);
if (_parsedValue == null) {
return "Please input a number";
}
},
onSaved: (String value) {
setState(() {
_price = double.parse(value);
});
},
);
Now that works as expected. However, if the user inputs for example 9,99 that would fail, because the parse expects 9.99 .
What I'm trying to do is, when the validator is called, I'd like to check the input string for any commas, and then if they are present, replace them with dots instead, and update the form value accordingly.
My question is - can we actually update the form state from within validators?
I think maybe what you need is a TextInputFormatter.
Here is a link to the docs https://docs.flutter.io/flutter/services/TextInputFormatter-class.html
There are pre-existing formatters you can use as a reference to convert comma's to dots.
I don't think you need to update the state in the validator. I would use only the save event to update the state. This way it gets very clear where the state is updated.
I believe nothing forbids you to update the state in the validate, but maybe it would get less organized. :)
Solution that do not exactly answer your question
I guess the best way to accomplish what you need would be using a TextInputFormatter with a WhitelistingTextInputFormatter, check it out:
Note the TextInputType.numberWithOptions(decimal: true) and that if the user pastes "-100,00" , it would become 100.0 - which for a price would be fine, but not for double values in general.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'ValidatorState',
theme: ThemeData(primarySwatch: Colors.yellow),
home: MyFormPage(),
);
}
}
class MyFormPage extends StatefulWidget {
#override
_MyFormPageState createState() => _MyFormPageState();
}
class _MyFormPageState extends State<MyFormPage> {
final _formKey = GlobalKey<FormState>();
double _price;
void _save() {
if (_formKey.currentState.validate()) {
_formKey.currentState.save();
Scaffold.of(_formKey.currentContext)
.showSnackBar(SnackBar(content: Text('New price defined! ($_price)')));
}
}
Widget _buildForm(BuildContext context) {
return Container(
padding: EdgeInsets.all(10.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
TextFormField(
inputFormatters: <TextInputFormatter>[
WhitelistingTextInputFormatter(RegExp("[0-9.]"))
],
decoration: InputDecoration(labelText: 'Price'),
keyboardType: TextInputType.numberWithOptions(decimal: true),
validator: (String value) {
double _parsedValue = double.tryParse(value);
if (_parsedValue == null) {
return "Please input a valid number";
}
if (_parsedValue == 0.0) {
return "Please input a valid price";
}
},
onSaved: (String value) {
setState(() {
_price = double.tryParse(value);
});
},
),
Text(""),
RaisedButton(
child: Text("Save"),
color: Theme.of(context).primaryColor,
textColor: Theme.of(context).primaryTextTheme.title.color,
onPressed: _save,
),
Text(""),
TextFormField(
decoration: InputDecoration(labelText: 'Copy and Paste area'),
),
],
),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Validator State"),
),
body: Form(
key:_formKey,
child: _buildForm(context),
),
);
}
}
Solution that answers your question
However, that is not exactly what you described. You want to automatically replace , to .. I would avoid doing that, as 1,234.56 would translate to 1.234.56, which is invalid. If you only strip out the commas, you end up with 1234.56 which is valid.
If you really want to do as you said, you have to use a TextEditingController and a function to normalize the text data. I've made the example below, check it out - specially the _priceController and the _parsePrice.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'ValidatorState',
theme: ThemeData(primarySwatch: Colors.yellow),
home: MyFormPage(),
);
}
}
class MyFormPage extends StatefulWidget {
#override
_MyFormPageState createState() => _MyFormPageState();
}
class _MyFormPageState extends State<MyFormPage> {
final _formKey = GlobalKey<FormState>();
TextEditingController _priceController;
double _price;
#override
void initState() {
super.initState();
_priceController = TextEditingController();
}
#override
void dispose() {
_priceController?.dispose();
super.dispose();
}
void _save() {
if (_formKey.currentState.validate()) {
_formKey.currentState.save();
Scaffold.of(_formKey.currentContext)
.showSnackBar(SnackBar(content: Text('New price defined! ($_price)')));
}
}
double _parsePrice(String text) {
var buffer = new StringBuffer();
text.runes.forEach((int rune) {
// acceptable runes are . or 0123456789
if (rune == 46 || (rune >= 48 && rune <= 57)) buffer.writeCharCode(rune);
// if we find a , we replace with a .
if (rune == 44) buffer.writeCharCode(46);
});
return double.tryParse(buffer.toString());
}
Widget _buildForm(BuildContext context) {
return Container(
padding: EdgeInsets.all(10.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
TextFormField(
controller: _priceController,
decoration: InputDecoration(labelText: 'Price'),
keyboardType: TextInputType.numberWithOptions(decimal: true),
validator: (String value) {
double _parsedValue = _parsePrice(value);
if (_parsedValue == null) {
return "Please input a valid number";
}
if (_parsedValue == 0.0) {
return "Please input a valid price";
}
},
onSaved: (String value) {
setState(() {
_price = _parsePrice(value);
_priceController.text = _price.toString();
});
},
),
Text(""),
RaisedButton(
child: Text("Save"),
color: Theme.of(context).primaryColor,
textColor: Theme.of(context).primaryTextTheme.title.color,
onPressed: _save,
),
],
),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Validator State"),
),
body: Form(
key:_formKey,
child: _buildForm(context),
),
);
}
}
hi did you get a fix for this?
I would rethink your strategy for this issue.
Maybe what you need is an observer function that is triggered when the user typing, which then looks at the comma and changes it to a dot.
TextFormField has a built in function,
onEditingCompleted and onFieldSubmitted which can run the function you have to make the check before the validate is run.

Make cards with texts and buttons dynamically

I'm making Notes app. I made cards with text and buttons dynamically (Create by clicking the button). But I have problem with Changing Text on CURRENT card. For example, I have 3 cards with own texts and buttons and I want to change text on 2nd card but text is changing on the 3rd card. How can I solve this problem?
3 cards with texts and buttons
Change Text Page
In the past, I've tried making list to collect texts, but i dont know how to identify current card.
full main.dart
import 'package:flutter/material.dart';
import './changeTextPage.dart';
int count = 0;
String titlecard = '';
String textcard = '';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Notes',
theme: ThemeData(
primarySwatch: Colors.deepPurple
),
home: HomePage(title: 'Notes',),
);
}
}
class HomePage extends StatefulWidget {
HomePage({Key key, this.title}) : super(key: key);
final title;
#override
HomePageState createState() => HomePageState();
}
class HomePageState extends State<HomePage> {
#override
Widget build(BuildContext context) {
List<Widget> cards = new List.generate(count, (int i) => new MyCard());
return Scaffold(
appBar: AppBar(
title: Text('Notes'),
),
body: LayoutBuilder(
builder: (context, constraint) {
return Column(
children: <Widget>[
Container(
height: 650.0,
child: new ListView(
children: cards,
scrollDirection: Axis.vertical,
),
),
],
);
}
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
setState(() {
Navigator.push(context, MaterialPageRoute(
builder: (context) => changeText())
);
});
},
),
);
}
}
class MyCard extends StatefulWidget {
#override
myCard createState() => myCard();
}
class myCard extends State<MyCard> {
int myCount = count;
void click() {
setState(() {
Navigator.push(context, MaterialPageRoute(
builder: (context) => setNewText())
);
});
}
#override
Widget build(BuildContext context) {
return Center(
child: Card(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ListTile(
leading: Icon(Icons.album),
title: Text(titlecard),
subtitle: Text(textcard),
),
ButtonTheme.bar( // make buttons use the appropriate styles for cards
child: ButtonBar(
children: <Widget>[
FlatButton(
child: const Text('Change Text'),
onPressed: click,
),
FlatButton(
child: const Text('LISTEN'),
onPressed: () { /* ... */ },
),
],
),
),
],
),
),
);
}
}
class setNewText extends StatefulWidget {
#override
SetNewText createState() => SetNewText();
}
class SetNewText extends State<setNewText> {
final titleController = TextEditingController();
final textController = TextEditingController();
final formkey = GlobalKey<FormState>();
void _submit() {
setState(() {
if (formkey.currentState.validate()) {
formkey.currentState.save();
Navigator.pop(context);
titlecard = titleController.text;
textcard = textController.text;
}
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Change Title'),
),
body: Column(
children: <Widget>[
Card(
child: Padding(
padding: EdgeInsets.all(2.0),
child: Form(
key: formkey,
child: Column(
children: <Widget>[
TextFormField(
controller: titleController,
decoration: InputDecoration(
labelText: 'Title'
),
validator: (value) => value.length < 1 ? 'Invalid Title' : null,
onSaved: (value) => value = titleController.text,
),
TextFormField(
controller: textController,
decoration: InputDecoration(
labelText: 'Text'
),
validator: (text) => text.length < 1 ? 'Invalid Text' : null,
onSaved: (text) => text = textController.text,
)
],
),
),
),
),
FlatButton(
textColor: Colors.deepPurple,
child: Text('SUBMIT'),
onPressed: _submit,
),
],
)
);
}
}
changeTextPage.dart
import 'package:flutter/material.dart';
import './main.dart';
class changeText extends StatefulWidget {
#override
ChangeText createState() => ChangeText();
}
class ChangeText extends State<changeText> {
myCard s = myCard();
final titleController = TextEditingController();
final textController = TextEditingController();
final formkey = GlobalKey<FormState>();
void _submit() {
setState(() {
if (formkey.currentState.validate()) {
formkey.currentState.save();
Navigator.pop(context);
count++;
titlecard = titleController.text;
textcard = textController.text;
}
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Change Title'),
),
body: Column(
children: <Widget>[
Card(
child: Padding(
padding: EdgeInsets.all(2.0),
child: Form(
key: formkey,
child: Column(
children: <Widget>[
TextFormField(
controller: titleController,
decoration: InputDecoration(
labelText: 'Title'
),
validator: (value) => value.length < 1 ? 'Invalid Title' : null,
onSaved: (value) => value = titleController.text,
),
TextFormField(
controller: textController,
decoration: InputDecoration(
labelText: 'Text'
),
validator: (text) => text.length < 1 ? 'Invalid Text' : null,
onSaved: (text) => text = textController.text,
)
],
),
),
),
),
FlatButton(
textColor: Colors.deepPurple,
child: Text('SUBMIT'),
onPressed: _submit,
),
],
)
);
}
}
Okay, so you happen to make some common mistakes, one of which is critical.
most importantly don't use global variables! As you do with count, titlecard and textcard.
there is a practice to name stateful widgets with PascalCase and corresponding states just like the widget but prefixed with an underscore (_) to make it private and suffixed by the State word.
The correct approach for this (or one of them) would be to have a widget that would be your screen with a form to edit stuff and it would pop some struct with user values on submit:
class ChangeTextScreen extends StatefulWidget {
_ChangeTextScreenState createState() => _ChangeTextScreenState();
}
class _ChangeTextScreenState extends State<ChangeTextScreen> {
void _submit() {
setState(() {
formkey.currentState.save();
Navigator.pop(ChangeTextResult(title: titleController.text, text: textController.text));
});
}
// Rest of your code...
}
class ChangeTextResult {
final String title;
final String text;
ChangeTextResult({#required this.title, #required this.text});
}
You should also have a place where you store your notes in some kind of a list. Your main screen looks like a good place for it. Once your app will be bigger, think about using scoped_model or Redux or something.
So let's add a Note class and a list with your notes to your main screen:
class Note {
String title;
String text;
Note(this.title, this.text);
}
class HomePageState extends State<HomePage> {
List<Note> _notes = [Note('Test', 'Some test note')];
#override
Widget build(BuildContext context) {
ListView cards = ListView.builder(
itemCount: _notes.length,
itemBuilder: (context, index) => MyCard(
title: _notes[index].title,
text: _notes[index].text,
onEdit: (title, text) => setState(() { // We'll get back to that later
_notes[index].title = title;
_notes[index].text = text;
})
));
// (...)
Your MyCard widget (try to use better names next time) should contain some kind of information about its content, one of the best approaches would be to pass this info to its constructor, just like that:
class MyCard extends StatefulWidget {
final String title;
final String text;
final Function onEdit;
MyCard({Key key, #required this.title, #required this.text, #required this.onEdit}) : super(key: key);
#override
_MyCardState createState() => _MyCardState();
}
Having this Key parameter is a good practice.
And use those parameters in your _MyCardState class (I renamed it from _myCard):
// (...)
children: <Widget>[
ListTile(
leading: Icon(Icons.album),
title: Text(widget.title),
subtitle: Text(widget.text),
),
// (...)
Returning to the moment where you open your ChangeTextScreen, you should assign the result of Navigation.push() to a variable. This is your result, you can deal with it (once we check it for null, the user could have returned from this screen and then the result would be null).
void click() {
setState(() {
final ChangeTextResult result = Navigator.push(context, MaterialPageRoute(
builder: (context) => ChangeTextScreen())
);
if (result != null) {
widget.onEdit(result.title, result.text);
}
});
}
Do you remember that onEdit parameter (I mentioned it in a comment in the code above)? We call that parameter here.
That's it I think. I could have mixed some concepts of your app, but I think you'll manage to get my point anyways.
I quite rewrote all of your code. I think it will be easier for you to start again from scratch and have those tips in mind. Also, try to Google some similar things (like a simple Todo application) or do Getting started from flutter.io with part two! That should give you a nice idea on how to resolve that common problem in Flutter.
And also, read about good practises in Flutter and Dart. Things like correctly formatting your code are really important.
BTW that's my longest answer on Stack Overflow so far. I hope you'll appreciate that.

Passing data between screens in Flutter

As I'm learning Flutter I've come to navigation. I want to pass data between screens similarly to passing data between Activities in Android and passing data between View Controllers in iOS. How do I do it in Flutter?
Related questions:
The best way to passing data between widgets in Flutter
Flutter pass data between widgets?
Flutter/ How to pass and get data between Statefulwidget
This answer will cover both passing data forward and passing data back. Unlike Android Activities and iOS ViewControllers, different screens in Flutter are just widgets. Navigating between them involves creating something called a route and using the Navigator to push and pop the routes on and off the stack.
Passing data forward to the next screen
To send data to the next screen you do the following things:
Make the SecondScreen constructor take a parameter for the type of data that you want to send to it. In this particular example, the data is defined to be a String value and is set here with this.text.
class SecondScreen extends StatelessWidget {
final String text;
SecondScreen({Key key, #required this.text}) : super(key: key);
...
Then use the Navigator in the FirstScreen widget to push a route to the SecondScreen widget. You put the data that you want to send as a parameter in its constructor.
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SecondScreen(text: 'Hello',),
));
The full code for main.dart is here:
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(
title: 'Flutter',
home: FirstScreen(),
));
}
class FirstScreen extends StatefulWidget {
#override
_FirstScreenState createState() {
return _FirstScreenState();
}
}
class _FirstScreenState extends State<FirstScreen> {
// this allows us to access the TextField text
TextEditingController textFieldController = TextEditingController();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('First screen')),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.all(32.0),
child: TextField(
controller: textFieldController,
style: TextStyle(
fontSize: 24,
color: Colors.black,
),
),
),
RaisedButton(
child: Text(
'Go to second screen',
style: TextStyle(fontSize: 24),
),
onPressed: () {
_sendDataToSecondScreen(context);
},
)
],
),
);
}
// get the text in the TextField and start the Second Screen
void _sendDataToSecondScreen(BuildContext context) {
String textToSend = textFieldController.text;
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SecondScreen(text: textToSend,),
));
}
}
class SecondScreen extends StatelessWidget {
final String text;
// receive data from the FirstScreen as a parameter
SecondScreen({Key key, #required this.text}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Second screen')),
body: Center(
child: Text(
text,
style: TextStyle(fontSize: 24),
),
),
);
}
}
Passing data back to the previous screen
When passing data back you need to do the following things:
In the FirstScreen, use the Navigator to push (start) the SecondScreen in an async method and wait for the result that it will return when it finishes.
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SecondScreen(),
));
In the SecondScreen, include the data that you want to pass back as a parameter when you pop the Navigator.
Navigator.pop(context, 'Hello');
Then in the FirstScreen the await will finish and you can use the result.
setState(() {
text = result;
});
Here is the complete code for main.dart for your reference.
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(
title: 'Flutter',
home: FirstScreen(),
));
}
class FirstScreen extends StatefulWidget {
#override
_FirstScreenState createState() {
return _FirstScreenState();
}
}
class _FirstScreenState extends State<FirstScreen> {
String text = 'Text';
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('First screen')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.all(32.0),
child: Text(
text,
style: TextStyle(fontSize: 24),
),
),
RaisedButton(
child: Text(
'Go to second screen',
style: TextStyle(fontSize: 24),
),
onPressed: () {
_awaitReturnValueFromSecondScreen(context);
},
)
],
),
),
);
}
void _awaitReturnValueFromSecondScreen(BuildContext context) async {
// start the SecondScreen and wait for it to finish with a result
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SecondScreen(),
));
// after the SecondScreen result comes back update the Text widget with it
setState(() {
text = result;
});
}
}
class SecondScreen extends StatefulWidget {
#override
_SecondScreenState createState() {
return _SecondScreenState();
}
}
class _SecondScreenState extends State<SecondScreen> {
// this allows us to access the TextField text
TextEditingController textFieldController = TextEditingController();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Second screen')),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.all(32.0),
child: TextField(
controller: textFieldController,
style: TextStyle(
fontSize: 24,
color: Colors.black,
),
),
),
RaisedButton(
child: Text(
'Send text back',
style: TextStyle(fontSize: 24),
),
onPressed: () {
_sendDataBack(context);
},
)
],
),
);
}
// get the text in the TextField and send it back to the FirstScreen
void _sendDataBack(BuildContext context) {
String textToSendBack = textFieldController.text;
Navigator.pop(context, textToSendBack);
}
}
This solution is very easy by passing variables in constructor:
first page:
Navigator.of(context).push(MaterialPageRoute(builder:(context)=>SecondPage('something')));
second page:
class SecondPage extends StatefulWidget {
String something;
SecondPage(this.something);
#override
State<StatefulWidget> createState() {
return SecondPageState(this.something);
}
}
class SecondPageState extends State<SecondPage> {
String something;
SecondPageState(this.something);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
//now you have passing variable
title: Text(something),
),
...
}
Get Perfect Solution :
From 1st Screen navigate to others as:
Navigator.pushNamed(context, "second",arguments: {"name" :
"Bijendra", "rollNo": 65210});
},
On Second Screen in build method get as :
#override
Widget build(BuildContext context) {
final Map<String, Object>rcvdData = ModalRoute.of(context).settings.arguments;
print("rcvd fdata ${rcvdData['name']}");
print("rcvd fdata ${rcvdData}");
return Scaffold(appBar: AppBar(title: Text("Second")),
body: Container(child: Column(children: <Widget>[
Text("Second"),
],),),);
}
Easiest way
FirstPage.dart
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PasswordRoute(usernameController)));
//usernameController is String value,If you want to pass multiple values add all
SecondPage.dart
class PasswordRoute extends StatefulWidget {
final String usernameController;//if you have multiple values add here
PasswordRoute(this.usernameController, {Key key}): super(key: key);//add also..example this.abc,this...
#override
State<StatefulWidget> createState() => _PasswordPageState();
}
class _PasswordPageState extends State<PasswordRoute> {
#override
Widget build(BuildContext context) {
...child: Text(widget.usernameController);
}
}
Answers above are useful for a small app, but if you want to remove the headache of continuously worrying about a widgets state, Google presented the Provider package.
https://pub.dev/packages/provider
Have a look into that one, or watch these videos from Andrea Bizzotto:
https://www.youtube.com/watch?v=MkFjtCov62g // Provider: The Essential Guide
https://www.youtube.com/watch?v=O71rYKcxUgA&t=258s // Provider: Introduction
Learn how to use the Provider package, and you are set for life :)
First Screen :
//send data to second screen
Navigator.push(context, MaterialPageRoute(builder: (context) {
return WelcomeUser(usernameController.text);
}));
Second Screen :
//fetch data from first screen
final String username;
WelcomeUser(this.username);
//use data to display
body: Container(
child: Center(
child: Text("Welcome "+widget.username,
textAlign: TextAlign.center,
),
),
),
Navigators in Flutter are similar to the Intent in Android.
There are two classes we are dealing with FirstScreen and SecondScreen.
In order to pass the data between the first screen to second do the following:
First of all add parameter in the SecondScreen class constructor
Now in the FirstScreen class provide the parameter
Navigator.push(context, MaterialPageRoute(builder: (context)=>SecondScreen(key_name:"Desired Data"));
So in the above line the "key_name" is the name of the parameter given in the SecondScreen class.
The "Desired Data" is data should be passed through the key to the SecondScreen class.
That's it you are done!!!
Passing Data to back screen flutter
Home Page
import 'package:flutter/material.dart';
import 'package:flutter/src/widgets/container.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:sqflite_offline/View/Add_data.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
#override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
List<Method> items = []; // => List of items that come form next page.
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Hello"),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.of(context)
.push<Method>(MaterialPageRoute(builder: (_) => AddData()))
// fetching data form next page.
.then((value) => setState(() {
if (value?.title_Ctr != "" && value?.desc_Ctr != "") {
items.add(Method(
title_Ctr: value!.title_Ctr,
desc_Ctr: value.desc_Ctr));
}
}));
},
child: Icon(Icons.add),
),
body: items.isNotEmpty
? Column(children: [
Expanded(
child: ListView.builder(
itemCount: items.length,
itemBuilder: ((context, index) {
return Container(
margin:
EdgeInsets.only(top: 10, left: 10, right: 10),
padding: EdgeInsets.only(left: 10, right: 10),
height: 80,
decoration: BoxDecoration(
color: Colors.pinkAccent,
borderRadius: BorderRadius.circular(10)),
child: Center(
child: ListTile(
title: Text(items[index].title_Ctr),
subtitle: Text(items[index].desc_Ctr),
leading: Icon(Icons.emoji_people),
),
),
);
})))
])
: Center(
child: Text("No Record Found"),
));
}
}
Add List Page
import 'package:flutter/material.dart';
import 'package:flutter/src/widgets/container.dart';
import 'package:flutter/src/widgets/framework.dart';
class AddData extends StatefulWidget {
const AddData({super.key});
#override
State<AddData> createState() => _AddDataState();
}
// Creating a Class and constructor.
class Method {
late String title_Ctr;
late String desc_Ctr;
Method({required this.title_Ctr, required this.desc_Ctr});
}
class _AddDataState extends State<AddData> {
// Creating a TextEditingController for two Fiends,
//one is for title TextField and second is for Description TextField.
TextEditingController titleCtr = TextEditingController();
TextEditingController descCtr = TextEditingController();
// Creating a Method for Passing a data to back page.
OnPressed(BuildContext context) {
var data = Method(title_Ctr: titleCtr.text, desc_Ctr: descCtr.text);
Navigator.pop(context, data);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Add Data")),
body: Form(child: Builder(builder: (context) {
return Column(children: [
TextFormField(
controller: titleCtr,
decoration: InputDecoration(hintText: "title"),
validator: (value) {
var newValue = value ?? "";
if (newValue.isEmpty) {
return 'title is Required';
}
return null;
},
),
TextFormField(
controller: descCtr,
decoration: InputDecoration(hintText: "Description"),
validator: (value) {
var newValue = value ?? "";
if (newValue.isEmpty) {
return 'Discription is Required';
}
return null;
},
),
MaterialButton(
color: Colors.red,
onPressed: () {
if (Form.of(context)?.validate() ?? false) {
OnPressed(context);
}
},
child: Text("Save"),
)
]);
})));
}
}
screenshot
1) From where you want to push :
onPressed: () async {
await Navigator.pushNamed(context, '/edit',
arguments: userData);
setState(() {
userData = userData;
});}
2) From Where you want to pop :
void updateData() async{
WorldTime instance = locations;
await instance.getData();
Navigator.pop(context, userData);
}
If you use get package then try this . passing data with get package
check get package package link
Here's another approach.
Nothing wrong with the other answers. I've tried all of the methods mentioned using global wide widgets like provider, third-party solutions, Navigator arguments, etc. This approach differs by allowing one to chain calls and pass precise data of any type required to the widget using it. We can also gain access to a completion handler event and can use this technique without being constrained to Navigator objects.
Here's the tldr:
tldr; We have to turn our thinking on its head a bit. Data can be
passed to the called widget when you navigate to it by using final
arguments with default values in the destination widget. Using an
optional function you can get data back from the 'child' (destination)
widget.
The complete explanation can be found using this SO answer., (Gist)
I just want to be here to help that 1% who might go through what I did Lol
Don't forget to put an "await" infront of "Navigator.push" in the first page,
otherwise no data will be returned to the first page when you pop from the second page...
Passing Data to back screen flutter
First Screen
final result = await Navigator.of(context).push(MaterialPageRoute(builder: (context)=>const PaymentScreen()));
Second Screen
String selected = "Credit/Debit";
Navigator.pop(context,selected);

Emit the data to parent Widget in Flutter

I'm trying to set the text from child widget to parent widget. But the text is not reflecting in parent widget.
Tried to use setState() also but still unable to get expected result.
Following is my code:
void main() => runApp(new TestApp());
class TestApp extends StatefulWidget {
#override
_TestState createState() => new _TestState();
}
class _TestState extends State<TestApp>{
String abc = "";
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new Scaffold(
body: new Column(
children: <Widget>[
new Text("This is text $abc"),
TestApp2(abc)
],
),
),
);
}
}
class TestApp2 extends StatefulWidget {
String abc;
TestApp2(this.abc);
#override
_TestState2 createState() => new _TestState2();
}
class _TestState2 extends State<TestApp2>{
#override
Widget build(BuildContext context) {
return new Container(
width: 150.0,
height: 30.0,
margin: EdgeInsets.only(top: 50.0),
child: new FlatButton(
onPressed: (){
setState(() {
widget.abc = "RANDON TEXT";
});
},
child: new Text("BUTTON"),
color: Colors.red,
),
);
}
}
Am i missing something ?
In your example, a few assumptions were made. I will try to remove one by one.
You pass abc from parent to child and you mutated the child value on press on button. As primitive types are pass by value in dart, change in the value of abc in child will not change the value of parent abc. Refer the below snippet.
void main() {
String abc = "oldValue";
changeIt(abc);
print(abc); // oldValue
}
void changeIt(String abc) {
abc = "newValue";
print(abc); //newValue
}
Let's assume the first one is wrong(for understanding purpose). Then changing the value of abc in child will change the value of abc in parent. But without calling that inside setState of parent, parent will not reflect the change. In your case if you change the code as below, it will change the button text alone on click (as setState of child is called).
new FlatButton(
onPressed: () {
setState(
() {
widget.abc = "RANDON TEXT";
},
);
},
child:
new Text(widget.abc), // setting the text based on abc
color: Colors.red,
),
Instead of using globalState which will be very difficult to maintain/debug as app grows, I would recommend using callbacks. Please refer the below code.
void main() => runApp(new TestApp());
class TestApp extends StatefulWidget {
#override
_TestState createState() => new _TestState();
}
class _TestState extends State<TestApp> {
String abc = "bb";
callback(newAbc) {
setState(() {
abc = newAbc;
});
}
#override
Widget build(BuildContext context) {
var column = new Column(
children: <Widget>[
new Text("This is text $abc"),
TestApp2(abc, callback)
],
);
return new MaterialApp(
home: new Scaffold(
body: new Padding(padding: EdgeInsets.all(30.0), child: column),
),
);
}
}
class TestApp2 extends StatefulWidget {
String abc;
Function(String) callback;
TestApp2(this.abc, this.callback);
#override
_TestState2 createState() => new _TestState2();
}
class _TestState2 extends State<TestApp2> {
#override
Widget build(BuildContext context) {
return new Container(
width: 150.0,
height: 30.0,
margin: EdgeInsets.only(top: 50.0),
child: new FlatButton(
onPressed: () {
widget.callback("RANDON TEXT"); //call to parent
},
child: new Text(widget.abc),
color: Colors.red,
),
);
}
}
To write the very precise answer. Just use the call back like the above answer use this.
So you want to call the state of ParentScreen from the another function/widget/class. Just follow this code
import 'package:showErrorMessage.dart';
class ParentScreen extends StatefulWidget {
ParentScreen({Key key}) : super(key: key);
#override
_ParentScreenState createState() => _ParentScreenState();
}
class _ParentScreenState extends State<ParentScreen> {
callback() {
setState(() {});
}
#override
Widget build(BuildContext context) {
String message = "hello";
return Container(
child: showErrorMessage(message, callback);,
);
}
}
And here is the child widget/function/class
import 'package:flutter/material.dart';
showErrorMessage(message, Function callback) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
message,
style: TextStyle(color: Colors.white, fontSize: 16),
),
GestureDetector(
onTap: () {
callback(); // ------ this will change/rebuild the state of its parent class
},
child: Icon(
Icons.refresh,
size: 30,
color: Colors.white,
)),
],
));
}
The point that you are missing is your setState method call. You call the setState of the TestState2.
For fixing that, there are two ways.
First way is to create a GlobalKey(https://docs.flutter.io/flutter/widgets/GlobalKey-class.html) and pass it as a parameter to the child widget.
And the second way is to create a global variable for the parent state and use it in the child state.
I modified the code below with the second approach.
_TestState _globalState = new _TestState();
class TestApp extends StatefulWidget {
#override
_TestState createState() => _globalState;
}
class _TestState extends State<TestApp>{
String abc = "";
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new Scaffold(
body: new Column(
children: <Widget>[
new Text("This is text $abc"),
TestApp2()
],
),
),
);
}
}
class TestApp2 extends StatefulWidget {
TestApp2();
#override
_TestState2 createState() => new _TestState2();
}
class _TestState2 extends State<TestApp2>{
#override
Widget build(BuildContext context) {
return new Container(
width: 150.0,
height: 30.0,
margin: EdgeInsets.only(top: 50.0),
child: new FlatButton(
onPressed: (){
_globalState.setState((){
_globalState.abc = "Button clicked";
});
},
child: new Text("BUTTON"),
color: Colors.red,
),
);
}
}

Maintain state for StatefulWidget during build in Flutter

I have a list of stateful widgets where the user can add, remove, and interact with items in the list. Removing items from the list causes subsequent items in the list to rebuild as they shift to fill the deleted row. This results in a loss of state data for these widgets - though they should remain unaltered other than their location on the screen. I want to be able to maintain state for the remaining items in the list even as their position changes.
Below is a simplified version of my app which consists primarily of a list of StatefulWidgets. The user can add items to the list ("tasks" in my app) via the floating action button or remove them by swiping. Any item in the list can be highlighted by tapping the item, which changes the state of the background color of the item. If multiple items are highlighted in the list, and an item (other than the last item in the list) is removed, the items that shift to replace the removed item lose their state data (i.e. the background color resets to transparent). I suspect this is because _taskList rebuilds since I call setState() to update the display after a task is removed. I want to know if there is a clean way to maintain state data for the remaining tasks after a task is removed from _taskList.
void main() => runApp(new TimeTrackApp());
class TimeTrackApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Time Tracker',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new TimeTrackHome(title: 'Task List'),
);
}
}
class TimeTrackHome extends StatefulWidget {
TimeTrackHome({Key key, this.title}) : super(key: key);
final String title;
#override
_TimeTrackHomeState createState() => new _TimeTrackHomeState();
}
class _TimeTrackHomeState extends State<TimeTrackHome> {
TextEditingController _textController;
List<TaskItem> _taskList = new List<TaskItem>();
void _addTaskDialog() async {
_textController = TextEditingController();
await showDialog(
context: context,
builder: (_) => new AlertDialog(
title: new Text("Add A New Task"),
content: new TextField(
controller: _textController,
decoration: InputDecoration(
border: InputBorder.none, hintText: 'Enter the task name'),
),
actions: <Widget>[
new FlatButton(
onPressed: () => Navigator.pop(context),
child: const Text("CANCEL")),
new FlatButton(
onPressed: (() {
Navigator.pop(context);
_addTask(_textController.text);
}),
child: const Text("ADD"))
],
));
}
void _addTask(String title) {
setState(() {
// add the new task
_taskList.add(TaskItem(
name: title,
));
});
}
#override
void initState() {
_taskList = List<TaskItem>();
super.initState();
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Align(
alignment: Alignment.topCenter,
child: ListView.builder(
padding: EdgeInsets.all(0.0),
itemExtent: 60.0,
itemCount: _taskList.length,
itemBuilder: (BuildContext context, int index) {
if (index < _taskList.length) {
return Dismissible(
key: ObjectKey(_taskList[index]),
onDismissed: (direction) {
if(this.mounted) {
setState(() {
_taskList.removeAt(index);
});
}
},
child: _taskList[index],
);
}
}),
),
floatingActionButton: new FloatingActionButton(
onPressed: _addTaskDialog,
tooltip: 'Click to add a new task',
child: new Icon(Icons.add),
),
);
}
}
class TaskItem extends StatefulWidget {
final String name;
TaskItem({Key key, this.name}) : super(key: key);
TaskItem.from(TaskItem other) : name = other.name;
#override
State<StatefulWidget> createState() => new _TaskState();
}
class _TaskState extends State<TaskItem> {
static final _taskFont =
const TextStyle(fontSize: 26.0, fontWeight: FontWeight.bold);
Color _color = Colors.transparent;
void _highlightTask() {
setState(() {
if(_color == Colors.transparent) {
_color = Colors.greenAccent;
}
else {
_color = Colors.transparent;
}
});
}
#override
Widget build(BuildContext context) {
return Column(children: <Widget>[
Material(
color: _color,
child: ListTile(
title: Text(
widget.name,
style: _taskFont,
textAlign: TextAlign.center,
),
onTap: () {
_highlightTask();
},
),
),
Divider(
height: 0.0,
),
]);
}
}
I ended up solving the problem by creating an intermediate class which contains a reference to the StatefulWidget and transferred over all the state variables. The State class accesses the state variables through a reference to the intermediate class. The higher level widget that contained and managed a List of the StatefulWidget now access the StatefulWidget through this intermediate class. I'm not entirely confident in the "correctness" of my solution as I haven't found any other examples of this, so I am still open to suggestions.
My intermediate class is as follows:
class TaskItemData {
// StatefulWidget reference
TaskItem widget;
Color _color = Colors.transparent;
TaskItemData({String name: "",}) {
_color = Colors.transparent;
widget = TaskItem(name: name, stateData: this,);
}
}
My StatefulWidget and its corresponding State classes are nearly unchanged, except that the state variables no longer reside in the State class. I also added a reference to the intermediate class inside my StatefulWidget which gets initialized in the constructor. Previous uses of state variables in my State class now get accessed through the reference to the intermediate class. The modified StatefulWidget and State classes is as follows:
class TaskItem extends StatefulWidget {
final String name;
// intermediate class reference
final TaskItemData stateData;
TaskItem({Key key, this.name, this.stateData}) : super(key: key);
#override
State<StatefulWidget> createState() => new _TaskItemState();
}
class _TaskItemState extends State<TaskItem> {
static final _taskFont =
const TextStyle(fontSize: 26.0, fontWeight: FontWeight.bold);
void _highlightTask() {
setState(() {
if(widget.stateData._color == Colors.transparent) {
widget.stateData._color = Colors.greenAccent;
}
else {
widget.stateData._color = Colors.transparent;
}
});
}
#override
Widget build(BuildContext context) {
return Column(children: <Widget>[
Material(
color: widget.stateData._color,
child: ListTile(
title: Text(
widget.name,
style: _taskFont,
textAlign: TextAlign.center,
),
onTap: () {
_highlightTask();
},
),
),
Divider(
height: 0.0,
),
]);
}
}
The widget containing the List of TaskItem objects has been replaced with a List of TaskItemData. The ListViewBuilder child now accesses the TaskItem widget through the intermediate class (i.e. child: _taskList[index], has changed to child: _taskList[index].widget,). It is as follows:
class _TimeTrackHomeState extends State<TimeTrackHome> {
TextEditingController _textController;
List<TaskItemData> _taskList = new List<TaskItemData>();
void _addTaskDialog() async {
_textController = TextEditingController();
await showDialog(
context: context,
builder: (_) => new AlertDialog(
title: new Text("Add A New Task"),
content: new TextField(
controller: _textController,
decoration: InputDecoration(
border: InputBorder.none, hintText: 'Enter the task name'),
),
actions: <Widget>[
new FlatButton(
onPressed: () => Navigator.pop(context),
child: const Text("CANCEL")),
new FlatButton(
onPressed: (() {
Navigator.pop(context);
_addTask(_textController.text);
}),
child: const Text("ADD"))
],
));
}
void _addTask(String title) {
setState(() {
// add the new task
_taskList.add(TaskItemData(
name: title,
));
});
}
#override
void initState() {
_taskList = List<TaskItemData>();
super.initState();
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Align(
alignment: Alignment.topCenter,
child: ListView.builder(
padding: EdgeInsets.all(0.0),
itemExtent: 60.0,
itemCount: _taskList.length,
itemBuilder: (BuildContext context, int index) {
if (index < _taskList.length) {
return Dismissible(
key: ObjectKey(_taskList[index]),
onDismissed: (direction) {
if(this.mounted) {
setState(() {
_taskList.removeAt(index);
});
}
},
child: _taskList[index].widget,
);
}
}),
),
floatingActionButton: new FloatingActionButton(
onPressed: _addTaskDialog,
tooltip: 'Click to add a new task',
child: new Icon(Icons.add),
),
);
}
}

Resources