35

I have an intro screen for my app, but it shows every time I open the app, I need to show that for the 1st time only.

How to do that?

//THIS IS THE SCREEN COMES 1ST WHEN OPENING THE APP (SPLASHSCREEN)

class SplashScreen extends StatefulWidget {
  @override
  _SplashScreenState createState() => _SplashScreenState();
}

class _SplashScreenState extends State<SplashScreen> {
  @override
  void initState() {
    super.initState();

    //After 2seconds of time the Introscreen will e opened by bellow code
    Timer(Duration(seconds: 2), () => MyNavigator.goToIntroscreen(context));
  }

  //The below code has the text to show for the spalshing screen
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: new Center(
        child: Text('SPLASH SCREEN'),
      ),
    );
  }
}

Every time this screen opens the intro screen with 2 seconds delay. but I want for the first time only How to do that with sharedpreference??

Please add the required code.

Arnold Parge
  • 6,684
  • 2
  • 30
  • 34
Rajesh
  • 3,562
  • 7
  • 24
  • 43
  • 1
    Flutter already has a built-in splashscreen, which can be found in the Android drawable folder. You rather not use a splashscreen that adds an addtional delay. – creativecreatorormaybenot Jun 02 '18 at 06:49
  • @creativecreatorormaybenot he's not looking for splashscreen. he's looking to manage **intro screen** – Arnold Parge Jun 02 '18 at 08:55
  • @ArnoldParge I did not realize this. The `'SPLASH SCREEN'` and `//ThIS IS THE SCREEN COMES 1ST WHEN OPENING THE APP (SPLASHSCREEN)` put me off a little bit. – creativecreatorormaybenot Jun 02 '18 at 17:51
  • @creativecreatorormaybenot i'm sorry for that,Let me give u more clear idea,....I have 3 screens,(1) SPLASH SCREEN (2)INTRO SCREEN (3) HOME SCREEN...Where the splash screen is nothing but contains simple text ie "Welcome".,,,the intro screen contains steps to use the app for the 1st time....The Home screen is default home of the app which must be loaded for 2nd 3rd openings if the app.In this case consider the "splash_screen" as Welcome screen which comes for 2seconds only.then it will decide to load Intro or Home screen. – Rajesh Jun 03 '18 at 02:39

6 Answers6

81

If you wish to show the intro screen only for the first time, you will need to save locally that this user has already seen intro.

For such thing you may use Shared Preference. There is a flutter package for Shared Preference which you can use

EDITED:

Please refer to the below complete tested code to understand how to use it:

import 'dart:async';

import 'package:after_layout/after_layout.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      color: Colors.blue,
      home: new Splash(),
    );
  }
}

class Splash extends StatefulWidget {
  @override
  SplashState createState() => new SplashState();
}

class SplashState extends State<Splash> with AfterLayoutMixin<Splash> {
  Future checkFirstSeen() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    bool _seen = (prefs.getBool('seen') ?? false);

    if (_seen) {
      Navigator.of(context).pushReplacement(
          new MaterialPageRoute(builder: (context) => new Home()));
    } else {
      await prefs.setBool('seen', true);
      Navigator.of(context).pushReplacement(
          new MaterialPageRoute(builder: (context) => new IntroScreen()));
    }
  }

  @override
  void afterFirstLayout(BuildContext context) => checkFirstSeen();

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      body: new Center(
        child: new Text('Loading...'),
      ),
    );
  }
}

class Home extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('Hello'),
      ),
      body: new Center(
        child: new Text('This is the second page'),
      ),
    );
  }
}

class IntroScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('IntroScreen'),
      ),
      body: new Center(
        child: new Text('This is the IntroScreen'),
      ),
    );
  }
}

Thanks to Ben B for noticing the incorrect use of delay in initState. I had used a delay because sometimes the context is not ready immediately inside initState.

So now I have replaced that with afterFirstLayout which is ready with the context. You will need to install the package after_layout.

Arnold Parge
  • 6,684
  • 2
  • 30
  • 34
  • yes Arnold,i used that Example code and MODIFIED It to increase count everytimethe app opens.........it stores the app opening counts,.....so that if(count>=0) it sould open home screen,If(count==0) it should open the intro screen,The main problem is i don't know where to put the if conditions,thats why i asked a sample code,...,i put the if(count>=0){Navitage.push...//code...(homescreen)}lines inside void initstate,,it shows up error ,I don't know where to put this code,If you can type a sample code or modify the above code,please give us an idea!! – Rajesh Jun 02 '18 at 13:10
  • ,the problem is Over,Thanks a lot bro!! Additionaly i've imported external page ./screen2.dart then called Home2() from that page and it worked fine thank you so much!! – Rajesh Jun 02 '18 at 15:30
  • I'm new to Dart/ Flutter, but something about this doesn't feel right. The initState is setting the **entire** application. The build method does nothing but the loading screen, which stays for an arbitrary 200ms (also a concern). – Ben Butterworth Jun 08 '20 at 19:04
  • I agree, @BenButterworth. I use a similar approach (with initState), but still I am not satisfied with it. Any other suggestions? – Valentin Seehausen Sep 02 '20 at 08:44
  • for me it get stuck on the text Loading – Lidor Eliyahu Shelef Oct 24 '22 at 18:07
7

I was able to do without using after_layout package and Mixins and instead I have used FutureBuilder.

class SplashState extends State<Splash> {
  Future checkFirstSeen() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    bool _seen = (prefs.getBool('seen') ?? false);

    if (_seen) {
      return HomeScreen.id;
    } else {
        // Set the flag to true at the end of onboarding screen if everything is successfull and so I am commenting it out
      // await prefs.setBool('seen', true);
      return IntroScreen.id;
    }
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
        future: checkFirstSeen(),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return Center(
              child: CircularProgressIndicator(),
            );
          } else {
            return MaterialApp(
              initialRoute: snapshot.data,
              routes: {
                IntroScreen.id: (context) => IntroScreen(),
                HomeScreen.id: (context) => HomeScreen(),
              },
            );
          }
        });
  }
}

class HomeScreen extends StatelessWidget {
static String id = 'HomeScreen';
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('Hello'),
      ),
      body: new Center(
        child: new Text('This is the second page'),
      ),
    );
  }
}

class IntroScreen extends StatelessWidget {
static String id = 'IntroScreen';
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('IntroScreen'),
      ),
      body: new Center(
        child: new Text('This is the IntroScreen'),
      ),
    );
  }
}
Raja C
  • 159
  • 2
  • 8
4

I always try to use minimum count of packages, because in future it can conflict with ios or android. So my simple solution without any package:

class SplashScreen extends StatefulWidget {
 @override
  _SplashScreenState createState() => _SplashScreenState();
}

class _SplashScreenState extends State<SplashScreen> {
  final splashDelay = 2;

  @override
  void initState() {
    super.initState();

    _loadWidget();
  }

  _loadWidget() async {
    var _duration = Duration(seconds: splashDelay);
    return Timer(_duration, checkFirstSeen);
  }

  Future checkFirstSeen() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    bool _introSeen = (prefs.getBool('intro_seen') ?? false);
    
    Navigator.pop(context);
    if (_introSeen) {
      Navigator.pushNamed(context, Routing.HomeViewRoute);
    } else {
      await prefs.setBool('intro_seen', true);
      Navigator.pushNamed(context, Routing.IntroViewRoute);
    }
  }

  @override
  Widget build(BuildContext context) {
  //your splash screen code
  }
}
Murat Kurbanov
  • 589
  • 7
  • 11
2

Use shared_preferences:

Full code:

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  var prefs = await SharedPreferences.getInstance();
  var boolKey = 'isFirstTime';
  var isFirstTime = prefs.getBool(boolKey) ?? true;

  runApp(MaterialApp(home: isFirstTime ? IntroScreen(prefs, boolKey) : RegularScreen()));
}

class IntroScreen extends StatelessWidget {
  final SharedPreferences prefs;
  final String boolKey;
  IntroScreen(this.prefs, this.boolKey);

  Widget build(BuildContext context) {
    prefs.setBool(boolKey, false); // You might want to save this on a callback.
    return Scaffold();
  }
}

class RegularScreen extends StatelessWidget {
  Widget build(BuildContext context) => Scaffold();
}
CopsOnRoad
  • 237,138
  • 77
  • 654
  • 440
1

I just had to do exactly the same thing, here's how I did it: First, in my main method, I open the normal main page and the tutorial:

MaterialApp(
        title: 'myApp',
        onGenerateInitialRoutes: (_) => [MaterialPageRoute(builder: mainPageRoute), MaterialPageRoute(builder: tutorialSliderRoute)],
)

...and then I use a FutureBuilder to build the tutorial only if necessary:

var tutorialSliderRoute = (context) => FutureBuilder(
      future: Provider.of<UserConfiguration>(context, listen: false).loadShowTutorial()  // does a lookup using Shared Preferences
          .timeout(Duration(seconds: 3), onTimeout: () => false),
      initialData: null,
      builder: (context, snapshot){
        if (snapshot.data == null){
          return CircularProgressIndicator();   // This is displayed for up to 3 seconds, in case data loading doesn't return for some reason...
        } else if (snapshot.data == true){
          return TutorialSlider();   // The Tutorial, implemented using IntroSlider()
        } else {
          // In case the tutorial shouldn't be shown, just return an empty Container and immediately pop it again so that the app's main page becomes visible.
          SchedulerBinding.instance.addPostFrameCallback((_){Navigator.of(context).pop();});
          return Container(width: 0, height: 0);
        }
      },
    );

Also, I think the tutorial should be shown again in case the user does not finish it, so I set only set the variable showTutorial to false once the user has completed (or skipped) the tutorial:

class TutorialSlider extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => TutorialSliderState();
}

class TutorialSliderState extends State<TutorialSlider> {
  ...
  @override
  Widget build(BuildContext context) => IntroSlider(
      ...
      onDonePress: (){
        Provider.of<UserConfiguration>(context, listen: false).setShowTutorial(false);
        Navigator.of(context).pop();
      }
  );
}
rgisi
  • 858
  • 6
  • 21
  • +1 for not mindlessly setting `prefs.setBool('seen', true);` when the app loads, and caring about the users actions. I didn't think of this – Ben Butterworth Jun 08 '20 at 21:18
0

I took a different approach. I agree with the other answers that you should save your isFirstRun status via SharedPreferences. The tricky part then is how to show the correct widget in such a way that when you hit back you close out of the app correctly, etc. I first tried doing this by launching a my SplashWidget while building my HomePageWidget, but this turned out to lead to some weird Navigator errors.

Instead, I wound up calling runApp() multiple times with my different widget as appropriate. When I need to close the SplashWidget, rather than pop it, I just call runApp() again, this time with my HomePageWidget as the child property. It is safe to call runApp() multiple times according to this issue, indeed even for splash screens.

So it looks something like this (simplified obviously):

Future<void> main() async {
  bool needsFirstRun = await retrieveNeedsFirstRunFromPrefs();

  if (needsFirstRun) {
    // This is will probably be an async method but no need to
    // delay the first widget. 
    saveFirstRunSeen();
    runApp(child: SplashScreenWidget(isFirstRun: true));
  } else {
    runApp(child: HomePageWidget());
  }
}

I have an isFirstRun property on SplashScreenWidget because I can launch it in two ways--once as a true splash screen, and once from settings so that users can see it again if they want. I then inspect that in SplashScreenWidget to determine how I should return to the app.

class SplashScreenWidget extends StatefulWidget {
  final bool isFirstRun;

  // <snip> the constructor and getState()
}

class _SplashScreenWidgetState extends State<SplashScreenWidget> {
  // This is invoked either by a 'skip' button or by completing the
  // splash screen experience. If they just hit back, they'll be
  // kicked out of the app (which seems like the correct behavior
  // to me), but if you wanted to prevent that you could build a
  // WillPopScope widget that instead launches the home screen if
  // you want to make sure they always see it.
  void dismissSplashScreen(BuildContext ctx) {
    if (widget.isFirstRun) {
      // Then we can't just Navigator.pop, because that will leave
      // the user with nothing to go back to. Instead, we will
      // call runApp() again, setting the base app widget to be
      // our home screen.
      runApp(child: HomePageWidget());
    } else {
      // It was launched via a MaterialRoute elsewhere in the
      // app. We want the dismissal to just return them to where
      // they were before.
      Navigator.of(ctx).pop();
    }
  }
}
user1978019
  • 3,008
  • 1
  • 29
  • 38