Can Riverpod detect changes to Objectbox state / data? - flutter

I am using Riverpod with ObjectBox. Can Riverpod detect changes to Objectbox state / data? Riverpod appears to just ignore changes, presumably treating it as the same object instance.
This reads the required value, but doesn't update when it changes
final reviewText = Provider.family.autoDispose((ref, int bookId) {
return ref.watch(objectBox).reviewBox.get(bookId)!.reviewText;
});
This works, but I have to consume it as a stream
final reviewTextStream =
StreamProvider.autoDispose.family((ref, int bookId) async* {
final _objectBox = ref.read(objectBox);
final query = _objectBox.reviewBox
.query(Review_.id.equals(bookId))
.watch(triggerImmediately: true);
await for (final review in query) {
ref.onDispose(() => review.close());
yield review.findFirst()?.reviewText;
}
});
Also in this second example, am I disposing of the query correctly?

Related

How to provide Future to inside Providers using Riverpod?

I'm trying to learn Riverpod with clean architecture.
I have following set/chain of providers:
final databaseFutureProvider = FutureProvider<Database>((ref) async {
final db = await DatabaseHelper().createDatabase(); // this is async because 'openDatabase' of sqflite is async
return db;
});
final toDoDatasourceProvider = Provider<ToDoDatasource>((ref) {
final db = ref.watch(databaseFutureProvider); // problem is here!!
return ToDoDatasourceImpl(db: db);
});
final toDoRepositoryProvider = Provider<ToDoRepository>((ref) {
final ds = ref.watch(toDoDatasourceProvider);
return ToDoRepositoryImpl(ds);
});
I am probably missing some small things or doing it completely wrong. How to properly provide DB (that is async in its nature)?
You don't need multiple providers, in your case since you would need ToDoRepository always you can just Initialized before running the app and use it later without worrying about the database connection state
Future<void> main(List<String> args) async {
// Initialization the db
final db = await DatabaseHelper().createDatabase();
ProviderScope(
overrides: [
// pass the db
toDoRepositoryProvider.overrideWithValue(db),
],
child: RootApp(),
);
}
final toDoRepositoryProvider = Provider<ToDoRepository>((ref) {
throw UnimplementedError();
});
I totally agree with Mohammed Alfateh's decision. In addition, you can use ProviderContainer()..read(toDoDatasourceProvider) and UncontrolledProviderScope, to asynchronously assign values in main method. And in ToDoDatasourceImpl call the async method init() to assign a value in the field late final db.

flutter X firebase: how to correctly initialize fields with data in different branches in firebase realtime database

I have a piece of code trying to initialize the fields with data from two different users in the firebase realtime database, I tried various ways but all of them don't work and the field is not initialized error keeps popping up.
Here is the code:
class _PartnerProfilePageState extends State<PartnerProfilePage> {
final userUID = FirebaseAuth.instance.currentUser!.uid;
final database = FirebaseDatabase.instance.ref();
late final partnerUID;
late final profilePath;
late final partnerName;
late final birthday;
late final en;
#override
initState() {
super.initState();
initInfo();
}
initInfo() async {
database.child(userUID).onValue.listen((event) {
final data = Map<dynamic, dynamic>.from(event.snapshot.value as Map);
setState(() {
partnerUID = data['partner'];
en = data['language'].toString().startsWith('en');
initPartnerInfo();
});
});
}
Future initPartnerInfo() async {
final ppsnapshot =
await database.child(partnerUID).child('profilePath').get();
profilePath = ppsnapshot.value.toString();
final nsnapshot = await database.child(partnerUID).child('username').get();
partnerName = nsnapshot.value.toString();
final bsnapshot = await database.child(partnerUID).child('birtday').get();
birthday = bsnapshot.value.toString();
}
//rest of other unrelated stuff like build down there
}
(My firebase realtime database has no 'user' branch but directly save every user in the root with their userid).
I think there is a problem with the async initializing. The build method can try to build before you initialize the last variable(Because initState method can not async). You can easily check my theory.Delete the 'method call'(the initInfo) inside the initState(). Just create a button in the screen, give it a anonymous async function, inside the function call your init method and try to call your variables like this:
() async{
await initInfo();
print('partnerUID');
}
Easy way to checking out initialization process. Hope it helps.

Riverpod StateNotifierProvider depend on a FutureProvider

I have a StateNotifierProvider that depends on a FutureProvider. Currently they look like below.
final catalogProvider = StateNotifierProvider<CatalogNotifier, CatalogState>((ref) {
final network = ref.watch(networkProvider.future); // future provider
return CatalogNotifier(network: network);
});
this makes my CatalogNotifier accept a Future<NetworkProvider> instead of NetworkProvider and requires me to do things like below.
await (await network).doGet(...)
What's the best way to avoid having to await multiple and allow CatalogNotifier to accept a bare NetworkProvider so I can write like await network.doGet(...) ?
for completeness as requested, below is the other related providers
final networkProvider = FutureProvider<Network>((ref) async {
final cache = await ref.watch(cacheProvider.future);
return Network(cacheManager: cache);
});
final cacheProvider = FutureProvider<CacheManager>((ref) async {
final info = await ref.watch(packageInfoProvider.future);
final key = 'cache-${info.buildNumber}';
return CacheManager(Config(
key,
stalePeriod: const Duration(days: 30),
maxNrOfCacheObjects: 100,
));
I'm sure I can take my cache provider as a future into the network provider, so it doesn't have to be a FutureProvider, but I'm interested in how to solve the issue above, since in another scenario, if I depend on say 3 or 4 FutureProviders, this may not be an option.
this makes my CatalogNotifier accept a Future instead of >NetworkProvider and requires me to do things like below.
I can't think of a way to get your desired result.
Could you not just accept an AsyncValue and handle it in the statenotifier?
final catalogProvider = StateNotifierProvider<CatalogNotifier, CatalogState>((ref) {
final network = ref.watch(networkProvider); // future provider
return CatalogNotifier(network: network);
});
Then you can:
void someFunction() async {
network.maybeWhen(
data: (network) => AsyncData(await network.doGet(...)),
orElse: () => state = AsyncLoading(),
);
}
with riverpod v2 and its codegen features this has become much easier since you no longer have to decide the type of the provider. (unless you want to)
StateNotifier in riverpod 2
#riverpod
Future<CatalogController> catalog(CatalogRef ref) async {
final network = await ref.watch(networkProvider.future);
return CatalogController(network: network);
}
Alternative approch in Riverpod 2
Quite often you want to have a value calculated and have a way to explicitely redo that calculation from UI. Like a list from network, but with a refresh button in UI. This can be modelled as below in riverpod 2.
#riverpod
Future<CatalogState> myFeed(MyFeedRef ref) async {
final json = await loadData('url');
return CatalogState(json);
}
// and when you want to refresh this from your UI, or from another provider
ref.invalidate(myFeedProvider);
// if you want to also get the new value in that location right after refreshing
final newValue = await ref.refresh(myFeedProvider);
Riverpod 2 also has loading and error properties for the providers. You can use these to show the UI accordingly. Though if you want to show the last result from the provider while your feed is loading or in an error state, you have to model this yourself with a provider that returns a stream/BehaviorSubject, caches the last value .etc.
you can make AsyncValue a subtype of StateNotifier, I use the Todo list as an example.
as follows:
class TodoNotifier extends StateNotifier<AsyncValue<List<Todo>>> {
TodoNotifier(this._ref) : super(const AsyncValue.loading()) {
_fetchData();
}
final Ref _ref;
Future<void> _fetchData() async {
state = const AsyncValue.loading();
// todoListProvider is of type FutureProvider
_ref.read(todoListProvider).when(data: (data) {
state = AsyncValue.data(data);
}, error: (err, stackTrace) {
state = AsyncValue.error(err, stackTrace: stackTrace);
}, loading: () {
state = const AsyncValue.loading();
});
}
void addTodo(Todo todo) {
if (state.hasValue) {
final todoList = state.value ?? [];
state = AsyncValue.data(List.from(todoList)..add(todo));
}
}
....
}

Flutter pagination with firestore stream

How to properly implement pagination with firestore stream on flutter (in this case flutter web) ?
my current approach with bloc which is most likely wrong is like this
function called on bloc when load next page, notice that i increased the lastPage variable of the state by 1 each time the function is called:
Stream<JobPostingState> _loadNextPage() async* {
yield state.copyWith(isLoading: true);
try {
service
.getAllDataByClassPage(state.lastPage+1)
.listen((List<Future<DataJob>> listDataJob) async {
List<DataJob?> listData = [];
await Future.forEach(listDataJob, (dynamic element) async {
DataJob data= await element;
listData.add(data);
});
bool isHasMoreData = state.listJobPostBlock.length!=listData.length;
//Update data on state here
});
} on Exception catch (e, s) {
yield StateFailure(error: e.toString());
}}
function called to get the stream data
Stream<List<Future<DataJob>>> getAllDataByClassPage(
String className, int page) {
Stream<QuerySnapshot> stream;
if (className.isNotEmpty)
stream = collection
.orderBy('timestamp', "desc")
.where('class', "==", className).limit(page*20)
.onSnapshot;
else
stream = collection.onSnapshot;
return stream.map((QuerySnapshot query) {
return query.docs.map((e) async {
return DataJob.fromMap(e.data());
}).toList();
});
}
With this approach it works as intended where the data loaded increased when i load next page and still listening to the stream, but i dont know if this is proper approach since it replace the stream could it possibly read the data twice and end up making my read count on firestore much more than without using pagination. Any advice is really appreciated, thanks.
Your approach is not very the best possible indeed, and as you scale you going to be more costly. What I would do in your shoes would be to create a global variable that represents your stream so you can manipulate it. I can't see all of your code so I am going to be as generic as possible so you can apply this to your code.
First let's declare the stream controller as a global variable that can hold the value of your stream:
StreamController<List<DocumentSnapshot>> streamController =
StreamController<List<DocumentSnapshot>>();
After that we need to change your getAllDataByClassPage function to the following:
async getAllDataByClassPage(String className) {
Stream stream = streamController.stream;
//taking out of the code your className logic
...
if(stream.isEmpty){
QuerySnapshot snap = await collection.orderBy('timestamp', "desc")
.where('class', "==", className)
.limit(20)
.onSnapshot
streamController.add(snap.docs);
}else{
DocumentSnapshot lastDoc = stream.last;
QuerySnapshot snap = await collection.orderBy('timestamp', "desc")
.where('class', "==", className)
.startAfterDocument(lastDoc)
.limit(20)
.onSnapshot;
streamController.add(snap.docs);
}
}
After that all you need to do in order to get the stream is invoke streamController.stream;
NOTE: I did not test this code but this is the general ideal of what you should try to do.
You can keep track of last document and if has more data on the list using startAfterDocument method. something like this
final data = await db
.collection(collection)
.where(field, arrayContains: value)
.limit(limit)
.startAfterDocument(lastDoc)
.get()
.then((snapshots) => {
'lastDoc': snapshots.docs[snapshots.size - 1],
'docs': snapshots.docs.map((e) => e.data()).toList(),
'hasMore': snapshots.docs.length == limit,
});

why the state could not get the newest in flutter after change the state in reducer

I am get the flutter state in buildView function like this:
Widget buildView(ArticleListState state, Dispatch dispatch, ViewService viewService) {
final articles = state.articles;
}
but the state could not get the newest, I am dispath to set articles in initial function in effect.dart like this:
Future _onInit(Action action, Context<ArticleListState> ctx) async {
ArticleListState articleListState = ctx.state;
List<int> ids = articleListState.articleIds;
List<Item> articles = new List();
for(final i in ids){
Item article = await Repo.fetchArticleItem(i);
if (article != null) {
articles.add(article);
}
}
if (articles != null) {
ctx.dispatch(ArticleListActionCreator.onSetArticles(articles));
}
}
and in reducer change the state:
ArticleListState _onSetArticles(ArticleListState state, Action action) {
ArticleListState newState = state.clone();
List<Item> articles = (action.payload as List<Item>);
newState.articles = articles;
newState.articleRequest.latestTime = 1;
return newState;
}
I think when return the newstate, the UI buildView should trigger the rerender,and the state may be the newest with article info. but the article info aways get null, and did not trigger rerender actuallly. what should I do to fix it?
you should implement the connector set function like this:
#override
void set(HomeListDefaultState state, ArticleListState subState) {
state.articleListState.articles = subState.articles;
}