Flutter: Invoking Provider method in another class outside of the widget tree - flutter

I am currently using Provider in my Flutter project to manage the authentication state of my mobile app.
The model for AuthProvider is as follow:
lib/models/auth_provider.dart
class AuthProvider with ChangeNotifier {
// Some methods and properties are removed for simplicity.
// ...
bool loggedIn;
void allowAccess() {
loggedIn = true;
notifyListeners();
}
void revokeAccess() {
loggedIn = false;
notifyListeners();
}
}
The application uses some services from another class to check the validity of the authentication token.
If the the token is not valid anymore, the method in the service in another class will need to revoke the access:
lib/services/auth_services.dart
import 'package:exampleapp/shared/global_context.dart' as global_context;
class AuthService {
// Some methods and properties are removed for simplicity.
// ...
void checkValidity() {
// ...
if(notValid) {
// Use provider to revoke access
Provider.of<AuthProvider>(
global_context.GlobalContext.globalContext!, listen: false)
.revokeAccess();
}
}
}
To achieve this (since there is no context outside of the widget tree), the app uses a global context to allow the services file to invoke the Provider method:
lib/shared/global_context.dart
import 'package:flutter/material.dart';
class GlobalContext {
static BuildContext? globalContext;
}
And add the following line in the build method of the widget that might involves auth state change:
global_context.GlobalContext.globalContext = context;
I read that we're not advised to access Provider outside the widget tree and I don't think using a GlobalContext is the best practice. Is there any other way that I could do this by using Provider?
P/S: I'm still learning Flutter, please comment below if any clarification is needed.

I usually use the get_it package, It gives the ability to call Provider without needing to specifiy a particular context.
https://pub.dev/packages/get_it
First I would call setupLocator()
import 'package:get_it/get_it.dart';
GetIt locator = GetIt.instance;
void setupLocator() {
locator
.registerLazySingleton(() => AuthProvider());
}
Then use it like this..
class SomeClass {
final _provider = locator<AuthProvider>();
void someMethod(){
_provider.revokeAccess();
}
}

This is a very good and important question obviously there are tons of approaches that we can follow here, you can use a global context but you have to make sure it's always the correct context so whenever you push/pop different routes you have to also sync the context
One easy approach i think would be to add the context to checkValidity() function as a parameter
Also i would recommend checking out stacked state management solution https://pub.dev/packages/stacked its perfect for situations like this.

Related

Clean Architecture why do we have use cases?

in Clean Architecture we have use cases as business logic rules. but we can also call functions in the repository directly so we don't need use cases.
what are the reasons behind this?
sample use case
class GetMarketUseCase implements UseCase<Stream<ResponseModel>, void> {
final PriceTrackerRepository priceTrackerRepository;
GetMarketUseCase(this.priceTrackerRepository);
#override
Stream<ResponseModel> call(void params) {
return priceTrackerRepository.getMarketWithSymbols();
}
}
sample repository
class PriceTrackerRepositoryImpl implements PriceTrackerRepository {
late final PriceTrackerDataSource priceTrackerDataSource;
PriceTrackerRepositoryImpl(this.priceTrackerDataSource);
#override
Stream<ResponseModel> getMarketWithSymbols() {
return _marketStreamController.stream;
}
Because it prevents your presenter become a God object when it must handle UI logic and buniness logic too.
For example: a logout use case, you need to call API logout inside AuthenRepo, unregister Firebase FCM token, close socket, and maybe clear some local data inside CartRepo, UserRepo, ... then imagine put all those things in Presenter, what a mess instead of create a LogoutUseCase call to every repositories you need
And moreover, you can use it for many places, like when user press Logout button, when user login token is expired, ... just call LogoutUseCase instead of copy code from this Presenter to another Presenter, also make is easy for you when you need to change something about logout requirement
Code example for Presenter is Bloc:
AuthBloc with UseCase:
class AuthBloc extends Bloc<AuthEvent, AuthState> {
AuthBloc(AuthState state) : super(state) {
on<AuthLogoutEvent>(_onLogout);
}
Future<void> _onLogout(
AuthLogoutEvent event,
Emitter<AuthState> emit,
) async {
await getIt<LogoutUseCase>().call(NoParams());
}
}
AuthBloc without UseCase:
class AuthBloc extends Bloc<AuthEvent, AuthState> {
AuthBloc(AuthState state) : super(state) {
on<AuthLogoutEvent>(_onLogout);
}
Future<void> _onLogout(
AuthLogoutEvent event,
Emitter<AuthState> emit,
) async {
await getIt<AuthRepo>().logout();
await FirebaseMessaging.instance.deleteToken();
await getIt<SocketRepo>().close();
await getIt<CartRepo>().clearData();
await getIt<UserRepo>().clearData();
// maybe more Repo need to call here :((
}
}
In your example above, it is only simple use case with only action getMarketWithSymbols(), then I agree Usecase here is redundant, but for consistency, it should have and who know, in the future this UseCase need to scale up, then it will easy for you then.
We need a usecase as a kind of intermediate link between presentation and domain layers, to ensure independence of all layers

How to call a riverpod provider outside of widget tree/widget class?

I am new to using RiverPod. previously I used Provider for state management.
in case of provider I could use a provider outside widget tree to get value using syntax
Provider.of<MyModel>(context,listen:true).someFunction();
how do I do the same in RiverPod? for now I am using Consumer Builder and Consumer Widget. I was wondering if there's a way to call a riverpod provider without using Consumer.
You can easily call riverpord provider without using Consumer. Please check out my below extension which is very helpful to call the provider based on context where it will work for the read function without any consumer or consumer widget.
extension Context on BuildContext {
// Custom call a provider for reading method only
// It will be helpful for us for calling the read function
// without Consumer,ConsumerWidget or ConsumerStatefulWidget
// Incase if you face any issue using this then please wrap your widget
// with consumer and then call your provider
T read<T>(ProviderBase<T> provider) {
return ProviderScope.containerOf(this, listen: false).read(provider);
}
}
After then under the build method, you must call context.read(yourProviderName)
let's say you have a separate class that is responsible for ui logic:
class UiController {
UiController(this._ref);
final Ref _ref;
Reader get _reader => _ref.read;
static final pr = Provider<UiController>((ref) => UiController(ref));
void someFunction() {
_reader(otherProvider).getValue();
}
}
from a widget or other functions where ref is accessed, you can call:
ref.read(UiController.pr).someFunction();

Handling api response with bloc pattern with rxdart

Im a bit confused on how rxdart works with bloc pattern. This is a code i copied from a youtube channel. Its a bloc that has a method which returns an API response. There's usually a mapEventToState method somewhere in the bloc but this one doesn't. I've added some comments to show what i understand and hope you guys can correct me. Thanks.
Source code: https://github.com/bilguunint/igdb/blob/master/lib/bloc/get_games_bloc.dart
class GetGamesBloc {
final GameRepository _repository = GameRepository(); // defining the api repository
final BehaviorSubject<GameResponse> _subject = BehaviorSubject<GameResponse>();
// defining a behaviour stream which will give only the latest item/data
getGames(int platformId) async {
GameResponse response = await _repository.getGames2(platformId);
_subject.sink.add(response);
}
// this method fetches the api data but not sure why add response to the sink. Isnt sink suppose to be an event? The response is an api json data so it's a stream right ?
dispose() {
_subject.close();
}
//closing the stream when not in use to prevent memory loss
BehaviorSubject<GameResponse> get subject => _subject;
// defining a getter to be used outside the class
}
final getGamesBloc = GetGamesBloc();
// I think this enables us to use the bloc as getGamesBloc ?
The point of BLoC pattern is to separate business logic from views to keep the code clean, readable and testable. The mapEventToState has the responsibility of converting events to state and you can use any other alternatives to do that. In Cubit from bloc package we're defining functions to emit and change state.
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() => emit(state + 1);
void decrement() => emit(state - 1);
}
In the example you provided, It's defining a function which will change the state. So, as far as I know that's correct and it counts as a business logic component.

What's the best way to implement requests with tokens clearly?

I have screen with tabs and each screen implements AutomaticKeepAliveClientMixin. When I navigate to this screen(with tabs), each tab in initState fetches data from server like that:
fetchData()async{
final token = await getToken();//refresh if it is expired.
return fetchData(token);
}
I think it'd better if I initialize data for all the tabs in one request, because I can catch only one refresh token expired and socket exception in single place.
fetchAllData()async{
final token = await getToken();//refresh if it is expired.
return fetchAllData(token);
}
How would you build logic for screen and requests like that? Is my approach is something similar to what you use?
I would recommend you to use a Provider (https://pub.dev/packages/provider). By subscribing to the same Provider, you will be able to reuse the data you've once fetched. For instance, I've used this approach to provide to my App (at different places) the current user:
class UserModel extends ChangeNotifier {
User _currentUser;
void setUser(User user) {
_currentUser = user;
notifyListeners();
}
Future<User> getUser(BuildContext context) async {
if (_currentUser == null) {
_currentUser = await getUserRequest(context, hasRedirect: false);
}
return _currentUser;
}
}
Hope it will fit your needs !
You can add your fetchAllData method to the initState of the widget that holds all of the tabbed widgets. Then, you can you can pass the relevant data to the contructors of each of the tabbed widgets. Not the best solution, but it should work.
I'd still recommend Provider. State management systems are not all inclusive, nor are exclusive. Depending on how your state is presented your could use more than one state management system. Helll, the bloc library already includes the provider library.

ChangeNotifier on non-widget classes

We've just started developing our first Flutter app and we are running into a small problem with ChangeNotifier applied to a non-widget class and the disposal of objects of this class. We get exceptions when a future returns after the class has been disposed, we call notifyListeners after the future returns.
I've seen questions suggesting to use the mounted property to check if the object is disposed, but this is only available in widgets. The ChangeNotifier class uses it's own check to see if it's been disposed, but we can't call those methods (and neither should we, I think)
My current idea is that we should use a widget class, and not apply ChangeNotifier on any non-widget class. But I can't find any similar problems, or some documentation telling me best practices/guidelines.
class ExampleClass extends ChangeNotifier {
ExampleClass() {
}
final ExampleService _service = ExampleService();
List<Stuff> stuff;
Future<void> DoStuff() async {
stuff = await _service.getStuff();
notifyListeners();
}
}
We're looking for a way to properly handle the callbacks/returning futures and only call notifyListeners if the object has not been disposed.