Riverpod - Create service with async dependency better/elegant way - flutter

I've writen some code that provides a ApiService to a StateNotifier. The ApiService has a dependency on a authenticatorclient - The auth client has to be created asynchronously as it uses sharedprefs to get a token.
Im just trying to figure out if theirs a more elegant way to how I've written this. Basically when the service apiService is injected into the StateNotifier it could be nullable... That to me is a bit of a code smell.
So in brief this is what im doing...
use a FutureProvider to Instantiate the RestClientwith a Dio
authenticatorClient = FutureProvider<RestClient>((ref) async {
final prefs = await SharedPreferences.getInstance();
final dio = Dio();
...
return RestClient(dio);
}
And then I watch that and use a MaybeWhen to return the service
final clientCreatorWatchProvider = Provider<ApiService?>((ref) => ref
.watch(authenticatorClient)
.whenData((value) => ApiService(value))
.maybeWhen(
data: (service) => service,
orElse: () => null,
));
So the bit I dont like is the orElse returning null
And then my StateNotifier is watching...
final AppState = StateNotifierProvider<AppNotifier, String>(
(ref) => AppNotifier(ref.watch(clientCreatorWatchProvider)));
class AppNotifier extends StateNotifier<String> {
final ApiService? apiService;
AppNotifier(this.apiService) : super("loading") {
init();
}
...
}
Any thoughts on the above approach?
Thanks

One way to solve this problem is to initialize SharedPreferences outside of a provider. You can then use ProviderScope to override a synchronous provider, eliminating the need to work with AsyncValue.
When you initialize your app, do the following:
final sharedPreferences = Provider<SharedPreferences>((_) => throw UnimplementedError());
Future<void> main() async {
final sharedPrefs = await SharedPreferences.getInstance();
runApp(
ProviderScope(
overrides: [
sharedPreferences.overrideWithValue(sharedPrefs),
],
child: MyApp(),
),
);
}
Now you could write your providers like so:
final authenticatorClient = Provider<RestClient>((ref) {
final prefs = ref.watch(sharedPreferences);
final dio = Dio();
...
return RestClient(dio);
}
final clientCreatorWatchProvider = Provider<ApiService>((ref) {
final authClient = ref.watch(authenticatorClient);
return ApiService(authClient);
});

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" Fetch data from shared_preferences and provider before running the app

I am creating a flutter app, I am using shared_preferences to store the themeMode and using provider to manage state. I am able to fetch the themeMode before my app starts but not able to run the provider function that sets the themeMode.
Following is my code
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
SharedPreferences.getInstance().then((prefs) {
var isDarkTheme = prefs.getBool("isDarkTheme") ?? false;
return runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(
create: (_) => ThemeManager(),
),
],
child: MyApp(isDarkActive: isDarkTheme),
),
);
});
}
My Provider "State Management" code
class ThemeManager with ChangeNotifier {
bool _isDark = false;
void setThemeMode(bool themeMode) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setBool("isDarkTheme", themeMode);
_isDark = themeMode;
debugPrint(_isDark.toString());
notifyListeners();
// return true;
}
void fetchTheme() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
var darkMode = prefs.getBool('isDarkTheme');
if (darkMode != null) {
_isDark = darkMode;
} else {
_isDark = false;
}
notifyListeners();
}
bool get isDark => _isDark;
}
You need to initialise a single global shared preference variable which you can use everywhere in your app so that you don't have to initialise it every time you when you want to use it.
so create a signleton class like this,
class SharedPreferencesHelper {
SharedPreferencesHelper._();
late SharedPreferences prefs;
Future<void> initialise() async {
prefs = await SharedPreferences.getInstance();
}
static final SharedPreferencesHelper instance = SharedPreferencesHelper._();
}
now initialise this in main() like this,
void main()async{
WidgetsFlutterBinding.ensureInitialized();
await SharedPreferencesHelper.instance.initialise();
}
now only use this variable where ever you want like this,
SharedPreferencesHelper.instance.prefs.getBool('key');
or create some functions inside your helper class,
Future<bool> getProperty(String key) async {
return await prefs.getBool(key);
}

Flutter Testing LateInitializationError in my class?

I have a class that helps me handle the sharedpreferences:
class SharedPref {
static late final SharedPreferences prefs;
static initialize() async {
prefs = await SharedPreferences.getInstance();
}
SharedPref._();
static Future<void> save(String key, String value) async {
await prefs.setString(key, value);
}
// more methods that help me to save/load values...
}
Then I have a testing function:
class MockNavigatorObserver extends Mock implements NavigatorObserver {}
void main() {
testWidgets('Button is present and triggers navigation after tapped',
(WidgetTester tester) async {
final mockObserver = MockNavigatorObserver();
await tester.pumpWidget(
MaterialApp(
home: FirstPage(),
navigatorObservers: [mockObserver],
),
);
expect(find.byType(RaisedButton), findsOneWidget);
await tester.tap(find.byType(RaisedButton));
await tester.pumpAndSettle();
expect(find.byType(MyDetailedPage), findsOneWidget);
});
}
Whenever I run the test:
The following LateError was thrown running a test:
LateInitializationError: Field 'prefs' has not been initialized.
How am I supposed to initialize that? I called the SharedPref.initialize(); before the final mockObserver... but made no change.
Thanks in advance.
I called the SharedPref.initialize(); before
You need to await the call, otherwise you have race conditions when your save is called and initalize may or may not already be finished.
As you mentioned problem was in
final mockObserver = MockNavigatorObserver();
not inside main() function.
But it was another silly mistake with this kind error if wright initialisation like this:
late final MockNavigatorObserver mockObserver;
setUp(() {
mockObserver= MockNavigatorObserver();
})
Problem in this case is using final keyword. If use like that, only one test will pass. You need not using final keyword, instead:
late MockNavigatorObserver mockObserver;
setUp(() {
mockObserver= MockNavigatorObserver();
})

Read a provider inside a FutureProvider

When we need to read (not watch) a provider inside another one the documentation is clear:
"DON'T CALL READ INSIDE THE BODY OF A PROVIDER"
final myProvider = Provider((ref) {
// Bad practice to call `read` here
final value = ref.read(anotherProvider);
});
And it suggest to pass to the value exposed the Reader function: https://riverpod.dev/docs/concepts/combining_providers#can-i-read-a-provider-without-listening-to-it
final userTokenProvider = StateProvider<String>((ref) => null);
final repositoryProvider = Provider((ref) => Repository(ref.read));
class Repository {
Repository(this.read);
/// The `ref.read` function
final Reader read;
Future<Catalog> fetchCatalog() async {
String token = read(userTokenProvider);
final response = await dio.get('/path', queryParameters: {
'token': token,
});
return Catalog.fromJson(response.data);
}
}
And that's ok, but what is the best practice when I need to read a provider inside a FutureProvider?
I find myself in this situation many time because I expose the api as providers and inside the FutureProvider I call watch to get the api I need.
But I noticed that, because I'm watching the Api provider inside the userProvider, this won't gets disposed after been used.
Here's an example of what I'm trying to say:
API CLASS
final userApiProvider = Provider((ref) => UserApi(ref.read));
class UserApi {
final Dio _dio;
const UserApi(Reader read):
_dio = read(dioProvider);
Future<Response> getUser(String id, { CancelToken? cancelToken }) async{
final _url = '$URL_TO_API/$id';
return _dio.get(_url, cancelToken: cancelToken);
}
}
When using the API inside a FutureProvider
final userProvider = FutureProvider.autoDispose.family<User, int>((ref, userId) async {
final userApi = **ref.watch(userApi);**
final cancelToken = CancelToken();
ref.onDispose(() { cancelToken.cancel(); });
final user = await userApi.getUser(cancelToken: cancelToken);
return user;
});
The same logic applies.
By "Don't use read inside a provider", it isn't talking about the class Provider specifically, but any provider – so FutureProvider included.
In general, you should avoid using read as much as possible.

How to mock Riverpod's Reader?

I have the following repository and I'd like to test it. I know this may be a silly question but I'm still learning.
class AuthRepository implements AuthBaseRepository {
final Reader _read;
const AuthRepository(this._read);
#override
Future<User> login({String email, String password}) async {
try {
final response = await _read(dioProvider).post(
'/sign_in',
data: {
"user": {
"email": email,
"password": password,
},
},
);
return _mapUserFromResponse(response);
} on DioError catch (_) {
throw const CustomException(message: 'Invalid login credentials.');
} on SocketException catch (_) {
const message = 'Please check your connection.';
throw const CustomException(message: message);
}
}
And this is what I've done so far:
void main() {
test('loadUser', () async {
Dio dio;
DioAdapterMockito dioAdapterMockito;
AuthRepository repository;
setUpAll(() {
dio = Dio();
dioAdapterMockito = DioAdapterMockito();
dio.httpClientAdapter = dioAdapterMockito;
repository = AuthRepository(_reader_here_);
});
test('mocks any request/response via fetch method', () async {
final responsePayload =
await parseJsonFromAssets("assets/api-response.json");
final responseBody = ResponseBody.fromString(
responsePayload,
200,
headers: {
Headers.contentTypeHeader: [Headers.jsonContentType],
},
);
when(dioAdapterMockito.fetch(any, any, any))
.thenAnswer((_) async => responseBody);
});
});
}
I have no idea of how to mock Reader. Basically, I've seen something like class MyMock extends Mock implements Something but Reader is not a class, it's a function so I'm completely lost.
Any help/tips/examples will be appreciated.
Thanks in advance!
Instead of trying to mock a Reader, create a provider for your repository and use ProviderContainer to read it.
class AuthRepository implements AuthBaseRepository {
const AuthRepository(this._read);
static final provider = Provider<AuthRepository>((ref) => AuthRepository(ref.read));
final Reader _read;
#override
Future<User> login({String email, String password}) async {
...
}
Example usage:
final user = createTestUser();
final container = ProviderContainer(
overrides: [
// Example of how you can mock providers
dio.overrideWithProvider(mockDio),
],
);
final repo = container.read(AuthRepository.provider);
expectLater(
await repo.login(email: 'AzureDiamond', password: 'hunter2'),
user,
);
You could also consider using the overrides in ProviderContainer to mock Dio instead of involving a mocking framework to simplify your tests further.
More on testing here.