New to Flutter.
Here is the content of my main page:
void main() => runApp(MaterialApp(
initialRoute: '/location',
routes: {
'/': (context)=> Loading(),
'/location': (context)=> Location(),
'/home': (context)=> Home(),
},
));
Consider that in my '/': (context)=> Loading(), there is a function that is executed in initState at the end of which it redirects to '/home', but as you can see in my main.dart I set the initial route to '/location' so the emulators is not supposed to open the loading file in order to be redirected to the home file, however it keeps happening.
When I clean & run my emulator, it opens on the location file and then somehow the function in the loading file gets executed which takes me back to the home file.
I do not know if I am making sense, please let me know if you have a hard time understanding.
Optional, here is my loading.dart function:
class _LoadingState extends State<Loading> {
String time;
void fetchIt() async {
final WorldTimeClass defaultinstance = WorldTimeClass(flag: 'Algiers', url: 'Africa/Algiers', location: 'Algiers');
DateTime getTimeFormat = await defaultinstance.getTimeFormat();
await defaultinstance.getTimeHumanFormat(getTimeFormat);
// here the navigator to home.dart
await Navigator.pushReplacementNamed(context, '/home', arguments: {
'location': defaultinstance.location,
'time': defaultinstance.time,
'isDayTime': defaultinstance.isDayTime
});
}
#override
void initState() {
time = 'loading...';
super.initState();
fetchIt(); // here the function executed
}
...
Kindly, explain to me why is this happening?
I solved this by changing -in the routes: argument- :
'/home' to 'home',
'/location' to 'location', and
kept '/' as is.
I do not know why this solved the problem but, it works now.
the route represented by '/' must be your initial route that is the screen that you want to show when your app is started . Otherwise if you use '/' for any other screen , that screen will run behind the scenes . Use the code below as an example . I want to show Page 2 as the home screen that is why I have set initial route to '/' and
'/':(BuildContext ctx)=>Page2()
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
// import 'package:path_provider/path_provider.dart';
void main() => runApp(Example());
class Example extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
title: "Example",
initialRoute: '/',
routes: {
'/':(BuildContext ctx)=>Page2(),
'/page1':(BuildContext ctx)=>Page1(),
'/page3':(BuildContext ctx)=>Page3()
},
);
}
}
class Page1 extends StatefulWidget{
Page1State createState()=> Page1State();
}
class Page1State extends State<Page1> {
#override
void initState() {
super.initState();
timerFunction();
}
Timer timerFunction(){
return new Timer(Duration(seconds: 3),handleTimeout);
}
handleTimeout(){
Navigator.pushNamed(context, '/page3');
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text("Page1"),
),
);
}
}
class Page2 extends StatelessWidget{
#override
Widget build(BuildContext context) {
return Scaffold(
body:Container(
alignment: Alignment.center,
child :Column(
children:<Widget>[
Padding(padding: EdgeInsets.only(top: 100)),
Text("Page2"),
RaisedButton(
child: Text("go to page1"),
onPressed: ()=>Navigator.pushNamed(context, '/page1'),
)
]
),
),
);
}
}
class Page3 extends StatelessWidget{
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text("Page3"),
),
);
}
}
First, try to restart the app by re-running flutter run
Related
I am trying to write widget tests for my Flutter application.
I have a page (MainMenuScreen) as follows (I've left out the imports):
class MainMenuScreen extends StatefulWidget {
#override
_MainMenuScreenState createState() => _MainMenuScreenState();
}
class _MainMenuScreenState extends State<MainMenuScreen> {
#override
void initState() {
super.initState();
}
late MainMenuViewModel vm;
#override
Widget build(BuildContext context) {
vm = Provider.of<MainMenuViewModel>(context);
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: Center(
child: Column(
children: <Widget>[
generateHeightSpacer(Dimensions().heightSpace),
generateLogo(Dimensions().fractionalWidth),
generateHeightSpacer(Dimensions().fractionalWidth),
generateMenuButton(
context,
Dimensions().fractionalWidth,
Dimensions().fractionalHeight,
AppLocalizations.of(context).menuPlay,
LevelSelectionScreen()),
generateHeightSpacer(Dimensions().heightSpace),
generateMenuButton(
context,
Dimensions().fractionalWidth,
Dimensions().fractionalHeight,
AppLocalizations.of(context).menuAbout,
AboutScreen()),
],
),
)),
);
}
}
My main.dart file looks like this:
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => MainMenuViewModel()),
ChangeNotifierProvider(create: (context) => LevelSelectionViewModel()),
ChangeNotifierProvider(create: (context) => LevelViewModel()),
],
child: MaterialApp(
onGenerateTitle: (context) {
return AppLocalizations.of(context).appTitle;
},
localizationsDelegates: [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: [
Locale('en', ''), // English, no country code
Locale('es', ''), // Spanish, no country code
],
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: ChangeNotifierProvider(
create: (context) => MainMenuViewModel(),
child: MainMenuScreen(),
// ),
),
),
);
}
}
I have looked into how to write a test that can identify when the About screen is displayed, i.e. after pressing the last Menu Button.
So, for now I have something as follows:
testWidgets("Flutter Main Menu Test Menu Button About",
(WidgetTester tester) async {
await tester.pumpWidget(MyApp());
var button = find.text("About");
expect(button, findsOneWidget);
});
This allows me to identify that the About button is displayed. (Note: I will be changing my code and tests to do this based on a 'key' rather than text.)
From what I've read the way to test is to use Mockito and a NavigationObserver but that seems to require the ability to inject into the screen. I have also been looking at other solutions which seem to suggest wrapping the provider, i.e.:
I've been following this page(https://iiro.dev/writing-widget-tests-for-navigation-events/) to try to test but I can't quite figure out how to pass the viewmodel in.
This is my test at the moment:
class MockMainMenuViewModel extends Mock implements MainMenuViewModel {}
class MockNavigatorObserver extends Mock implements NavigatorObserver {}
void main() {
group('MainMenuScreen navigation tests', () {
late NavigatorObserver mockObserver;
MockMainMenuViewModel? mockViewModel;
setUp(() {
mockObserver = MockNavigatorObserver();
mockViewModel = MockMainMenuViewModel();
});
Future<void> _buildMainPage(WidgetTester tester) {
return tester.pumpWidget(MaterialApp(
home: MainMenuScreen(mockViewModel),
navigatorObservers: <NavigatorObserver>[observer],
));
}
});
}
Any help is appreciated please.
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 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();
}
}
I am trying to reopen last opened screen after boot, Is there any simple way to do so ? sample codes are welcome !
So far I tried a code(which I got somewhere) with SharedPreferences, but it's not working.
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
String lastRouteKey = 'last_route';
void main() async {
SharedPreferences preferences = await SharedPreferences.getInstance();
String lastRoute = preferences.getString(lastRouteKey);
runApp(MyApp(lastRoute));
}
class MyApp extends StatelessWidget {
final String lastRoute;
MyApp(this.lastRoute);
#override
Widget build(BuildContext context) {
bool hasLastRoute = getWidgetByRouteName(lastRoute) != null;
return MaterialApp(
home: Foo(),
initialRoute: hasLastRoute ? lastRoute : '/',
onGenerateRoute: (RouteSettings route) {
persistLastRoute(route.name);
return MaterialPageRoute(
builder: (context) => getWidgetByRouteName(route.name),
);
},
);
}
Widget getWidgetByRouteName(String routeName) {
switch (routeName) {
case '/':
return MainWidget();
case '/':
return SecondRoute();
// Put all your routes here.
default:
return null;
}
}
void persistLastRoute(String routeName) async {
SharedPreferences preferences = await SharedPreferences.getInstance();
preferences.setString(lastRouteKey, routeName);
}
}
class Foo extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Foo'),
),
body: Column(
children: <Widget>[
RaisedButton(
child: Text('Open route second'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondRoute()),
);
},
),
RaisedButton(
child: Text('Open route main'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => MainWidget()),
);
},
),
],
),
);
}
}
class SecondRoute extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Second Route"),
),
body: Center(
child: RaisedButton(
onPressed: () {
Navigator.pop(context);
},
child: Text('Go back!'),
),
),
);
}
}
class MainWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("MainWidget"),
),
body: Center(
child: RaisedButton(
onPressed: () {
Navigator.pop(context);
},
child: Text('Go back!'),
),
),
);
}
}
should I use SQLite or JSON instead of SharedPreferences to make the code simple? thanks.
Demo
A. Navigation
when we are navigating through different screens within app, actually, the route stacks are changing.
So, firstly, we need to figure out how to listen to this changes e.g Push screen, Pop back to users screen.
1. Attaching saving method in each action button
we can actually put this on every navigation-related button.
a. on drawer items
ListTile(
title: Text("Beta"),
onTap: () {
saveLastScreen(); // saving to SharedPref here
Navigator.of(context).pushNamed('/beta'); // then push
},
),
b. on Titlebar back buttons
appBar: AppBar(
title: Text("Screen"),
leading: IconButton(
icon: Icon(Icons.menu),
onPressed: () {
saveLastScreen(); // saving to SharedPref here
Navigator.pop(context); // then pop
},
),
),
c. and also capturing event of Phone Back button on Android devices
#override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: (){ // will triggered as we click back button
saveLastScreen(); // saving to SharedPref here
return Future.value(true);
},
child: Scaffold(
appBar: AppBar(
title: Text("Base Screen"),
),
Therefore, we will have more code and it will be harder to manage.
2. Listening on Route Changes using Route observer
Nonetheless, Flutter provides on MaterialApp, that we can have some "middleware" to capture those changes on route stacks.
We may have this on our MyApp widget :
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Save Last Route',
navigatorObservers: <NavigatorObserver>[
MyRouteObserver(), // this will listen all changes
],
routes: {
'/': (context) {
return BaseScreen();
},
'/alpha': (context) {
return ScreenAlpha();
},
We can define MyRouteObserver class as below :
class MyRouteObserver extends RouteObserver {
void saveLastRoute(Route lastRoute) async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setString('last_route', lastRoute.settings.name);
}
#override
void didPop(Route route, Route previousRoute) {
saveLastRoute(previousRoute); // note : take route name in stacks below
super.didPop(route, previousRoute);
}
#override
void didPush(Route route, Route previousRoute) {
saveLastRoute(route); // note : take new route name that just pushed
super.didPush(route, previousRoute);
}
#override
void didRemove(Route route, Route previousRoute) {
saveLastRoute(route);
super.didRemove(route, previousRoute);
}
#override
void didReplace({Route newRoute, Route oldRoute}) {
saveLastRoute(newRoute);
super.didReplace(newRoute: newRoute, oldRoute: oldRoute);
}
}
B. How to Start the App
As users interacting through the screens, the Shared Preferences will always store last route name. To make the app navigate correspondingly, we need to make our BaseScreen statefull and override its initState method as below :
return MaterialApp(
routes: {
'/': (context) {
return BaseScreen(); // define it as Main Route
},
class BaseScreen extends StatefulWidget {
#override
_BaseScreenState createState() => _BaseScreenState();
}
class _BaseScreenState extends State<BaseScreen> {
#override
void initState() {
super.initState();
navigateToLastPage();
}
void navigateToLastPage() async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
String lastRoute = prefs.getString('last_route');
// No need to push to another screen, if the last route was root
if (lastRoute.isNotEmpty && lastRoute != '/') {
Navigator.of(context).pushNamed(lastRoute);
}
}
C. Working Repo
You may look at this repository that overrides RouteObserver as explained in second option above
Saving and Opening Screen Beta and Screen Delta in different starts
D. Shared Preferences / JSON / SQLite
I suggest to use Shared preferences for simplicity. As we only record simple String for route name, we can only write two lines of code to Save and two lines of code to Load.
If we use JSON file, we need to manually set Path for it using path_provider package.
Moreover, if we use SQLite, we need to setup DB (may consist > 8 more lines), and setup table and also inserting table method.
I am creating a loading screen for an app. This loading screen is the first screen to be shown to the user. After 3 seconds the page will navigate to the HomePage. everything is working fine. But when the user taps back button the loading screen will be shown again.
FIRST PAGE CODE
import 'dart:async';
import 'package:flutter/material.dart';
import 'home_page.dart';
void main() {
runApp(MaterialApp(
home: MyApp(),
));
}
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => new _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
void initState() {
super.initState();
Future.delayed(
Duration(
seconds: 3,
), () {
// Navigator.of(context).pop(); // THIS IS NOT WORKING
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => HomePage(),
),
);
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: FlutterLogo(
size: 400,
),
),
);
}
}
HOMEPAGE CODE
import 'package:flutter/material.dart';
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: Text('HomePage'),
),
),
);
}
}
I tried to add Navigator.of(context).pop(); before calling the HomePage but that is not working. This will show a blank black screen.
Any ideas??
You need to use pushReplacement rather than just push method. You can read about it from here: https://docs.flutter.io/flutter/widgets/Navigator/pushReplacement.html
And to solve your problem just do as explain below.
Simply replace your this code:
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => HomePage(),
),
);
with this:
Navigator. pushReplacement(
context,
MaterialPageRoute(
builder: (context) => HomePage(),
),
);
Yes, I found the same problem as you. The problem with replace is that it only works once, but I don't know why it doesn't work as it should. For this after a few attempts, I read the official guide and this method exists: pushAndRemoveUntil (). In fact, push on another widget and at the same time remove all the widgets behind, including the current one. You must only create a one Class to management your root atrough the string. This is the example:
class RouteGenerator {
static const main_home= "/main";
static Route<dynamic> generatorRoute(RouteSettings settings) {
final args = settings.arguments;
switch (settings.name) {
case main_home:
return MaterialPageRoute(builder: (_) => MainHome());
break;
}
}
}
This class must be add to the Main in:
MaterialApp( onGenerateRoute: ->RouteGenerator.generatorRoute)
Now to use this method, just write:
Navigator.of(context).pushNamedAndRemoveUntil(
RouteGenerator.main_home,
(Route<dynamic> route) => false
);