I'm new with Flutter and I want to upgrade my code. I have a form that uses multiple textformfields and I want to convert this code using provider and riverpod to improve readability but I'm not sure how to do it.
For the example I simplified my code to only one distance field but there are many others.
This is my CalculatorScreen :
import 'dart:async' show Future;
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:app/core/models/model_form_calculator.dart';
import 'package:app/core/services/service_form_validator.dart';
import 'package:app/core/utils/utils_app_color.dart';
class CalculatorScreen extends StatefulWidget
{
CalculatorScreen({Key key}) : super(key: key);
#override
_CalculatorScreenState createState() => _CalculatorScreenState();
}
class _CalculatorScreenState extends State<CalculatorScreen>
{
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
final _formKey = GlobalKey<FormState>();
FormCalculatorModel _formData = FormCalculatorModel();
bool _autoValidateForm = false;
final TextEditingController _controllerDistance = TextEditingController();
#override
void initState() {
super.initState();
}
#override
void dispose()
{
_controllerDistance.dispose();
super.dispose();
}
#override
Widget build(BuildContext context)
{
return GestureDetector(
onTap: (() => FocusScope.of(context).requestFocus(FocusNode())),
child: Scaffold(
key: _scaffoldKey,
backgroundColor: AppColors.colorBgDark,
body : _buildBody()
),
);
}
Widget _buildBody()
{
return SingleChildScrollView(
child: Column(
children: [
Form(
key: _formKey,
autovalidate: _autoValidateForm,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFormField(
controller: _controllerDistance,
keyboardType: TextInputType.number,
decoration: InputDecoration(
hintText: "Enter a value",
),
validator: (value){
return FormValidatorService.isDistanceValid(value);
},
onSaved: (var value) {
_formData.distance = num.tryParse(value).round();
},
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: FlatButton(
child: Text("Erase"),
onPressed: _buttonResetAction
),
),
Expanded(
child: FlatButton(
child: Text("Send"),
onPressed: _buttonSubmitAction
),
),
],
),
]
),
),
],
),
);
}
void _buttonResetAction()
{
_eraseForm();
}
void _eraseForm(){
setState(() {
_formKey.currentState.reset();
_formData = FormCalculatorModel();
_autoValidateForm = false;
_controllerDistance.clear();
});
}
void _buttonSubmitAction() async
{
if (!_formKey.currentState.validate()) {
setState(() {
_autoValidateForm = true;
});
return;
}
_formKey.currentState.save();
try{
// some actions
}catch(e){
_eraseForm();
print(e.toString());
}
}
}
This is my formModel (This model contains all the fields that I can fill in my form and allows me to store the values of the form once validated to then make calculations with these values
):
class FormCalculatorModel{
int distance;
FormCalculatorModel({
this.distance,
});
#override
String toString() {
return '{ '
'${this.distance}, '
'}';
}
}
And my FormValidatorService :
class FormValidatorService{
static String isDistanceValid(String value)
{
num _distance = num.tryParse(value);
if (_distance == null) {
return "is required";
}
if (_distance < 200) {
return "Min distance is 200";
}
if (_distance > 1000) {
return "Max dist is 1000";
}
return null;
}
}
Now I want to convert this with riverpod. I'm a little lost, there are few examples on the internet and I don't really see how to manage my form
At first I'm just trying to handle the validation of the form but it doesn't work.
My calculatorScreen :
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class CalculatorScreen extends HookWidget{
final _formKey = GlobalKey<FormState>();
bool _autoValidateForm = false;
FormCalculatorModel _formData = FormCalculatorModel();
final TextEditingController _controllerDistance = TextEditingController();
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: (() => FocusScope.of(context).requestFocus(FocusNode())),
child: Scaffold(
body : _buildBody(context)
),
);
}
Widget _buildBody(BuildContext context){
final _formModel = useProvider(formCalculatorProvider.state);
return SingleChildScrollView(
child: Column(
children: [
TitleComponent(
title: "Calcul",
description: "Parametrer",
),
ContainerComponent(
background: AppColors.colorBgLight,
children: [
Form(
key : _formKey,
autovalidate: _autoValidateForm,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFormField(
decoration: InputDecoration(
labelText: "Distance",
//errorText: _formModel.distance.error,
),
controller: _controllerDistance,
validator: (String value){
return FormValidatorService.isDistanceValid(value);
},
onSaved: (var value) {_formData.distance = num.tryParse(value).round();}
),
],
),
),
ButtonComponent.primary(
text: "Calculer",
context: context,
onPressed : context.read(formCalculatorProvider).submitData(key: _formKey),
),
],
)
],
),
);
}
}
And my FormCalculatorNotifier :
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
enum FormState
{
EMPTY,
SUCCESS,
ERROR
}
class FormCalculatorModelNew {
const FormCalculatorModelNew({this.formState, this.autoValidate, this.distance});
final FormState formState;
final bool autoValidate;
final String distance;
}
class FormCalculatorNotifier extends StateNotifier<FormCalculatorModelNew>
{
FormCalculatorNotifier() : super(_initial);
static const FormState _initialState = FormState.EMPTY;
static const _initial = FormCalculatorModelNew(
formState : _initialState,
autoValidate: false,
distance: null
);
submitData({key}){
print(key);
if (!key.currentState.validate()) {
state = FormCalculatorModelNew(
autoValidate: true,
);
return;
}
key.currentState.save();
}
}
The provider :
final formCalculatorProvider = StateNotifierProvider((ref) => FormCalculatorNotifier());
It does not really make sense to use Provider in your example code because I don't see anywhere listen to the state of formCalculatorProvider. Also, the form itself should be managed in the form widget itself.
I assume you want to share the distance value with other widgets. Here are what I will do:
_autoValidate: leave it inside the widget and handle it by Hook
add copyWith inside FormCalculatorModelNew (can easily update partial value)
formCalculatorProvider part:
final formCalculatorProvider = StateNotifierProvider((ref) => FormCalculatorNotifier());
enum MyFormState { EMPTY, SUCCESS, ERROR }
class FormCalculatorModelNew {
const FormCalculatorModelNew({this.formState, this.distance});
final MyFormState formState;
final int distance;
FormCalculatorModelNew copyWith({
MyFormState formState,
int distance,
}) {
return FormCalculatorModelNew(
formState: formState ?? this.formState,
distance: distance ?? this.distance,
);
}
}
class FormCalculatorNotifier extends StateNotifier<FormCalculatorModelNew> {
FormCalculatorNotifier() : super(_initial);
static const MyFormState _initialState = MyFormState.EMPTY;
static const _initial =
FormCalculatorModelNew(formState: _initialState, distance: null);
void update(int distance) {
state = state.copyWith(distance: distance, formState: MyFormState.SUCCESS);
}
void error() {
state = state.copyWith(distance: null, formState: MyFormState.ERROR);
}
void clear() {
state = state.copyWith(distance: null, formState: MyFormState.EMPTY);
}
}
CalculatorScreen part: (simplify)
class CalculatorScreen extends HookWidget {
final _formKey = GlobalKey<FormState>();
#override
Widget build(BuildContext context) {
final _autoValidate = useState<bool>(false);
final _controller = useTextEditingController();
return Scaffold(
body: Form(
key: _formKey,
autovalidate: _autoValidate.value,
child: Column(
children: [
TextFormField(
controller: _controller,
keyboardType: TextInputType.number,
validator: (value) {
return FormValidatorService.isDistanceValid(value);
},
onSaved: (value) {
context.read(formCalculatorProvider).update(num.tryParse(value).round());
},
),
Row(
children: [
FlatButton(
child: Text('Erase'),
onPressed: () {
_formKey.currentState.reset();
_controller.clear();
_autoValidate.value = false;
context.read(formCalculatorProvider).clear();
},
),
FlatButton(
child: Text('Send'),
onPressed: () {
if(_formKey.currentState.validate()){
_formKey.currentState.save();
}else{
_autoValidate.value = true;
context.read(formCalculatorProvider).error();
}
},
),
],
),
],
),
),
);
}
}
You can use TextEditingController.
Further create a provider like so, and you may now listen to text changes and store them where desired using the same provider
final formControllerProvider =
StateProvider<TextEditingController>((ref) => TextEditingController());
Related
I new to Flutter and i was trying to find a solution for the below issue for several hours. I have searched and every solution provided does not work form me.
I have page where one of the widgets is the autocomplete text input. I have created this autocomplete widget on different class. I have added this widget as StatefulBuilder within my main widget. it is working fine however, i am not able to access its value so I can store it with other fields.
My code look like
class ItemDetails extends StatefulWidget {
const ItemDetails({Key? key}) : super(key: key);
static const routeName = '/item_details';
#override
State<ItemDetails> createState() => _ItemDetails();
}
class _ItemDetails extends State<ItemDetails> {
late TextEditingController labelController;
late TextEditingController valueController;
late TextEditingController notesController;
bool _submitted = false;
late var args;
String _itemLabel2 = "";
// var labelAutoComp = LabelSugg();
#override
void initState() {
super.initState();
labelController = TextEditingController();
valueController = TextEditingController();
notesController = TextEditingController();
}
#override
void dispose() {
labelController.dispose();
valueController.dispose();
notesController.dispose();
// Hive.close();
super.dispose();
}
String? _labelErrorText(context) {
final text = labelController.value.text;
if (text.isEmpty) {
// return 'Can\'t be empty';
return AppLocalizations.of(context)!.noEmpty;
}
}
String? _valueErrorText(context) {
final text = valueController.value.text;
if (text.isEmpty) {
// return 'Can\'t be empty';
return AppLocalizations.of(context)!.noEmpty;
}
}
#override
Widget build(BuildContext context) {
try {
args = ModalRoute.of(context)!.settings.arguments as Map;
} on Exception catch (e) {
// print(e);
}
// print(args);
return Scaffold(
appBar: AppBar(
title: Text(args['title']),
),
body: Container(
padding: const EdgeInsets.all(20),
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(20),
child: Column(
// mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
LabelSugg(getLabelText: (String val) {
print(val);
_itemLabel2 = val;
}),
TextField(
autofocus: true,
decoration: InputDecoration(
labelText: AppLocalizations.of(context)!.label,
hintText: AppLocalizations.of(context)!.labelHint,
errorText:
_submitted ? _labelErrorText(context) : null,
),
controller: labelController,
onChanged: (_) => setState(() {}),
),
const SizedBox(height: 5),
TextField(
autofocus: false,
decoration: InputDecoration(
labelText: AppLocalizations.of(context)!.value,
hintText: AppLocalizations.of(context)!.valueHint,
errorText:
_submitted ? _valueErrorText(context) : null,
),
controller: valueController,
keyboardType: const TextInputType.numberWithOptions(
decimal: true, signed: false),
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.allow(
RegExp(r"[0-9.]")),
TextInputFormatter.withFunction(
(oldValue, newValue) {
try {
final text = newValue.text;
if (text.isNotEmpty) double.parse(text);
return newValue;
} catch (e) {}
return oldValue;
}),
], // Only numbers can be entered
onChanged: (_) => setState(() {}),
),
const SizedBox(height: 5),
TextField(
autofocus: true,
decoration: InputDecoration(
labelText: AppLocalizations.of(context)!.notes,
hintText: AppLocalizations.of(context)!.noteHint,
),
controller: notesController,
onChanged: (_) => setState(() {}),
),
]),
// ],
),
Expanded(
child: Align(
alignment: FractionalOffset.bottomCenter,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton.icon(
onPressed: () {
setState(() => _submitted = true);
if (_labelErrorText(context) == null &&
_valueErrorText(context) == null) {
//insert
var localLabel = labelController.value.text;
var _localValue = 0.0;
if (valueController.value.text != '') {
_localValue =
double.parse(valueController.value.text);
} else {
_localValue = 0.0;
}
var localNotes = notesController.value.text;
addItemToList(
localLabel, _localValue, localNotes);
Navigator.of(context).pop();
labelController.clear();
valueController.clear();
notesController.clear();
}
},
label: Text(AppLocalizations.of(context)!.add),
icon: const Icon(Icons.save, size: 18),
),
const SizedBox(width: 10),
ElevatedButton.icon(
onPressed: () => {Navigator.pop(context)},
label: Text(AppLocalizations.of(context)!.cancel),
icon: const Icon(Icons.cancel, size: 18),
),
],
)),
),
// )
],
)));
}
void addItemToList(String localLabel, double localValue, String localNotes) {
var _item = YearItems()..yearID = args['year'];
_item.itemLabel = localLabel;
_item.itemValue = localValue;
_item.itemNote = localNotes;
print(_itemLabel2);
final itemsBox = ItemsBoxes.getTransactions();
itemsBox.add(_item);
}
}
my labelAutoComp widget code look like
class LabelSugg extends StatefulWidget {
final ValueChanged<String> getLabelText;
const LabelSugg({Key? key, required this.getLabelText}) : super(key: key);
#override
State<LabelSugg> createState() => _LabelSugg();
}
class _LabelSugg extends State<LabelSugg> {
late TextEditingController fieldTextEditingController2;
#override
void initState() {
super.initState();
}
#override
void dispose() {
super.dispose();
}
getLabel() {
return widget.getLabelText(fieldTextEditingController2.text);
}
#override
Widget build(BuildContext context) {
List<LabelsAc> labelOptions = <LabelsAc>[
LabelsAc(label: AppLocalizations.of(context)!.labelClothes),
LabelsAc(label: AppLocalizations.of(context)!.labelFood),
LabelsAc(label: AppLocalizations.of(context)!.labelPerfumes),
LabelsAc(label: AppLocalizations.of(context)!.labelCapital),
];
return Autocomplete<LabelsAc>(
optionsBuilder: (TextEditingValue textEditingValue) {
return labelOptions
.where((LabelsAc _label) => _label.label
.toLowerCase()
.startsWith(textEditingValue.text.toLowerCase()))
.toList();
},
displayStringForOption: (LabelsAc option) => option.label,
fieldViewBuilder: (BuildContext context,
TextEditingController fieldTextEditingController,
// fieldTextEditingController,
FocusNode fieldFocusNode,
VoidCallback onFieldSubmitted) {
return TextField(
controller: fieldTextEditingController,
focusNode: fieldFocusNode,
style: const TextStyle(fontWeight: FontWeight.bold),
// onChanged: getLabel(),
onChanged: (String val) {
fieldTextEditingController2 = fieldTextEditingController;
getLabel();
});
},
onSelected: (LabelsAc selection) {
fieldTextEditingController2 =
TextEditingController(text: selection.label);
getLabel();
},
optionsViewBuilder: (BuildContext context,
AutocompleteOnSelected<LabelsAc> onSelected,
Iterable<LabelsAc> options) {
return Align(
alignment: Alignment.topLeft,
child: Material(
child: Container(
// width: 350,
// color: Colors.cyan,
child: ListView.builder(
padding: const EdgeInsets.all(10.0),
itemCount: options.length,
itemBuilder: (BuildContext context, int index) {
final LabelsAc option = options.elementAt(index);
return GestureDetector(
onTap: () {
onSelected(option);
},
child: ListTile(
title: Text(option.label,
style: const TextStyle(color: Colors.black)),
),
);
},
),
),
),
);
},
);
// ),
// );
}
}
class LabelsAc {
LabelsAc({required this.label});
String label;
}
first is redundant when you wrap your class that extend StatefullWidget with StatefullBuilder. LabelSugg is a component Widget. you can use it like other widget.
benefit to separate widget with StatefullWidget class is, we can update the value inside the class without re-build the current page. which is good for performance. that's why developer recomend to separete with class insted compared to make local method.
as you see, when you create LabelSugg extend StatefullWidget class , we will have _LabelSugg . underscore means that: all variable only accessible on current file.
thats why we can't call getLabel() or other variable from different file.
its used for handle the State in 'LabelSugg` widget.
now how to pass the value from LabelSugg is by created variable outside the state. here you are:
class LabelSugg extends StatefulWidget {
// use this to pass any changes when we use LabelSugg
final ValueChanged<String> getLabelText;
const LabelSugg({Key? key, required this.getLabelText}) : super(key: key);
#override
State<LabelSugg> createState() => _LabelSugg();
}
then we can call the onChaged inside _LabelSugg state. because its Statefull widget, we can acces by : widget.getLabelText()
class _LabelSugg extends State<LabelSugg> {
late TextEditingController fieldTextEditingController;
.....
getLabel() {
return widget.getLabelText(fieldTextEditingController.text);
}
then in other class we call LabelSugg like common widget
import 'package:../labelsug.dart';
class ItemDetails extends StatefulWidget {
.....
return Scaffold(
appBar: AppBar(
title: Text(args['title']),
),
body: Container(
padding: const EdgeInsets.all(20),
child: Column(
children: <Widget>[
// now use it like a widget
LabelSug(
getLabelText: (String val){
print(val);
}
:)
I'm new with Flutter and provider.
I'm trying to make a form with provider in order to separate my logic in my code but I'm struggling ...
My form in the screen :
class CalculatorScreen extends StatefulWidget{
CalculatorScreen({Key key}) : super(key: key);
#override
_CalculatorScreenState createState() => _CalculatorScreenState();
}
class _CalculatorScreenState extends State<CalculatorScreen> {
final TextEditingController _controllerDistance = TextEditingController();
#override
void dispose(){
_controllerDistance.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: (() => FocusScope.of(context).requestFocus(FocusNode())),
child: Scaffold(
body : _buildBody(context)
),
);
}
Widget _buildBody(BuildContext context)
{
var _formCalculatorProvider = Provider.of<FormCalculatorNotifier>(context);
return SingleChildScrollView(
child: Column(
children: [
ContainerComponent(
background: AppColors.colorBgLight,
children: [
Form(
key : _formCalculatorProvider.globalFormKey,
autovalidate: _formCalculatorProvider.autovalidate,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFormField(
decoration: InputDecoration(
labelText: "Distance",
),
controller: _controllerDistance,
keyboardType : TextInputType.number,
validator: (String value){
return FormValidatorService.isDistanceValid(value);
},
onSaved: (var value) {
_formCalculatorProvider.saveDistance(value);
}
),
],
),
),
ButtonComponent.primary(
context: context,
text: "Send",
onPressed: _formCalculatorProvider.submit,
),
],
)
],
),
);
}
}
And my notifier :
enum FormCalculatorState{
READY,
SUCCESS,
ERROR
}
class FormCalculatorNotifier with ChangeNotifier {
final GlobalKey<FormState> globalFormKey = GlobalKey<FormState>();
FormCalculatorState formState = FormCalculatorState.READY;
bool autovalidate = false;
FormCalculatorModel formData = FormCalculatorModel();
void saveDistance(String value){
print("save");
formData.distance = num.tryParse(value).round();
notifyListeners();
}
void submit(){
if (!globalFormKey.currentState.validate()) {
print("submit");
print(formData);
autovalidate = true;
formState = FormCalculatorState.ERROR;
return;
}
else{
globalFormKey.currentState.save();
}
notifyListeners();
}
Future showErrorNotification(){
// Here I need to know the context
return InfoBarComponent.error(title: AppTextInfobar.ERROR_TITLE, description: AppTextInfobar.ERROR_DESCRIPTION, context: context);
}
How to use my showErrorNotification because I need the context to show my notificationBar ? When I try to add context in the scrren on the submit function I have an error.
Is this the right method?
Did not go through your entire code. But I immediately noticed that notifyListeners is missing in FormCalculatorNotifier class.
I'm new in flutter and I try to update a variable in my main.dart from a customWidget in another file.
I search on internet but I'm a little lost.
How to update my variable userDistance with the value that the user enter ?
Is my widget structure correct? or does it have to be stateless or stateful?
Main.dart :
class HomeController extends StatefulWidget {
HomeController({Key key, this.title}) : super(key: key);
final String title;
#override
_HomeControllerState createState() => _HomeControllerState();
}
class _HomeControllerState extends State<HomeController> {
// ************************************
// Variable
// ************************************
int userDistance = null;
final _formKey = GlobalKey<FormState>();
// ************************************
// Initialisation
// ************************************
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: (() => FocusScope.of(context).requestFocus(FocusNode())),
child: Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: FutureBuilder(
future: loadJson(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return CircularProgressIndicator();
}
return SingleChildScrollView(
padding: EdgeInsets.all(24),
child: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
FormValidatorWidgets.buildDistance(), // => Here I call my external widget
...
And my customWidget :
import 'package:flutter/material.dart';
class FormValidatorWidgets{
static buildDistance() {
return TextFormField(
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: "Distance",
hintText: "Enter a distance",
),
keyboardType: TextInputType.number,
validator: (String value) {
int distance = int.tryParse(value);
if (distance == null) {
return "Distance is required";
}
if (distance <= 0) {
return "Distance must be greater than zero";
}
},
onSaved: (String value) {
userDistance = int.tryParse(value);
},
);
}
}
How to update my variable userDistance ?
For your FormValidatorWidgets add callback function
class FormValidatorWidgets{
static buildDistance(FormFieldSetter<String> onSaved) { // onSaved is a callback
return TextFormField(
...
onSaved: onSaved,
);
}
}
And then in your form:
Column(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[
FormValidatorWidgets.buildDistance(
(String value) {
userDistance = int.tryParse(value);
},
),
...
In my application there is a field for input of a certain value in double format, but this is not working.
What am I doing wrong?
My Bloc
class BudgetBloc extends BlocBase {
String _documentId;
double _movimentos;
BudgetBloc() {
_movimentosController.listen((value) => _movimentos = value);
}
void setBudget(Budget budget) {
_documentId = budget.documentId();
setMovimentos(budget.movimentos);
}
var _movimentosController = BehaviorSubject<double>();
Stream<double> get outMovimentos => _movimentosController.stream;
void setMovimentos(double value) => _movimentosController.sink.add(value);
bool insertOrUpdate() {
var budget = Budget()
..movimentos = _movimentos;
if (_documentId?.isEmpty ?? true) {
_repository.add(budget);
} else {
_repository.update(_documentId, budget);
}
return true;
}
#override
void dispose() {
_movimentosController.close();
super.dispose();
}
}
My BudgetPage
class BudgetPage extends StatefulWidget {
BudgetPage(this.budget);
final Budget budget;
#override
_BudgetPageState createState() => _BudgetPageState();
}
class _BudgetPageState extends State<BudgetPage> {
TextEditingController _movimentosController;
final _bloc = BudgetBloc();
#override
void initState() {
_movimentosController =
TextEditingController(text: widget.budget.movimentos.toString());
super.initState();
}
#override
void dispose() {
_movimentosController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Add Movimento"),
),
body: Container(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: ListView(
children: <Widget>[
Container(
child: TextField(
decoration: InputDecoration(labelText: "Movimento"),
controller: _movimentosController,
onChanged: _bloc.setMovimentos,
),
),
Container(
height: 20,
),
RaisedButton(
child: Text("Save"),
onPressed: () {
if (_bloc.insertOrUpdate()) {
Navigator.pop(context);
}
},
)
],
),
),
),
);
}
}
Thanks
Function(double) can't be assigned to the parameter type void Function(String)
The error is telling you that you are providing a String when it expects a double.
I would try passing a double to the BLoC, something like this:
onChanged: (value) => _bloc.setMovimentos(double.parse(value)),
I'm making a command and control application using Flutter, and have come across an odd problem. The main status page of the app shows a list of stateful widgets, which each own a WebSocket connection that streams state data from a connected robotic platform. This worked well when the robots themselves were hardcoded in. However now that I'm adding them dynamically (via barcode scans), only the first widget is showing status.
Further investigation using the debugger shows that this is due to the fact that a state is only getting created for the first widget in the list. Subsequently added widgets are successfully getting constructed, but are not getting a state. Meaning that createState is not getting called for anything other than the very first widget added. I checked that the widgets themselves are indeed being added to the list and that they each have unique hash codes. Also, the IOWebSocketChannel's have unique hash codes, and all widget data is correct and unique for the different elements in the list.
Any ideas as to what could be causing this problem?
Code for the HomePageState:
class HomePageState extends State<HomePage> {
String submittedString = "";
StateContainerState container;
List<RobotSummary> robotList = [];
List<String> robotIps = [];
final GlobalKey<ScaffoldState> scaffoldKey = new GlobalKey<ScaffoldState>();
void addRobotToList(String ipAddress) {
var channel = new IOWebSocketChannel.connect('ws://' + container.slsData.slsIpAddress + ':' + container.slsData.wsPort);
channel.sink.add("http://" + ipAddress);
var newConnection = new RobotSummary(key: new UniqueKey(), channel: channel, ipAddress: ipAddress, state: -1, fullAddress: 'http://' + container.slsData.slsIpAddress + ':' + container.slsData.wsPort,);
scaffoldKey.currentState.showSnackBar(new SnackBar(
content: new Text("Adding robot..."), duration: Duration(seconds: 2),));
setState(() {
robotList.add(newConnection);
robotIps.add(ipAddress);
submittedString = ipAddress;
});
}
void _onSubmit(String val) {
// Determine the scan data that was entered
if(Validator.isIP(val)) {
if(ModalRoute.of(context).settings.name == '/') {
if (!robotIps.contains(val)) {
addRobotToList(val);
}
else {
scaffoldKey.currentState.showSnackBar(new SnackBar(
content: new Text("Robot already added..."), duration: Duration(seconds: 5),));
}
}
else {
setState(() {
_showSnackbar("Robot scanned. Go to page?", '/');
});
}
}
else if(Validator.isSlotId(val)) {
setState(() {
_showSnackbar("Slot scanned. Go to page?", '/slots');
});
}
else if(Validator.isUPC(val)) {
setState(() {
_showSnackbar("Product scanned. Go to page?", '/products');
});
}
else if (Validator.isToteId(val)) {
}
}
#override
Widget build(BuildContext context) {
container = StateContainer.of(context);
return new Scaffold (
key: scaffoldKey,
drawer: Drawer(
child: CategoryRoute(),
),
appBar: AppBar(
title: Text(widget.topText),
),
bottomNavigationBar: BottomAppBar(
child: new Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
IconButton(icon: Icon(Icons.camera_alt), onPressed: scan,),
IconButton(icon: Icon(Icons.search), onPressed: _showModalSheet,),
],
),
),
body: robotList.length > 0 ? ListView(children: robotList) : Center(child: Text("Please scan a robot.", style: TextStyle(fontSize: 24.0, color: Colors.blue),),),
);
}
void _showModalSheet() {
showModalBottomSheet(
context: context,
builder: (builder) {
return _searchBar(context);
});
}
void _showSnackbar(String message, String route) {
scaffoldKey.currentState.showSnackBar(new SnackBar(
content: new Text(message),
action: SnackBarAction(
label: 'Go?',
onPressed: () {
if (route == '/') {
Navigator.popUntil(context,ModalRoute.withName('/'));
}
else {
Navigator.of(context).pushNamed(route);
}
},),
duration: Duration(seconds: 5),));
}
Widget _searchBar(BuildContext context) {
return new Scaffold(
body: Container(
height: 75.0,
color: iam_blue,
child: Center(
child: TextField(
style: TextStyle (color: Colors.white, fontSize: 18.0),
autofocus: true,
keyboardType: TextInputType.number,
onSubmitted: (String submittedStr) {
Navigator.pop(context);
_onSubmit(submittedStr);
},
decoration: new InputDecoration(
border: InputBorder.none,
hintText: 'Scan a tote, robot, UPC, or slot',
hintStyle: TextStyle(color: Colors.white70),
icon: const Icon(Icons.search, color: Colors.white70,)),
),
)));
}
Future scan() async {
try {
String barcode = await BarcodeScanner.scan();
setState(() => this._onSubmit(barcode));
} on PlatformException catch (e) {
if (e.code == BarcodeScanner.CameraAccessDenied) {
setState(() {
print('The user did not grant the camera permission!');
});
} else {
setState(() => print('Unknown error: $e'));
}
} on FormatException{
setState(() => print('null (User returned using the "back"-button before scanning anything. Result)'));
} catch (e) {
setState(() => print('Unknown error: $e'));
}
}
}
Code snippet for the RobotSummary class:
import 'package:flutter/material.dart';
import 'package:meta/meta.dart';
import 'package:test_app/genericStateSummary_static.dart';
import 'dart:convert';
import 'package:web_socket_channel/web_socket_channel.dart';
import 'package:test_app/StateDecodeJsonFull.dart';
import 'dart:async';
import 'package:test_app/dataValidation.dart';
class RobotSummary extends StatefulWidget {
final String ipAddress;
final String _port = '5000';
final int state;
final String fullAddress;
final WebSocketChannel channel;
RobotSummary({
Key key,
#required this.ipAddress,
#required this.channel,
this.state = -1,
this.fullAddress = "http://10.1.10.200:5000",
}) : assert(Validator.isIP(ipAddress)),
super(key: key);
#override
_RobotSummaryState createState() => new _RobotSummaryState();
}
class _RobotSummaryState extends State<RobotSummary> {
StreamController<StateDecodeJsonFull> streamController;
#override
void initState() {
super.initState();
streamController = StreamController.broadcast();
}
#override
Widget build(BuildContext context) {
return new Padding(
padding: const EdgeInsets.all(20.0),
child: new StreamBuilder(
stream: widget.channel.stream,
builder: (context, snapshot) {
//streamController.sink.add('{"autonomyControllerState" : 3, "pickCurrentListName" : "69152", "plannerExecutionProgress" : 82, "pickUpcCode" : "00814638", "robotName" : "Adam"}');
return getStateWidget(snapshot);
},
),
);
}
#override
void dispose() {
streamController.sink.close();
super.dispose();
}
}
Based on what Jacob said in his initial comments, I came up with a solution that works and is a combination of his suggestions. The code solution he proposed above can't be implemented (see my comment), but perhaps a modification can be attempted that takes elements of it. For the solution I'm working with now, the builder call for HomePageState becomes as follows:
Widget build(BuildContext context) {
List<RobotSummary> tempList = [];
if (robotList.length > 0) {
tempList.addAll(robotList);
}
container = StateContainer.of(context);
return new Scaffold (
key: scaffoldKey,
drawer: Drawer(
child: CategoryRoute(),
),
appBar: AppBar(
title: Text(widget.topText),
),
bottomNavigationBar: BottomAppBar(
child: new Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
IconButton(icon: Icon(Icons.camera_alt), onPressed: scan,),
IconButton(icon: Icon(Icons.search), onPressed: _showModalSheet,),
],
),
),
body: robotList.length > 0 ? ListView(children: tempList) : Center(child: Text("Please scan a robot.", style: TextStyle(fontSize: 24.0, color: iam_blue),),),
);
}
The problem is you are holding on to the StatefulWidgets between build calls, so their state is always the same. Try separating RobotSummary business logic from the view logic. Something like
class RobotSummary {
final String ipAddress;
final String _port = '5000';
final int state;
final String fullAddress;
final WebSocketChannel channel;
StreamController<StateDecodeJsonFull> streamController;
RobotSummary({
#required this.ipAddress,
#required this.channel,
this.state = -1,
this.fullAddress = "http://10.1.10.200:5000",
}) : assert(Validator.isIP(ipAddress));
void init() => streamController = StreamController.broadcast();
void dispose() => streamController.sink.close();
}
And then in your Scaffold body:
...
body: ListView.builder(itemCount: robotList.length, itemBuilder: _buildItem)
...
Widget _buildItem(BuildContext context, int index) {
return new Padding(
padding: const EdgeInsets.all(20.0),
child: new StreamBuilder(
stream: robotList[index].channel.stream,
builder: (context, snapshot) {
//streamController.sink.add('{"autonomyControllerState" : 3, "pickCurrentListName" : "69152", "plannerExecutionProgress" : 82, "pickUpcCode" : "00814638", "robotName" : "Adam"}');
return getStateWidget(snapshot); // not sure how to change this.
},
),
);
}