I have a simple controller like this
class UserController with ChangeNotifier {
UserData user = UserData();
UserData get userdata => user;
void setUser(UserData user) {
user = user;
print(user.sId);
notifyListeners();
}
login(data) async {
var response = await ApiService().login(data);
final databody = json.decode(response);
if (databody['success']) {
UserData authUser = UserData.fromJson(databody['data']);
setUser(authUser);
notifyListeners();
return true;
} else {
return false;
}
}
}
I am trying to just print it like this on both widget and in initstate function but values are showing null. I can see in set function value is not null.
print('id ${context.watch<UserController>().user.sId.toString()}');
print(
'id2 ${Provider.of<UserController>(context, listen: false).user.sId.toString()}');
I already have added
ChangeNotifierProvider(create: (_) => UserController()),
],
in main.dart in MultiProvider
Also on Tap of login button I am doing this
showLoader(context);
UserController auth = Provider.of<UserController>(
context,
listen: false);
var data = {
"userEmail":
emailController.text.trim().toLowerCase(),
"userPassword": passwordController.text.trim(),
};
auth.login(data).then((v) {
if (v) {
hideLoader(context);
context.go('/homeroot');
} else {
hideLoader(context);
Fluttertoast.showToast(
backgroundColor: green,
textColor: Colors.white,
msg:
'Please enter correct email and password');
}
});
Try to include this while naming is same,
void setUser(UserData user) {
this.user = user;
print(user.sId);
notifyListeners();
}
Follow this structure
class UserController with ChangeNotifier {
UserData user = UserData();
UserData get userdata => user;
void setUser(UserData user) {
this.user = user;
print(user.sId);
notifyListeners();
}
Future<bool> login(String data) async {
await Future.delayed(Duration(seconds: 1));
UserData authUser = UserData(sId: data);
setUser(authUser);
notifyListeners();
return true;
}
}
class HPTest extends StatelessWidget {
const HPTest({super.key});
#override
Widget build(BuildContext context) {
return Scaffold(
body: Consumer<UserController>(
builder: (context, value, child) {
return Text(value.user.sId);
},
),
floatingActionButton: FloatingActionButton(onPressed: () async {
final result = await Provider.of<UserController>(context, listen: false)
.login("new ID");
print("login $result");
;
}),
);
}
}
The list which stores the task data is integrated with provider for state management, but once I close the app and reopen it again, all tasks vanish.
With resources, I got to know about SharedPreferences.
How do I go about saving and getting the data using shared preferences. I have given the code a try, but does not seem to work in my favor.
void saveData() async {
final prefs = await SharedPreferences.getInstance();
final String encodedData = Task.encode(tasks);
await prefs.setString('task_data', encodedData);
}
void getData() async {
final prefs = await SharedPreferences.getInstance();
final String taskString = prefs.getString('task_data').toString();
List<Task> tasksData = Task.decode(taskString);
_tasks = tasksData;
}
encode() and decode() functions help in mapping List to String and String to List respectively.
static String encode(List<Task> tasks) {
return jsonEncode(
tasks.map<Map<String, dynamic>>((task) => Task.toMap(task)).toList(),
);
}
static List<Task> decode(String tasks) {
var data = (jsonDecode(tasks) as List<dynamic>?);
if (data != null) {
return (jsonDecode(tasks) as List<dynamic>?)!.map<Task>((task) {
return Task.fromJson(task);
}).toList();
} else {
return <Task>[];
}
}
The Task list in displayed using ListView.
Widget build(BuildContext context) {
return Consumer<TaskData>(
builder: (context, taskData, child) {
taskData.getData();
return ListView.builder(
itemCount: taskData.taskCount,
itemBuilder: (context, index) {
taskData.sortTaskList();
final task = taskData.tasks[index];
return TaskTile(
taskTitle: task.name,
isChecked: task.isDone,
checkboxCallBack: (checkBoxState) async {
taskData.upDateTask(task);
taskData.saveData();
},
longPressCallBack: () async {
taskData.removeTask(task);
taskData.saveData();
},
);
},
);
},
);
}
I am expecting that you're using ChangeNotifier with Provider package in TaskData class.
In this case you have to add notifyListener() inside getData() because it is async task and you are updating values.
Future<void> getData() async {
final prefs = await SharedPreferences.getInstance();
final String taskString = prefs.getString('task_data').toString();
List<Task> tasksData = Task.decode(taskString);
_tasks = tasksData;
notifyListener(); // Add this line
}
I have a Future in my initState function that gets jwt from cache and uses it to get the logged in user's details. The initState function is:
#override
void initState() {
super.initState();
Future.delayed(Duration.zero, () async {
final token = await CacheService().readCache(key: "jwt");
if (token != null) {
await Provider.of<ProfileNotifier>(context, listen: false)
.decodeUserData(
context: context,
token: token,
option: 'home',
);
}
});
}
Now, it does work and I do get the data, but not on the first run. I have to either hot reload the emulator or navigate to another page and come back for the page to rebuild itself and show the data on screen. I don't understand why it doesn't show the data on the first run itself.
ProfileNotifier class:
class ProfileNotifier extends ChangeNotifier {
final ProfileAPI _profileAPI = ProfileAPI();
final CacheService _cacheService = CacheService();
ProfileModel _profile = ProfileModel(
profileImage: "",
profileName: "",
profileBio: "",
);
AccountModel _account = AccountModel(
userId: "",
userEmail: "",
userPassword: "",
);
ProfileModel get profile => _profile;
AccountModel get account => _account;
Future decodeUserData({
required BuildContext context,
required String token,
required String option,
}) async {
try {
_profileAPI.decodeUserData(token: token).then((value) async {
final Map<String, dynamic> parsedData = await jsonDecode(value);
var userData = parsedData['data'];
if (userData != null) {
List<String>? userProfileData = await _cacheService.readProfileCache(
key: userData['userData']['id'],
);
if (userProfileData == null) {
final isProfileAvailable =
await Provider.of<ProfileNotifier>(context, listen: false)
.getProfile(
context: context,
userEmail: userData['userData']['userEmail'],
);
if (isProfileAvailable is ProfileModel) {
_profile = isProfileAvailable;
} else {
_account = AccountModel(
userId: userData['userData']['id'],
userEmail: userData['userData']['userEmail'],
userPassword: userData['userData']['userPassword'],
);
_profile = ProfileModel(
profileImage: '',
profileName: '',
);
}
if (option != 'profileCreation' && isProfileAvailable == false) {
Navigator.of(context).pushReplacementNamed(ProfileCreationRoute);
}
} else {
_account = AccountModel(
userId: userData['userData']['id'],
userEmail: userData['userData']['userEmail'],
userPassword: userData['userData']['userPassword'],
);
_profile = ProfileModel(
profileName: userProfileData[3],
profileImage: userProfileData[4],
profileBio: userProfileData[5],
);
}
} else {
Navigator.of(context).pushReplacementNamed(AuthRoute);
}
notifyListeners();
});
} catch (e) {
debugPrint('account/profileNotifier decode error: ' + e.toString());
}
}
Future getProfile({
required BuildContext context,
required String userEmail,
}) async {
try {
var getProfileData = await _profileAPI.getProfile(
userEmail: userEmail,
);
final Map<String, dynamic> parsedProfileData =
await jsonDecode(getProfileData);
bool isReceived = parsedProfileData["received"];
dynamic profileData = parsedProfileData["data"];
if (isReceived && profileData != 'Fill some info') {
Map<String, dynamic> data = {
'id': (profileData['account']['id']).toString(),
'userEmail': profileData['account']['userEmail'],
'userPassword': profileData['account']['userPassword'],
'profile': {
'profileName': profileData['profileName'],
'profileImage': profileData['profileImage'],
'profileBio': profileData['profileBio'],
}
};
AccountModel accountModel = AccountModel.fromJson(
map: data,
);
return accountModel;
} else {
return false;
}
} catch (e) {
debugPrint('profileNotifier getProfile error: ' + e.toString());
}
}
Future setProfile({
required String profileName,
required String profileImage,
required String profileBio,
}) async {
_profile.profileName = profileName;
_profile.profileImage = profileImage;
_profile.profileBio = profileBio;
await _cacheService.writeProfileCache(
key: _account.userId,
value: [
_account.userId,
_account.userEmail,
_account.userPassword as String,
profileName,
profileImage,
profileBio,
],
);
notifyListeners();
}
}
CacheService class:
class CacheService {
Future<String?> readCache({
required String key,
}) async {
final SharedPreferences sharedPreferences =
await SharedPreferences.getInstance();
String? cache = await sharedPreferences.getString(key);
return cache;
}
Future<List<String>?> readProfileCache({
required String key,
}) async {
final SharedPreferences sharedPreferences =
await SharedPreferences.getInstance();
List<String>? cachedData = await sharedPreferences.getStringList(key);
return cachedData;
}
Future writeCache({required String key, required String value}) async {
final SharedPreferences sharedPreferences =
await SharedPreferences.getInstance();
await sharedPreferences.setString(key, value);
}
Future writeProfileCache(
{required String key, required List<String> value}) async {
final SharedPreferences sharedPreferences =
await SharedPreferences.getInstance();
await sharedPreferences.setStringList(key, value);
}
Future deleteCache({
required BuildContext context,
required String key,
}) async {
final SharedPreferences sharedPreferences =
await SharedPreferences.getInstance();
await sharedPreferences.remove(key).whenComplete(() {
Navigator.of(context).pushReplacementNamed(AuthRoute);
});
}
}
I can't seem to figure out the problem here. Please help.
EDIT: The data is used to show profileImage of user in CircleAvatar like this:
#override
Widget build(BuildContext context) {
ProfileModel profile =
Provider.of<ProfileNotifier>(context, listen: false).profile;
return GestureDetector(
onTap: () => FocusManager.instance.primaryFocus?.unfocus(),
child: Scaffold(
drawer: const ProfileDrawer(),
appBar: AppBar(
backgroundColor: Colors.white,
leading: Row(children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 9),
child: Builder(builder: (BuildContext context) {
return InkWell(
onTap: () => Scaffold.of(context).openDrawer(),
child: CircleAvatar(
maxRadius: 20.0,
backgroundImage: profile.profileImage.isNotEmpty
? NetworkImage(profile.profileImage)
: null,
child: profile.profileImage.isEmpty
? SvgPicture.asset(
'assets/images/profile-default.svg')
: null),
);
}),
), ....
This CircleAvatar in the appBar shows the image only after the page is rebuilt. There's nothing else on the page except the appbar for now.
When we use ChangeNotifier, it provides two options to access the data. These are:
Read the data - You read the data, it doesn't act as a Stream or State and only one time. This is what you're doing in your case.
Advantage - Whenever the data is needed only one time, for example - Mathematical calculation, you use this.
Disadvantage - It doesn't listen to the changes and the data returned is static.
Watch the data - What you need. It provides the data in a state manner, wherever you access the data using Watch, it (or the widget in the data is used) will be updated whenever the underlying data is updated, even from other Screens/Widgets.
Advantage - The data result is dynamic and the widget is updated whenever the data is updated.
Disadvantage - In case where static data works, it is unnecessary plus it may affect any operations dependent on the data.
There are two ways to use Read and Watch.
The normal functions provided by the Author of the package
//For reading the data
var yourData = Provider.of<YourNotifier>(context, listen: false);
//For watching the data
var yourData = Provider.of<ProfileNotifier>(context, listen: true);
The extension functions on BuildContext provided by the Author:
//For reading the data
var yourData = context.read<YourNotifier>();
//For watching the data
var yourData = context.watch<YourNotifier>();
So, what you need to do is:
Change
ProfileModel profile =
Provider.of<ProfileNotifier>(context, listen: false).profile;
to
ProfileModel profile =
Provider.of<ProfileNotifier>(context, listen: true).profile;
//Or
ProfileModel profile = context.watch<ProfileNotifier>().profile;
Edit: Also, considering good UX, you can use a bool flag to update the UI whenever the data is loaded and if it's loading, show a CircularProgressIndicator.
I have this function that is working inside a future builder:
Future<Data> fetchData(String barCode, String url) async {
final response = await http.get(Uri.parse(url + barCode));
Map<String, dynamic> novoJson = json.decode(utf8.decode(response.bodyBytes));
novoJson.forEach((key, value) {
if (value == null) {
novoJson.update(key, (value) => "Não encontrado");
}
});
if (response.statusCode == 200) {
return Data.fromJson(novoJson);
} else {
throw Exception('Failed to load album');
}
}
class Data {
final Map<String, dynamic> response;
Data({required this.response});
factory Data.fromJson(Map<String, dynamic> json) {
return Data(response: json);
}
}
Now i'm trying to access the json it returns inside a ElevatedButton, like this:
onPressed: () {
if (_formKey.currentState!.validate()) {
var futureData = fetchData(myController.text, args.url);
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(title: animacaoRespostaApi());
});
futureData.whenComplete(() {
Navigator.of(context).pop();
print(futureData);
return Navigator.pushNamed(context, args.rota, arguments: ParametrosRetornoConsulta(myController.text, args.url));
});
}
}
But when print it I get an instance of Future instead of the object, and I can't access it's content with futureData['id'] for example.
How can I make futureData stop being an Future and become a iterable object ?
Future<Data> fetchData is Future, you need to use await for data.
onPressed: () async {
if (_formKey.currentState!.validate()) {
var futureData = await fetchData(myController.text, args.url);
I am trying to check if the product already added to the basket and give the message with snackbar "This product already added". So user can not add the same product to the cart again.
here is the code of mine :
//Button calling controller adding item to cart
InkWell(
onTap:(){
_con.addItemToCart(_con.product);
});
//Controller adding item to cart
Future addItemToCart(Product product, {bool reset = false}) async {
setState(() {
this.loadCart = true;
});
Cart value =
await addToCart(product, scaffoldKey, context, quantity: quantity);
setState(() {
this.cart = value;
this.loadCart = false;
this.totalProductsInStock = this.product.availability;
this.totalProductsInStock -= this.totalProductsInStock - this.quantity ;
});
}
Future<Cart> addToCart(Product product, GlobalKey<ScaffoldState> scaffoldKey, BuildContext buildContext, {bool reset = false, int quantity = 1}) async {
var _cartPoduct = new CartProduct();
_cartPoduct.productId = product.id;
_cartPoduct.optionId = product.optionId;
_cartPoduct.quantity = quantity;
Cart value = await CartRepository.addCart(_cartPoduct, reset);
return value;
}
//Request
static Future<Cart> addCart(CartProduct cartProduct, bool reset) async {
User _user = userRepo.currentUser.value;
if (_user.apiToken == null) {
return new Cart();
}
Map<String, dynamic> decodedJSON = {};
cartProduct.userId = _user.id;
final String url =
'${GlobalConfiguration().getString('api_base_url')}';
final client = new http.Client();
final response = await client
.get(url, headers: {HttpHeaders.contentTypeHeader: 'application/json'});
try {
decodedJSON = json.decode(response.body)['data'] as Map<String, dynamic>;
} on FormatException catch (e) {
}
return Cart.fromJSON(decodedJSON);
}
I created a temporary list to check if the product has been added to the cart, and I have it checked with provider. You can check it maybe it will be useful for you
link