Flutter: How to pass data between screens? - flutter

How can I change the visibility of a button on screen "X" from a button on screen "Y".

One popular approach (using the provider architecture) would be something like this:
Define a provider that handles all the logic and holds your data:
class MyProvider extends ChangeNotifier {
bool showMyButton = false;
MyProvider() {}
void showButton() {
showMyButton = true;
// This line notifies all consumers
notifyListeners();
}
void refresh() {
notifyListeners();
}
}
To access the provider everywhere you need to register it:
void main() => runApp(
// You can wrap multiple providers like this
MultiProvider(
providers: [
ChangeNotifierProvider<MyProvider>(create: (_) => MyProvider()),
],
child: const MyApp(),
),
);
On the button that you want to control you can use a Consumer to listen to the providers values:
Consumer<MyProvider>(builder: (_, model, __) {
return Visibility(
visible: model.showMyButton,
child: MaterialButton(...),
);
})
Now in your second screen you can access that provider with:
Provider.of<MyProvider>(context, listen: false)
.showButton();
However you might have to call notifyListener one more time when returning from screen Y to screen X:
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ScreenY()));
Provider.of<MyProvider>(context, listen: false).refresh();
Keep in mind that there is a lot more to provider so please have a look at their official docs.
Also be aware of the fact that there are easier ways to just pass data between screens but you will often arrive at a point where you will need a better way of managing state and provider provides just that ;)

You can pass the data via push and pop of navigation. Or else use ChangeNotifier class to notify the state of button easily.

Related

Persistent BottomNavigationBar with Routing in Flutter

I have a hard time implementing a persistent BottomNavigationBar in Flutter. My goal is to create a app with several screens and therefore several routes (minimal example):
I found this medium article and after struggling a bit with the implementation, I thought that I found the perfect solution. BUT as I wanted to implement a logout function that sends the user back to the LoginScreen the routing doesn't work as expected...
As you see in the gif, the problem occours after clicking on the logout button. Instead of navigating back to the LoginScreen, the LoginScreen get's embedded into the MainScreen with the BottomNavigationBar.
How can I change this behaviour? I thought I would remove all routes with pushAndRemoveUntil...
// Navigate back to the LoginScreen (this doesn't work as expected...)
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(
builder: (context) => LoginScreen(),
),
(Route<dynamic> route) => false);
Here is a minimal reproducable example: https://github.com/klasenma/persistent_bottomnavigationbar
After several attempts, I managed to solve the problem. I needed to save the context of the MainScreen (index.dart -> holds the BottomNavigationBar).
class ContextKeeper {
static BuildContext buildContext;
void init(BuildContext context) {
buildContext = context;
}
}
lib/screens/main/index.dart:
#override
void initState() {
super.initState();
ContextKeeper().init(context); // Save the context
}
Then change
Navigator.of(context).pushAndRemoveUntil(MaterialPageRoute(builder: (context) => LoginScreen(),),(Route<dynamic> route) => false);
to
Navigator.of(ContextKeeper.buildContext).pushNamedAndRemoveUntil(LoginScreen.id, (route) => false);
and it work's.

Flutter: UI reactions with Provider

On some event, I want to navigate to another screen with Navigator.
I could easily achieve it with BlocListener:
BlocListener<BlocA, BlocAState>(
bloc: blocA,
listener: (context, state) {
if (state is Success) {
Navigator.of(context).pushNamed('/details');
}
},
child: Container(),
)
But I can't find the direct equivalent for it in a pure Provider.
The only way I see is to swap screens:
home: Consumer<Auth>(
builder: (_, auth, __) => auth.user == null ? LoginPage() : MainPage()
)
It's a common way. But it will not use Navigator, hence it will just 'pop' MainPage without screen transition.
On some event, I want to play some animation in UI.
I found in the documentation that Listenable class is intended for dealing with Animations, but it's not explained in details.
On some event, I want to clear a TextEditingController.
On some event, I want to show a dialog.
And more similar tasks...
How to solve it? Thanks in advance!
After some research I found a way. I'm not sure if it's the only or the best way, or the way foreseen by Provider's creator, however it works.
The idea is to keep a helper Stream inside of my Store class (I mean business-logic class provided with Provider), and to subscribe to its changes in my widget.
So in my Store class I have:
final _eventStream = StreamController.broadcast();
Stream get eventStream => _eventStream.stream;
void dispose() {
_eventStream.close();
super.dispose();
}
I add events to this stream inside of actions:
void navigateToNextScreen() {
_eventStream.sink.add('nav');
}
void openDialog() {
_eventStream.sink.add('dialog');
}
In my UI widget I have:
#override
void afterFirstLayout(BuildContext context) {
context.read<Transactions>().eventStream.listen((event) {
if (event == 'nav') {
Navigator.push(
context,
MaterialPageRoute(
builder: (ctx) => SecondScreen(),
),
);
} else if (event == 'dialog') {
showDialog(
context: context,
builder: (context) => AlertDialog(content: Text("Meow")));
}
});
}
I used here afterFirstLayout lifecycle method from the after_layout package, which is just a wrapper for WidgetsBinding.instance.addPostFrameCallback
07.07.20 UPD.: Just found a package that can be used for event reactions:
https://pub.dev/packages/event_bus
It basically uses the same approach with StreamController under the hood.

How to use a provider inside of another provider in Flutter

I want to create an app that has an authentication service with different permissions and functions (e.g. messages) depending on the user role.
So I created one Provider for the user and login management and another one for the messages the user can see.
Now, I want to fetch the messages (once) when the user logs in. In Widgets, I can access the Provider via Provider.of<T>(context) and I guess that's a kind of Singleton. But how can I access it from another class (in this case another Provider)?
From version >=4.0.0, we need to do this a little differently from what #updatestage has answered.
return MultiProvider(
providers: [
ChangeNotifierProvider(builder: (_) => Auth()),
ChangeNotifierProxyProvider<Auth, Messages>(
update: (context, auth, previousMessages) => Messages(auth),
create: (BuildContext context) => Messages(null),
),
],
child: MaterialApp(
...
),
);
Thanks for your answer. In the meanwhile, I solved it with another solution:
In the main.dart file I now use ChangeNotifierProxyProvider instead of ChangeNotifierProvider for the depending provider:
// main.dart
return MultiProvider(
providers: [
ChangeNotifierProvider(builder: (_) => Auth()),
ChangeNotifierProxyProvider<Auth, Messages>(
builder: (context, auth, previousMessages) => Messages(auth),
initialBuilder: (BuildContext context) => Messages(null),
),
],
child: MaterialApp(
...
),
);
Now the Messages provider will be rebuilt when the login state changes and gets passed the Auth Provider:
class Messages extends ChangeNotifier {
final Auth _authProvider;
List<Message> _messages = [];
List<Message> get messages => _messages;
Messages(this._authProvider) {
if (this._authProvider != null) {
if (_authProvider.loggedIn) fetchMessages();
}
}
...
}
Passing another provider in the constructor of the ChangeNotifierProxyProvider may cause you losing the state, in that case you should try the following.
ChangeNotifierProxyProvider<MyModel, MyChangeNotifier>(
create: (_) => MyChangeNotifier(),
update: (_, myModel, myNotifier) => myNotifier
..update(myModel),
);
class MyChangeNotifier with ChangeNotifier {
MyModel _myModel;
void update(MyModel myModel) {
_myModel = myModel;
}
}
It's simple: the first Provider provides an instance of a class, for example: LoginManager. The other Provides MessageFetcher. In MessageFetcher, whatever method you have, just add the Context parameter to it and call it by providing a fresh context.
Perhaps your code could look something like this:
MessageFetcher messageFetcher = Provider.of<ValueNotifier<MessageFetcher>>(context).value;
String message = await messageFetcher.fetchMessage(context);
And in MessageFetcher you can have:
class MessageFetcher {
Future<String> fetchMessage(BuildContext context) {
LoginManager loginManager = Provider.of<ValueNotifier<LoginManager>>(context).value;
loginManager.ensureLoggedIn();
///...
}
}
Seems like this would be a lot easier with Riverpod, especially the idea of passing a parameter into a .family builder to use the provider class as a cookie cutter for many different versions.

How to call provider on condition?

On app homepage I set up Model2 which make API call for data. User can then navigate to other page (Navigator.push). But I want make API call from Model2 when user press back (_onBackPress()) so can refresh data on homepage.
Issue is Model2 is not initialise for all user. But if I call final model2 = Provider.of<Model2>(context, listen: false); for user where Model2 is not initialise, this will give error.
How I can call Provider only on condition? For example: if(user == paid)
StatefulWidget in homepage:
#override
Widget build(BuildContext context) {
return ChangeNotifierProxyProvider<Model1, Model2>(
initialBuilder: (_) => Model2(),
builder: (_, model1, model2) => model2
..string = model1.string,
),
child: Consumer<Model2>(
builder: (context, model2, _) =>
...
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondRoute(context: context)),
In Page2:
Future<void> _onBackPress(context) async {
// if(user == paid)
final model2 = Provider.of<Model2>(context, listen: false);
return showDialog<void>(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return
// if(user == paid)
Provider.value(value: model2, child:
AlertDialog(
title: Text('Back'),
content: SingleChildScrollView(
child: ListBody(
children: <Widget>[
Text('Go back'),
],
),
),
actions: <Widget>[
FlatButton(
child: Text('OK'),
onPressed: () async {
// if(user == paid)
await model2.getData();
Navigator.of(context).pop();
},
),
],
),
);
},
);
}
Alternative method (maybe more easy): How to call provider on previous page (homepage) on Navigator.of(context).pop();?
TLDR: What is best solution for call API so can refresh data when user go back to previous page (but only for some user)?
You can wrap your second page interface builder in a WillPopScope widget, and then, pass whatever method you want to call to the onWillPop callback of the WillPopScope widget. This way, you can make your API call when user presses the back button. Find more about the WillPopScope widget on this WillPopScope Flutter dev documentation article.
tldr; Establish and check your single point of truth before the call to the Provider
that may result in a null value or evaluate as a nullable reference.
Perhaps you can change the architecture a bit to establish a single (nullable or bool) reference indicating whether the user has paid. Then use Darts nullability checks (or just a bool) to implement the behavior you want. This differs from your current proposal in that there would be no need to call on the Provider to instantiate the model. Just add a single point of truth to your User object that is initialized to null or false, and then change that logic only when the User has actually paid.
Toggling widgets/behavior in this way could be a solution.
Alternatives considered:
Packaging critical data points into a separate library so that the values can be imported where needed.
Other state management methods for key/value use.
If you want to simply hide/show parts of a page consider using the OffStage class or the Visibility class
Ref
Dart null-checking samples

How to navigate without context in flutter app?

I have an app that recieves push notification using OneSignal. I have made a notification opened handler that should open specific screen on click of the notification. How can i navigate to a screen without context. or how can I open specific screen on app startup. My code:
OneSignal.shared.setNotificationOpenedHandler((notification) {
var notify = notification.notification.payload.additionalData;
if (notify["type"] == "message") {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => DM(user: notify['id']),
),
);
}
if (notify["type"] == "user") {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => Profileo(notify["id"]),
),
);
}
if (notify["type"] == "post") {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ViewPost(notify["id"]),
),
);
}
});
I am able to achieve this when the app is opened for the first time but It only opens the homepage If i close the app and even if I re-open it. I guess that is because the context is changed.
Please Help!!
Look at this here:
https://github.com/brianegan/flutter_redux/issues/5#issuecomment-361215074
You can set a global key for your navigation:
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
Pass it to MaterialApp:
new MaterialApp(
title: 'MyApp',
onGenerateRoute: generateRoute,
navigatorKey: navigatorKey,
);
Push routes:
navigatorKey.currentState.pushNamed('/someRoute');
You can use this wonderful plugin:
https://pub.dev/packages/get
Description from the package: A consistent navigation library that lets you navigate between screens, open dialogs, and display snackbars from anywhere in your code without context.
Get.to(NextScreen()); // look at this simplicity :)
Get.back(); // pop()
Get.off(NextScreen()); // clears the previous routes and opens a new screen.
This solution is general if you want to navigate or to show dialog without context using globalKey especially with Bloc or when your logic is separated from your UI part.
Firstly install this package:
Not: I'm using null safety version
get_it: ^7.2.0
Then create a separate file for your service locator:
service_location.dart
import 'package:get_it/get_it.dart';
GetIt locator = GetIt.instance;
class NavigationService {
final GlobalKey<NavigatorState> navigatorKey =
new GlobalKey<NavigatorState>();
Future<dynamic> navigateTo(String routeName) {
return navigatorKey.currentState!.pushNamed(routeName);
}
void setupLocator() {
locator.registerLazySingleton(() => NavigationService());
}
void showMyDialog() {
showDialog(
context: navigatorKey.currentContext!,
builder: (context) => Center(
child: Material(
color: Colors.transparent,
child: Text('Hello'),
),
));
}
}
on main.dart:
void main() {
WidgetsFlutterBinding.ensureInitialized();
NavigationService().setupLocator();
runApp(MyApp());
}
// add navigatorKey for MaterialApp
MaterialApp(
navigatorKey: locator<NavigationService>().navigatorKey,
),
at your business logic file bloc.dart
define this inside the bloc class or at whatever class you want to use navigation inside
Then start to navigate inside any function inside.
class Cubit extends Cubit<CubitState> {
final NavigationService _navigationService = locator<NavigationService>();
void sampleFunction(){
_navigationService.navigateTo('/home_screen'); // to navigate
_navigationService.showMyDialog(); // to show dialog
}
}
Not: I'm using generateRoute for routing.
Quickest fix is above using global navigatorKey (like #tsdevelopment answered).
To fix undefined navigatorKey, it must be imported from where it is instantiated (for this example in main.dart).
Your main.dart
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
void main() {
runApp(CupertinoApp(
title: 'Navigate without context',
initialRoute: '/',
navigatorKey: navigatorKey, // important
onGenerateRoute: ...
));
}
For example you are in your lib/utils/api.dart
import 'package:your_package_name/main.dart'; // important
abstract class API {
static Future<dynamic> get() async {
// call some api
...
// then you want to navigate to specific screen like login
navigatorKey.currentState?.pushNamed('/login'); // navigate to login, with null-aware check
}
}
Also have a gist example if you prefer in a service approach.
Check this: https://gist.github.com/josephdicdican/81e59fad70530eac251ad6c28e2dcd4b
I know this is an old post, but there is a package that handles navigation without the build context (Using a navigator key) called flutter_navigator: https://pub.dev/packages/flutter_navigator
It allows you to navigate something like this:
_flutterNavigation.push(//YourRoute);
Everything seems to be mapped 1:1 with Flutter's Navigator API, so there is no worries there!
You can use this no_context_navigation package
as the name suggests, we can navigate without context
navService.pushNamed('/detail_screen', args: 'From Home Screen');