Using Hive with Riverpod, not sure what to load during loading phase - flutter

I am trying to learn how to use Hive for local storage and Riverpod for state managment in my flutter app.
final _localStorageProvider = FutureProvider<Box>(
(ref) async {
final appDocumentDir = await path_provider.getApplicationDocumentsDirectory();
Hive
..init(appDocumentDir.path)
..registerAdapter(QuizOptionModelAdapter());
return Hive.openBox(keys.localStorageBoxKey);
}
);
final _localDataSourceProvider = Provider<QuestionLocalDataSourceImpl>(
(ref) => QuestionLocalDataSourceImpl(
uploadQuestionBox: ref.watch(_localStorageProvider).maybeWhen(
data:(d) => d,
error:(e, trace) {
print("***************************");
print(e);
print(trace);
print("***************************");
throw Exception("FAILED");
},
orElse: () => throw Exception("OR ELSE")
),
),
);
My problem is, I am not sure what other return value I can use during error & orElse phase other than exceptions. Since I am using exception, very briefly before the actual page loads, I get an ugly error page as shown in the screenshot, which disappers in 1 second after the actual elements are rendered.
Any help to avoid throwing errors during error & orElse phase would be really helpful.
So far have tried these solutions without any luck
final _localDataSourceProvider = Provider<QuestionLocalDataSourceImpl>(
(ref) => QuestionLocalDataSourceImpl(
uploadQuestionBox: ref.watch(_localStorageProvider).maybeWhen(
data:(d) => d,
orElse: () => AsyncValue.loading() as Box
),
),
);

I think the issue is that you try to deal with error and orElse within the Provider. A more common way to do this is to watch the FutureProvider within a Widget's build method, and to render a proper data Widget if the data from the FutureProvider is available, and another Widget if the data is not yet available (loading) or if there is an error. That way, all three cases of the Provider's when method return the same type: a Widget.
From the Riverpod docs, see this example:
final configurationsProvider = FutureProvider<Configuration>((ref) async {
final uri = Uri.parse('configs.json');
final rawJson = await File.fromUri(uri).readAsString();
return Configuration.fromJson(json.decode(rawJson));
});
class Example extends ConsumerWidget {
#override
Widget build(BuildContext context, WidgetRef ref) {
final configs = ref.watch(configurationsProvider);
// Use Riverpod's built-in support
// for error/loading states using "when":
return configs.when(
loading: () => const CircularProgressIndicator(),
error: (err, stack) => Text('Error $err'),
data: (configs) => Text('data: ${configs.host}'),
);
}
}
In your case, if you want to retain the _localDataSourceProvider then my suggestion would be to make that also a FutureProvider that 'wraps' the _localStorageProvider, and to watch the _localDataSourceProvider within the build method of the Widget where you render the data. There, you can use the .when(...) clauses for loading (for example to show a CircularProgressIndicator until the data is loaded), data to render the data, and if necessary error to capture any errors that Hive may generate through the FutureProvider.

Related

Flutter: accessing providers from other providers

For my flutter project, I am using the following multiple providers below:
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<FirstProvider>(
create: (context) => FirstProvider(),
),
ChangeNotifierProvider<SecondProvider>(
create: (context) => SecondProvider(),
),
ChangeNotifierProvider<ThirdProvider>(
create: (context) => ThirdProvider(),
),
ChangeNotifierProvider<FourthProvider>(
create: (context) => FourthProvider(),
),
],
child: const MainApp(),
);
}
Because sometimes I need to either get data or call functions from different providers from another provider, I am using it like this:
//First Provider
class FirstProvider with ChangeNotifier {
void callFunctionFromSecondProvider({
required BuildContext context,
}) {
//Access the SecondProvider
final secondProvider= Provider.of<SecondProvider>(
context,
listen: false,
);
secondProvider.myFunction();
}
}
//Second Provider
class SecondProvider with ChangeNotifier {
bool _currentValue = true;
void myFunction(){
//Do something
}
}
The callFunctionFromSecondProvider()of the FirstProvider is called from a widget and it will call myFunction() successfully, most of times.
Depending on the complexity of the function, I am sometimes experiencing that I can't access the SecondProvider, presumably due to context being null, when the widget state changes.
I am reading some documents online regarding provider, and they are suggesting changenotifierproxyprovider for what I understood as 1 to 1 provider relationship.
However, in my case, one provider needs to be accessed by multiple providers and vice versa.
Question:
Is there a more appropriate way that I can approach my case where one provider can be accessed by multiple providers?
EDIT:
Accessing provider should also be able to access different variable values without creating a new instance.
Instead of passing context to the callFunctionFromSecondProvider function add the second provider as the parameter. So the function looks like the below.
Not sure this is the correct way of doing that but my context null issue was fixed this way.
void callFunctionFromSecondProvider({
required SecondProvider secondProvider,
}) {
secondProvider.myFunction();
}
}
Alright.
So it looks like Riverpod by the same author is the way to go as it addresses alot of flaws such as Provider being dependent on the widget tree, in my case, where the underlying issue came from.
—--------
For the time being, I still need to use the provider and for a quick and dirty solution, I am providing the context of not only the current widget that I am trying to access the provider, but also passing the parent context of the widget directly, so that in case a modal (for example) is closed, then any subsequent provider call can still be executed using the parent context.
Hope this helps.

Flutter Widget Test passes on device but not without one

I have a widget that displays a list of data from an api and I'm trying to write tests for it's various states starting with it's empty state.
Currently my test pumps the widget in question, the widget makes a network call which I'm mocking and then it checks to see if the empty state text is shown.
This test passes when I run the test on a device using
flutter run --flavor myTestApp -t test/booking/booking_list_widget_test.dart
But fails when I run it from the IDE (IntelliJ) the failure exception is:
The following TestFailure object was thrown running a test:
Expected: exactly one matching node in the widget tree
Actual: _TextFinder:<zero widgets with text "You're all caught up." (ignoring offstage widgets)>
Which: means none were found but one was expected
It seems to not be waiting for the Duration given to tester.pump and I've tried wrapping it in a tester.runAsync and using pump and settle etc but i cannot get it to pass.
Any ideas welcome I cant share the widget tree but I can share the test
void main() {
setupFirebaseAuthMocks();
setUpAll(
() async {
SharedPreferences.setMockInitialValues(
{
Constants.USER_ID: '1234567890',
},
);
},
);
testWidgets(
'emptyListTest',
(tester) async {
await _initializeDependencies(tester);
final dio = di.getIt.get<Dio>();
final dioAdapter = DioAdapter(dio: dio);
dioAdapter.onGet(
'/url/user/1234567890',
(server) => server.reply(
200,
BookingResponse(
(b) => b
..data = <Booking>[].toBuiltList().toBuilder()
..pagination = Pagination((b) => b
..last = ''
..first = ''
..next = ''
..skip = 0
..count = 0).toBuilder(),
),
),
);
final testApp = await tester.runAsync(
() => wordskiiTestApp(
widgetUnderTest: BookingView(),
),
);
await tester.pumpWidget(testApp!);
await tester.pump(const Duration(seconds: 1));
expect(find.text('AVAILABLE'), findsOneWidget);
// debugDumpApp();
expect(
find.text(
'You\'re all caught up.',
),
findsOneWidget,
);
},
);
}
Future<void> _initializeDependencies(WidgetTester tester) async {
await tester.runAsync(di.getIt.reset);
await tester.runAsync(di.initTesting);
await tester.runAsync(di.getIt.allReady);
}
On which Widget is 'You\'re all caught up' expected? I had similar issues encountered previously when I tried find.textContaining() on a ListView item. I was able to solve my issue by finding out which widget the Text should appear.
On mine, I had to use find.widgetWithText(InkWell, String). To find out the which Widget it is, you can tap on the widget displayed on screen when you've run the test (i.e. on an emulator). The logs should display the tapped Widget.

FutureProvider using .whenData() shorthand

Riverpod provides a shorthand using .whenData() where you do not need to supply loading and error parameters. But I can't find example of how this code can be used to return widget in build() function.
Widget build(BuildContext context, ScopedReader watch) {
final cityListFuture = watch(cityListFutureProvider);
// This one is working fine
return cityListFuture.when(
data: (value) {
return Text("Data goes here");
},
loading: () => CircularProgressIndicator(),
error: (error, stack) {
return Container();
});
// This is shorthand for .when() without the need of loading and error
// ERROR: The return type 'AsyncValue<Text>' isn't a 'Widget', as required by the closure's context.
return cityListFuture.whenData((value) => Text("Data goes here"));
}
Anyone knows how can we use .whenData() to return a widget?
Quite an interesting documentation I had to go through for this.
It seems like the whenData is probably not exactly what is sounds like it should do.
Because all it does is that it return an AsyncValue<Widget> instead of directly returning Widget like the when function does.
So one way to use it would be,
return cityListFuture.whenData((val) => Text(val)).data!.value;
Here, the value would be your Text(value) itself.
One thing to be careful of here is the ! symbol since the data can be null. So you would have to manually have if checks.
Other thing to note here is that, you might as well achieve the same thing with something like this,
return Text(watch(cityListFutureProvider).data?.value);
Assumptions have been made that your value is a String.

ChangeNotifierProxyProvider not initiated on build

I'm trying to understand multiproviders in Flutter. In my App, one Provider need to change based on a value from an other Provider.
AuthProvider gets initiated higher up in the widget tree on build. Works like a charm with automatic sign in if possible...
In a lower placed widget, I try to initiate two other Providers. One, WhatEver, is not depended on other data and gets initiated on build like it is supposed to using ChangeNotifierProvider.
ProductList however is depended on AuthProvider. If log in status is changed, the ProducList should update accordingly.
In my attempts, I've found out, ie found on SO, that ChangeNotifierProxyProvider is the right way to go. But when I run the App, it seems like the 'create'-part of ChangeNotifierProxyProvider is not initiated when the widget gets build. It seems like the ProductList provider is not initiated until it's read or written to.
What have I misunderstood using MultiProviders and ChangeNotifierProxyProvider?
return MultiProvider(
providers: [
ChangeNotifierProvider<WhatEver>(create: (context) => WhatEver()),
ChangeNotifierProxyProvider<AuthProvider, ProductList>(
create: (_) => ProductList(Provider.of<AuthProvider>(context, listen: false)),
update: (_, auth, productList) => productList..reloadList(auth)
),
],
The ProductList looks like this:
final AuthProvider _authProvider;
static const String _TAG = "Shop - product_list.dart : ";
ProductList(this._authProvider) {
print(_TAG + "ProductList Provider initiated");
reloadList(this._authProvider);
}
void reloadList(AuthProvider authProvider) {
print(_TAG + "ProductList reload started");
if (authProvider.user==null) {
print(_TAG + "ProductList: _authProvider == null");
_loadBuiltInList();
} else {
print(_TAG + "ProductList: user = " + authProvider.user.displayName);
_loadFirestoreList();
}
}
I have code that does this:
ChangeNotifierProxyProvider<AuthService, ProfileService>(
create: (ctx) => ProfileService(),
update: (ctx, authService, profileService) =>
profileService..updateAuth(authService),
),
My ProfileService() does not rely on AuthService being available when it is constructed. The code works fine :)
The ChangeNotifierProxyProvider documentation explicitly describes this approach:
Notice how MyChangeNotifier doesn't receive MyModel in its constructor
anymore. It is now passed through a custom setter/method instead.

Flutter StreamProvider 'List<dynamic>' is not a subtype of type 'List<??>' in type cast

I'm trying to user StreamProvider to make data from a firestore doc stream available throughout my app. Its a recipe app and this is the shopping list.
I have a model RecipeItem that contains details about the item from the recipe. The firestore doc holds one value which is an array, called 'list', containing maps for each item that is in the list.
Below is my connection to firestore and setting up the stream. I try to get the doc, then user map to create a RecipeItem instance for each item in the list. Here is the method:
Stream<List<RecipeItem>> getPersonalList() {
print('Fetching personal list');
return _db.collection('shopping_lists').document(userId).snapshots().map(
(DocumentSnapshot documentSnapshot) => documentSnapshot.data['list']
.map(
(item) =>
// print(item);
RecipeItem(
category: item['category'],
wholeLine: item['wholeLine'],
recipeTitle: item['recipeTitle'],
recipeId: item['recipeId'],
purchased: item['purchased'],
),
)
.toList(),
);
}
Now in main.dart I have a StreamProvider that looks for type <List<RecipeItem>>
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
StreamProvider<FirebaseUser>(
//Access withing the app -> var user = Provider.of<FirebaseUser>(context);
create: (_) => AuthService().user),
StreamProvider<List<RecipeItem>>(
create: (_) => PersonalListDB().getPersonalList(),
catchError: (context, error) {
print('This is the error from stream provider *** $error');
},
),
ChangeNotifierProvider(
create: (_) => RecipesDB(),
)
],
child: MaterialApp(
etc etc...
When I run this I get this error:
type 'List' is not a subtype of type 'List' in type cast
The only way I can fix this is if I change everywhere I have List<RecipeItem> to List<dynamic>. This works but it doesn't seem like the right solution.
I have tried a few (a million) things.
I found this post here: Getting type 'List<dynamic>' is not a subtype of type 'List<...>' error in JSON
This tells me that .toList() might be the problem because it creates List. So I have tried using List.from and using .cast<List> but have had no luck. What is further confusing me is that I pretty closely follow other tutorials doing similar things.
Any help to resolve this and help me understand the problem is greatly appreciated.
The List of Firestore (db, httpRequest, etc) are of type dynamic to avoid problems when calling them, you can try to tell the map of what type are you trying to cast your object
return _db.collection('shopping_lists').document(userId).snapshots().map<List<RecipeItem>>( //I'm not sure what kind of object this map should return, is this the method map part of the stream? if it is then it should be of type List<RecipeItem>
(DocumentSnapshot documentSnapshot) => documentSnapshot.data['list']
.map<RecipeItem>( //now it will know it's not a dynamic value but of type RecipeItem
(item) =>
// print(item);
RecipeItem(
category: item['category'],
wholeLine: item['wholeLine'],
recipeTitle: item['recipeTitle'],
recipeId: item['recipeId'],
purchased: item['purchased'],
),
)
.toList(),
);