I am trying to implement custom logout solution for my application, where no matter where user currently is, once the Logout button is clicked, app will navigate back to Login page.
My idea was, that instead of listening on every component for state changes, I would have one single listener on a master component -> MyApp.
For the sake of simplicity, I have stripped down items to bare minimum. Here is how my Profile class could look like:
class Profile with ChangeNotifier {
bool _isAuthentificated = false;
bool get isAuthentificated => _isAuthentificated;
set isAuthentificated(bool newVal) {
_isAuthentificated = newVal;
notifyListeners();
}
}
Now, under Main, I have registered this provider as following:
void main() => runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(
create: (_) => Profile(),
)
],
child: MyApp(),
),
);
And finally MyApp component:
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return Consumer<Profile>(
builder: (context, profile, _) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
brightness: Brightness.light,
primaryColor: Color.fromARGB(255, 0, 121, 107),
accentColor: Color.fromARGB(255, 255, 87, 34),
),
home: buildBasePage(context, profile),
);
},
);
}
Widget buildBasePage(BuildContext context, Profile currentProfile) {
return !currentProfile.isAuthentificated
? LoginComponent()
: MyHomePage(title: 'Flutter Demo Home Page test');
}
}
My idea was, that as MyApp component is the master, I should be able to create a consumer, which would be notified if current user is authentificated, and would respond accordingly.
What happens is, that when I am in e.g. MyHomePage component and I click Logout() method which looks like following:
void _logout() {
Provider.of<Profile>(context, listen: false).isAuthentificated = false;
}
I would be expecting that upon changing property, the initial MyApp component would react and generate LoginPage; which is not the case. I have tried changing from Consumer to Provider.of<Profile>(context, listen: false) yet with the same result.
What do I need to do in order for this concept to work? Is it even correct to do it this way?
I mean I could surely update my Profile class in a way, that I add the following method:
logout(BuildContext context) {
_isAuthentificated = false;
Navigator.push(
context, MaterialPageRoute(builder: (context) => LoginComponent()));
}
And then simply call Provider.of<Profile>(context, listen: false).logout(), however I thought that Provider package was designed for this...or am I missing something?
Any help in respect to this matter would be more than appreciated.
I don't know why it wasn't working for you. Here is a complete example I built based on your description. It works!
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class Profile with ChangeNotifier {
bool _isAuthentificated = false;
bool get isAuthentificated {
return this._isAuthentificated;
}
set isAuthentificated(bool newVal) {
this._isAuthentificated = newVal;
this.notifyListeners();
}
}
void main() {
return runApp(
MultiProvider(
providers: [
ChangeNotifierProvider<Profile>(
create: (final BuildContext context) {
return Profile();
},
)
],
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
#override
Widget build(final BuildContext context) {
return Consumer<Profile>(
builder: (final BuildContext context, final Profile profile, final Widget child) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: profile.isAuthentificated ? MyHomePage() : MyLoginPage(),
);
},
);
}
}
class MyHomePage extends StatelessWidget {
#override
Widget build(final BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Home [Auth Protected]")),
body: Center(
child: RaisedButton(
child: const Text("Logout"),
onPressed: () {
final Profile profile = Provider.of<Profile>(context, listen: false);
profile.isAuthentificated = false;
},
),
),
);
}
}
class MyLoginPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Login")),
body: Center(
child: RaisedButton(
child: const Text("Login"),
onPressed: () {
final Profile profile = Provider.of<Profile>(context, listen: false);
profile.isAuthentificated = true;
},
),
),
);
}
}
You don't need to pass listen:false, instead simply call
Provider.of<Profile>(context).logout()
So your Profile class would look like
class Profile with ChangeNotifier {
bool isAuthentificated = false;
logout() {
isAuthentificated = false;
notifyListeners();
}
}
Related
I choose to use provider as my state management so I saw I have to use Multi provider.
My struggle is how to architect my code that I can initialize all the data I need when my app first run and give the providers to the multi provider.
Provider example
import 'package:cron/cron.dart';
import 'package:flutter/material.dart';
import 'package:web_app/models/fixture.dart';
import 'package:web_app/services/fixture_service.dart';
class HighlightsProvider extends ChangeNotifier {
final List<Fixture> _highlights = [];
List<Fixture> get() => _highlights;
Future<void> fetchHighlights() async {
try {
List<Fixture> highlightFixtures = [];
final response = await FixtureService().getAppHighlightFixtures();
[...response].asMap().forEach((index, element) {
highlightFixtures.add(new Fixture.fromJson(element));
});
_highlights.clear();
_highlights.addAll(highlightFixtures);
notifyListeners();
} catch (e) {
print('error');
print(e);
}
}
runJob(cron) {
cron.schedule(Schedule.parse('* * * * *'), () async {
fetchHighlights();
print('fetch highlights every one minute');
});
}
}
Let's say this class will get all my providers and initialize theme:
class InitializeApp {
final cron = Cron();
Future run(HighlightsProvider highlightsProvider) async {
return Future.wait([
initiakizeHighlights(highlightsProvider),
]);
}
Future initiakizeHighlights(HighlightsProvider highlightsProvider) async {
highlightsProvider.runJob(cron);
await highlightsProvider.fetchHighlights();
}
}
Then I have to deliver those provider to the multi provider:
void main() async {
final highlightsProvider = HighlightsProvider();
await InitializeApp().run(highlightsProvider);
print('ready');
runApp(MyApp(highlightsProvider: highlightsProvider));
}
class MyApp extends StatelessWidget {
final highlightsProvider;
const MyApp({Key key, this.highlightsProvider}) : super(key: key);
#override
Widget build(BuildContext context) {
print('build');
return MultiProvider(
providers: [
ChangeNotifierProvider<HighlightsProvider>.value(
value: highlightsProvider,
)
],
child: MaterialApp(
title: 'tech',
theme: ThemeData(
primarySwatch: Colors.amber,
brightness: Brightness.light,
),
routes: <String, WidgetBuilder>{
'/': (BuildContext context) {
return MyHomePage(title: 'Flutter Demo Home Page');
}
}),
);
}
}
Normally you just wrap your MaterialApp with the MultiProvider, then you already have access to all Providers you will define.
Widget build(BuildContext context) {
return MultiProvider(
providers: [
StreamProvider<RecipeStreamService>.value(value: RecipeStreamService().controllerOut)
],
child: MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Home Food',
routes: {
'/register': (BuildContext context) => RegisterPage(),
'/login': (BuildContext context) => LoginPage()
},
),
home: HomePage(title: 'Home'),
),
);
}
this is my first question and I hope it's readable.
I've created a sample flutter project where I've illustrated the problem specified in my question.
If the code runs on Android the AndroidAppVersion-Object creates a Material App that contains an AndroidHomeScreen-Object with a red Scaffold and a Button with the title "Android". When the user presses the button the AnotherPageView-Object appears that contains an orange Scaffold.
The AnotherPageView-Object is just a sample page.
If the Platform isn't Android an (IOS-)Cupertino-App will be created by the IOSAppVersion-Object that contains an IOSHomeScreen with a green Scaffold with a Button with the title "IOS". Again the user presses the button to create an AnotherPageView-Object.
So here is the Problem:
When I use
Navigator.pushNamed(context, AnotherPageView.anotherPage);
The code doesn't work and it gives me the error I've added down below.
However, it works if I use
Navigator.push(context,
CupertinoPageRoute(builder: (context) => AnotherPageView()));
but I'd like to know why it doesn't work with the first one.
The code:
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'dart:io';
void main() {
runApp(Platform.isAndroid ? IOSAppVersion() : AndroidAppVersion());
}
class AndroidAppVersion extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
routes: {
AnotherPageView.anotherPage: (context) {
return AnotherPageView();
}
},
home: AndroidHomeScreen(),
);
}
}
class AndroidHomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.red,
body: MaterialButton(
onPressed: () {
Navigator.pushNamed(context, AnotherPageView.anotherPage);
},
child: Center(
child: Text('Android'),
),
),
);
}
}
class IOSAppVersion extends StatelessWidget {
#override
Widget build(BuildContext context) {
return CupertinoApp(
home: IOSHomeScreen(),
);
}
}
class IOSHomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.green,
body: CupertinoButton(
child: Center(child: Text('Hallo IOS')),
onPressed: () {
Navigator.pushNamed(context, AnotherPageView.anotherPage);
},
),
);
}
}
class AnotherPageView extends StatelessWidget {
static String anotherPage = "anotherPage";
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.orange,
);
}
}
Here's the error that gets shown:
The following assertion was thrown while handling a gesture:
Could not find a generator for route RouteSettings("anotherPage", null) in the _WidgetsAppState.
Generators for routes are searched for in the following order:
For the "/" route, the "home" property, if non-null, is used.
Otherwise, the "routes" table is used, if it has an entry for the route.
Otherwise, onGenerateRoute is called. It should return a non-null value for any valid route not handled by "home" and "routes".
Finally if all else fails onUnknownRoute is called.
Unfortunately, onUnknownRoute was not set.
...
This
return CupertinoApp(
home: IOSHomeScreen(),
);
doesn't know any routes, you need to provide your routes to both materialapp and cupertinoapp, just do
return CupertinoApp(
routes: {
AnotherPageView.anotherPage: (context) {
return AnotherPageView();
}
},
home: IOSHomeScreen(),
);
I am trying to create a simple authentication flow using Provider. I have three pages :
LoginPage
OnboardingPage
HomePage
The flow of this app is:
if a user opens the app for the first time, he/she will be redirected to the onboarding then to login to home.
For the second time user, the app first checks the login status and redirected to either log in -> home or straight to home page.
Here is my setup in code :
main.dart
void main() {
runApp(MultiProvider(providers: [
ChangeNotifierProvider<StorageHelper>(create: (_) => StorageHelper()),
ChangeNotifierProvider<AuthProvider>(create: (_) => AuthProvider()),
], child: MyApp()));
}
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return Consumer<AuthProvider>(builder: (final BuildContext context,
final AuthProvider authProvider, final Widget child) {
print(authProvider.isAuthenticated); // this is false whenever I //click the logout from category(or other pushed pages) but the below ternary //operation is not executing
return MaterialApp(
title: 'My Poor App',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primaryColor: Color(0xff29c17e),
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: authProvider.isAuthenticated ? HomeScreen() : LoginScreen(),
onGenerateRoute: Router.onGenerateRoute,
);
});
}
}
LoginScreen.dart
class LoginScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
final authProvider = Provider.of<AuthProvider>(context, listen: false);
return Scaffold(
body: Center(
child: MaterialButton(
onPressed: () async {
await authProvider.emailLogin('user#email.com', 'pass');
},
child: Text('Login'))),
);
}
}
HomeScreen.dart
class HomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
final auth = Provider.of<AuthProvider>(context, listen: false);
return Scaffold(
body: Center(
child: MaterialButton(
elevation: 2,
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CategoryScreen()));
},
child: Text('Reset')),
),
);
}
}
AuthProvider.dart
class AuthProvider extends ChangeNotifier {
bool _isAuthenticated = false;
bool get isAuthenticated => _isAuthenticated;
set isAuthenticated(bool isAuth) {
_isAuthenticated = isAuth;
notifyListeners();
}
Future emailLogin(String email, String password) async {
isAuthenticated = true;
}
Future logout() async {
isAuthenticated = false;
}
}
If i logout from home page using Provider.of<AuthProvider>(context).logout() it works fine. But if I push or pushReplacement a new route and try to logout from the new route (just say I navigated from home to category page and try to logout from there), I am not redirected to LoginPage. If I print the value of isAuthenticated it prints false but the consumer is not listening or at least not reacting to the variable change.
Please don't mark this question as duplicate, I have searched many other similar questions and none of them worked for my case.
Edit:
CategoryScreen.dart
class CategoryScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: RaisedButton(
onPressed: () {
final auth = Provider.of<AuthProvider>(context, listen: false);
auth.logout();
// print(auth.isAuthenticated);
},
child: Text('Category Logout'),
),
),
);
}
}
I guess your problem is that you did not use Consumer for the logout, in your home in the MaterialApp. Just see, that if it works out for you
main.dart
// needs to listen to the changes, to make changes
home: Consumer<AuthProvider>(
builder: (context, authProvider, child){
return authProvider.isAuthenticated ? HomeScreen() : LoginScreen();
}
)
Since, Consumer was not there for your home, even if the value was being changed, it was not able to work on updating the view for you as per the Provider.
I want to add a popup on the app load (or let's say each time the user opens the app) in Flutter. Can anyone help me with that? I understand that I have to use the AlertDialog widget, but I can't find a proper condition to determine the app start trigger.
Any help is highly appreciated.
You can use Shared Preferences
When launching the app, get the value from shared pref (Ex: isFirstLoaded). If isFirstLoaded == true then show the dialog.
When the dialog is dismissed, set isFirstLoaded = false and save to shared preferences.
Below is an example (please note that the example doesn't handle the dismiss event when tapping on Back key).
Add shared_preferences to your pubspec.yaml
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
runApp(MyStatelessApp());
}
class MyStatelessApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Stateless Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: StatelessWidgetDemo(),
);
}
}
class StatelessWidgetDemo extends StatelessWidget {
final keyIsFirstLoaded = 'is_first_loaded';
#override
Widget build(BuildContext context) {
Future.delayed(Duration.zero, () => showDialogIfFirstLoaded(context));
return SafeArea(
child: Scaffold(
appBar: AppBar(
title: Text('Flutter Stateless Demo'),
),
body: Center(
child: Text('Hello'),
)));
}
showDialogIfFirstLoaded(BuildContext context) 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 AlertDialog(
title: new Text("Title"),
content: new Text("This is one time dialog"),
actions: <Widget>[
// usually buttons at the bottom of the dialog
new FlatButton(
child: new Text("Dismiss"),
onPressed: () {
// Close the dialog
Navigator.of(context).pop();
prefs.setBool(keyIsFirstLoaded, false);
},
),
],
);
},
);
}
}
There are many options to make a dialog or something else.
You can use Alert Dialog, Dialog Awesome, and more.
But if you want to make your custom dialog or something like that then you can use Overlay Widget.
Check the link: https://api.flutter.dev/flutter/widgets/OverlayEntry-class.html
also tutorial: https://www.youtube.com/watch?v=A3co3Tskjtg
call your dailog inside initState method (from main or statup page):
#override
void initState() {
WidgetsBinding.instance.addPostFrameCallback(_showOpenDialog);
super.initState();
}
// show Open Dialog method
_showOpenDialog(_) {
showDialog(
context: context,
builder: (context) {
return YourCustomDialog();
});
}
class MyWidget extends StatelessWidget {
bool firstBuild = true;
// show Open Dialog method
_showOpenDialog(context) {
showDialog(
context: context,
builder: (context) {
return Dialog();
});
}
#override
Widget build(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (firstBuild) {
firstBuild = false;
_showOpenDialog(context);
}
});
return Text('Hello, World!');
}
}
I want to make an app which connects to server and sends it different requests, gets responses etc. For that I have a Client class (not a widget), which handles all the network stuff. I use bloc pattern to marshal requests to Client and get back responses. This Client instance should be accessible all over the widget tree, so I use InheritedWidget. My idea is that the first thing user sees after opening an app is splash screen, which should change to some other page when connection to server is established. I subscribe to response stream in SplashScreen's build method and navigate to different page when response is received, but for some reason accessing InheritedWidget triggers rebuild of SplashScreen which leads to accessing InheritedWidget which leads to rebuild and so on and so forth. And obviously stream starts complaining that I already subscribed to it. I do not change InheritedWidget anywhere, and updateShouldNotify is not called, and I set it to return false anyway. Here is minimal reproducible example. It obviously does not perform any real network communication, but it demonstrates what I try to say.
var client = ClientInheritedData.of(context).client leads to rebuild. If I comment it and lines that use client, rebuild is not triggered.
class Client {
StreamController<int> _eventStreamController;
StreamSink<int> get eventSink => _eventStreamController.sink;
Stream<int> _eventStream;
StreamController<String> _responseStreamController;
StreamSink<String> _responseSink;
Stream<String> get responseStream => _responseStreamController.stream;
Client() {
_eventStreamController = StreamController();
_eventStream = _eventStreamController.stream;
_responseStreamController = StreamController<String>();
_responseSink = _responseStreamController.sink;
_eventStream.listen((event) async {
if (event == 1) {
await Future.delayed(Duration(seconds: 2)); // simulate some work
_responseSink.add('Connected!');
}
});
}
}
void main() => runApp(ClientInheritedData(Client(), MyApp()));
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'My App',
theme: ThemeData(
primarySwatch: Colors.green,
),
initialRoute: '/',
onGenerateRoute: RouteGenerator.generate,
);
}
}
class SplashScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
var client = ClientInheritedData.of(context).client;
client.eventSink.add(1);
client.responseStream.listen((response) {
Navigator.of(context).pushNamed('/Sign Up');
});
return Scaffold(
appBar: AppBar(
title: Text('Splash Screen'),
),
body: Center(
child: Text('Greetings!'),
),
);
}
}
class SignUp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Sign Up'),
),
body: Center(
child: Text('Hey man'),
),
);
}
}
class ClientInheritedData extends InheritedWidget {
final Client client;
const ClientInheritedData(this.client, Widget child) : super(child: child);
static ClientInheritedData of(BuildContext context) {
return context.inheritFromWidgetOfExactType(ClientInheritedData) as ClientInheritedData;
}
#override
bool updateShouldNotify(InheritedWidget oldWidget) {
return false;
}
}
class RouteGenerator {
static Route<dynamic> generate(RouteSettings settings) {
switch (settings.name) {
case '/':
return MaterialPageRoute(
builder: (context) => SplashScreen(),
);
case '/Sign Up':
return MaterialPageRoute(
builder: (context) => SignUp(),
);
}
}
}