How to read StateNotifierProvider.family without passing value? - flutter

I have implemented StateNotifierProvider with ".family" modifier:
class OrderReviewNotifier extends StateNotifier<OrderReviewState> {
final OrderReviewRepository repository;
OrderReviewNotifier(
this.repository,
int orderId,
) : super(OrderReviewState.initial(orderId));
Future<void> getOrderItems() async {
//.....
}
}
final orderReviewProvider = StateNotifierProvider.autoDispose
.family<OrderReviewNotifier, OrderReviewState, int>(
(ref, orderId) {
return OrderReviewNotifier(
ref.watch(orderReviewRepositoryProvider),
orderId,
);
},
);
Then in Consumer I watch it:
Consumer(
builder: (context, watch, child) {
final state = watch(orderReviewProvider(order.id));
//.....
},
);
But when I want to read it, I need to pass order.id too:
onTap: () {
context
.read(orderReviewProvider(order.id).notifier)
.getOrderItems();
},
When I want to send events to notifier from another file, I don't have order.id.
How to get out of this situation?
Thanks for any help!

I figured out.
All I needed was StateProvider.
final selectedOrderProvider = StateProvider<Order?>((ref) => null);
Then in orderReviewProvider I can easily get orderId.
final orderReviewProvider =
StateNotifierProvider.autoDispose<OrderReviewNotifier, OrderReviewState>(
(ref) {
return OrderReviewNotifier(
ref.read,
orderId: ref.watch(selectedOrderProvider).state!.id,
repository: ref.watch(orderReviewRepositoryProvider),
);
},
);
class OrderReviewNotifier extends StateNotifier<OrderReviewState> {
OrderReviewNotifier(
this.read, {
required int orderId,
required this.repository,
}) : super(OrderReviewState.initial(orderId)) {
getOrderItems();
}
final Reader read;
final OrderReviewRepository repository;
Future<void> getOrderItems() async {
state = state.copyWith(
isLoading: true,
error: null,
);
final result = await repository.getOrderItems(state.orderId);
final checkedItemIds = await repository.getCheckedItemIds(state.orderId);
if (!mounted) {
return;
}
result.when(
data: (data) {
final isAllItemsChecked = !checkedItemIds.containsValue(false) &&
checkedItemIds.length >= data.length;
state = state.copyWith(
orderItems: data,
checkedItemIds: checkedItemIds,
isAllItemsChecked: isAllItemsChecked,
);
},
error: (message) {
state = state.copyWith(
error: message,
);
},
);
state = state.copyWith(
isLoading: false,
);
}
}
The documentation describes the work with this well: link.

Related

I'm making a note app and the NotesView clears all other notes and replaces it with my most recent note

This is the NotesView:
import 'package:app_two/lib/constants/routes.dart';
import 'package:app_two/lib/enums/menu_action.dart';
import 'package:app_two/lib/services/auth/auth_service.dart';
import 'package:app_two/lib/services/auth/crud/notes_service.dart';
import 'package:flutter/material.dart';
class NotesView extends StatefulWidget {
const NotesView({super.key});
#override
State<NotesView> createState() => _NotesViewState();
}
class _NotesViewState extends State<NotesView> {
late Future _myFuture;
late final NotesService _notesService;
String get userEmail => AuthService.firebase().currentUser!.email!;
#override
void initState() {
super.initState();
_notesService = NotesService();
_myFuture = getGet();
}
// #override //delete
// void dispose() {
// _notesService.close();
// super.dispose();
// }
getGet() async {
return await _notesService.getOrCreateUser(email: userEmail);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Your Notes'),
actions: [
IconButton(
onPressed: () {
Navigator.of(context).pushNamed(newNoteRoute);
},
icon: const Icon(Icons.add),
),
PopupMenuButton<MenuAction>(
onSelected: (value) async {
switch (value) {
case MenuAction.logout:
final shouldLogout = await showLogOutDialog(context);
if (shouldLogout) {
await AuthService.firebase().logOut();
Navigator.of(context).pushNamedAndRemoveUntil(
loginRoute,
(_) => false,
);
}
}
},
itemBuilder: (context) {
return const [
PopupMenuItem<MenuAction>(
value: MenuAction.logout,
child: Text('Logout'),
),
];
},
)
],
),
body: FutureBuilder(
future: _myFuture,
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.done:
return StreamBuilder(
stream: _notesService.allNotes,
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
case ConnectionState.active:
if (snapshot.hasData) {
final allNotes = snapshot.data as List<DatabaseNote>;
return ListView.builder(
itemCount: allNotes.length,
itemBuilder: (context, index) {
final note = allNotes[index];
return ListTile(
title: Text(
note.text,
maxLines: 1,
softWrap: true,
overflow: TextOverflow.ellipsis,
),
);
},
);
} else {
return const CircularProgressIndicator();
}
default:
return const CircularProgressIndicator();
}
},
);
default:
return const CircularProgressIndicator();
}
},
),
);
}
Future<bool> showLogOutDialog(BuildContext context) {
return showDialog<bool>(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Sign out'),
content: const Text('Are you sure you want to sign out?'),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop(false);
},
child: const Text('Cancel')),
TextButton(
onPressed: () {
Navigator.of(context).pop(true);
},
child: const Text('Logout'))
],
);
},
).then((value) => value ?? false);
}
}
//Notes_service
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:sqflite/sqflite.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' show join;
import 'crud_exceptions.dart';
class NotesService {
Database? _db;
List<DatabaseNote> _notes = [];
static final NotesService _shared = NotesService._sharedInstance();
NotesService._sharedInstance() {
_notesStreamController = StreamController<List<DatabaseNote>>.broadcast(
onListen: () {
_notesStreamController.sink.add(_notes);
//populate stream with a stream of notes that we've already read from the database
},
);
}
factory NotesService() => _shared; //singleton
late final StreamController<List<DatabaseNote>> _notesStreamController;
Stream<List<DatabaseNote>> get allNotes => _notesStreamController.stream;
Future<DatabaseUser?> getOrCreateUser({required String email}) async {
try {
final user = await getUser(email: email);
return user;
} on CouldNotFindUser {
final createdUser = await createUser(email: email);
return createdUser;
} on DatabaseIsNotOpen {
open();
} catch (e) {
print(e.toString());
rethrow;
}
}
Future<void> _cacheNotes() async {
final allNotes = await getAllNotes();
_notes = allNotes.toList();
_notesStreamController.add(_notes);
}
Future<DatabaseNote> updateNote({
required DatabaseNote note,
required String text,
}) async {
await _ensureDbIsOpen();
final db = _getDatabaseorThrow();
//make sure note exists
await getNote(id: note.id);
//update db
final updatesCount = await db.update(noteTable, {
textColumn: text,
isSyncedwithCloudColumn: 0,
});
if (updatesCount == 0) {
throw CouldNotUpdateNote();
} else {
final updatedNote = await getNote(id: note.id);
_notes.removeWhere((note) => note.id == updatedNote.id);
_notes.add(updatedNote);
_notesStreamController.add(_notes);
return updatedNote;
}
}
Future<Iterable<DatabaseNote>> getAllNotes() async {
await _ensureDbIsOpen();
final db = _getDatabaseorThrow();
final notes = await db.query(noteTable);
return notes.map((noteRow) => DatabaseNote.fromRow(noteRow));
}
Future<DatabaseNote> getNote({required int id}) async {
await _ensureDbIsOpen();
final db = _getDatabaseorThrow();
final notes = await db.query(
noteTable,
limit: 1,
where: 'id = ?',
whereArgs: [id],
);
if (notes.isEmpty) {
throw CouldNotFindNote();
} else {
final note = DatabaseNote.fromRow(notes.first);
_notes.removeWhere((note) => note.id == id);
_notesStreamController.add(_notes);
return note;
}
}
Future<int> deleteAllNotes() async {
await _ensureDbIsOpen();
final db = _getDatabaseorThrow();
final numberOfdeletions = await db.delete(noteTable);
_notes = [];
_notesStreamController.add(_notes);
return numberOfdeletions;
}
Future<void> deleteNote({required int id}) async {
await _ensureDbIsOpen();
final db = _getDatabaseorThrow();
final deletedCount = await db.delete(
noteTable,
where: 'id = ?',
whereArgs: [id],
);
if (deletedCount == 0) {
throw CouldNotDeleteNote();
} else {
_notes.removeWhere((note) => note.id == id);
_notesStreamController.add(_notes);
}
}
Future<DatabaseNote> createNote({required DatabaseUser owner}) async {
await _ensureDbIsOpen();
final db = _getDatabaseorThrow();
//Make sure owner exists in database withthe correct id
final dbUser = await getUser(email: owner.email);
if (dbUser != owner) {
throw CouldNotFindUser();
}
const text = '';
//create the note
final noteId = await db.insert(noteTable, {
userIdColumn: owner.id,
textColumn: text,
isSyncedwithCloudColumn: 1,
});
final note = DatabaseNote(
id: noteId,
userId: owner.id,
text: text,
isSyncedWithCloud: true,
);
_notes.add(note);
_notesStreamController.add(_notes);
return note;
}
Future<DatabaseUser> getUser({required String email}) async {
await _ensureDbIsOpen();
final db = _getDatabaseorThrow();
final results = await db.query(
userTable,
limit: 1,
where: 'email = ?',
whereArgs: [email.toLowerCase()],
);
if (results.isEmpty) {
throw CouldNotFindUser();
} else {
return DatabaseUser.fromRow(results.first);
}
}
Future<DatabaseUser> createUser({required String email}) async {
await _ensureDbIsOpen();
final db = _getDatabaseorThrow();
final results = await db.query(
userTable,
limit: 1,
where: 'email = ?',
whereArgs: [email.toLowerCase()],
);
if (results.isNotEmpty) {
throw UserAlreadyExists();
}
final userId = await db.insert(userTable, {
emailColumn: email.toLowerCase(),
});
return DatabaseUser(id: userId, email: email);
}
Future<void> deleteUser({required String email}) async {
final db = _getDatabaseorThrow();
final deletedCount = await db.delete(
userTable,
where: 'email = ?',
whereArgs: [email.toLowerCase()],
);
if (deletedCount != 1) {
throw CouldNotDeleteUser();
}
}
Database _getDatabaseorThrow() {
final db = _db;
if (db == null) {
throw DatabaseIsNotOpen();
} else {
return db;
}
}
Future<void> close() async {
final db = _db;
if (db == null) {
try {
await open();
} on DatabaseIsNotOpen {
open();
}
// throw DatabaseIsNotOpen();
} else {
await db.close();
_db = null;
}
}
Future<void> _ensureDbIsOpen() async {
if (_db != null) {
try {
await open();
} on DatabaseAlreadyOpenException {
//empty
}
}
}
Future<void> open() async {
if (_db != null) {
throw DatabaseAlreadyOpenException();
}
try {
await _ensureDbIsOpen();
final docsPath = await getApplicationDocumentsDirectory();
final dbPath = join(docsPath.path, dbName);
final db = await openDatabase(dbPath);
_db = db;
//create user table
await db.execute(createUserTable);
//create note table
await db.execute(createNoteTable);
await _cacheNotes();
} on MissingPlatformDirectoryException {
throw UnableToGetDocumentDirectory();
}
}
}
#immutable
class DatabaseUser {
final int id;
final String email;
const DatabaseUser({
required this.id,
required this.email,
});
DatabaseUser.fromRow(Map<String, Object?> map)
: id = map[idColumn] as int,
email = map[emailColumn] as String;
#override
String toString() => 'Person, ID = $id, email= $email';
#override
bool operator ==(covariant DatabaseUser other) => id == other.id;
#override
int get hashCode => id.hashCode;
}
class DatabaseNote {
final int id;
final int userId;
final String text;
final bool isSyncedWithCloud;
DatabaseNote({
required this.id,
required this.userId,
required this.text,
required this.isSyncedWithCloud,
});
DatabaseNote.fromRow(Map<String, Object?> map)
: id = map[idColumn] as int,
userId = map[userIdColumn] as int,
text = map[textColumn] as String,
isSyncedWithCloud =
(map[isSyncedwithCloudColumn] as int) == 1 ? true : false;
#override
String toString() =>
'Note, ID = $id, userId = $userId, isSyncedWithCloud = $isSyncedWithCloud, text = $text';
#override
bool operator ==(covariant DatabaseNote other) => id == other.id;
#override
int get hashCode => id.hashCode;
}
const dbName = 'notes.db';
const noteTable = 'note';
const userTable = 'user';
const idColumn = 'id';
const emailColumn = 'email';
const userIdColumn = 'user_id';
const textColumn = 'text';
const isSyncedwithCloudColumn = 'is_synced_with_cloud';
const createUserTable = '''CREATE TABLE IF NOT EXISTS "user" (
"id" INTEGER NOT NULL,
"email" TEXT NOT NULL UNIQUE,
PRIMARY KEY("id" AUTOINCREMENT)
);''';
const createNoteTable = '''CREATE TABLE IF NOT EXISTS "note" (
"id" INTEGER NOT NULL,
"user_id" INTEGER NOT NULL,
"text" TEXT,
"is_synced_with_cloud" INTEGER DEFAULT 0,
FOREIGN KEY("user_id") REFERENCES "user"("id"),
PRIMARY KEY("id" AUTOINCREMENT)
);''';
I'm making a note app and the NotesView clears all other notes and replaces it with my most recent not. I have no idea why this is happening
Each note you input should display on a tile. But instead this happens:
But only after I restart the app. In the next images, I add a new note and then restart the application
I believe I did something very in my StreamBuilder but don't know what.
I'd appreciate any assistance.
Try changing the case ConnectionState.active to case ConnectionState.done in your StreamBuilder.
Duplication may be occurring due to building your list inside case ConnectionState.active.
I don't know what your db.update does or your SQL query looks like but if you don't pass the id how are you supposed to update the note?
Future<DatabaseNote> updateNote({
required DatabaseNote note,
required String text,
}) async {
await _ensureDbIsOpen();
final db = _getDatabaseorThrow();
//make sure note exists
await getNote(id: note.id);
//update db
final updatesCount = await db.update(noteTable, { ///how does magically know what note to update?
textColumn: text,
isSyncedwithCloudColumn: 0,
});
if (updatesCount == 0) {
throw CouldNotUpdateNote();
} else {
final updatedNote = await getNote(id: note.id);
_notes.removeWhere((note) => note.id == updatedNote.id);
_notes.add(updatedNote);
_notesStreamController.add(_notes);
return updatedNote;
}
}
If you're using INSERT OR REPLACE INTO because you don't pass the id you're creating a new row instead of updating it and your cache list is giving you false information because at the end you think you update it correctly and just replace the note with the same id, your cache list doesn't reflect the real information anymore.
If you're updating all notes in your table because the where clause doesn't have an id you will present your current problem when restarting
Check your db layer apart to see if you're actually doing what you want.

How to compose async action and StateNotifierProvider?

I have some stream source (from FlutterReactiveBle library) and reflect it to state managed by StateNotifier.
But I can't sure whether it is right way from the following source. I'm especially afraid of _setState invalidates connectionProvider. And it looks like a bit complicated.
How can I improve this?
It may not work because I wrote it just for illustration.
#freezed
class DeviceConnections with _$DeviceConnections {
const DeviceConnections._();
const factory DeviceConnections({
Map<String, StreamSubscription<void>> connectings,
MapEntry<String, StreamSubscription<void>>? connected,
}) = _DeviceConnections;
}
class SimpleStateNotifier<T> extends StateNotifier<T> {
SimpleStateNotifier(super.state);
void update(T newState) {
state = newState;
}
}
StateNotifierProvider<SimpleStateNotifier<T>, T> simpleStateNotifierProvider<T>(
T initialState,
) {
return StateNotifierProvider<SimpleStateNotifier<T>, T>((ref) {
return SimpleStateNotifier(initialState);
});
}
class DeviceConnector {
DeviceConnector({
required FlutterReactiveBle ble,
required DeviceConnections state,
required Function(DeviceConnections) setState,
required Iterable<String> deviceIds,
}) : _ble = ble,
_state = state,
_setState = setState,
_deviceIds = deviceIds;
final FlutterReactiveBle _ble;
final DeviceConnections _state;
final Function(DeviceConnections) _setState;
final Iterable<String> _deviceIds;
void connect() {
final subscriptions = <String, StreamSubscription<void>>{};
for (final id in _deviceIds) {
subscriptions[id] = _connectInterval(id).listen((event) {});
}
_setState(_state.copyWith(connectings: subscriptions));
}
void disconnect() {
for (final subscription in _state.connectings.values) {
subscription.cancel();
}
_state.connected?.value.cancel();
_setState(DeviceConnections());
}
Stream<void> _connectInterval(String id) async* {
while (true) {
final connection = _ble.connectToDevice(
id: id,
connectionTimeout: Duration(seconds: 10),
);
await for (final update in connection) {
switch (update.connectionState) {
case DeviceConnectionState.connected:
final subscription = _state.connectings[id];
if (subscription != null) {
final others =
_state.connectings.entries.where((x) => x.key != id).toList();
for (final connection in others) {
connection.value.cancel();
}
_setState(
DeviceConnections(connected: MapEntry(id, subscription)),
);
}
break;
default:
break;
}
}
}
}
}
final connectionStateProvider = simpleStateNotifierProvider(
DeviceConnections(),
);
final bleProvider = Provider((_) => FlutterReactiveBle());
class AnotherState extends StateNotifier<List<String>> {
AnotherState(super.state);
}
final anotherStateNotifierProvider = StateNotifierProvider<AnotherState, List<String>>((ref) {
return AnotherState([]);
});
final connectionProvider = Provider((ref) {
final ble = ref.watch(bleProvider);
final connectorState = ref.watch(connectionStateProvider);
final connectorNotifier = ref.watch(connectionStateProvider.notifier);
final deviceIds = ref.watch(anotherStateNotifierProvider);
final connector = DeviceConnector(
ble: ble,
deviceIds: deviceIds,
state: connectorState,
setState: connectorNotifier.update,
);
ref.onDispose(connector.disconnect);
return connector;
});

Flutter autoDispose riverpod StateNotifierProvider

This is my shared riverpod class that i want to use that on multiple screens, but after navigate to another screen using ref.listen couldn't dispose or cancel and using another ref.listen work twice, how can i cancel each ref.listen on screen which i used that? for example you suppose i have two screen A and B and into A screen i have
A screen
final future = ref.watch(requestProvider);
ref.listen<NetworkRequestState<int?>>(requestProvider, (
NetworkRequestState? previousState,
NetworkRequestState newState,
) {});
on this ref.listen i navigate to another screen when server return 200 ok? now in B screen which i have ref.listen again:
B screen
final future = ref.watch(requestProvider);
ref.listen<NetworkRequestState<int?>>(requestProvider, (
NetworkRequestState? previousState,
NetworkRequestState newState,
) {});
without sending any request to server this listener work and listen to previous listener
requestProvider on this class shared between multiple screens and autoDispose don't work for that, because after creating another StateNotifierProvider such as requestProviderA_Screen work fine without problem, for example:
final requestProvider = StateNotifierProvider.autoDispose<RequestNotifier,
NetworkRequestState<int?>>(
(ref) => RequestNotifier(ref.watch(requestRepositoryProvider)));
final requestProviderA_Screen = StateNotifierProvider.autoDispose<RequestNotifier,
NetworkRequestState<int?>>(
(ref) => RequestNotifier(ref.watch(requestRepositoryProvider)));
my request riverpod class:
final requestRepositoryProvider =
Provider.autoDispose<Repository>((ref) => Repository(ref.read));
final requestProvider = StateNotifierProvider.autoDispose<RequestNotifier,
NetworkRequestState<int?>>(
(ref) => RequestNotifier(ref.watch(requestRepositoryProvider)));
class Repository {
final Reader _reader;
Repository(this._reader);
Future<int?> getResponse(
HTTP method, String endPoint, Map<String, dynamic> parameters) async {
try {
const r = RetryOptions(maxAttempts: 3);
final response = await r.retry(
() => _submit(method, endPoint, parameters),
retryIf: (e) => e is SocketException || e is TimeoutException,
);
return response.statusCode;
} on DioError catch (e) {
throw (e.response != null
? e.response!.statusCode
: e.error.osError.errorCode) as Object;
}
}
Future<Response> _submit(
HTTP method, String endPoint, Map<String, dynamic> parameters) {
final Options options = Options(
headers: {'Content-Type': 'application/json'},
);
late Future<Response> _r;
switch (method) {
case HTTP.GET:
_r = _reader(dioProvider).get(
endPoint,
queryParameters: parameters,
options: options,
);
break;
case HTTP.POST:
_r = _reader(dioProvider).post(
endPoint,
queryParameters: parameters,
options: options,
);
break;
}
return _r.timeout(const Duration(seconds: 30));
}
}
class RequestNotifier extends RequestStateNotifier<int?> {
final Repository _repository;
RequestNotifier(this._repository);
Future<NetworkRequestState<int?>> send({
required HTTP method,
required String endPoint,
required Map<String, dynamic> parameters,
}) =>
makeRequest(
() => _repository.getResponse(method, endPoint, parameters));
}
and one of screen which i use this class:
class SignUp extends HookConsumerWidget {
final String mobileNumber;
const SignUp({Key? key, required this.mobileNumber}) : super(key: key);
#override
Widget build(BuildContext context, WidgetRef ref) {
final _formKey = useMemoized(() => GlobalKey<FormState>());
final _nameFamily = useTextEditingController();
final future = ref.watch(requestProvider);
useEffect(() {
_nameFamily.dispose();
}, [_nameFamily]);
ref.listen<NetworkRequestState<int?>>(requestProvider, (
NetworkRequestState? previousState,
NetworkRequestState newState,
) {
newState.when(
idle: () {},
//...
}
success: (status) {
//...
Routes.seafarer.navigate(
'/complete-register',
params: {
'mobile_number': mobileNumber.trim(),
'name_family': _nameFamily.text.trim()
},
);
},
error: (error, stackTrace) {
//...
});
});
final _onSubmit = useMemoized(
() => () {
if (_nameFamily.text.trim().isEmpty) {
//...
} else {
//..
ref.read(requestProvider.notifier).send(
method: HTTP.GET,
endPoint: Server.$updateNameFamily,
parameters: {
'mobile_number': mobileNumber,
'name_family': _nameFamily.text.trim()
});
}
},
[_formKey],
);
return Scaffold(
//...
);
}
}

Initialize StateProvider in FutureProvider

My app starts with retrieving data that is important throughout the flow of the app mainContent. Most of this data is static
Navigation screens
Widget stackPages(WidgetRef ref) {
AsyncValue<Map<String, Object>> mainContent = ref.watch(mainContentFutureProvider);
return mainContent.when(
loading: () => Center(child: CircularProgressIndicator()),
error: (e, st) => Center(child: Text("Error: " + e.toString() + " " + st.toString())),
data: (content) {
return Stack(
children: [
_buildOffstageNavigator(ref, "Home", content),
_buildOffstageNavigator(ref, "Page1", content),
_buildOffstageNavigator(ref, "Page2", content),
_buildOffstageNavigator(ref, "Page3", content)
],
);
},
);
}
content retrieval (mainContentFutureProvider)
final mainContentFutureProvider= FutureProvider<Map<String, Object>>((ref) async {
List response = await Future.wait([
DataController.userInfoDB.getUsers(),
DataController.userInfoDB.getAnotherList(),
DataController.userInfoDB.getAnotherList,
]);
return {
"users": response[0],
"some_list": response[1],
"some_list": response[2],
};
},
);
User class (simplified)
class User{
String id;
String email;
List<Vehicle> vehicles = [];
User(this.email, this.vehicles);
User.fromJson(Map<String, dynamic> json)
: id = json['id'],
displayName = json['display_name'],
}
problem
in the garage screen of the app the user can add or remove vehicles. When a user adds or removes a vehicle this affects the entire flow of the app. So this User needs to have its Notifier class
CurrentUserNotifier
class CurrentUserNotifier extends StateNotifier<User> {
final User user;
CurrentUserNotifier(this.user) : super(null);
void addUserVehicle(Vehicle vehicle) {
state..vehicles.add(vehicle);
}
void removeUserVehicle(int vehicleId) {
state..vehicles.removeWhere((v) => v.id == vehicleId);
}
}
currentUserProvider
final currentUserProvider = StateNotifierProvider.family<CurrentUserNotifier, User, User>((ref, user) {
return CurrentUserNotifier(user);
});
Currently I am retrieving a List<User> and want only to have the current user to be coming from a provider in my app. As you see I have made a .family from StateNotifierProvider so I can perform the following thing:
content retrieval (mainContentFutureProvider)
final mainContentFutureProvider= FutureProvider<Map<String, Object>>((ref) async {
List response = await Future.wait([
DataController.userInfoDB.getUsers(),
DataController.userInfoDB.getAnotherList(),
DataController.userInfoDB.getAnotherList,
]);
---> currentUserProvider(response[0].first);
return {
"users": response[0],
"some_list": response[1],
"some_list": response[2],
};
},
);
But for any page that deals with my User object it needs to pass through the user object as parameter to my currentUserProvider
like:
press: () async {
ref.read(currentUserProvider(user).notifier).addUserVehicle(vehicle);
}
I want the provider just set the value of the StateNotifierProvider once, am I making a pattern/flow mistake here?
Try this:
Have your CurrentUserNotifier like so.
final currentUserProvider = StateNotifierProvider<CurrentUserNotifier, User>((ref) {
return CurrentUserNotifier();
});
class CurrentUserNotifier extends StateNotifier<User?> {
CurrentUserNotifier() : super(null);
void setUser(User user){
state = user;
}
void addUserVehicle(Vehicle vehicle) {
state = state..vehicles.add(vehicle);
}
void removeUserVehicle(int vehicleId) {
state = state..vehicles.removeWhere((v) => v.id == vehicleId);
}
}
Then set the user like so:
final mainContentFutureProvider= FutureProvider<Map<String, Object>>((ref) async {
List response = await Future.wait([
DataController.userInfoDB.getUsers(),
DataController.userInfoDB.getAnotherList(),
DataController.userInfoDB.getAnotherList,
]);
ref.read(currentUserProvider.notifier).setUser(response[0].first);
return {
"users": response[0],
"some_list": response[1],
"some_list": response[2],
};
},
);
Then you can do:
press: () async {
ref.read(currentUserProvider.notifier).addUserVehicle(vehicle);
}

Refresh page if data isn't shown on screen

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.