Store a List of Map Strings using Shared Preferences - flutter

I'm trying to understand how to store a List of Maps in Flutter, display it, and later add and subtract from it by index. I started to use jsonEncode/Decode to save the whole thing as a String but I don't think thats the right way and plus I can't add back to it since its a String and not a List<Map<String, dynamic>> anymore after Encoding. Any help is greatly appreciated.
class Favs extends StatefulWidget {
#override
_Favs createState() => _Favs();
}
class _Favs extends State<Favs> {
SharedPreferences sharedPreferences;
List<Map<String, dynamic>> _favList=[{id: 1, bookTxt: Here is my text., bookAuthor: Isaiah},{id: 2, bookTxt: Here is my text again., bookAuthor: Matt}];
List<dynamic> _newList = [];
#override
void initState(){
super.initState();
getSavedInfo();
}
getSavedInfo() async {
sharedPreferences = await SharedPreferences.getInstance();
var myFavList = sharedPreferences.getString('myFavList');
if (myFavList != null){
var myFavListCheck = jsonDecode(myFavList);
_newList = myFavListCheck;
}
}
_saveToList(List<Map<String, dynamic>> _favList) async {
var s = json.encode(_favList);
sharedPreferences = await SharedPreferences.getInstance();
sharedPreferences.setString('myFavList', s);
print('DONE WITH _saveToList');
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: AppBar(title: Text('ListView Builder'),),
body: ListView.builder(
itemCount: _newList.length,
itemBuilder: (BuildContext context, int index) {
return Card(
child: Container(
height: 80.0,
child: Center(
child: Text(_newList[index]['bookTxt'])
)
),
);
},
),
floatingActionButton: _addMoreButton(),
);
}
_addMoreButton(){
_favList.add({'id': '3','bookTxt': 'Here is 3rd text','bookAuthor': 'Johnny'});
_saveToList(_favList);
}
}

Decoding and encoding is the write way. Why dont you try flutter_secure_storage as a safer option?
On you pubspec, add: flutter_secure_storage: ^3.2.1+1 as a dependency.
Then you can use FlutterSecureStorage().write(key: key, value: value) which is async.
and to read just use encodedJson = FlutterSecureStorage().read(key: key) which is also async.
Also, you would have to use yourModel.fromJson(json.decode(encodedJson)), so make sure you also add import 'dart:convert';

Related

shared preferences does not save radio button checkmark in Flutter

I implemented the shared preferences package in my Flutter app, with a list widget as radio button, that only save the language preference and not the checkmark.
So when i close the Language screen and come back, the language checkmark goes the the default one even if the language, saved in shared preferences is French or Italian.
This is my Language screen:
class LanguagesScreen extends StatefulWidget {
const LanguagesScreen({Key? key}) : super(key: key);
#override
State<LanguagesScreen> createState() => _LanguagesScreenState();
}
class Item {
final String prefix;
final String? helper;
const Item({required this.prefix, this.helper});
}
var items = [
Item(prefix: 'English', helper: 'English',), //value: 'English'
Item(prefix: 'Français', helper: 'French'),
Item(prefix: 'Italiano', helper: 'Italian'),
];
class _LanguagesScreenState extends State<LanguagesScreen> {
var _selectedIndex = 0;
final _userPref = UserPreferences();
var _selecLangIndex;
int index = 0;
final List<String> entries = <String>['English', 'French', 'Italian'];*/
//init shared preferences
#override
void initState() {
super .initState();
_populateField();
}
void _populateField() async {
var prefSettings = await _userPref.getPrefSettings();
setState((){
_selecLangIndex = prefSettings.language;
});
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: AppBar(...
),
body: CupertinoPageScaffold(
child: Container(
child: SingleChildScrollView(
child: CupertinoFormSection.insetGrouped(
children: [
...List.generate(items.length, (index) => GestureDetector(
onTap: () async {
setState(() => _selectedIndex = index);
if (index == 0){
await context.setLocale(Locale('en','US'));
_selecIndex = Language.English;
}
else if (index == 1){
await context.setLocale(Locale('fr','FR'));
_selecIndex = Language.French;
}
child: buildCupertinoFormRow(
items[index].prefix,
items[index].helper,
selected: _selectedIndex == index,
)
)),
TextButton(onPressed:
_saveSettings,
child: Text('save',
)
buildCupertinoFormRow(String prefix, String? helper, {bool selected = false,}) {
return CupertinoFormRow(
prefix: Text(prefix),
helper: helper != null
? Text(helper, style: Theme.of(context).textTheme.bodySmall,)
:null, child: selected ? const Icon(CupertinoIcons.check_mark,
color: Colors.blue, size: 20,) :Container(),
);
}
void _saveSettings() {
final newSettings = PrefSettings(language:_selecIndex);
_userPref.saveSettings(newSettings);
Navigator.pop(context);
}
}
this is the UserPreference:
class UserPreferences {
Future saveSettings(PrefSettings prefSettings) async {
final preferences = await SharedPreferences.getInstance();
await preferences.setInt('language' , prefSettings.language.index );
}
Future<PrefSettings> getPrefSettings() async {
final preferences = await SharedPreferences.getInstance();
final language = Language.values[preferences.getInt('language') ?? 0 ];
return PrefSettings(language: language);
}
}
enum Language { English, French, Italian}
class PrefSettings{
final Language language;
PrefSettings (
{required this.language});
}
I'm betting that the issue is in initState. You are calling _populateField, but it doesn't complete before building because it's an async method, and you can't await for it: so the widget gets build, loading the default position for the checkmark, and only after that _populateField completes...but then it's too late to show the saved data correctly.
In my experience, if I have not already instantiated a SharedPreferences object somewhere else in the code, I use this to load it:
class _LanguagesScreenState extends State<LanguagesScreen> {
[...]
#override
Widget build(BuildContext context) {
return FutureBuilder(
//you can put any async method here, just be
//sure that you use the type it returns later when using 'snapshot.data as T'
future: await SharedPreferences.getInstance(),
builder: (context, snapshot) {
//error handling
if (!snapshot.hasData || snapshot.connectionState != ConnectionState.done) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Center(child: Text(snapshot.error.toString()));
}
var prefs= snapshot.data as SharedPreferences;
//now you have all the preferences available without need to await for them
return Scaffold((
[...]
);
EDIT
I started writing another comment, but there are so many options here that there wasn't enough space.
First, the code I posted should go in your _LanguagesScreenState build method. The FutureBuilder I suggested should wrap anything that depends on the Future you must wait for to complete. I put it up at the root, above Scaffold, but you can move it down the widgets' tree as you need, just remember that everything that needs to read the preferences has to be inside the FutureBuilder.
Second, regarding SharedPreferences.getInstance(), there are two ways: the first is declaring it as a global variable, and loading it even in the main method where everything starts. By doing this you'll be able to reference it from anywhere in your code, just be careful to save the changes everytime is needed. The second is to load it everytime you need, but you'll end up using a FutureBuilder a lot. I don't know if any of these two options is better than the other: the first might have problems if somehow the SharedPreferences object gets lost, while the second requires quite more code to work.

Flutter - Async function not being waited for

appreciate the help! I've looked through some of the other responses on here and I can't find an answer.
I have a Provider, in which I have an async function defined. It reaches out to an external API, gets data, and then is meant to update the attributes in the Provider with the data received.
The Widget that uses the provider is meant to build a ListView with that data. projects is null until the response is received. That's why I need the async await functionality to work here. The error I'm getting says that "length can't be called on null", which means projects is still null at the time is reaches that line. That is because the async functionality isn't working.
Here is the Provider, in which my async function is defined:
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import '../../constants/urls.dart';
import 'project.dart';
class Projects with ChangeNotifier{
List<Project> _projects;
List<Project> _myProjects;
final String authToken;
final List<Project> previousProjects;
final bool _initialLoad = true;
Projects(this.authToken, this.previousProjects);
List<Project> get projects {
return _projects;
}
List<Project> get myProjects {
return _myProjects;
}
bool get initialLoad {
return _initialLoad;
}
Future<void> fetchProjects() async {
print('inside future, a');
try {
var response = await http.get(
Uri.parse(Constants.fetchProjectsURL),
headers: {"Authorization": "Bearer " + authToken},
);
print('inside future, b');
if (response.statusCode == 200) {
final extractedData = json.decode(response.body) as List;
final List<Project> tempLoadedProjects = [];
extractedData.forEach((project) {
tempLoadedProjects.add(
Project(
// insert project params
),
);
});
_projects = tempLoadedProjects;
print(_projects);
print(projects);
notifyListeners();
} else {
print('something happened');
}
} catch (error) {
throw error;
}
}
}
Then, I used this provider in the following Widget:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../../providers/projects/projects_provider.dart';
class ProjectsColumn extends StatelessWidget {
Future<void> fetchProjects(ctx) async {
await Provider.of<Projects>(ctx).fetchProjects();
}
Widget build(BuildContext context) {
print('Before fetch');
fetchProjects(context);
print('After fetch');
final projects = Provider.of<Projects>(context, listen: false).projects;
return ListView.builder(
itemCount: projects.length,
itemBuilder: (BuildContext ctx, int index) {
return Card(
child: Text(
'Project Name:${projects[index]}',
),
);
});
}
}
Thoughts?
You need to put await before the method to a wait, but you can't do this in build() method, So you can use future builder like the answer of #jamesdlin
or you can call fetchProjects method in intState first like this way:
class ProjectsColumn extends StatefulWidget {
#override
State<ProjectsColumn> createState() => _ProjectsColumnState();
}
class _ProjectsColumnState extends State<ProjectsColumn> {
bool _isLoading = true;
Future<void> _fetchProjects() async {
await Provider.of<Projects>(context, listen: false).fetchProjects();
_isLoading = false;
if (mounted) setState(() {});
}
#override
void initState() {
super.initState();
_fetchProjects();
}
#override
Widget build(BuildContext context) {
return _isLoading
? const Center(child: CircularProgressIndicator())
: Consumer<Projects>(
builder: (context, builder, child) => builder.projects.isEmpty
? const Center(child: Text('No Projects Found'))
: ListView.builder(
shrinkWrap: true,
itemCount: builder.projects.length,
itemBuilder: (BuildContext ctx, int index) {
return Card(
child: Text(
'Project Name:${builder.projects[index]}',
),
);
},
),
);
}
}
EDIT:
a) From the docs HERE BuildContext objects are passed to WidgetBuilder functions (such as StatelessWidget.build), and are available from the State.context member., and in the previous example I used StatefulWidget widget that extends state class, then you can use context outside build but inside the class extends state, not like StatelessWidget.
b) mounted condition, it represents whether a state is currently in the widget tree, i used it to prevent the famous error: setState() called after dispose()
see docs HERE, also this useful answer HERE

Flutter: Bars are not displaying

I am trying to display the api value on bar chart,
my api data is look like this
{leaves: 3, sick: 2, annual: 0, maternity: 1}
charts.dart
i am getting this data from login screen, and passing it to the leave screen using shared perference.
i want to display it in bar chart, here is my try to do this
class LeavesCharts extends StatelessWidget {
final List<LeavesSeries> data;
LeavesCharts({#required this.data});
#override
Widget build(BuildContext context) {
List<charts.Series<LeavesSeries, String>> series=[
charts.Series(
id: "Leaves",
data: data,
domainFn: (LeavesSeries series, _)=>series.totalleave,
measureFn: (LeavesSeries series,_)=>series.annual,
colorFn: (LeavesSeries series,_)=>series.barColor,
)
];
return
Container(
height: 400,
padding: EdgeInsets.all(20),
child: Card(child: Padding(padding: const EdgeInsets.all(8.0),
child: Column(children: <Widget>[
Text("History of leaves"),
Expanded(child: charts.BarChart(series,animate: true,))
],),
)
,),);
}
}
here i am displaying the chart
String annualCount="4";
String totalleaveCount="4";
String sickCount="4";
String maternityCount="1";
class _RequestForLeaveState extends State<RequestForLeave> {
//getting values using shared perference
_userDetails() async {
SharedPreferences myPrefs = await SharedPreferences.getInstance();
setState(() {
sickCount=myPrefs.getString('sickcount');
totalleaveCount=myPrefs.getString('totalleave');
maternityCount=myPrefs.getString('maternitycount');
annualCount=myPrefs.getString('annualcount');
;
});
}
final List<LeavesSeries> data=[
LeavesSeries(
totalleave: totalleaveCount,
maternity: maternityCount,
sick:sickCount,
annual: 3,
barColor:charts.ColorUtil.fromDartColor(Colors.blue)
),
];
Widget build(BuildContext context) {
return new Scaffold(
appBar: new MyAppBar(
title: Text("Request for leave"),
// onpressed: () {
// Navigator.push(
// context, MaterialPageRoute(builder: (context) => Profile()));
// },
),
drawer: drawer(),
body: LeavesCharts(data:data)
);
}
}
class LeavesSeries {
String totalleave;
int annual;
String sick;
String maternity;
final charts.Color barColor;
LeavesSeries({#required this.totalleave,#required this.annual,#required this.sick,#required this.maternity, #required this.barColor});
}
but it is not displaying any bar, here is the output
update output:
i initialize varibles with some values and it display bar according to that, but i want 4 bars for each variable, how i can do that?
please help where i am doing wrong
Your data object does not contain the newly set data.
In _userDetails() the 4 variables get updated from the prefs, that should work just fine. Because the state has updated, build will be called. However, build will not use these newly set variables in the state, but instead it will access and use the 'old' data object, which contained only empty strings. It does not get overwritten with your new object.
I suggest putting the data object into the state, using the prefs you already retrieve
class _RequestForLeaveState extends State<RequestForLeave> {
LeavesCharts data;
#override
initState() {
super.initState();
data = LeavesSeries(
totalleave: 0,
maternity: 0,
sick: 0,
annual: 0,
barColor: charts.ColorUtil.fromDartColor(Colors.blue)
);
}
//getting values using shared perference
_userDetails() async {
SharedPreferences myPrefs = await SharedPreferences.getInstance();
setState(() {
data = LeavesSeries(
totalleave: myPrefs.getString('totalleave'),
maternity: myPrefs.getString('maternitycount'),
sick: myPrefs.getString('sickcount'),
annual: myPrefs.getString('annualcount'),
barColor: charts.ColorUtil.fromDartColor(Colors.blue)
)
});
}
...
}
Then in build, you can access this data object and use it as expected to retrieve all of its values.

Widgets with future builder not removing widget after provider was updated with async

I have been learning flutter for 2-3 months now and I feel I have a reached a fundamental roadblock with understanding state management. This post will be long unfortunately so please bare with me and I hope I put the right detail.
Problem Definition
I have a list of widgets in a shopping cart,im at the point where I click minus and it only has 1 left the widget must be removed.No matter what I try I cant get that widget to be removed. If I click back button and go back into cart the Item will not appear anymore.
I have considered other methods, like disposing the widget(that didn't seem to work) and I was busy implementing Visibility Show/hide widgets in Flutter programmatically
but that doesn't feel like the right way.If my understanding of providers,changeNotifiers,async and future builders,is correct the below method should work and I think its fundamental to my flutter journey to understand why it doesn't work.
Overview:The idea was to use the minus button on CartItemWidget to call a method that updates Json stored on the local device, then repopulate the List cartProdList in ProductProvider which calls
notifyListeners() and then should propagate everywhere the provider is used. Now I have used this pattern successfully 5 times now, the only different this time is it will be removing a widget which I haven't done before. But this should work dynamically if the future is based of the same provider right ?
function call order
CartItemWidget.onPressed:()
calls >>>
ProductProvider.cartMinusOne(String id)
calls >>>
ProductProvider.Future<List<Product>> cartProducts()
well here goes the code.I also wouldn't mind comments on things I could be doing better in all areas.
CartWidget
class CartWidget extends StatefulWidget {
#override
_CartWidgetState createState() => _CartWidgetState();
}
class _CartWidgetState extends State<CartWidget> {
var providerOfProd;
ProductProvider cartProdProvider = new ProductProvider();
#override
void initState() {
_productsList = new ProductsList();
super.initState();
providerOfProd = Provider.of<ProductProvider>(context, listen: false).cartProducts();
}
#override
Widget build(BuildContext context) {
........
Column(children: <Widget>[
FutureBuilder(
future: providerOfProd,
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return Container(
width: 0,
height: 0,
);
case ConnectionState.done:
return ListView.separated(
..............
},
itemBuilder: (context, index) {
return CartItemWidget(
product: cartProdProvider.cartProdList.elementAt(index),
heroTag: 'cart',
quantity: cartProdProvider.cartProdList.elementAt(index).cartqty,
key: UniqueKey(),
);
},
);
.........
CartItemWidget
class CartItemWidget extends StatefulWidget {
CartItemWidget({Key key, this.product, this.heroTag, this.quantity = 1}) : super(key: key);
// ProductProvider cartProd = new ProductProvider();
String heroTag;
Product product;
int quantity;
#override
_CartItemWidgetState createState() => _CartItemWidgetState();
}
class _CartItemWidgetState extends State<CartItemWidget> {
#override
Widget build(BuildContext context) {
return Consumer<ProductProvider>(
builder: (context, productProv, _) => InkWell(
child: Container(
.............
child: Row(
children: <Widget>[
.............
IconButton(
onPressed: () {
setState(() {
productProv.cartMinusOne(widget.product.id);
widget.quantity = this.decrementQuantity(widget.quantity);
});
}
.............
ProductProvider
class ProductProvider with ChangeNotifier {
ProductProvider() {
cartProducts();
}
List<Product> cartProdList;
cartMinusOne(String id) async {
//Code to minus item,then return as a string to save as local jason
var test = jsonEncode(cartList);
saveLocalJson(test, 'cart.json');
cartProducts();
notifyListeners();
}
Future<List<Product>> cartProducts() async {
String jsonString = await JsonProvider().getProductJson();
String cartString = await getCartJson();
var filterProdList = (json.decode(jsonString) as List).map((i) => Product.fromJson(i)).toList();
//code to get match cart list to product list
cartProdList = filterProdList.where((element) => element.cartqty > 0).toList();
notifyListeners();
return cartProdList;
}
........................

How to write this Flutter code more efficiently?

As you can see in first part I'm checking that a certain value contains in a document from Firestore and returns a boolean value. Now I'm calling that function in a build and based on that return value I'm changing a chip color (second part).
Now the problem is maybe because I'm calling it in a build function so its being called continuously and on that build and it costing me a ton of reads in Firestore or maybe the function is inefficient. How can I write this more efficiently?
checkAtt(String name, id , date) async{
var ref = _db.collection('subjects').document(id).collection('Att').document(date);
var docref = await ref.get();
return docref.data.containsKey(name)
?true
:false;
}
class PresentChip extends StatefulWidget {
final candidate;
PresentChip(
this.candidate, {
Key key,
}) : super(key: key);
#override
_PresentChipState createState() => _PresentChipState();
}
class _PresentChipState extends State<PresentChip> {
var isSelected = false;
var c = false;
#override
Widget build(BuildContext context) {
final SelectSub selectSub = Provider.of<SelectSub>(context);
final Date date = Provider.of<Date>(context);
db.checkAtt(widget.candidate, selectSub.selectsub, date.datenew).then((result){
print(result);
setState(() {
c = result;
});
});
return Container(
child: ChoiceChip(
label: Text('Present'),
selected: isSelected,
onSelected: (selected) {
db.gibAtt(
widget.candidate, selectSub.selectsub, date.datenew.toString());
setState(() {
isSelected = selected;
});
},
backgroundColor: !c ?Colors.red :Colors.green ,
selectedColor: !c ?Colors.red :Colors.green ,
));
}
}
Assuming you only want to read once from firestore, you need a FutureBuilder.
return Container(
child: FutureBuilder(
future: db.checkAtt(widget.candidate, selectSub.selectsub, date.datenew),
builder: (context, snapshot) {
if(snapshot.hasData)
return ChoiceChip(
...
backgroundColor: !snapshot.data ?Colors.red :Colors.green,
selectedColor: !snapshot.data ?Colors.red :Colors.green,
);
//Return another widget if the future has no data
return Text('Future has no data');
}
)
);
If you need your UI to react to changes from firestore, use a StreamBuilder.
You can remove the following bloc from your build method:
db.checkAtt(widget.candidate, selectSub.selectsub, date.datenew).then((result){
print(result);
setState(() {
c = result;
});
});