call cubit function inside background callback home widget - flutter

I want to call cubit function when user click refresh button on a Home Widget and refresh weather data from cubit function and show new data. I used Home_widget
package and cubit hydrated bloc.
Actually I am really confused. I want to know how I can access state of the cubit inside the Future<void> backgroundCallBack(Uri? uri)
main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
HomeWidget.registerBackgroundCallback(backgroundCallBack);
HydratedBloc.storage = await HydratedStorage.build(
storageDirectory: await getApplicationDocumentsDirectory());
Bloc.observer = WeatherObserver();
runApp(MyApp(
airRepository: AirRepository(),
weatherRepository: WeatherRepository(),
));
}
Future<void> backgroundCallBack(Uri? uri) async {
if (uri!.host == "updateweahter") {
BlocProvider<WeatherCubit>(
create: (context) => WeatherCubit(
context.read<WeatherRepository>(), context.read<AirRepository>()),
child: BlocBuilder<WeatherCubit, WeatherState>(
builder: (context, state) {
context.read<WeatherCubit>().refereshWeather(state.weather);
return WeatherPopulated(
weather: state.weather,
cityName: state.cityName,
airQuality: state.airQuality,
onRefresh: () {
return context.read<WeatherCubit>().fetchWeather(
state.weather.latitude!,
state.weather.longitude!,
state.cityName,
dateToJalaliWithTime(),
2);
},
);
},
),
);
await HomeWidget.saveWidgetData<String>(
'temp', state.weather.currentWeather!.temperature!.toStringAsFixed(0));
await HomeWidget.saveWidgetData('city', state.cityName.toString());
await HomeWidget.saveWidgetData(
'time', state.weather.currentWeather!.time!.substring(11));
await HomeWidget.saveWidgetData(
'day',
dateToJalaliDayName(
DateTime.parse(state.weather.currentWeather!.time.toString())));
await HomeWidget.saveWidgetData(
'tvCond', weatherCondition(state.weather.currentWeather!.weathercode));
await HomeWidget.saveWidgetData(
'ivCond', code(state.weather.currentWeather!.weathercode!.toInt()));
await HomeWidget.updateWidget(
name: 'AppWidgetProvider',
androidName: 'AppWidgetProvider',
iOSName: 'AppWidgetProvider',
qualifiedAndroidName:
'com.example.flutter_bloc_concept.AppWidgetProvider');
}
the cubit function is:
Future<void> refereshWeather(WeatherModel weatherModel) async {
As you see for refreshWeather Condition I need WeatherModel that contain last position and data of location and city name. I try to call cubit function from blocprovider and blocbuilder but not helpful.
can every one help me to figure out how can access to the cubit function and also state of it.
thanks

Related

How to refactor this code to avoid passing BuildContext between async scopes?

I have a PermissionsManager class, and I'm getting a "Do not use BuildContext across async gaps" for this particular method:
class PermissionsManager {
static Future<void> requestLocationPermission(BuildContext context) async {
final status = await Permission.location.request();
if (!status.isGranted) {
await showOpenSettingsDialog(context,
title: "Grant Location Access",
message:
"TODO");
}
}
}
I thought about splitting this into multiple functions, but then the caller needs to check the status, and based on the status call another method that will show this dialog box.
Is there a way to do this in the same method and handle this build context issue?
Good question! Assuming you are in a "Stateful Widget", add if (mounted) check before using BuildContext across an async gap.
For example:
onPressed: () async {
final status = await Future.delayed(const Duration(seconds: 1));
if (mounted) { // <-- add this check
if (!status.isGranted) {
showOpenSettingsDialog(context);
}
}
}
The reason we don't want to use BuildContext across an async gap is because the widget could've become unmounted during the wait. If we check if (mounted) we won't have this concern. Basically, if the widget is somehow destroyed during the wait, we just don't show the dialog any more.
If this is a stateless widget, you can convert it into a stateful one.
You can also read my detailed explanation on this topic here.
Store the NavigatorState before executing your requestLocationPermission function, and then use it to handle the navigation:
onPressed: () async {
final navigator = Navigator.of(context); // store the Navigator
await requestLocationPermission(navigator); // use the Navigator, not the BuildContext
},
class PermissionsManager {
static Future<void> requestLocationPermission(NavigatorState navigator) async {
final status = await Permission.location.request();
if (!status.isGranted) {
await showOpenSettingsDialog(
context,
title: "Grant Location Access",
message: "TODO",
);
}
navigator.pop(); // Do whatever with your navigator;
}
}
This answer is basically a shorthand of: https://stackoverflow.com/a/69512692
Which I highly suggest for you to look at it for a more detailed explanation.

Flutter RepositoryProvider and Hive LateInitializationError

I have app where I am using Bloc and Hive.
main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final appDocumentDirectory =
await path_provider.getApplicationDocumentsDirectory();
Hive.init(appDocumentDirectory.path);
runApp(
const MyApp(),
);
}
On MyApp widget registered MultiRepositoryProvider
return MultiRepositoryProvider(
providers: [
RepositoryProvider(create: (context) => AccountService()),
],
child: MultiBlocProvider(
providers: [
BlocProvider<AccountBloc>(
create: (context) => AccountBloc(context.read<AccountService>()),
),
],
child: MaterialApp(
home: const AppPage(),
),
),
);
AppPage Contains bottomNavigationBar and some pages
account.dart
class AccountService {
late Box<Account> _accounts;
AccountService() {
init();
}
Future<void> init() async {
Hive.registerAdapter(AccountAdapter());
_accounts = await Hive.openBox<Account>('accounts');
}
On appPage have BlocBuilder
BlocBuilder<AccountBloc, AccountState>(
builder: (context, state) {
if (state.accountStatus == AccountStatus.loading) {
return const CircularProgressIndicator();
} else if (state.accountStatus == AccountStatus.error) {
Future.delayed(Duration.zero, () {
errorDialog(context, state.error);
});
}
return SingleChildScrollView(....
When app first loaded I receive LateInitializationError that late Box <Account> _accounts from account Repository not initialized. But as soon as I navigate to another page and go back, the Box <Account> _accounts are initialized and the data appears.
How can I avoid this error and initialize the Hive box on application load?
Can you try this? I think you need to await Hive init function
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final appDocumentDirectory =
await path_provider.getApplicationDocumentsDirectory();
await Hive.init(appDocumentDirectory.path);
runApp(
const MyApp(),
);
}
It's been like 7 months, but if you are still looking for an answer, not sure if it's optimal but below should work.
My understanding on the issue you are having is that the reason why there is that "LateInitializationError" is because that your init function call in your constructor is asynchronously invoked without await for its result. As a result, there is a possibility that when you are calling functions on the box, the initialisation is not yet finished. When you navigate to another page and go back, the function init run happened to be finished. Hence, the error is gone. The complexity here is that constructor can not be marked as async for you to use that await keyword. Since you are using bloc, one possible workaround is to call the init function of your repo when bloc is in init state.
For demo purpose I defined below bloc states and events,
you can absolutely change them based on your needs.
// bloc states
abstract class AccountState{}
class InitState extends AccountState{}
class LoadedState extends AccountState{
LoadedState(this.accounts);
final List<Account> accounts;
}
class LoadingErrorState extends AccountState{}
//bloc events
abstract class AccountEvent {}
class InitEvent extends AccountEvent {}
... // other events
in your bloc logic you can call the init function from you repo on InitEvent
class AccountBloc extends Bloc<AccountEvent, AccountState> {
AccountBloc(this.repo) : super(InitState()) {
on<InitEvent>((event, emit) async {
await repo.init();
emit(LoadedState(account: repo.getAccounts()));
});
...// define handlers for other events
}
final AccountRepository repo;
}
in your service class you can remove the init from the constructor like:
class AccountService {
late Box<Account> _accounts;
AccountService();
Future<void> init() async {
Hive.registerAdapter(AccountAdapter());
_accounts = await Hive.openBox<Account>('accounts');
}
List<Account> getAccounts(){
return _accounts.values.toList();
}
}
Then in your bloc builder, you can add init event to your bloc when the state is InitState as below:
BlocBuilder<AccountBloc, AccountState>(
builder: (context, state) {
if (state is InitState) {
context.read<AccountBloc>.add(InitEvent());
return const CircularProgressIndicator();
} else if (state is LoadingErrorState) {
Future.delayed(Duration.zero, () {
errorDialog(context, state.error);
});
}
else if (state is LoadedState){
return SingleChildScrollView(....
}
Also, FYI, you can if you want the init to be called when the object of your account service is instantiated, you can take a look at below answer:
https://stackoverflow.com/a/59304510/16584569
However, you still going to need to await for the initialisation of your service. One possible way is just do it in your main function and pass down to your app, but it makes the structure of your code messy and when you want to swap to another repo, you need to remember to change code in main function as well.

call one asynchronous function when the first one runs flutter

I have two asynchronous functions, one returns a popup, the other makes a permission request. I call them in the init method. But they are called simultaneously, i.e. the first window appears and immediately the second. How do I fix this?
class _MyAppState extends State<MyApp> {
final keyIsFirstLoaded = 'is_first_loaded';
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
final context = MyApp.navKey.currentState.overlay.context;
await showDialogIfFirstLoaded(context);
await initPlatformState();
});
}
showDialogIfFirstLoaded(BuildContext context, prefs) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
bool isFirstLoaded = prefs.getBool(keyIsFirstLoaded);
if (isFirstLoaded == null) {
showDialog(
context: context,
builder: (BuildContext context) {
// return object of type Dialog
return new AlertDialog(
// title: new Text("title"),
content: new Text("//"),
actions: <Widget>[
new FlatButton(
child: new Text(".."),
onPressed: () {
Navigator.of(context).pop();
prefs.setBool(keyIsFirstLoaded, false);
},
),
],
);
},
);
}
}
initPlatformState() async {
print('Initializing...');
await BackgroundLocator.initialize();
print('Initialization done');
final _isRunning = await BackgroundLocator.isRegisterLocationUpdate();
setState(() {
isRunning = _isRunning;
});
onStart();
print('Running ${isRunning.toString()}');
}
add return to showDialog statement, you're not returning a Future so await isn't doing anything
Personal advice: always specify return types, cause if you don't, you get dynamic return type. If you do specify it, the IDE/dart analysis server will help you with problems such as this one.

I have a question about navigating to the next page conditionally in initstate

I want to implement Auto Login with Shared preferences.
What I want to implement is that as soon as 'LoginPage' starts, it goes to the next page without rendering LoginPage according to the Flag value stored in Shared preferences.
However, there is a problem in not becoming Navigate even though implementing these functions and calling them from initstate. What is the problem?
//Login Page
void autoLogIn() async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
final String userId = prefs.getString('username');
print("ddddddddddddddd");
SocketProvider provider = Provider.of<SocketProvider>(context);
Future.delayed(Duration(milliseconds: 100)).then((_) {**//I tried giving Delay but it still didn't work.**
Navigator.of(context).pushNamedAndRemoveUntil("/MainPage", (route) => false);
});
}
#override
void initState() {
// TODO: implement initState
loginBloc = BlocProvider.of<LoginBloc>(context);
if(!kReleaseMode){
_idController.text = "TESTTEST";
_passwordController.text = "1234123";
}
initBadgeList();
autoLogIn();**//This is the function in question.**
super.initState();
print("1111111111111111");
}
I don't think you should show LoginPage widget if user is already logged in and then navigate to main page.
I suggest you to use FutureBuilder and show either splash screen or loader while performing await SharedPreferences.getInstance(). In this case your App widget should look like this:
class App extends MaterialApp {
App()
: super(
title: 'MyApp',
...
home: FutureBuilder(
future: SharedPreferences.getInstance(),
builder: (context, snapshot) {
if (snapshot.data != null) {
final SharedPreferences prefs = snapshot.data;
final userId = prefs.getString('username');
...
return userId == null ?? LoginPage() : MainPage();
} else {
return SplashScreenOrLoader();
}
}));
}
But if you still want to show LoginPage first, just replace SplashScreenOrLoader() with LoginPage() in code above.

How can I display a dialog whenever my stream gets a new value?

I have the following bloc provider
class CloudMessagingBloc{
StreamController<NotificationModel> _streamController = StreamController<NotificationModel>();
Stream<NotificationModel> get stream => _streamController.stream;
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
CloudMessagingBloc() {
if (Platform.isIOS) _firebaseMessaging.requestNotificationPermissions(IosNotificationSettings());
_firebaseMessaging.configure(
onMessage: (Map<String, dynamic> message) async {
_streamController.add(NotificationModel.fromMap(message));
},
onLaunch: (Map<String, dynamic> message) async {
_streamController.add(NotificationModel.fromMap(message));
},
onResume: (Map<String, dynamic> message) async {
_streamController.add(NotificationModel.fromMap(message));
},
);
}
void dispose(){
_streamController.close();
}
}
And implement it like this
static Widget create() {
return MultiProvider(
providers: [
Provider(create: (_) => DelayBloc(seconds: 2)),
Provider(
create: (_) => CloudMessagingBloc(),
dispose: (BuildContext context, CloudMessagingBloc bloc) => bloc.dispose(),
lazy: false,
),
],
child: TheRootPage(),
);
}
In my root page stateless widget. But now I have a problem, because I want to show a dialog once whenever the stream emits a new value. So I impemented a streambuilder for this and the notification shows correctly when a new value get's added
StreamBuilder(
stream: cloudMessagingBloc.stream,
builder: (BuildContext context, AsyncSnapshot<NotificationModel> snapshot) {
if (snapshot.connectionState == ConnectionState.active && snapshot.hasData)
SchedulerBinding.instance
.addPostFrameCallback((_) => _showNotificationDialog(context, snapshot.data));
But the problem is whenever the widget rebuilds that holds this streambuilder, the notification get's shown again because the condition is met, which is not something I want, because I only want to show a notification once. So how do I prevent this from happening? It feels like I have a structural problem, and I just can't figure it out.
I see 3 possible solutions:
You need to use https://pub.dev/packages/equatable to check is it new state or not. This is also from ferangel(author of BLOC package). You can find examples with equitable and Bloc in google.
You can create a global key and then use it to show dialogs(if you have the global key, you can retrieve context from it and than call dialogService.showMyDialog() from bloc).
You can use for example https://pub.dev/packages/get package to show dialogs/make navigation from your bloc, without notifying Page about the new state.
Personally I prefer 2, or 3.
But also sometimes use first one.