Flutter - Load variables with SharedPreferences - flutter

I am learning how to use the SharedPreferences library in Flutter.
I created this code and I would like the counter and counter2 variables once I close and reopen the app to remain as the last save.
However, when I reopen the app the counter and counter2 values return to 0.
Can anyone explain to me where I am going wrong?
Thank you.
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'data.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int counter = 0;
int counter2 = 0;
increment() {
setState(() {
counter += 1;
counter2 += 2;
});
}
loadData() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
setState(() {
String? json = prefs.getString('UserData');
print('loaded json: $json');
if (json == null) {
print('NO DATA (null)');
} else {
Map<String, dynamic> map = jsonDecode(json);
print('map $map');
final data = Data.fromJson(map);
print('Data ${data.counter}, ${data.counter2}');
}
});
}
saveData() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
final _data = Data(counter: counter, counter2: counter2);
String json = jsonEncode(_data);
print('saved json: $json');
prefs.setString('UserData', json);
}
clearData() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.clear();
print('data cleared');
}
/// dichiarare l' initState()
#override
void initState() {
super.initState();
loadData();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'You have pushed the button this many times:',
),
Text(
'c: $counter, c2: $counter2',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
increment();
saveData();
},
tooltip: 'Increment',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
class Data {
int counter = 0;
int counter2 = 0;
Data({required this.counter, required this.counter2});
Map<String, dynamic> toJson() {
return {
'counter': counter,
'counter2': counter2,
};
}
Data.fromJson(Map<String, dynamic> json) {
counter = json['counter'];
counter2 = json['counter2'];
}
}

I agree with the other answer, the best is to use a FutureBuilder. But you can make your current code work with simply adding two lines at the end of loadData:
loadData() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
setState(() {
String? json = prefs.getString('UserData');
print('loaded json: $json');
if (json == null) {
print('NO DATA (null)');
} else {
Map<String, dynamic> map = jsonDecode(json);
print('map $map');
final data = Data.fromJson(map);
print('Data ${data.counter}, ${data.counter2}');
// add these lines
counter = data.counter;
counter2 = data.counter2;
}
});
}
What happens (as the other answer says) is that your widget is first built without knowing the values from SharedPreferences. After a little time this first build is done, the loadData future completes, and with setState the widget is rebuilt.
In a real application you'd like to avoid unnecessary builds, so you'd rather display a progress indicator while async data is being loaded, check FutureBuilder.

A short answer is that when you call loadData(); inside initState the function is performed asynchronously relative to the rest of the widget, so your Scaffold is built before the data is available. This is why you are seeing the data in from your print but not in the app.
One way to address it is to us a https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html

Related

StateProvider does not rebuild when state changed

I have a serious problem with my Riverpod. Specifically, I am using StateProvider in Riverpod package. But when I update state, the widget tree does not rebuild. I checked the new state whether is updated by printing out state to see, I see that they are actually updated.
I have some same situations but when I click hot restart/reload page/scroll up,down mouse to change size chrome window, the widget tree rebuild one time.
Please help me and explain everything the most detail and easy to understand. Thank you very much
new state print out but UI not update
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:math';
void main() {
runApp(const ProviderScope(child: MyApp()));
}
class Data {
final String data;
Data({required this.data});
}
final helloWorldProvider = StateProvider<Data?>((ref) => Data(data: 'No data'));
class MyApp extends ConsumerStatefulWidget {
const MyApp({super.key});
#override
ConsumerState<MyApp> createState() => _MyAppState();
}
class _MyAppState extends ConsumerState<MyApp> {
#override
void initState() {
// TODO: implement initState4
print("Init state");
super.initState();
// getData();
}
// getData() async {
// // http.Response response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/todos/1'));
// // final title = jsonDecode(response.body)["title"];;
// // ref.read(helloWorldProvider.notifier).update((state) => title);
// SharedPreferences prefs = await SharedPreferences.getInstance();
// prefs.setString('valueTemp', 'newValue');
// String? valueTemp = prefs.getString('valueTemp');
// String value = valueTemp ?? '';
// Data data = Data(data: value);
// ref.read(helloWorldProvider.notifier).update((state) => data);
// print("Đã thực hiện xong");
// }
void _change() {
print("change");
final rawString = generateRandomString(5);
Data data = new Data(data: rawString);
ref.watch(helloWorldProvider.notifier).update((state) => data);
print(ref.read(helloWorldProvider.notifier).state?.data);
}
String generateRandomString(int len) {
var r = Random();
return String.fromCharCodes(List.generate(len, (index) => r.nextInt(33) + 89));
}
#override
Widget build(BuildContext context) {
print('Rebuild');
final data = ref.watch(helloWorldProvider.notifier).state;
final dataText = data?.data ?? 'No text';
print(dataText);
return MaterialApp(
title: 'Google Docs Clone',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
body: Center(
child: Column(children: [
Text(dataText)
]
)
),
floatingActionButton: FloatingActionButton(
onPressed: _change,
tooltip: 'Change',
child: const Icon(Icons.add),
),
));
}
}
I don't want to use other pattern as Provider, Bloc, StateNotifierProvider, ChangeNotifierProvider... I only want to run StateProvider successfully. I have refered to many articles and stackoverflows answer but I did't found any useful helps to my case.
final data = ref.watch(helloWorldProvider.notifier).state;
is watching the notifier, which rarely changes. You want to watch the state change, as in:
final data = ref.watch(helloWorldProvider);
Fixed, Tested your code.
I recommend this article Flutter Riverpod 2.0: The Ultimate Guide to Advanced Riverpod 😀
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'dart:math';
void main() {
runApp(const ProviderScope(child: MyApp()));
}
class Data {
final String data;
Data({required this.data});
}
final helloWorldProvider = StateProvider<Data?>((ref) => Data(data: 'No data'));
class MyApp extends ConsumerStatefulWidget {
const MyApp({super.key});
#override
ConsumerState<MyApp> createState() => _MyAppState();
}
class _MyAppState extends ConsumerState<MyApp> {
#override
void initState() {
// TODO: implement initState4
print("Init state");
super.initState();
// getData();
}
// getData() async {
// // http.Response response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/todos/1'));
// // final title = jsonDecode(response.body)["title"];;
// // ref.read(helloWorldProvider.notifier).update((state) => title);
// SharedPreferences prefs = await SharedPreferences.getInstance();
// prefs.setString('valueTemp', 'newValue');
// String? valueTemp = prefs.getString('valueTemp');
// String value = valueTemp ?? '';
// Data data = Data(data: value);
// ref.read(helloWorldProvider.notifier).update((state) => data);
// print("Đã thực hiện xong");
// }
void _change() {
print("change");
final rawString = generateRandomString(5);
Data data = Data(data: rawString);
ref.read(helloWorldProvider.notifier).update((state) => data);
print(ref.read(helloWorldProvider.notifier).state?.data);
}
String generateRandomString(int len) {
var r = Random();
return String.fromCharCodes(
List.generate(len, (index) => r.nextInt(33) + 89));
}
#override
Widget build(BuildContext context) {
print('Rebuild');
final data = ref.watch(helloWorldProvider)?.data;
final dataText = data ?? 'No text';
print(dataText);
return MaterialApp(
title: 'Google Docs Clone',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
body: Center(
child: Column(children: [Text(dataText)]),
),
floatingActionButton: FloatingActionButton(
onPressed: _change,
tooltip: 'Change',
child: const Icon(Icons.add),
),
),
);
}
}

How to use value from this function in widget tree?

This is my function:
getData() async {
final response = await Provider.of<PostApiService>(context, listen: false)
.getData(1.toString() + '/service_contracts');
print(response.statusCode);
print(response.body);
var getData = GetModel.fromJson(response.body);
print(getData.company_name);
}
I want to use getData in my widget tree.
You'll need a StatefulWidget:
class YellowBird extends StatefulWidget {
const YellowBird({ Key? key }) : super(key: key);
#override
_YellowBirdState createState() => _YellowBirdState();
}
class _YellowBirdState extends State<YellowBird> {
// create variable to hold your data:
dynamic data;
// change dynamic to the type of the data you have
// note: it will be null by default so might have to give
// it an initial value.
Future<void> getData() async {
final response = await Provider.of<PostApiService>(context, listen:
false).getData(1.toString() + '/service_contracts');
final _getData = GetModel.fromJson(response.body);
print(_getData.company_name);
// now set the state
// this will rebuild the ui with the latest
// value of the data variable
setState(() => data = _getData);
}
#override
Widget build(BuildContext context) {
return Scaffold(body: Column(
children: <Widget>[
Text('Data is: $data'),
TextButton(
child: Text('Get data'),
onPressed: getData,
),
], ), );
}
}

Using Flutter and Dart, is this the best way to achieve this Future?

I know that there are other ways of achieving this, but I want to use a Future via initState() to obtain a List using SharedPreferences without using await. The following illustrates what I want to achieve, and it does work. Because I have never used this pattern previously, I'm just unsure if it's the best way (without using await). Is there a better way without using await?
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
class _MyHomePageState extends State<MyHomePage> {
SharedPreferences _prefs;
String _sMessage1;
String _sMessage2;
List<String> _lsCategories;
#override
void initState() {
super.initState();
Future<void> future = _initPreferences();
future.then((_) {
if (_prefs != null) {
try {
_lsCategories = _prefs.getStringList("categories") ?? [];
debugPrint("Categories = $_lsCategories");
_sMessage2 = "Categories loaded OK";
} catch (vError) {
_sMessage2 = ("Error loading categories = $vError");
}
}
setState(() {});
});
}
Future<void> _initPreferences() {
return SharedPreferences.getInstance().then((prefs) {
_prefs = prefs;
_sMessage1 = "Preferences initialized OK";
}).catchError((vError) {
_sMessage1 = "Error initializing preferences = ${vError.toString()}";
_sMessage2 = "Unable to load categories";
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
_createText(_sMessage1),
SizedBox(height: 20),
_createText(_sMessage2),
],
),
),
);
}
}
Text _createText(String sText) {
return Text(sText == null ? "" : sText,
style: TextStyle(
color: Colors.red[500], fontWeight: FontWeight.bold, fontSize: 20));
}
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Future Test',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: "Flutter Future Test"),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
What you did is fine in terms of asynchrony. However, it's too verbose. For example, you do not need to transfer the prefs instance through state.
Another improvement could be to check whether the widget is still mounted when the Future is done. (If you use a FutureBuilder, you don't need to worry about this.)
A problem with your code is that you're setting state variables outside setState(). It is not guaranteed to work well that way, I swear I had old state used sometimes when I did that. You should set them within setState()
Here are a couple of different ways I would prefer to code it:
#override
void initState() {
super.initState();
SharedPreferences.getInstance().then((prefs) {
if(!mounted) return;
setState(() {
_prefs = prefs;
_lsCategories = _prefs.getStringList("categories") ?? [];
debugPrint("Categories = $_lsCategories");
_sMessage2 = "Categories loaded OK";
});
}).catchError((vError) {
setState(() {
_sMessage2 = ("Error loading categories = $vError");
});
});
}
#override
void initState() {
super.initState();
_initPreferences();
}
Future<void> _initPreferences() async {
final prefs = await SharedPreferences.getInstance();
if (!mounted) return;
setState(() {
try {
_prefs = prefs;
_lsCategories = _prefs.getStringList("categories") ?? [];
debugPrint("Categories = $_lsCategories");
_sMessage2 = "Categories loaded OK";
} catch (vError) {
_sMessage2 = ("Error loading categories = $vError");
}
});
}

OnSharedPreferenceChangeListener for Flutter

In Android, you can do the following to listen to shared preference change
SharedPreferences.OnSharedPreferenceChangeListener spChanged = new
SharedPreferences.OnSharedPreferenceChangeListener() {
#Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
String key) {
// your stuff here
}
};
Is it possible to do this using flutter? I have read through the official flutter shared_preference and this features seems not yet implemented.
Is there any other library or ways to achieve the above without diving into native code. Thanks.
You can easily "listen" to SharedPreferences using a package like flutter_riverpod.
Initialize sharedPreferences
SharedPreferences? sharedPreferences;
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
sharedPreferences = await SharedPreferences.getInstance();
runApp(const ProviderScope(child: MyApp()));
}
Create the stateProvider
import 'package:hooks_riverpod/hooks_riverpod.dart';
final keepOnTopProvider = StateProvider<bool>((ref) {
return sharedPreferences?.getBool('on_top') ?? true;
});
Update your UI when something changes
class SettingsView extends ConsumerWidget {
const SettingsView({Key? key}) : super(key: key);
#override
Widget build(BuildContext context, WidgetRef ref) {
bool onTop = ref.watch(keepOnTopProvider);
return Scaffold(
appBar: AppBar(title: const Text('Settings'), centerTitle: false),
body: ListView(
padding: const EdgeInsets.symmetric(horizontal: 12),
children: [
SwitchListTile(
title: const Text('Keep on top'),
value: onTop,
onChanged: (value) async {
sharedPreferences?.setBool('on_top', value);
ref.read(keepOnTopProvider.notifier).state = value;
await windowManager.setAlwaysOnTop(value);
},
),
],
),
);
}
}
As a work around, add the following codes to your main():
void funTimerMain() async {
// here check any changes to SharedPreferences, sqflite, Global Variables etc...
if (bolAnythingChanged) {
// do something
// 'refresh' any page you want (below line using Redux as example)
GlobalVariables.storeHome.dispatch(Actions.Increment);
}
// recall this timer every x milliseconds
new Future.delayed(new Duration(milliseconds: 1000), () async {
funTimerMain();
});
}
// call the timer for the first time
funTimerMain();

Flutter - Drawer as sub-class not updating

I'm a fairly inexperienced coder.
I have a Drawer which I have created as a separate class. The issue I'm having is the dynamic data for the Drawer is not populating.
I am expecting the data being retrieved from Shared Preferences should populate the third line of my view with the value of currUserL.
It's being evaluated correctly, and returns the value of currUserL to the console, but is not updated in the Drawer.
I've loaded up a about button (triggering the update method) that works when pressed manually, but data persists only while the drawer remains open. It reverts when the drawer is closed.
drawerPatient.dart
class DrawerPatient extends StatefulWidget {
DrawerPatient({Key key}) : super(key: key);
#override
_DrawerPatientState createState() => new _DrawerPatientState();
}
class _DrawerPatientState extends State<DrawerPatient> {
String currUserL = "nv3";
Future getPref() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
currUserL = prefs.getString('currUserLast');
debugPrint('user: $currUserL');
}
#override
void initState() {
getPref();
}
void update() {
setState(() {
getPref();
});
}
#override
Widget build(BuildContext context) {
return Drawer(
child: new ListView(
children: <Widget>[
new DrawerHeader(
child: new Text('Patient Management'),
),
new ListTile(
title: new Text('search'),
onTap: () {},
),
new ListTile(
title: new Text(currUserL),
onTap: () {},
),
new Divider(),
new ListTile(
title: new Text('About'),
onTap: update,
),
],
));
}
}
userList.dart
class UserList extends StatefulWidget {
UserList({Key key, this.title}) : super(key: key);
final String title;
final String titleHead = "User List";
#override
_UserListState createState() => new _UserListState();
}
class _UserListState extends State<UserList> {
: sortStr}, headers: {"Accept": "application/json"});
setState(() {
data = json.decode(response.body);
});
}
#override
void initState() {
this.makeRequest();
// DrawerPatient().createState().update();
}
void _refresh() {
setState(() {});
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Patient List"),
drawer: new DrawerPatient(key: new UniqueKey()),
...
Drawer when opened
Drawer after clicking about (update)
So I found the answer, thanks to #Dinesh for pointing me in the right direction.
The answer was to put the setState as a dependency on the async get prefs.
Future getPref() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
setState(() {
currUserI = prefs.getString('currUserId');
currUserF = prefs.getString('currUserFirst');
currUserL = prefs.getString('currUserLast');
debugPrint('user: $currUserL');
});
}
Can you try this,
Future getCurrentUser() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
return prefs.getString('currUserLast');
}
void update() {
val tempName = getCurrentUser();
setState(() {
currUserL = tempName;
});
}
Reason: Basically wait for the async method before calling setState