Flutter Riverpod StateNotifier load data after logged in - flutter

I am trying initializing themeStateNotifier by fetching data after user logged in, from my code I saw that constructor of themeStateNotifier only initialize once after application starts when I didn't login, even though I add isLoggedIn as dependency.
So what is the solution I can make themeStateNotifier fetching theme data after login?
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:seeder/main.dart';
class ThemeStateNotifier extends StateNotifier<bool> {
final FirebaseAuth auth = FirebaseAuth.instance;
final dbInstance = FirebaseFirestore.instance;
ThemeStateNotifier() : super(false) {
if (auth.currentUser != null) {
var userSettingDoc =
dbInstance.collection('user').doc(auth.currentUser!.uid);
Future theme = userSettingDoc.get().then((value) {
print('theme' + value.toString());
return value['themeMode'];
});
theme.then((value) => print(value));
}
}
void changeTheme() {
state = !state;
String themeMode = state == false ? 'light' : 'dark';
if (auth.currentUser != null) {
var userSettingDoc =
dbInstance.collection('user').doc(auth.currentUser!.uid);
userSettingDoc.set({'themeMode': themeMode});
// print(userSettingDoc);
}
}
}
final themeStateNotifierProvider =
StateNotifierProvider<ThemeStateNotifier, bool>(
(ref) => ThemeStateNotifier(),
dependencies: [isLoggedIn]);
and console didn't print anything when app started up.
Launching lib/main.dart on Chrome in debug mode...
lib/main.dart:1
This app is linked to the debug service: ws://127.0.0.1:64779/Xf5d1A8qvMc=/ws
Debug service listening on ws://127.0.0.1:64779/Xf5d1A8qvMc=/ws
💪 Running with sound null safety 💪
Connecting to VM Service at ws://127.0.0.1:64779/Xf5d1A8qvMc=/ws
Flutter Web Bootstrap: Programmatic
Application finished.
Exited
attached with nisLoggedIn, it works well for all places
final isLoggedIn = StateNotifierProvider<GenericStateNotifier<bool>, bool>(
(ref) => GenericStateNotifier<bool>(false));
and GenericStateNotifier is just a class with getter setter, no worry about it, it always works.
class GenericStateNotifier<V> extends StateNotifier<V> {
GenericStateNotifier(V d) : super(d);
set value(V v) {
state = v;
}
V get value => state;
}

Related

Listen to changes on a Future object in Flutter

I need to know wether NFC is active or inactive inside the application. I'm using nfc_manager package and riverpod for state management. Here is my providers and ChangeNotifier class:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:nfc_manager/nfc_manager.dart';
final nfcProvider = ChangeNotifierProvider.autoDispose<NfcService>((ref) {
return NfcService();
});
final nfcIsAvabilableProvider = StreamProvider.autoDispose<bool>((ref) {
ref.maintainState = true;
final stream = ref.watch(nfcProvider).isAvailable.stream;
return stream;
});
class NfcService extends ChangeNotifier {
NfcManager nfcManager = NfcManager.instance;
StreamController<bool> isAvailable = StreamController.broadcast();
NfcService() {
init();
}
init() async {
isAvailable.addStream(nfcManager.isAvailable().asStream());
}
}
When I close the NFC inside the settings, nfcManager.isAvailable() does not return the new state of NFC availability because it is a future not a stream.
So my question is: how to listen to changes on a future object in Flutter?

CombineLatestStream.combine2 combiner never gets called

I'm trying to combine two streams of my entities, Category and Menu.
I tried this way but it seems like the combiner never gets executed (notice there was print statements inside, it never prints).
I'm very sure I properly followed the sample snippet on rxdart's API docs. And able to show the the data by directly binding the stream() function into the observable (.obs), but I need to combine it to be able to refresh Category's menus property every time I add, edit, or remove them.
I tried to rebuild the app (ctrl + shift + F5)
Switching from CombineLatestStream.combine2 to Rx.combineLatest2
Performing flutter clean and flutter pub run build_runner build --delete-conflicting-outputs and re-run the app.
But it doesn't work. How can I fix it?
I'm using GetX as my state management and ObjectBox as my database.
category_repository.dart
import 'package:flutter/foundation.dart' hide Category;
import 'package:myapp/app/data/entities/category.dart';
import 'package:myapp/app/data/entities/menu.dart';
import 'package:myapp/app/data/repositories/menu_repository.dart';
import 'package:myapp/app/services/objectbox_service.dart';
import 'package:rxdart/rxdart.dart';
class CategoryRepository {
final _box = ObjectBoxService.instance.store.box<Category>();
Stream<List<Category>> stream() {
debugPrint('streaming List<Category>....');
return _box.query().watch().map((query) => query.find());
}
Stream<List<CategoryWithMenus>> streamWithMenus() {
debugPrint('combining streams into List<CategoryWithMenus>....');
final categoriestream = stream();
final menustream = MenuRepository().stream();
final combined = CombineLatestStream.combine2(
categoriestream,
menustream,
(List<Category> categories, List<Menu> menus) {
debugPrint('mapping...'); // this never printed out
final idToCat = {for (var cat in categories) cat.id: cat};
final ids = idToCat.keys;
final cidToMenus = <int, List<Menu>>{};
for (var men in menus) {
cidToMenus.putIfAbsent(men.category.targetId, () => []).add(men);
}
final result = [
for (var id in ids)
CategoryWithMenus(idToCat[id]!, cidToMenus[id] ?? []),
];
for (var r in result) {
debugPrint(r.toString());
}
return result;
},
);
debugPrint('streams combined...');
debugPrint('streaming List<CategoryWithMenus>....');
return combined;
}
}
class CategoryWithMenus {
final Category category;
final List<Menu> menus;
CategoryWithMenus(this.category, this.menus);
}
menu_repository.dart
import 'package:myapp/app/data/entities/menu.dart';
import 'package:myapp/app/services/objectbox_service.dart';
class MenuRepository {
final _box = ObjectBoxService.instance.store.box<Menu>();
Stream<List<Menu>> stream() {
print('streaming List<Menu>....');
return _box.query().watch().map((query) => query.find());
}
}
category.dart
import 'package:flutter/material.dart';
import 'package:myapp/app/data/entities/menu.dart';
import 'package:objectbox/objectbox.dart';
#Entity()
class Category {
int id = 0;
String name = 'Unknown';
int labelColor = Colors.purple.shade900.value;
#Backlink('category')
final menus = ToMany<Menu>();
}
menu.dart
import 'package:myapp/app/data/entities/addition.dart';
import 'package:myapp/app/data/entities/category.dart';
import 'package:objectbox/objectbox.dart';
#Entity()
class Menu {
int id = 0;
String name = 'Unknown';
double price = 0.0;
final category = ToOne<Category>();
#Backlink('menu')
final additions = ToMany<Addition>();
}
home_controller.dart
import 'package:myapp/app/data/entities/category.dart';
import 'package:myapp/app/data/entities/menu.dart';
import 'package:myapp/app/data/repositories/category_repository.dart';
import 'package:get/get.dart';
class HomeController extends GetxController {
HomeController({
required CategoryRepository categoryRepository,
}) : _categoryRepository = categoryRepository;
final CategoryRepository _categoryRepository;
final categories = <CategoryWithMenus>[].obs;
final activeCategory = Rxn<CategoryWithMenus>();
#override
void onInit() {
final cstream = _categoryRepository.streamWithMenus();
categories.bindStream(cstream);
print(categories.length);
super.onInit();
}
}
debug console
Restarted application in 1,213ms.
[GETX] Instance "ObjectBoxService" has been created
[GETX] Instance "ObjectBoxService" has been initialized
[GETX] Instance "GetMaterialController" has been created
[GETX] Instance "GetMaterialController" has been initialized
[GETX] GOING TO ROUTE /home
[GETX] Instance "HomeController" has been created
flutter: combining streams into Stream<List<CategoryWithMenus>>....
flutter: streaming List<Category>....
flutter: streaming List<Menu>....
flutter: streams combined...
flutter: streaming List<CategoryWithMenus>....
flutter: 0
[GETX] Instance "HomeController" has been initialized
EDIT
I have created and shown 10 records from a form screen before switching from direct stream() binding to rxdart.
I found the problem! I have to set triggerImmediately to true in every watch function inside my stream().
Thanks #pskink for pointing me out about event emission. It works now.
class CategoryRepository {
final _box = ObjectBoxService.instance.store.box<Category>();
Stream<List<Category>> stream() {
debugPrint('streaming List<Category>....');
return _box
.query()
.watch(triggerImmediately: true)
.map((query) => query.find());
}
...
}
class MenuRepository {
final _box = ObjectBoxService.instance.store.box<Menu>();
Stream<List<Menu>> stream() {
print('streaming List<Menu>....');
return _box
.query()
.watch(triggerImmediately: true)
.map((query) => query.find());
}
}

Mocking GetStorage for testing in flutter

I am unit testing and widget testing my code. I have tried mokito and moktail to mock the Get storage but get this error:
package:get_storage/src/storage_impl.dart 47:7 GetStorage._init
===== asynchronous gap ===========================
package:get_storage/src/storage_impl.dart 28:7 new GetStorage._internal.<fn>
the class that I am testing:
class ShowCaseController extends GetxController {
final box = GetStorage();
displayAnySC(String playKey, String replayKey, GetStorage box) async {
bool? showcasePlayStatus = box.read(playKey);
if (showcasePlayStatus == null) {
box.write(playKey, false);
// box.remove(replayKey);
box.write(replayKey, false);
return true;
}
bool? showcaseReplayStatus = box.read(replayKey);
if (showcaseReplayStatus == null) {
box.write(replayKey, false);
return true;
}
return false;
}
}
here is one empty simple test using mock that gives error:
class MockStorage extends Mock implements GetStorage {}
void main() {
group('Show case controller', () {
final showCaseCnt = ShowCaseController();
late bool _data;
late MockStorage _mockStorage;
setUp(() {
_mockStorage = MockStorage();
_data = showCaseCnt.displayAnySC('playKey', 'replayKey', _mockStorage);
});
test(
'displayAnySC should return false when the play and replay are not null',
() {
});
});
}
I have tried #GenerateMocks([ShowCaseController]) and also added GetStorage.init() inside the main function of the test but got the same error.
P.S. I haven't seen any article or question related to mocking the GetStorage for test in Flutter. Appreciate any explanation or link that helps in this regard.
I am using the GetX package for dependency injection and state management. And using the GetStorage package for keeping the theme persistent and storing keys for notifying the app to play or replay ShowCaseView.

Flutter pass value to bloc repository

Trying to study bloc in flutter. I have bloc, events and repository files. I know how to pass data between pages, but i can't pass the parameter to the repository
I need to pass a parameter to the repository for make query to db. For example category id
products_by_category_repository.dart
import 'package:billfort/models/products_model.dart';
import 'package:billfort/strings/strings.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
abstract class ProductsByCategoryRepository {
Future<List<HydraMember>> getProducts();
}
class ProductsByCategoryRepositoryImpl implements ProductsByCategoryRepository {
#override
Future<List<HydraMember>> getProducts() async {
var response = await http.get(Uri.parse("${AppStrings.api}products?category=here need pass id"));
if (response.statusCode == 200) {
var data = json.decode(response.body);
List<HydraMember> products = Products.fromJson(data).hydraMember;
return products;
} else {
throw Exception();
}
}
}
products_bloc.dart
import 'package:bloc/bloc.dart';
import 'package:billfort/bloc/products_event.dart';
import 'package:billfort/bloc/products_state.dart';
import 'package:billfort/models/products_model.dart';
import 'package:billfort/repository/products_repository.dart';
import 'package:meta/meta.dart';
class ProductsBloc extends Bloc < ProductsEvent, ProductsState > {
ProductsRepository repository;
ProductsBloc({
#required this.repository
}): super(null);
#override
// TODO: implement initialState
ProductsState get initialState => ProductsInitialState();
#override
Stream < ProductsState > mapEventToState(ProductsEvent event) async *{
if (event is FetchProductsEvent) {
yield ProductsLoadingState();
try {
List < HydraMember > products = await repository.getProducts();
yield ProductsLoadedState(products: products);
} catch (e) {
yield ProductsErrorState(message: e.toString());
}
}
}
}
If your process of fetching products needs input, then your class FetchProductsEvent should have a property that provides that input, just as your state class ProductsLoadedState for example has a products property that provides the output.
That said, did you upgrade your bloc package without really understanding the changes? Why are you passing null to your base constructor, but still providing an overload for initialState? Your base constructor should get ProductsInitialState() directly and you should remove the overload. If it does not fire warnings for you, you may want to crank up your compiler settings, so your compiler can guide you better.

why does my global variable keeps getting set back to default when switching pages?

I have a GlobalVariables.dart where i store all the global variables.
library lumieres.globals;
String appLanguage;
bool languageChanged = false;
String appMode = 'dev';
in my main.dart file, I'm checking if shared preference contains 'user', if not I am setting the language to be english.
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';
import 'package:intl/intl.dart';
import 'GlobalVariables.dart' as globals;
// void main() => runApp(MyApp());
void main(){
runApp(MyApp());
startingAppFunction();
}
startingAppFunction() async{
print('calling startingappfunction from main');
SharedPreferences sharedpref = await SharedPreferences.getInstance();
var user = sharedpref.getString('user');
if (user != "" && user != null) {
var decodedUser = json.decode(user);
globals.appLanguage = decodedUser['lang'];
}else{
globals.appLanguage = "EN";
}
print('here is the app language from main dart');
print(globals.appLanguage); //prints EN which is correct but on homepage it says null
}
But in my homepage, it's set back to null seems like the global variable is some how getting set back to String appLanguage; which is null.
import 'GlobalVariables.dart' as globals;
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
//limiting all results to only 6
// var refreshKey = GlobalKey<RefreshIndicatorState>();
Future<String> dataFuture;
#override
void initState(){
super.initState();
print('printing from home page...');
print(globals.appLanguage); //prints null
}
}
what am I missing? I followed this suggestion from this stackoverflow answer: Global Variables in Dart
It might not be that it is set back to null but because it is an async process your print execute before your value is set.
Your print in startingApp will always display the correct value because it is in the same method.
Try running startingAppFunction() before your runApp(). (using then might be a good option too)
void main(){
startingAppFunction().then((_) {
runApp(MyApp());
});
}