Flutter One time Intro Screen?

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();
      }
  );
}

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
  }
}

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'),
      ),
    );
  }
}

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.