Show Snackbar in any screen when task finishes - flutter

I have a task that will run in the background (in an isolate) and when it finishes, I want to show a Snackbar. However, the user may navigate to a different screen from the one where the task was initiated. How do I show an 'app-level' Snackbar, not bound to any particular screen?
Edit: I found this: How to show a SnackBar from async executions when no context is available?, has some good information and option 1 (Display errors in page scaffolds) seems to be what I want, but I need to implement all by myself. I was hoping for something built in into Flutter.

I'd give this a try where you go for an abstract base class where you implement the 'listener', which then all your pages extend from instead of e.g. StatelessWidget.
Instead of overriding the normal build method, just override your new special build.
Pseudo code (here with BlocListener):
abstract class MySnackbarShowingPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocListener<SubjectBloc, SubjectState>(
listener: (context, state) {
// TODO: implement snackbar display
},
child: mySpecialBuild(context),
);
}
Widget mySpecialBuild(BuildContext context);
}
class MySpecificPage extends MySnackbarShowingPage {
#override
Widget mySpecialBuild(BuildContext context) {
// TODO: implement mySpecialBuild as your normal page does today
}
}

Related

Flutter: How to trigger a bloc event in the controller?

In summary I want the "context.read().add(EnabledEvent())" to work from the controller itself or some other alternative I can use to trigger an event from the controller and update the state:
class SplashCtrl extends GetxController {
#override
void onReady() {
User user = loadUser();
if (user.isExampleEnabled) {
context.read<ExampleBloc>().add(EnabledEvent());
}
}
Another thing I tried was this:
ExampleBloc testBloc = ExampleBloc();
testBloc.add(TestEvent());
in the controller, and it does seem to trigger the event, but the UI doesn't update with the state change. I seem to really need the Context for that to change. Any ideas?
MORE DETAILS AND CONTEXT:
I want to trigger bloc events not just in the page itself, but in the page's controller!
So I have pages in my flutter app with each view having a controller set up like this:
class SplashPage extends GetView<SplashCtrl> {
#override
Widget build(BuildContext context) {
Get.lazyPut(() => SplashCtrl());
return Scaffold(...);
}
}
SplashCtrl is the controller for this page. This splash view page is one of the first pages that loads it runs functions when it's added and ready (basically instantly), such as checking if the user is logged in and loading their data to the app, like this:
class SplashCtrl extends GetxController {
#override
void onReady() {
// run stuff here
}
I have been able to get away with creating lambda functions in the pages and triggering events based on what they do and toggle like this:
IconSwitchedButtonUi(
value: state.isExampleOn,
icon: Images.toggleIcon,
title: "Is Example enabled?",
onChanged: (value) {
if (value) {
context.read<ExampleBloc>().add(EnabledEvent());
} else {
context.read<ExampleBloc>().add(DisabledEvent());
}
},
),
but now I need a bloc event to trigger and change the state a bit if a user has a certain value for one of their fields. How do I do that? How do I trigger an event from within the controller? The "context.read" doesn't work because the controller doesn't have context, or does it?
ExampleBloc testBloc = ExampleBloc(); testBloc.add(TestEvent());
This does not work because you were declaring totally new instance of bloc that wasnt connected by any BlocProvider to your widget tree, hence there is no way you could trigger this bloc events that would emit state your BlocBuilder could react to. Since there is no bloc provided to widget tree read() wont find this bloc.
In order for this work you need to:
Pass BuildContext that have some Bloc or Cubit provided by BlocProvider
void foo(BuildContext context) {
User user = loadUser();
if (user.isExampleEnabled) {
context.read<ExampleBloc>().add(EnabledEvent());
}
}
..or listen to bloc changes directly inside controller
class SplashCtrl extends GetxController {
SplashCtrl(MyBloc bloc) {
bloc.stream.listen((event) {
// React to bloc changes here
if (event is MyBlocEvent) {
// Do something
}
}
}
}
Remember that even in this approach bloc instance given to SplashCrtl constructor must be the same that is provided by BlocProvider to a widget tree in order for this to work.
I was able to resolve this by adding a late BuildContext to my controller:
class SplashCtrl extends GetxController {
late BuildContext context;
#override
void onReady() {
// run stuff here
}
After that, in the page itself, I passed the context like this:
class SplashPage extends GetView<SplashCtrl> {
#override
Widget build(BuildContext context) {
controller.context = context;
Get.lazyPut(() => SplashCtrl());
return Scaffold(...);
}
}
Then I was able to use the context within the controller itself!!
if (value) {
context.read<ExampleBloc>().add(EnabledEvent());
} else {
context.read<ExampleBloc>().add(DisabledEvent());
}
},

decide the route based on the initialization phase of flutter_native_splash

I'm writing a flutter application using flutter 2.10 and I'm debugging it using an Android Emulator
I included the flutter_native_splash plugin from https://pub.dev/packages/flutter_native_splash and I use version 2.0.1+1
the problem that I'm having is that I decide what's the first screen that the user will see based on the initialization phase. I check the stored user token, see his premissions, verify them with the server, and forward him to him relevant route.
since the runApp() function executes in the background while the initialization phase is running I cannot choose the page that will be shown. and if I try to nativgate to a route in the initialization function I get an exception.
as a workaround for now I created an init_home route with FutureBuilder that awaits for a global variable called GeneralService.defaultRoute to be set and then changes the route.
class _InitHomeState extends State<InitHome> {
#override
Widget build(BuildContext context) {
return FutureBuilder<dynamic>(
future: () async {
var waitCount=0;
while (GeneralService.defaultRoute == "") {
waitCount++;
await Future.delayed(const Duration(milliseconds: 100));
if (waitCount>20) {
break;
}
}
if (GeneralService.defaultRoute == "") {
return Future.error("initialization failed");
}
Navigator.of(context).pushReplacementNamed(GeneralService.defaultRoute);
...
any ideas how to resolve this issue properly ?
I use a Stateful widget as Splash Screen.
In the build method, you just return the 'loading' view such as Container with a background color etc. (with texts or whatever you like but just consider it as the loading screen).
In the initState(), you call a function that we can name redirect(). This should be an async function that performs the queries/checks and at the end, calls the Navigator.of(context).pushReplacementNamed etc.
class _SplashState extends State<Splash> {
#override
void initState() {
super.initState();
redirect();
}
#override
Widget build(BuildContext context) {
return Container(color: Colors.blue);
}
Future<void> redirect() async {
var name = 'LOGIN';
... // make db calls, checks etc
Navigator.of(context).pushReplacementNamed(name);
}
}
Your build function just creates the loading UI, and the redirect function in the initState is the one running in the background and when it has finished computing, calls the Navigator.push to your desired page.

Which is better when using provider instance in a new widget?

Let's say I've written my code as below.
I've got a provider called SampleProvider, and I'm using it in my main widget.
class SampleProvider extends ChangeNotifier {}
class MainWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
SampleProvider provider = Provider.of<SampleProvider>(context);
}
}
And then, I want to make a new widget and use this provider in the new widget.
There will be two choices.
First, I just instantiate another provider in the new widget as below.
class NewWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
SampleProvider provider = Provider.of<SampleProvider>(context);
}
}
Or, I can send it from the main widget to the new widget as a constructor parameter.
Like this:
class NewWidget extends StatelessWidget {
final SampleProvider provider;
NewWidget(this.provider);
#override
Widget build(BuildContext context) {
}
}
I guess the first option is better because flutter draws a widget based on its build context, but I'm not sure.
I've googled it quite long, but there was no success.
Can anybody tell me whether I am right or wrong? Or Do they have no difference?
Prefer the first solution, it's easier to refactor.
Suppose you need move NewWidget in your widget tree, you also need to modify the "paramter pass" code if you choose second solution, which is not necessary with first solution.
One of Provider pacakage's purpose is avoid passing parameter deep in the widget tree by the way.
Depend on preference not like first or second one.
Have an exception when obtaining Providers inside initState. What can I do?
This exception happens because you're trying to listen to a provider from a life-cycle that will never ever be called again.
It means that you either should use another life-cycle (build), or explicitly specify that you do not care about updates.
As such, instead of:
initState() {
super.initState();
print(context.watch<Foo>().value);
}
you can do:
Value value;
Widget build(BuildContext context) {
final value = context.watch<Foo>.value;
if (value != this.value) {
this.value = value;
print(value);
}
}
which will print value whenever it changes (and only when it changes).
Alternatively, you can do:
initState() {
super.initState();
print(context.read<Foo>().value);
}
SRC: https://github.com/rrousselGit/provider#i-have-an-exception-when-obtaining-providers-inside-initstate-what-can-i-do
Yes, I believe the first option is the better way, of the top of my head I can't think of any situation in which you would prefer the second option to the first.
If you don't use new widget as children of any other widget , first choice is better .
otherwise , second is better .

Flutter: Provider and how to update records from the DB in the background

I am new to Flutter and I have this simple use case: in my Cloud Firestore DB I have a list of JSON representing events. I want to show them through my Flutter app in a ListView.
My requirements is that the ListView doesn't refresh in real-time but only when a pull-on refresh (implemented using RefreshIndicator) is done by the user or when the app resumes from background
I tried to implement this in 2 ways (I am using provider package for state management):
Using StreamProvider to create a stream of records from the DB. This continuosly updates the list view (basically the widget changes while the user is looking at it and I don't want this)
Using a ChangeNotifierProvider that refers to a EventManager class which holds a List<Event>. This class has a pull method which updates its internal state. I call this method when the user does the pull-on refresh (in the onRefresh callback of RefreshIndicator).
Option 2 seems to work well however I do not know how to implement the refresh when the app resumes from background. As I said I am using provider (and therefore StatelessWidget) and apparently there is no way to bind to these events when using StatelessWidgets
Do you have any suggestions and best practices for this use case?
You need to access Flutters lifecycle methods and fire a callback when the app resumes.
You can add a stateful widget with WidgetsBindingObserver and put that somewhere in the scope of your Provider, but as a parent of whatever widget you use to display the info.
Or you can make your PullToRefresh widget stateful and do the same thing.
class LifeCycleWidget extends StatefulWidget {
#override
_LifeCycleWidgetState createState() => _LifeCycleWidgetState();
}
class _LifeCycleWidgetState extends State<LifeCycleWidget>
with WidgetsBindingObserver {
AppLifecycleState _appLifecycleState;
#override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
refreshOnResume();
}
#override
void didChangeAppLifecycleState(AppLifecycleState state) {
setState(() {
_appLifecycleState = state;
});
refreshOnResume();
}
void refreshOnResume() {
if (_appLifecycleState == AppLifecycleState.resumed) {
print('resumed');
// your refresh method here
}
}
#override
Widget build(BuildContext context) {
return HomePage();
}
}
Add the following to your main method if it's not there already.
WidgetsFlutterBinding.ensureInitialized();
Another way to do it without adding a stateful widget would be with GetX. You can still keep all your Provider stuff but only use the SuperController which provides lifecycle methods. This I can't test because I don't have your Provider code but you can probably get away with creating the class below and initializing the controller somewhere within the scope of the relevant Provider widget with
Get.put(LifeCycleController());
Then call the function in the onResumed override and you can use Get.context if you need context.
class LifeCycleController extends SuperController {
#override
void onDetached() {
debugPrint('on detached');
}
#override
void onInactive() {
debugPrint('on inactive');
}
#override
void onPaused() {
debugPrint('on pause');
}
#override
void onResumed() {
// your refresh function here. Access context with Get.context
debugPrint('on resume');
}
}

how to pass data to another screen using bloc in flutter

hello I am new in flutter and bloc, I imagine that I have 2 screens (login and home screen). In login screen I am using bloc that post data and I want to call that data in my home screen. Can someone give me example to do that?
There are many ways to do that, I can name a few.
You navigate to the new Widget (the screen) and pass to that Widget constructor the data you want it to have.
You can use Provider to provide that data and wrap the new screen on it, then navigate to the new screen.
If this is some data that should be accessed across the app, you could provide the entire BLoC to the entire App and get the BLoC's reference on this new screen and then get the data directly from the BLoC.
If you just want to pass a value to home page from login page, you can do like this:
class Home extends StatelessWidget {
final String username;
Home(this.username);
#override
Widget build(BuildContext context) {
return Container();
}
}
class Login extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(onPressed: () {
Navigator.of(context).push(MaterialPageRoute(builder: (ctx) {
return Home('flutter');
}));
}),
);
}
}