CheckboxListTile state not changing when clicked inspite of calling setState - flutter

I am creating a List of checkboxes using ListView and CheckboxListTile as below. However, when I click or unclick on an item, the CheckboxListTile does not update and reflect the check or uncheck.
The code creating the ListView is below:
void _showDialog(context) {
showDialog(
context: context,
builder: (context) {
return Material(
child: ListView(
children: this.selectionList.map((e) {
return CheckboxListTile(
value: e.isSelected,
title: Text(e.name),
onChanged: (value) {
setState(() {
e.isSelected = value ?? false;
});
});
}).toList()),
);
});
}
Also including the full code:
import 'package:flutter/material.dart';
import 'package:recipevault/models/user_profile.dart';
class MultiSelectComponent extends StatefulWidget {
List<Interest> displayList; //initial values are marked as selected=true
MultiSelectComponent({Key? key, this.displayList = const <Interest>[]})
: super(key: key);
#override
_MultiSelectComponentState createState() => _MultiSelectComponentState();
}
class _MultiSelectComponentState extends State<MultiSelectComponent> {
List<Interest> selectionList = <Interest>[];
#override
void initState() {
// TODO: implement initState
super.initState();
setState(() {
this.selectionList = List.from(widget.displayList);
});
}
void _showDialog(context) {
showDialog(
context: context,
builder: (context) {
return Material(
child: ListView(
children: this.selectionList.map((e) {
return CheckboxListTile(
value: e.isSelected,
title: Text(e.name),
onChanged: (value) {
setState(() {
e.isSelected = value ?? false;
});
});
}).toList()),
);
});
}
#override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
GestureDetector(
onTap: () {
_showDialog(context);
},
child: Container(
decoration: BoxDecoration(
border: Border.all(width: 1.2),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text("Select"),
Icon(Icons.arrow_downward),
],
),
),
),
Wrap(children: [
...selectionList
.where((element) => element.isSelected)
.map((e) => Chip(
label: Text(e.name),
onDeleted: () {
setState(() {
e.isSelected = false;
});
},
))
.toList(),
]),
]);
}
}
class Interest {
final String type, name;
final int id;
bool isSelected;
Interest(
{required this.id,
required this.name,
required this.type,
this.isSelected = false});
Interest.fromJson(Map<String, Object?> json)
: this(
id: json['id']! as int,
type: json['type']! as String,
name: json['name']! as String,
);
Map<String, Object?> toJson() {
return {
'id': this.id,
'type': this.type,
'name': this.name,
};
}

You have to extract the content of showDialog's builder to a separate StatefulWidget.

Related

TextField widgets lose focus (Flutter)

I am creating an app with Flutter TextField widgets:
class CategoryData {
int? id;
String name;
String description;
CategoryData({this.id, required this.name, required this.description});
}
class CategoriesEdit extends StatefulWidget {
Database? db;
CategoryData? category;
CategoriesEdit({super.key, required this.db, required this.category});
#override
State<StatefulWidget> createState() => CategoriesEditState();
}
class CategoriesEditState extends State<CategoriesEdit> {
CategoryData? category;
void saveState(BuildContext context) {
// ...
}
#override
Widget build(BuildContext context) {
if (category == null) {
setState(() {
category = widget.category ?? CategoryData(name: "", description: "");
});
}
return Scaffold(
appBar: AppBar(
leading: InkWell(
child: const Icon(Icons.arrow_circle_left),
onTap: () => Navigator.pop(context)),
title: const Text("Edit Category"),
),
body: Column(children: [
Column(key: const Key('name'), children: [
const Text("Category name:*"),
TextField(
controller: TextEditingController(text: category!.name),
onChanged: (value) {
setState(() {
category!.name = value;
});
})
]),
Column(key: const Key('description'), children: [
const Text("Description:"),
TextField(
controller: TextEditingController(text: category!.description),
onChanged: (value) {
setState(() {
category!.description = value;
});
})
]),
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
ElevatedButton(
onPressed: () => saveState(context), // passing false
child: const Text('OK'),
),
OutlinedButton(
onPressed: () => Navigator.pop(context, false),
// passing false
child: const Text('Cancel'),
),
]),
]));
}
}
But after I type a character in one of these two widgets, the cursor moves before the first character and the Android keyboard widget disappears. Why? And how to fix that bug?
I tried adding widget keys, but as you see it didn't help.
There is a lot of things going wrong here, not only the stuff mentioned in the other answer.
Move the setState in the builder into initState:
if (category == null) {
setState(() {
category = widget.category ?? CategoryData(name: "", description: "");
});
}
Don't use setState in the onChanged callback. Change:
onChanged: (value) {
setState(() {
category!.description = value;
});
}
to this:
onChanged: (value) {
category!.description = value;
}
Store the TextEditingControllers, because you have to dispose them once we dispose the state.
If you are already using TextEditingControllers, then you don't need the onChanged callback. Just take text from the controller like explained in the other answer.
You do not have to do
controller: TextEditingController(text: category!.name)
because the controller's text automatically changes once you connect it to TextField.
The reason is once you set some text to the controller, it re-applies the text thus moving the cursor to the front.
I have solved this for you :
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class CategoryData {
int? id;
String name;
String description;
CategoryData({this.id, required this.name, required this.description});
}
class CategoriesEdit extends StatefulWidget {
CategoryData? category;
CategoriesEdit({required this.category});
#override
State<StatefulWidget> createState() => CategoriesEditState();
}
class CategoriesEditState extends State<CategoriesEdit> {
CategoryData? category;
// Database? db;
TextEditingController nametextController = TextEditingController();
TextEditingController descriptionTextController = TextEditingController();
void saveState(BuildContext context) {
// ...
}
#override
Widget build(BuildContext context) {
if (category == null) {
setState(() {
category = widget.category ?? CategoryData(name: "", description: "");
});
}
nametextController.text = category!.name??"";
descriptionTextController.text = category!.description??"";
return Scaffold(
appBar: AppBar(
leading: InkWell(
child: const Icon(Icons.arrow_circle_left),
onTap: () => Navigator.pop(context)),
title: const Text("Edit Category"),
),
body: Column(children: [
Column(key: const Key('name'), children: [
const Text("Category name:*"),
TextField(
controller: nametextController,
onChanged: (value) {
setState(() {
category!.name = value;
});
})
]),
Column(key: const Key('description'), children: [
const Text("Description:"),
TextField(
controller: descriptionTextController,
onChanged: (value) {
setState(() {
category!.description = value;
});
})
]),
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
ElevatedButton(
onPressed: () => saveState(context), // passing false
child: const Text('OK'),
),
OutlinedButton(
onPressed: () => Navigator.pop(context, false),
// passing false
child: const Text('Cancel'),
),
]),
]));
}
}
I have tested this code and it is working fine, let me know if you have any doubt. Hope this helps you.

Infinite-scroll listview.builder - to expand or not to expand... and more provider value not updating and how to fix "RenderFlex overflowed"

I am trying to build a view/route that will list items fetched from a REST source.
I want to show a notification item below the list while the data is being fetched.
But my ListView builder is constructed around the fetched data's structure, so I figured just have a ListTile fit some appropriate UX elements below the generated list inside a Column - which was kinda working great - or so I thought - until the list grows to fill the screen causing RenderFlex overflowed error. Wrapping the ListView builder in Expanded fixed that but moved the indicator to the bottom of the screen.
In trying to fix it I seem to have broken more of the plumbing and the boolean variable that should control the idicator widget; isLoading: stockSet.isBusyLoading doesn't seem to update.
At the moment if I hardcode it as `` it does sit in the appropraite position but I am back with the RenderFlex overflow.
Once all of this is working I'll be wanting to automatically load items untill the screen is full - not sure where I'll be triggering that from yet.
class MyStockSet extends StatefulWidget {
const MyStockSet({super.key});
static const indexStr = 'stocks';
static const labelStr = 'Properties';
#override
State<MyStockSet> createState() => _MyStockSetState();
}
class _MyStockSetState extends State<MyStockSet> {
#override
Widget build(BuildContext context) {
const String imagePath = 'assets/images/${MyStockSet.indexStr}.png';
var assetImage = const AssetImage(imagePath);
//var stockSet = context.watch<StockSet>(); <- didn't work either
var stockSet = Provider.of<StockSet>(context,listen: false);
return Scaffold(
appBar: AppBar(
title: Row(
children: [
AscHero(
assetImage: assetImage,
tag: MyStockSet.indexStr,
title: MyStockSet.labelStr,
radius: 32,
),
const SizedBox(width: 12),
const Text(MyStockSet.labelStr),
],
),
actions: [
IconButton(
onPressed: () {
var stockSet = context.read<StockSet>();
int newNr = stockSet.stocks.length + 1;
Stock tmpstock = Stock(
id: newNr,
title: 'test$newNr',
thumbUrl: 'url',
description: 'desc');
stockSet.add(tmpstock);
},
icon: const Icon(Icons.add),
),
IconButton(
onPressed: () {
developer.log('btn before isBusyLoading ${stockSet.isBusyLoading}');
stockSet.fetch();
developer.log('after btn isBusyLoading ${stockSet.isBusyLoading}');
},
icon: const Icon(Icons.handshake),
),
],
),
body: Column(
children: [
Row(
// these will be filters, order toggle etc.
children: [
ElevatedButton(
onPressed: () => developer.log('Btn pressed.'),
child: Text('Btn')),
],
),
Expanded(
child: Column(children: [
_StockListView(),
LoadingStockListItemNotif(
isLoading: true,
),
]),
),
],
),
);
}
}
class _StockListView extends StatefulWidget {
#override
State<_StockListView> createState() => _StockListViewState();
}
class _StockListViewState extends State<_StockListView> {
#override
void didChangeDependencies() {
super.didChangeDependencies();
developer.log('_StockListView didChangeDependencies()');
// developer.log('scroll pos ${_scrollController.position}');
}
#override
Widget build(BuildContext context) {
var stockSet = context.watch<StockSet>();
return ListView.builder(
// controller: _scrollController,
shrinkWrap: true,
itemCount: stockSet.stocks.length,
itemBuilder: (context, index) => InkWell(
child: StockListItem(
stock: stockSet.stocks[index],
),
onTap: () => Navigator.pushNamed(
context,
'/stocks/stock',
arguments: ScreenArguments(stockSet.stocks[index]),
),
),
);
}
void _scrollListener() {
developer.log('_scrollListener');
}
}
and
class StockSet extends ChangeNotifier {
final List<Stock> _stocks = [];
late bool isBusyLoading = false;
List<Stock> get stocks => _stocks;
void add(Stock stock) {
_stocks.add(stock);
developer.log('added stock :${stock.title}');
notifyListeners();
}
void remove(Stock stock) {
_stocks.remove(stock);
notifyListeners();
}
Future<void> fetch() async {
developer.log('fetch() iL T');
isBusyLoading = true;
notifyListeners();
Stock tmpStock = await _fetchAStock();
developer.log('fetch() iL F');
isBusyLoading = false;
notifyListeners();
add(tmpStock);
}
Future<Stock> _fetchAStock() async {
developer.log('fetch stock ');
final response = await http.get(
Uri.https(
//...
),
);
developer.log('response.statusCode:${response.statusCode}');
if (response.statusCode == 200) {
final Map<String, dynamic> map = json.decode(response.body);
return Stock(
id: map['id'] as int,
title: map['title'] as String,
description: map['description'] as String,
thumbUrl: map['thumbUrl'] as String,
);
}
throw Exception('error fetching stocks:');
}
}
Apologies for the convoluted question.
Add mainAxisSize : MainAxisSize.min for the column inside the expanded widget. The expanded doesn't have any bounds and that's why it throws an error. You can wrap the column with a SingleChildScrollView if you have long content to display
This worked for me!
Just set the shrinkWrap attribute to true
Main lesson:
Don't fight the framework.
Answer:
Instead of tying yourself into Möbius knots trying to put the ListView's functionality outside of itself; use the fact that the ListView.builder allows you to sculpt the logic of how it gets built and what it will contain - given that the provider can trigger its rebuild when the variable in the data set changes.
In other words; by increasing the loop of the builder, you can insert a kind of footer to the Listview. The appearance (or not) of that can depend on the provider, provided it fires the appropriate notifyListeners()s etc.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:equatable/equatable.dart';
import 'dart:async';
class ItemSetRoute extends StatefulWidget {
const ItemSetRoute({Key? key}) : super(key: key);
#override
State<ItemSetRoute> createState() => _ItemSetRouteState();
}
class _ItemSetRouteState extends State<ItemSetRoute> {
#override
Widget build(BuildContext context) {
var itemSet = Provider.of<ItemSet>(
context,
listen: true /* in order to rebuild */,
);
return Scaffold(
appBar: AppBar(title: const Text('Test'), actions: [
IconButton(
onPressed: () {
itemSet.fetch();
},
icon: const Icon(Icons.download),
)
]),
body: Column(
//screen
children: [
Row(
children: [
ElevatedButton(
onPressed: () {
itemSet.fetch();
},
child: const Text('Btn')),
],
),
Expanded(
child: ListView.builder(
scrollDirection: Axis.vertical,
shrinkWrap: true,
itemCount: itemSet.items.length + 1,
itemBuilder: (context, index) {
/* logic here to create a kind of footer of the ListView */
if (index <= itemSet.items.length - 1) {
return InkWell(
child: ItemListItem(
item: itemSet.items[index],
),
onTap: () {
//('Item tapped, navigate etc.');
});
} else {
return LoadingItemNotifier(
isLoading: itemSet.isBusyLoading,
);
}
},
),
),
],
),
);
}
}
//Models
class ItemListItem extends StatelessWidget {
const ItemListItem({Key? key, required this.item}) : super(key: key);
final Item item;
#override
Widget build(BuildContext context) {
return Material(
child: ListTile(
title: Text(item.title),
subtitle: Text(item.description),
),
);
}
}
class LoadingItemNotifier extends StatefulWidget {
const LoadingItemNotifier({Key? key, this.isLoading = false})
: super(key: key);
final bool isLoading;
#override
State<LoadingItemNotifier> createState() => _LoadingItemNotifierState();
}
class _LoadingItemNotifierState extends State<LoadingItemNotifier> {
#override
Widget build(BuildContext context) {
if (widget.isLoading) {
return Material(
child: ListTile(
leading: SizedBox(
width: 48,
height: 48,
child: ClipOval(
child: Material(
color: Colors.lightBlue.withOpacity(0.25),
child: const Center(
child: Icon(Icons.download),
),
),
),
),
title: const Text('Loading'),
isThreeLine: true,
subtitle: const Text('One moment please...'),
dense: true,
),
);
} else {
return const SizedBox(height: 0);
}
}
}
class ItemSet extends ChangeNotifier {
final List<Item> _items = [];
late bool isBusyLoading = false;
List<Item> get items => _items;
void add(Item item) {
_items.add(item);
notifyListeners();
}
void remove(Item item) {
_items.remove(item);
notifyListeners();
}
Future<void> fetch() async {
isBusyLoading = true;
notifyListeners();
/* handling REST call here */
await Future.delayed(const Duration(milliseconds: 500));
Item newItem = const Item(id: 123, title: 'Title', description: 'Desc');
isBusyLoading = false;
add(newItem);
}
}
class Item extends Equatable {
const Item({
required this.id,
required this.title,
required this.description,
});
final int id;
final String title;
final String description;
#override
List<Object> get props => [id, title, description];
}
Caveats
I don't know if this is the most efficient way of doing this - perhaps there should be fewer states, etc. ...

Flutter make a form with provider and riverpod

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());

How to implement a process that uses flutter_secure_storage package to read and write the data [with Provider package]

I'm new to Flutter and Dart. I made a simple Todos app with Provider package and Consumer. I'm trying to implement a process that uses flutter_secure_storage package to read and write the list data on the device.
But I don't know how to implement it.
Checkbox widget requires a bool, while secure_storage requires the type of String. Therefore, it is necessary to convert both types. This also confuses me a little.
The code is below.
I would be happy if you could give me some advice.
main.dart
// Todos app example
import 'package:consumer_samp/list_model.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: ChangeNotifierProvider<ListModel>(
create: (context) => ListModel(),
child: MyHomePage('Todos app example'),
),
);
}
}
class MyHomePage extends StatelessWidget {
MyHomePage(this.title);
final String title;
final TextEditingController eCtrl = TextEditingController();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: SingleChildScrollView(
child: Column(
children: <Widget>[
Container(
padding: EdgeInsets.all(10),
child: Row(
children: <Widget>[
Expanded(
child: TextField(
decoration: InputDecoration(
border: InputBorder.none,
hintText: 'Enter your ToDo item',
),
controller: eCtrl,
),
),
Consumer<ListModel>(
builder: (_, listModel, __) => FlatButton(
child: Text('Add'),
onPressed: () {
if (eCtrl.text != '') {
Map<String, dynamic> item = {
'value': false,
'text': eCtrl.text
};
listModel.addItem(item);
eCtrl.clear();
}
},
),
),
],
),
),
Center(
child: Consumer<ListModel>(builder: (_, listModel, __) {
var items = listModel.getItems;
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
height: 300,
child: ListView.builder(
itemCount: items.length,
itemBuilder: (BuildContext context, int idx) =>
Container(
padding: const EdgeInsets.all(5),
color: Colors.green,
child: Row(
children: <Widget>[
Checkbox(
value: items[idx]['value'],
onChanged: (val) {
listModel.toggleValue(idx, val);
}),
Text(
items[idx]['text'],
style: TextStyle(
fontSize: 21, color: Colors.white),
),
],
),
),
),
),
],
);
}),
),
],
),
),
);
}
}
list_model.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class ListModel with ChangeNotifier {
List<Map<String, dynamic>> _items = [];
List<Map<String, dynamic>> get getItems => _items;
void addItem(Map<String, dynamic> item) {
_items.add(item);
notifyListeners();
}
void toggleValue(int idx, bool val) {
_items[idx]['value'] = val;
notifyListeners();
}
}
You can copy paste run full code below
You can use class TodoItem and save/load with JSON String
Provider logic for secure storage is in full code, too long to describe detail
code snippet
List<TodoItem> todoItemFromJson(String str) =>
List<TodoItem>.from(json.decode(str).map((x) => TodoItem.fromJson(x)));
String todoItemToJson(List<TodoItem> data) =>
json.encode(List<dynamic>.from(data.map((x) => x.toJson())));
class TodoItem {
TodoItem({
this.item,
this.checked,
});
String item;
bool checked;
factory TodoItem.fromJson(Map<String, dynamic> json) => TodoItem(
item: json["item"],
checked: json["checked"],
);
Map<String, dynamic> toJson() => {
"item": item,
"checked": checked,
};
}
working demo
full code
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'dart:convert';
List<TodoItem> todoItemFromJson(String str) =>
List<TodoItem>.from(json.decode(str).map((x) => TodoItem.fromJson(x)));
String todoItemToJson(List<TodoItem> data) =>
json.encode(List<dynamic>.from(data.map((x) => x.toJson())));
class TodoItem {
TodoItem({
this.item,
this.checked,
});
String item;
bool checked;
factory TodoItem.fromJson(Map<String, dynamic> json) => TodoItem(
item: json["item"],
checked: json["checked"],
);
Map<String, dynamic> toJson() => {
"item": item,
"checked": checked,
};
}
class ListModel with ChangeNotifier {
FlutterSecureStorage _storage;
List<TodoItem> _items = [];
List<TodoItem> get getItems => _items;
initilaize() async {
print("initialize");
String jsonString = await _storage.read(key: "todo");
if (jsonString != null) {
_items = todoItemFromJson(jsonString);
notifyListeners();
}
}
ListModel.init(FlutterSecureStorage storage) {
print("init");
_storage = storage;
initilaize();
}
void update(FlutterSecureStorage storage) {
print("update");
_storage = storage;
}
void addItem(TodoItem item) {
_items.add(item);
notifyListeners();
}
void toggleValue(int idx, bool val) {
_items[idx].checked = val;
notifyListeners();
}
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: MultiProvider(
providers: [
Provider<FlutterSecureStorage>(create: (_) => FlutterSecureStorage()),
ChangeNotifierProxyProvider<FlutterSecureStorage, ListModel>(
create: (_) {
return ListModel.init(
Provider.of<FlutterSecureStorage>(_, listen: false));
},
update: (_, storage, listModel) => listModel..update(storage),
),
],
child: MyHomePage('Todos app example'),
),
);
}
}
class MyHomePage extends StatelessWidget {
MyHomePage(this.title);
final String title;
final TextEditingController eCtrl = TextEditingController();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
actions: <Widget>[
Consumer2<ListModel, FlutterSecureStorage>(
builder: (_, listModel, storage, __) => IconButton(
icon: Icon(Icons.save),
onPressed: () async {
await storage.write(
key: "todo", value: todoItemToJson(listModel._items));
print("save done");
},
))
],
),
body: SingleChildScrollView(
child: Column(
children: <Widget>[
Container(
padding: EdgeInsets.all(10),
child: Row(
children: <Widget>[
Expanded(
child: TextField(
decoration: InputDecoration(
border: InputBorder.none,
hintText: 'Enter your ToDo item',
),
controller: eCtrl,
),
),
Consumer<ListModel>(
builder: (_, listModel, __) => FlatButton(
child: Text('Add'),
onPressed: () {
if (eCtrl.text != '') {
Map<String, dynamic> item = {
'value': false,
'text': eCtrl.text
};
listModel.addItem(
TodoItem(item: eCtrl.text, checked: false));
eCtrl.clear();
}
},
),
),
],
),
),
Center(
child: Consumer<ListModel>(builder: (_, listModel, __) {
var items = listModel.getItems;
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
height: 300,
child: ListView.builder(
itemCount: items.length,
itemBuilder: (BuildContext context, int idx) =>
Container(
padding: const EdgeInsets.all(5),
color: Colors.green,
child: Row(
children: <Widget>[
Checkbox(
value: items[idx].checked,
onChanged: (val) {
listModel.toggleValue(idx, val);
}),
Text(
items[idx].item,
style: TextStyle(
fontSize: 21, color: Colors.white),
),
],
),
),
),
),
],
);
}),
),
],
),
),
);
}
}
As mentioned in the readme files by the following link
https://pub.dev/packages/flutter_secure_storage
The following code instantiate the storage, and the following code's must be in a async method as this returns a future.
final storage = new FlutterSecureStorage();
And you can pass the _items list as a value and you can set some key name
await storage.write(key: key, value: _items);
And then you could get that value by using the key name (which is set while storing)
List<Map<String, dynamic>> _value = await storage.read(key: key);
And then you could map the _value you get from storage and you can store it in _items. And then you could do various operations and queries inside your app now with all data you have.
Make a note! I haven't tried this approach in my code base. Just I said what I thought. Please try this in your code and comment me please.
The following code executes correctly use this in model, for storing data:
Future<void> _storingData() async {
final storage = new FlutterSecureStorage();
for (int i = 0; i < _items.length; i++) {
await storage
.write(key: _items[i]['text'], value: "${_items[i]['value']}")
.then((value) => print("success"));
}
}
For retrieving data:
Future<void> retrivingData() async {
final storage = new FlutterSecureStorage();
Map<String, String> allValues = await storage.readAll();
print(allValues);
allValues.forEach((key, value) {
bool val = bool.fromEnvironment(value, defaultValue: false);
Map<String, dynamic> item = {'value': val, 'text': key};
addItem(item);
});
}
and finally I stored all values again to a list.
You should do some changes in your code in main.dart according to the above methods based on your usage.

how to refresh state on Navigator.Pop or Push in flutter

Here I have two pages first is called BSP_signup_terms page and the second is Bsp_Service_page. when I am on BSP_signup_terms on that page I have to select some checkbox based on the selected checkbox it will show me some data. but problem is that it will show me the complete data but when I get back to the BSP_signup_terms from Bsp_signup_page and I am changing the checkbox and then again when I am pressing next button it will not change the result it same as the previous result.
Here is the Image of Output Page
In this image I've attached both screen output when I am selecting only one checkbox it will render some value in service page and when I am back to the Terms and Condition page and select one more checkbox then it will not updating service page
Here is the code I've tried.
BSP_Signup_Terms_Page
class BspLicensedSignupTermsPage extends StatefulWidget {
static const String routeName = "/bspLicensedSignupTerms";
final BspSignupCommonModel bspSignupCommonModel;
BspLicensedSignupTermsPage({
Key key,
#required this.bspSignupCommonModel,
}) : super(key: key);
#override
_BspLicensedSignupTermsPageState createState() =>
_BspLicensedSignupTermsPageState();
}
class _BspLicensedSignupTermsPageState
extends State<BspLicensedSignupTermsPage> {
#override
void initState() {
super.initState();
}
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
bool _isWalkIn = false;
bool _isHome = false;
bool _isOnDemand = false;
Widget _buildselectcheckbox() {
return Text(
AppConstantsValue.appConst['bsplicensedsignupterms']['selectcheck']
['translation'],
);
}
// Walkin
_onCustomerWalkin(value) {
setState(() {
_isWalkIn = value;
});
}
Widget _buildCustomerWalkIn() {
return TudoConditionWidget(
text: AppConstantsValue.appConst['bsplicensedsignupterms']
['CustomerWalkIn']['translation'],
onChanged: (value) {
print(value);
_onCustomerWalkin(value);
},
validate: false,
);
}
// Home
_onCustomerInHome(value) {
setState(() {
_isHome = value;
});
}
Widget _buildCustomerInHome() {
return TudoConditionWidget(
text: AppConstantsValue.appConst['bsplicensedsignupterms']
['CustomerInHome']['translation'],
onChanged: (value) {
_onCustomerInHome(value);
},
validate: false,
);
}
Widget _buildCustomerInHomeHelp() {
return Text(
AppConstantsValue.appConst['bsplicensedsignupterms']['businesscheckhelp']
['translation'],
);
}
// On Demand
_onCustomerOnDemand(value) {
setState(() {
_isOnDemand = value;
});
}
Widget _buildBusinessOnDemand() {
return TudoConditionWidget(
text: AppConstantsValue.appConst['bsplicensedsignupterms']
['BusinessOnDemand']['translation'],
onChanged: (value) {
_onCustomerOnDemand(value);
},
validate: false,
);
}
Widget _buildBusinessOnDemandHelp() {
return Text(AppConstantsValue.appConst['bsplicensedsignupterms']
['businessprovidehelp']['translation']);
}
#override
Widget build(BuildContext context) {
final appBar = AppBar(
title: Text("Bsp Licensed Signup Terms and Condition"),
leading: IconButton(
icon: Icon(Icons.arrow_back_ios),
onPressed: () {
NavigationHelper.navigatetoBack(context);
},
),
centerTitle: true,
);
final bottomNavigationBar = Container(
height: 56,
//margin: EdgeInsets.symmetric(vertical: 24, horizontal: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
new FlatButton.icon(
icon: Icon(Icons.close),
label: Text('Clear'),
color: Colors.redAccent,
textColor: Colors.black,
padding: EdgeInsets.symmetric(vertical: 10, horizontal: 30),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(7),
),
onPressed: () {
_formKey.currentState.reset();
},
),
new FlatButton.icon(
icon: Icon(FontAwesomeIcons.arrowCircleRight),
label: Text('Next'),
color: colorStyles["primary"],
textColor: Colors.white,
padding: EdgeInsets.symmetric(vertical: 10, horizontal: 30),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(7),
),
onPressed: () {
if (_formKey.currentState.validate()) {
if (_isHome == false &&
_isOnDemand == false &&
_isWalkIn == false) {
showDialog(
barrierDismissible: false,
context: context,
builder: (context) => ShowErrorDialog(
title: Text('Select Service'),
content: Text(
'Please select atleast one service type to proceed next',
),
));
} else {
BspSignupCommonModel model = widget.bspSignupCommonModel;
model.isWalkin = _isWalkIn;
model.isHome = _isHome;
model.isOnDemand = _isOnDemand;
print(model.toJson());
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
BspServicePage(bspSignupCommonModel: model),
),
);
}
}
},
),
],
),
);
return new Scaffold(
appBar: appBar,
bottomNavigationBar: bottomNavigationBar,
body: Container(
height: double.infinity,
width: double.infinity,
child: Stack(
children: <Widget>[
SingleChildScrollView(
child: SafeArea(
child: Form(
autovalidate: true,
key: _formKey,
child: Scrollbar(
child: SingleChildScrollView(
dragStartBehavior: DragStartBehavior.down,
padding: const EdgeInsets.symmetric(horizontal: 10.0),
child: new Container(
decoration: BoxDecoration(
borderRadius: new BorderRadius.circular(25)),
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_buildselectcheckbox(),
_buildCustomerWalkIn(),
_buildCustomerInHome(),
_buildCustomerInHomeHelp(),
_buildBusinessOnDemand(),
_buildBusinessOnDemandHelp(),
],
),
),
),
),
),
),
),
],
),
),
);
}
}
BSP_Service_Page
class BspServicePage extends StatefulWidget {
static const String routeName = "/bspService";
final BspSignupCommonModel bspSignupCommonModel;
BspServicePage({
Key key,
#required this.bspSignupCommonModel,
}) : super(key: key);
#override
_BspServicePageState createState() => _BspServicePageState();
}
class _BspServicePageState extends State<BspServicePage> {
List<int> servicesIds = [];
Map<String, bool> selection = {};
List<BspServices.Service> selectedServices = [];
SearchBarController _controller = new SearchBarController();
String _searchText = '';
bool refreshservices = true;
#override
void initState() {
super.initState();
}
void _showErrorDialog(String message) {
showDialog(
barrierDismissible: false,
context: context,
builder: (context) => ShowErrorDialog(
title: Text('An Error Occurred!'),
content: Text(message),
),
);
}
void refresh() {
setState(() {
refreshservices = !refreshservices;
});
}
#override
Widget build(BuildContext context) {
var _bspServiceBloc = new BspServiceBloc();
final appBar = SearchBar(
controller: _controller,
onQueryChanged: (String query) {
print('Search Query $query');
setState(() {
_searchText = query;
});
},
defaultBar: AppBar(
centerTitle: true,
leading: IconButton(
icon: Icon(Icons.arrow_back_ios),
onPressed: () {
refresh();
NavigationHelper.navigatetoBack(context);
}),
title: Text('Select Services'),
),
);
final bottomNavigationBar = Container(
height: 56,
// margin: EdgeInsets.symmetric(vertical: 24, horizontal: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
new FlatButton.icon(
icon: Icon(Icons.close),
label: Text('Clear'),
color: Colors.redAccent,
textColor: Colors.black,
padding: EdgeInsets.symmetric(vertical: 10, horizontal: 30),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(7),
),
onPressed: () {
print('reseting the state');
setState(() {
selection = {};
servicesIds = [];
});
},
),
new FlatButton.icon(
icon: Icon(FontAwesomeIcons.arrowCircleRight),
label: Text('Next'),
color: colorStyles["primary"],
textColor: Colors.white,
padding: EdgeInsets.symmetric(vertical: 10, horizontal: 30),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(7),
),
onPressed: () {
BspSignupCommonModel model = widget.bspSignupCommonModel;
model.servicesIds = servicesIds;
model.services = selectedServices;
print('servicesIds at the next button');
print(servicesIds);
print(model.toJson());
if (servicesIds.length == 0) {
_showErrorDialog(
'You need to select at least one service to proceed next!');
} else {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => BusinessProfilePage(
bspSignupCommonModel: model,
),
),
);
}
},
),
],
),
);
return new Scaffold(
appBar: appBar,
bottomNavigationBar: bottomNavigationBar,
body: new BspServiceScreen(
bspServiceBloc: _bspServiceBloc,
bspSignupCommonModel: widget.bspSignupCommonModel,
servicesIds: servicesIds,
selection: selection,
searchQuery: _searchText,
selectedServices: selectedServices,
refresh: refresh,
),
);
}
}
Bsp_service_screen
class BspServiceScreen extends StatefulWidget {
final BspServiceBloc _bspServiceBloc;
final String searchQuery;
final List<int> servicesIds;
final Map<String, bool> selection;
final BspSignupCommonModel bspSignupCommonModel;
final List<BspServices.Service> selectedServices;
final Function refresh;
const BspServiceScreen({
Key key,
#required BspServiceBloc bspServiceBloc,
#required this.bspSignupCommonModel,
#required this.servicesIds,
#required this.selection,
#required this.selectedServices,
#required this.refresh,
this.searchQuery,
}) : _bspServiceBloc = bspServiceBloc,
super(key: key);
#override
BspServiceScreenState createState() {
return new BspServiceScreenState(_bspServiceBloc);
}
}
class BspServiceScreenState extends State<BspServiceScreen> {
final BspServiceBloc _bspServiceBloc;
BspServiceScreenState(this._bspServiceBloc);
// Map<String, bool> _selection = {};
#override
void initState() {
super.initState();
bool isHome = widget.bspSignupCommonModel.isHome;
bool isWalkIn = widget.bspSignupCommonModel.isWalkin;
bool isOnDemand = widget.bspSignupCommonModel.isOnDemand;
this._bspServiceBloc.dispatch(LoadBspServiceEvent(
countryId: 1,
isHome: isHome,
isOnDemand: isOnDemand,
isWalkin: isWalkIn,
));
}
#override
void dispose() {
super.dispose();
}
#override
Widget build(BuildContext context) {
return BlocBuilder<BspServiceBloc, BspServiceState>(
bloc: widget._bspServiceBloc,
builder: (
BuildContext context,
BspServiceState currentState,
) {
if (currentState is UnBspServiceState) {
return Center(child: CircularProgressIndicator());
}
if (currentState is ErrorBspServiceState) {
return new Container(
child: new Center(
child: new Text(currentState.errorMessage ?? 'Error'),
),
);
}
if (currentState is InBspServiceState) {
// print(
// 'in bsp service state, ${currentState.bspServices.servicesByCountry.length}');
if (currentState.bspServices.servicesByCountry.length == 0) {
return Container(
child: Center(
child: Text("No Services available for this combination"),
),
);
} else {
return new Container(
child:
_renderServices(currentState.bspServices.servicesByCountry),
);
}
}
return Container();
},
);
}
List<ServicesByCountry> finalList = new List();
ListView _renderServices(List<ServicesByCountry> lovCountryServices) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (widget.searchQuery != '') {
finalList.clear();
lovCountryServices.forEach((ServicesByCountry data) {
if (data.name
.toLowerCase()
.contains(widget.searchQuery.toLowerCase())) {
setState(() {
finalList.add(data);
});
} else {
data.services.forEach((ServiceList.Service services) {
if (services.name
.toLowerCase()
.contains(widget.searchQuery.toLowerCase())) {
setState(() {
finalList.add(data);
});
}
});
}
});
} else {
setState(() {
finalList.clear();
finalList.addAll(lovCountryServices);
});
}
});
return ListView.builder(
shrinkWrap: true,
padding: const EdgeInsets.all(8.0),
itemCount: finalList.length,
itemBuilder: (BuildContext context, int index) {
ServicesByCountry item = finalList[index];
List itemsList = item.services;
return ExpansionTile(
title: Text(item.name),
children: List.generate(itemsList.length, (i) {
widget.selection[itemsList[i].name] =
widget.selection[itemsList[i].name] ?? itemsList[i].isSelected;
return CheckboxListTile(
title: Text(itemsList[i].name),
value: widget.selection[itemsList[i].name],
onChanged: (val) {
setState(() {
widget.selection[itemsList[i].name] = val;
if (val) {
widget.servicesIds.add(itemsList[i].id);
List<BspServices.Service> services =
widget.selectedServices.where((service) {
return service.mainCategory == item.name;
}).toList();
SubCategory subService = new SubCategory(
id: itemsList[i].id,
name: itemsList[i].name,
);
List<SubCategory> subCategories = [];
if (services.length == 0) {
subCategories.add(subService);
widget.selectedServices.add(
new BspServices.Service(
mainCategory: item.name,
mainCategoryId: item.id,
subCategory: subCategories,
),
);
} else {
print('services in else');
print(services[0].subCategory);
subCategories = services[0].subCategory;
subCategories.add(subService);
}
} else {
widget.servicesIds.removeWhere((service) {
return service == itemsList[i].id;
});
List<BspServices.Service> services =
widget.selectedServices.where((service) {
return service.mainCategory == item.name;
}).toList();
services[0].subCategory.removeWhere((subService) {
return subService.id == itemsList[i].id;
});
}
});
print('widget.servicesIds after set state');
print(widget.servicesIds);
},
);
}),
);
},
);
}
}
You can use setState() after return to the first page:
Navigator.push(context, MaterialPageRoute(builder: (context) => Page2())).then((value) {
setState(() {
// refresh state
});
});
Please try below code:-
First you add one method async method:-
void redirectToNextScreen() async {
final Route route = MaterialPageRoute(
builder: (context) => BspServicePage(bspSignupCommonModel: model));
final result = await Navigator.push(mContext, route);
try {
if (result != null) {
if (result) {
//Return callback here.
}
}
} catch (e) {
print(e.toString());
}
}
Then Next you can call this method in "BSP_Signup_Terms_Page" on Next button Pressed event.
Second you can add below line in "BspServicePage" screen Next and Cancel Events.
Navigator.pop(mContext, true); //true means refresh back page and false means not refresh.