I am using riverpod for my state manegement in my flutter app.
Riverpod offers a feature for combined providers, but my dependent provider does not update and always returns null.
By clicking one of the pins (secrets) on the map, my "selectedSecretProvider" is updated (default is null). This should trigger the initialization of my audio player. And by clicking play, the sound of the current _selectedSecret should play. So my "selectedTrackProvder" is dependent on my "selectedSecretProvider":
final selectedTrackProvider = StateNotifierProvider<SelectedTrack, Track>((ref) {
Secret? selectedSecret = ref.watch(selectedSecretProvider);
return SelectedTrack(selectedSecret);
});
Here is my selectedTrack class:
class SelectedTrack extends StateNotifier<Track> {
SelectedTrack(this.selectedSecret) : super(Track.initial());
Secret? selectedSecret;
#override
void dispose() {
...
}
void initAudioPlayer() {
...
}
Future<int> play() async {
print(selectedSecret);
return ...
}
}
So why does it always print null, when clicking play?
(Btw. my bottom_panel_sheet is showing the correct data and also consumes the "selectedSecretProvider".)
I wouldn't say the way you're creating your StateNotifierProvider is wrong, but I think the following is a better approach that should solve your problem.
final selectedTrackProvider = StateNotifierProvider<SelectedTrack, Track>((ref) {
return SelectedTrack(ref);
});
class SelectedTrack extends StateNotifier<Track> {
SelectedTrack(this.ref) : super(Track.initial());
final ProviderReference ref;
Future<int> play() async {
final selectedSecret = ref.read(selectedSecretProvider);
print(selectedSecret);
return ...
}
}
This way you don't create a new StateNotifier every time the selectedSecretProvider updates, instead opting to read the current value of the selectedSecretProvider when attempting to call play.
Related
I want to keep my return value as AsyncValue rather than Stream so I am returning StreamProvider from build method of Notifier. After reading the codebase of riverpod I can't see any drawback of this, but I have never come across any project doing something like this. Is this fine, or is there any straight forward way to convert Stream to AsyncValue.
final _userProvider = StreamProvider.autoDispose<User?>((ref) {
final repository = ref.watch(repositoryProvider);
return repository.getUser(); //returns Stream<User?>
});
class AuthNotifier extends AutoDisposeNotifier<AsyncValue<User?>> {
#override
AsyncValue<User?> build() {
return ref.watch(_userProvider);
}
Future<void> singOut() {
return ref.read(repositoryProvider).signOut();
}
}
final authProvider =
AutoDisposeNotifierProvider<AuthNotifier, AsyncValue<User?>>(
AuthNotifier.new);
This is fine, yes.
Being able to do such a thing is the goal of the build method & ref.watch
As long as you don't return the provider itself but the value exposed by the provider, there is no problem:
build() {
return ref.watch(provider); // OK
}
build() {
return provider // KO
}
This is an issue related to the getx in flutter.
I have 2 controllers. ContractsController and NotificationController.
In ContractsController I have put the value into observer variable by calling the Api request.
What I want now is to get that variable's data in another controller - NotificationController.
How to get that value using getx functions?
ContractsController
class ContractsController extends GetxController {
ExpiringContractRepository _expiringContractRepository;
final expiringContracts = <ExpiringContract>[].obs; // This is the value what I want in another controller
ContractsController() {
_expiringContractRepository = new ExpiringContractRepository();
}
#override
Future<void> onInit() async {
await refreshContracts();
super.onInit();
}
Future refreshContracts({bool showMessage}) async {
await getExpiringContracts();
if (showMessage == true) {
Get.showSnackbar(Ui.SuccessSnackBar(message: "List of expiring contracts refreshed successfully".tr));
}
}
Future getExpiringContracts() async {
try {
expiringContracts.value = await _expiringContractRepository.getAll(); // put the value from the api
} catch (e) {
Get.showSnackbar(Ui.ErrorSnackBar(message: e.toString()));
}
}
}
The expiringContracts is updated successfully with data after the api request.
Now, I want to get that value in NotificationController
NotificationController
class NotificationsController extends GetxController {
final notifications = <Notification>[].obs;
ContractsController contractsController;
NotificationsController() {
}
#override
void onInit() async {
contractsController = Get.find<ContractsController>();
print(contractsController.expiringContracts); // This shows an empty list ?????
super.onInit();
}
}
Overview
A couple solutions come to mind:
pass the expiringContracts list as a constructor argument to NotificationsController if you only need this done once at instantiation, or
use a GetX worker to update NotificationsController every time expiringContracts is updated
The first solution isn't related to GetX, rather it's just async coordination between ContractsController and NotificationsController, so lets focus on the 2nd solution: GetX Workers.
Details
In NotificationsController, create a method that will receive expiringContracts.
Something like:
class NotificationsController extends GetxController {
void refreshContracts(List<ExpiringContract> contracts) {
// do something
}
}
Please note: none of this code is tested. I'm writing this purely in StackOverflow, so consider this pseudo-code.
In ContractsController we'll supply the above callback method as a constructor arg:
In ContractsController, something like:
class ContractsController {
final expiringContracts = <ExpiringContract>[].obs
final Function(List<ExpiringContract>) refreshContractsCallback;
ContractsController(this.refreshContractsCallback);
#override
void onInit() {
super.onInit();
refreshContracts(); // do your stuff after super.onInit
ever(expiringContracts, refreshContractsCallback);
// ↑ contracts → refreshContractsCallback(contracts)
// when expiringContracts updates, run callback with them
}
}
Here the GetX ever worker takes the observable as first argument, and a function as 2nd argument. That function must take an argument of type that matches the observed variable, i.e. List<ExpiringContract>, hence the Type of refreshContractsCallback was defined as Function(List<ExpiringContract>).
Now whenever the observable expiringContracts is updated in ContractsController, refreshContractsCallback(contracts) will be called, which supplies the list of expiring contracts to NotificationsController via refreshContracts.
Finally, when instantiating the two controllers inside the build() method of your route/page:
NotificationsController nx = Get.put(NotificationsController());
ContractsController cx = Get.put(ContractsController(nx.refreshContracts));
Timeline of Events
NotificationsController gets created as nx.
nx.onInit() runs, slow call of refreshContracts() starts
ContractsController gets created, with nx.refreshContracts callback
your page paints
nx has no contracts data at this point, so you'll prob. need a FutureBuilder or an Obx/ GetX + StatelessWidget that'll rebuild when data eventually arrives
when refreshContracts() finishes, ever worker runs, sending contracts to nx
nx.refreshContracts(contracts) is run, doing something with contracts
Notes
async/await was removed from nx.onInit
ever worker will run when refreshContract finishes
There were some powerful approaches in GetX. I solved this issue with Get.put and Get.find
Here is the code that I added.
ContractsController
class ContractsController extends GetxController {
ExpiringContractRepository _expiringContractRepository;
final expiringContracts = <ExpiringContract>[].obs; // This is the value what I want in another controller
ContractsController() {
_expiringContractRepository = new ExpiringContractRepository();
}
#override
Future<void> onInit() async {
await refreshContracts();
super.onInit();
}
Future refreshContracts({bool showMessage}) async {
await getExpiringContracts();
if (showMessage == true) {
Get.showSnackbar(Ui.SuccessSnackBar(message: "List of expiring contracts refreshed successfully".tr));
}
}
Future getExpiringContracts() async {
try {
expiringContracts.value = await _expiringContractRepository.getAll(); // put the value from the API
// ******************************** //
Get.put(ContractsController()); // Added here
} catch (e) {
Get.showSnackbar(Ui.ErrorSnackBar(message: e.toString()));
}
}
}
NotificationController
class NotificationsController extends GetxController {
final notifications = <Notification>[].obs;
ContractsController contractsController;
NotificationsController() {
}
#override
void onInit() async {
// ******************************** //
contractsController = Get.find<ContractsController>(); // Added here.
print(contractsController.expiringContracts); // This shows the updated value
super.onInit();
}
}
Finally, I have found that GetX is simple but powerful for state management in flutter.
Thanks.
Updates:
2021/06/11 After hours of debugging yesterday, I confirmed that the problem is caused by aws amplify configuration: _configureAmplify(). Because the location of the amplify server was set wrong, so _configureAmplify() takes several seconds to work... and therefore, the readPost() function did not work on initialization, as it must run after _configureAmplify()...
2021/06/10I made changes to my code according to S. M. JAHANGIR's advice, and updated the question. The issue still presists. The value of posts is not updated when called in initialization and the data only shows up after reload. (if I commented out the _controller.readPost() in UI, the value of posts is always empty.
I have this page that loads information from aws amplify with getx implemented. However, I found out the readPost() async funtion in getx controller dart file is not reading from database, when the controller instance is initialized. I have to add a _controller.readPost() in UI file to make it work. And the data only shows up after a reload of that UI page...
Getx Controller dart file:
class ReadPostController extends GetxController {
var isLoading = true.obs;
var posts = <Posty>[].obs;
#override
void onInit() {
_configureAmplify();
await readPost();
super.onInit();
// print('show post return value: $posts');
}
void _configureAmplify() {
final provider = ModelProvider();
final dataStorePlugin = AmplifyDataStore(modelProvider: provider);
AmplifyStorageS3 storage = new AmplifyStorageS3();
AmplifyAuthCognito auth = new AmplifyAuthCognito();
AmplifyAPI apiRest = AmplifyAPI();
// Amplify.addPlugin(dataStorePlugin);
Amplify..addPlugins([dataStorePlugin, storage, auth, apiRest]);
Amplify.configure(amplifyconfig);
print('Amplify configured');
}
// read all posts from databases
Future readPost() async {
try {
isLoading(true);
var result = await Amplify.DataStore.query(Posty.classType);
print('finish loading request');
result = result.sublist(1);
posts.assignAll(result);
// print(the value of posts is $posts');
} finally {
isLoading(false);
}
}
#override
void onClose() {
// called just before the Controller is deleted from memory
super.onClose();
}
}
And in the UI part:
class TabBody extends StatelessWidget {
TabBody({Key? key}) : super(key: key);
final ReadPostController _controller = Get.put(ReadPostController());
#override
Widget build(BuildContext context) {
_controller.readPost();//if commented out, _controller.post is empty
return Container(
child: Obx(
() => Text('showing:${_controller.posts[1].title}'),
));
}
}
In my understanding, the readPost() function should be called when the ReadPost_controller is initiallized. And the UI will update when the posts = <Posty>[].obs changes. Guys, what am I doing wrong here?
First, when you are calling readPost on onInit you are not awaiting. So change it to:
onInit() async{
...
await readPost();
...
}
Secondly, posts is a RxList so you need to use the assignAll method to update it.
Therefore, in your readPost method, instead of posts.value = reault you need to use posts.assignAll(result)
Calling from the UI works because readPost every time the build method is called by the Flutter framework and actually the UI shows the data from every previous call.
I think try with GetBuilder instead of Obx.
GetBuilder<ReadPostController>(
builder: (value) => Text('showing:${value.posts[1].title}'),
)
and also use update(). in readPost() method.
I have home.dart class which on build, fetchs an api result and assigns the response as well as the fetched time (DateTime.now()).
here is the StatefulWidget Home class
class HomeView extends StatefulWidget {
int maxCacheDurationInMinutes = 2;
DateTime _lastUpdatedTime;
Usage previousFetchedUsage;
#override
_HomeView createState() => _HomeView();
}
on the state, i access the elements using widget._lastUpdatedTime like so.
here is the state class and the requesting function
class _HomeView extends State<HomeView> {
bool get isValidCache =>
widget._lastUpdatedTime != null &&
DateTime.now().difference(widget._lastUpdatedTime).inMinutes <
widget.maxCacheDurationInMinutes;
int get minsSinceLastCached => widget._lastUpdatedTime != null
? DateTime.now().difference(widget._lastUpdatedTime).inMinutes
: 0;
Future<Usage> requestUsage(BuildContext context) async {
if (isValidCache) {
// is valid cache
if (widget.previousFetchedUsage != null) {
// return cached if it isn't null
return widget.previousFetchedUsage;
}
}
// if cache is invalid or cache is null
// fetch the latest data and assign to cache variable
widget._lastUpdatedTime = DateTime.now();
return widget.previousFetchedUsage = await ClientHandler.of(context).client.fetchUsage();
}
}
// some code is omitted to keep it brief
The problem here is, on each UI rebuild (during hot reload), the request is send. where technically it only should be send once during the 2 minutes period.
I also tried with make previousFetchedUsage as a static variable. still nothing changes.
Is there any concepts i should be aware about in dart ? i am from C# background and recently started with flutter.
your requestUsage is future function and when you use it with FutureBuilder in your ui its never refresh the ui event you fire it in every two minutes.
you can refresh your ui for read the new value with SetState or using Streams in your UI not just FutureBuilder
I wanna check users internet connection and firebase auth state changes in my app. I am using flutter bloc for my app's state management. But when call different 2 .add(event) in one initstate always the first one is run and changes states but second one didnt run didnt change state. What is the my wrong ?
my bloc:
class ControllerBloc extends Bloc<ControllerEvent, ControllerState> {
ControllerBloc() : super(ControllerInitial());
AuthApiClient _authApiClient = getIt<AuthApiClient>();
#override
Stream<ControllerState> mapEventToState(
ControllerEvent event,
) async* {
if (event is ControllInternetConnection) {
yield* internetControll();
}
if (event is ControllUserAuth) {
debugPrint("wwwwgeldi");
yield* userAuthControl();
}
// TODO: implement mapEventToState
}
Stream<ControllerState> internetControll() async* {
Stream<DataConnectionStatus> connectionState =
DataConnectionChecker().onStatusChange;
await for (DataConnectionStatus status in connectionState) {
switch (status) {
case DataConnectionStatus.connected:
debugPrint("Bağlandı");
yield InternetConnectedState();
break;
case DataConnectionStatus.disconnected:
debugPrint("Kesildi");
yield InternetConnectionLostState();
break;
}
}
}
Stream<ControllerState> userAuthControl() async* {
FirebaseAuth firebaseAuth = _authApiClient.authInstanceAl();
debugPrint("geldi");
Stream<User> authStream = firebaseAuth.authStateChanges();
_authApiClient.authInstanceAl().signOut();
await for (User authUserResult in authStream) {
if (authUserResult == null) {
yield UserAuthControlError();
}
}
}
}
my page where call my events
class _NavigationPageState extends State<NavigationPage> {
ControllerBloc controllerBloc;
#override
void initState() {
controllerBloc= BlocProvider.of<ControllerBloc>(context);
controllerBloc.add(ControllInternetConnection());
controllerBloc.add(ControllUserAuth());
super.initState();
}
If I am understanding this right, it looks to me that you are trying to solve two different problems with one BLoC. I don't see a reason why internet connection and user auth have to be in one BLoC, rather I would just separate the two in separate BLoCs.
As the discussion in this thread points out, the point of using a BLoC revolves around the idea of predictability purposes. You can override the existing BLoC event stream, but I personally think that is too complicated for what you are trying to do.
So I would suggest, either make two separate BLoCs or combine the entire process into one event, where the internet connection would be checked before the user is authenticated, you will then return different states depending on the errors.