can someone please help me how to make an onboarding screen after a custom splash screen be viewed only once by new users as it is very annoying to be viewed every time?
splashscreen and main codes are added below , a gif which is loaded for 3 seconds and then goes to boarding screen
My main code where it has its routes is attached below.
void main() async{
WidgetsFlutterBinding.ensureInitialized();
BusApp.sharedPreferences = await SharedPreferences.getInstance();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
print(BusApp.sharedPreferences
.getString('users'));
return StreamProvider<User>.value(
value: AuthService().user,
child: MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => CalculateRent()),
ChangeNotifierProvider(create: (_) => CardChanger()),
],
child: MaterialApp(
debugShowCheckedModeBanner: false, // Disables the debug ribbon
home:
SplashScreen(), // Shows splash screen as the first screen
routes: <String, WidgetBuilder>{
'/Intro_Slider': (BuildContext context) => new Boarding(),
'/Navig': (BuildContext context) => new Navig(),
'/Wrapper': (BuildContext context) => new Wrapper(),
'/Home': (BuildContext context) => new Home(),
'/Settings': (BuildContext context) => new Settings(),
}),
),
);
}
}
Splash Screen code :
class SplashScreenState extends State<SplashScreen> {
startTime() async {
var _duration = new Duration(seconds: 3);
return new Timer(_duration, navigationPage);
}
void navigationPage() {
Navigator.of(context).pushReplacementNamed('/Intro_Slider');
}
#override
void initState() {
super.initState();
startTime();
}
#override
Widget build(BuildContext context) {
return new Scaffold(
body: Container(
decoration: BoxDecoration(color: Colors.white),
child: Column(
children: <Widget>[
Expanded(
child: Center(
child: new Image.asset('assets/images/splashscreen/bus.gif'),
),
),
Align(
alignment: FractionalOffset.bottomCenter,
child: Padding(
padding: EdgeInsets.all(45.0),
child: Text(
"MoBis",
style: GoogleFonts.fredokaOne(
fontSize: 20,
),
),
)
)
],
),
),
);
}
}
You need to store a flag somewhere that the user has already seen the onboarding screen. For example, you can store that flag in your shared preferences.
Then when you would navigate after the splash screen you check this flag and depending on its value you go to the onboard or the home screen.
Something like this:
void navigationPage() {
if (BusApp.sharedPreferences.getBool('seenIntro')) {
Navigator.of(context).pushReplacementNamed('/Home');
} else {
BusApp.sharedPreferences.setBool('seenIntro', true);
Navigator.of(context).pushReplacementNamed('/Intro_Slider');
}
}
As you are already using shared pref. You can save a Boolean key 'isNewUser'. Show the onboarding screen if true else show HomeScreen.
And When show onboarding screen also update the isNewUser = false.
You need to save a boolean in your shared_preferences when user views the intro page for the first time. And you've to check for that bool in your splash screen and based on that you've to navigate to corresponding screens.
Splash Screen:
void navigationPage() {
var isExistingUser = BusApp.sharedPreferences.getBool('isExistingUser');
if (isExistingUser == null) {
Navigator.of(context).pushReplacementNamed('/Intro_Slider');
} else {
Navigator.of(context).pushReplacementNamed('/Home');
}
}
Intro Page:
Set the boolean to true, when user finish viewing the intro slider.
BusApp.sharedPreferences.setBool('isExistingUser', true);
Related
Am new to flutter. I am creating a flutter app involving google mobile ads. I want to implement a banner AD on the bottom navigation bar of every screen. Is there a way to implement the banner ad once on one screen (on the bottom nav bar) and persist it throughout all screens than creating instances of a banner Ad on each screen and creating a bottom navigation bar? I Am using getx for routes and here's one of my screens where I implemented the banner ad.
class ProgrammeScreen extends StatefulWidget {
const ProgrammeScreen({Key? key}) : super(key: key);
static const routeName = '/ProgrammeScreen';
#override
State<ProgrammeScreen> createState() => _ProgrammeScreenState();
}
class _ProgrammeScreenState extends State<ProgrammeScreen> {
late BannerAd _bottomBannerAd1;
bool _isBottomBannerLoaded = false;
void createBottomBannerAd() {
_bottomBannerAd1 = BannerAd(
listener: BannerAdListener(onAdLoaded: (_) {
setState(() {
_isBottomBannerLoaded = true;
});
}, onAdFailedToLoad: (ad, error) {
ad.dispose(); }),
adUnitId: bannerKey,
size: AdSize.banner,
request: const AdRequest());
_bottomBannerAd1.load();
}
#override
void initState() {
createBottomBannerAd();
super.initState();
}
#override
void dispose() {
_bottomBannerAd1.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Programme'),
backgroundColor: appBarColor(context),
),
bottomNavigationBar: _isBottomBannerLoaded ? SizedBox(
height: _bottomBannerAd1.size.height.toDouble(),
width: _bottomBannerAd1.size.width.toDouble(),
child: AdWidget(ad: _bottomBannerAd1),
) : const SizedBox(),
body: SizedBox(
width: UIParameters.getWidth(context),
height: UIParameters.getHeight(context),
child: Padding(
padding: const EdgeInsets.all(kMobileScreenPadding),
child: Column(
children: [
Expanded(
child: StreamBuilder<QuerySnapshot>(
stream: estudieeFR.snapshots(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return const Text('Something went wrong');
} else if (snapshot.hasData) {
return ListView.separated(
shrinkWrap: true,
itemCount: snapshot.data!.docs.length,
itemBuilder: (BuildContext context, int index) {
final data = snapshot.data!.docs[index];
return ContentCard(
title: data['name'],
icon: Icons.arrow_forward_ios,
onPressed: () => Get.to(
() => YearScreen(
programId: snapshot.data!.docs[index].id),
),
);
},
separatorBuilder: (BuildContext context, int index) {
return const Divider();
},
);
} else {
return Indicators.circularIndicator;
}
}),
)
],
),
),
),
);
}
}
Heres my main.dart file with the root widget(MyApp)
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
static final GlobalKey<NavigatorState> navigatorKey = GlobalKey();
#override
Widget build(BuildContext context) {
return GetMaterialApp(
navigatorKey: navigatorKey,
defaultTransition: Transition.rightToLeftWithFade,
initialRoute: '/',
getPages: AppRoutes.pages(),
debugShowCheckedModeBanner: false,
);
}
}
I don't want to copy the same code in all the stateful widgets but rather implement a single bottom navigation banner Ad that will persist all screens. Is there any way to achieve this?
If you do not want to write the same code in all screens, then you could make modifications in the builder function of MaterialApp to include your banner ad there as:
MaterialApp(
builder: (BuildContext context, Widget? child) {
return Column(
children: [
Expanded(child: child!),
// your banner widget here,
],
);
},
home: HomeScreen(),
);
Create another dart file and add this code
import 'package:google_mobile_ads/google_mobile_ads.dart';
late BannerAd bannerAd;
loadBanner() {
bannerAd = BannerAd(
adUnitId: BannerAd.testAdUnitId,
size: AdSize.fullBanner,
request: const AdRequest(),
listener: BannerAdListener(
// Called when an ad is successfully received.
onAdLoaded: (Ad ad) => print('Ad loaded.'),
// Called when an ad request failed.
onAdFailedToLoad: (Ad ad, LoadAdError error) {
// Dispose the ad here to free resources.
ad.dispose();
print('Ad failed to load: $error');
},
// Called when an ad opens an overlay that covers the screen.
onAdOpened: (Ad ad) => print('Ad opened.'),
// Called when an ad removes an overlay that covers the screen.
onAdClosed: (Ad ad) => print('Ad closed.'),
// Called when an impression occurs on the ad.
onAdImpression: (Ad ad) => print('Ad impression.'),
),
)..load();
}
Call the loadBanner once and this bannerAd is available in all classes since it's defined in the root
so I'm trying to implement a splash screen where for two reasons.
the time given to the splash screen will be used to load all the data
For beautification
I'm using flutter_spinkit
So here's my code:
class _SplashScreenState extends State<SplashScreen> {
void initState() {
super.initState();
navigateToHomeScreen();
}
Future navigateToHomeScreen() async {
return Timer(
const Duration(milliseconds: 4000),
() {
Navigator.pushReplacement(context,
MaterialPageRoute(builder: (BuildContext context) => App())); ---> doesn't go to new screen
},
);
}
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
backgroundColor: Color(0xff75c760),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildAppName(),
SizedBox(height: 20),
_buildSpinner(),
],
),
),
);
}
Widget _buildAppName() {
return Text(
"MasterChef",
style: GoogleFonts.robotoMono(
color: Colors.black,
fontStyle: FontStyle.normal,
fontSize: 30,
fontWeight: FontWeight.bold,
),
);
}
Widget _buildSpinner() {
return SpinKitDoubleBounce(
color: Colors.white,
);
}
}
and here's the App() from app.dart:
class _AppState extends State<App> {
//Contains the simple drawer with switch case for navigation
//Taken directly from the flutter docs
}
The basic idea is to load the splash screen for 4000 milliseconds in which the app will load all the necessary data and then navigate to App() which contains the navigation routes and all. But for some reason I'm getting Unhandled Exception: Navigator operation requested with a context that does not include a Navigator.
I cannot be certain to what the cause is as cannot see your entire app structure but I am guessing the cause is that you do not have a MaterialApp() at the root of your app.
If this doesn't work please update your question to contain more code of how you get the splash screen app to be displayed.
This is because you used return Timer() so remove the return keyword so, try changing your code to:
Future navigateToHomeScreen() async {
Timer(
const Duration(milliseconds: 4000),
() {
Navigator.pushReplacement(context,
MaterialPageRoute(builder: (BuildContext context) => App()));
},
);
}
Alternatively, you can use Future instead of Timer
Future navigateToHomeScreen() async {
Future.delayed(
const Duration(milliseconds: 4000),
() {
Navigator.pushReplacement(context,
MaterialPageRoute(builder: (BuildContext context) => App()));
},
);
}
I have 2 screens homepage and detailpage. I want to rebuild widget inside detailpage based on a certain action but the issue is the data come from homepage through http request and a specific object is passed to the second screen in purpose to display detail on it.
class ListPage extends StatefulWidget {
#override
_ListPageState createState() => _ListPageState();
}
class _ListPageState extends State<ListPage> {
ParcelModel model = ParcelModel();
#override
void initState() {
model.loadData();
super.initState();
}
Widget build(BuildContext context) {
width = MediaQuery.of(context).size.width;
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => ParcelModel()),
],
child: DefaultTabController(
length: 3,
child: ChangeNotifierProvider(
create: (context) => model,
child: Consumer<ParcelModel>(
builder:(context, model, child){
if(model.failed == true){
return Scaffold(
backgroundColor: Colors.white,
body:TabBarView(children:[
mescolis(context.watch<ParcelModel().listParcel),futureWidget_quotation(),
Payezcolisencoursdelivraison(context.watch<ParcelModel>().listParcel),
]),
floatingActionButton:FloatingActionButton(
onPressed: (){
MaterialPageRoute(
builder:(context) => ChangeNotifierProvider.value(
value:model,
child:Formtest(dataPays:context.watch<ParcelModel().listParcel,newsecontext:context,) //trying to pass the model here
)
);
}
)
),
),
);
Now I am trying to pass the model to the second screen inside a button but it does'nt work this is the function from the detail page when I press this it does not rebuild the homepage (screen 1)
new RaisedButton(
onPressed: (){
try{
Provider.of<ParcelModel>(newContext,listen: false).postData(
formData,
dio,
context,
nom_coli_Controller,
description_coli_Controller,
nature_Controller,
nom_coli_Controller
);
}catch(e){
FlutterFlexibleToast.showToast(
message: "error",
toastLength: Toast.LENGTH_LONG,
timeInSeconds:30,
radius: 70,
);
}
},
child: new Text("Press here"),),
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 have asked similar question here and based on the feedback I got, have tried few approaches, but couldn't get it working, as the original question was little old and already closed, I am posting with my new findings.
Ideally, this is what I am trying to achieve: If the Flutter Bottomsheet is open, I would like to keep it open and let the app go to background when the 'back' button is pushed, i.e. when the app is bought back I have Bottomsheet in view as is.
Have a MyApp with a root NavigationKey to start with and it opens (on default route) the RealApp with its own Key, Bottomsheet, Tabs etc. If any Tabs are pushed, clicking the 'Back' button will Pop those views. And if there aren't any more views to Pop, the default behavior of Flutter is to Pop the BottomNavigation which I am trying to override and instead want the app to go to background as is.
I tried different options including Poping the Root key from onWillPop without much Success when there are no more views to Pop.
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: rootGlobalKey,
home: RealApp()
);
}
}
class RealApp extends StatelessWidget {
final navigatorKey = GlobalKey<NavigatorState>();
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
final pagesRouteFactories = {
"/": () => MaterialPageRoute(
builder: (context) => Center(
child: Text(
"HomePage",
style: Theme.of(context).textTheme.body1,
),
),
),
"takeOff": () => MaterialPageRoute(
builder: (context) => Center(
child: Text(
"Take Off",
style: Theme.of(context).textTheme.body1,
),
),
),
"landing": () => MaterialPageRoute(
builder: (context) => Center(
child: Text(
"Landing",
style: Theme.of(context).textTheme.body1,
),
),
),
"settings": () => MaterialPageRoute(
builder: (context) => Center(
child: Text(
"Settings",
style: Theme.of(context).textTheme.body1,
),
),
),
};
final RealBottomSheet bottomSheet = new RealBottomSheet();
#override
Widget build(BuildContext context) => MaterialApp(
home: Scaffold(
key: _scaffoldKey,
body: _buildBody(context),
bottomNavigationBar: _buildBottomNavigationBar(context),
),
);
Widget _buildBody(context) => WillPopScope(
onWillPop: () async {
if(navigatorKey.currentState.canPop()) {
// Navigator.pop(context);
navigatorKey.currentState.pop();
return false;
}else {
// Returning true will remove BottomSheet from view, followed by moving the app to background state
// Need a way where the BottomSheet is kept open while the app can go to background state
// Navigator.of(context, rootNavigator: true).pop();
rootGlobalKey.currentState.pop();
// SystemChannels.platform.invokeMethod('SystemNavigator.pop');
return false;
}
},
child: MaterialApp(
navigatorKey: navigatorKey,
onGenerateRoute: (route) => pagesRouteFactories[route.name]())
);
Widget _buildBottomNavigationBar(context) => BottomNavigationBar(
items: [
_buildBottomNavigationBarItem("Home", Icons.home),
_buildBottomNavigationBarItem("Take Off", Icons.flight_takeoff),
_buildBottomNavigationBarItem("Landing", Icons.flight_land),
_buildBottomNavigationBarItem("Settings", Icons.settings)
],
onTap: (routeIndex) {
if (routeIndex == 0) return routeToView(routeIndex);
if (routeIndex == 1) return routeToView(routeIndex);
if (routeIndex == 2) return routeToView(routeIndex);
if (routeIndex == 3) return _showBottomSheet();
});
_buildBottomNavigationBarItem(name, icon) => BottomNavigationBarItem(
icon: Icon(icon), title: Text(name), backgroundColor: Colors.black45);
void routeToView(routeIndex) {
navigatorKey.currentState.pushNamed(pagesRouteFactories.keys.toList()[routeIndex]);
}
void _showBottomSheet() {
_scaffoldKey.currentState.showBottomSheet<void>((BuildContext context) {
return _buildBottomSheet(context);
});
}
Widget _buildBottomSheet(BuildContext context) {
return bottomSheet;
}
}