To get started, go ahead and clone my Github repo: https://github.com/cybdom/medical_appointment

If you find this tutorial useful please make a donation at: https://www.buymeacoffee.com/bi3cp0Zk5.

Onboarding Screen:

Have you ever wondered how you can show an onboarding screen for the first time your user opens your flutter app? Let’s see how I’ve done it.

Bear in mind that this is not the only way to do it, but I think it’s pretty straight forward and works fine.

Everything happens in the Main.dart file, as the main function runs MyApp which is a StatefulWidget the initState triggers the checkIsFirstSeen future function.

class _MyAppState extends State { var _checkIsFirstSeen; @override void initState() { _checkIsFirstSeen = checkIsFirstSeen(); super.initState(); }

We use the _checkIsFirstSeen variable in order not to have to await for the function to finish.

Now we can use the results returned by this Future using a FutureBuilder.

Inside the Build function that returns MaterialApp we give the home a FutureBuilder as follows:

home: FutureBuilder( future: _checkIsFirstSeen, builder: (context, snapshot) { if (snapshot.hasData) { if (snapshot.data) return HomeScreen(); else return OnBoardingScreen(); } else { return Scaffold( body: Center( child: CircularProgressIndicator(), ), ); } }, ),

While the Future doesn’t have data yet it shows a centered CircularProgressIndicator inside a Scaffold. Once the data is received we check if the Future returned true (oddly enough the function returns false if the user has opened the app for the first time, sorry about that.) and show the HomeScreen otherwise the OnBoardingScreen is shown.

The CheckIsFirstSeen Future Function is pretty simple as you can see:

Future checkIsFirstSeen() async { SharedPreferences prefs = await SharedPreferences.getInstance(); if (prefs.containsKey("seen")) { return true; } else { prefs.setBool("seen", true); return false; } }

First we get an instance of SharedPreferences, then we check if it contains the “seen” key, if so then we return true, otherwise we create the “seen” key and give it a bool value of true then return false.

For the design part of this onboarding screen it’s pretty simple.

We start by declaring a PageController final which we will use later.

final _pageController = PageController();

Then it’s a matter of Aligning simple widgets inside a Column.

The “Skip” FlatButton is right aligned and once clicked it takes us straight to the Home Screen.

Align( alignment: Alignment.centerRight, child: FlatButton( child: Text("Skip"), onPressed: () => Navigator.pushReplacementNamed(context, 'home'), ), ),

At the bottom we have a RaisedButton that is used to go through the onboarding instructions and once we the user sees the last instruction the next click would lead to the Home Screen.

SizedBox( width: double.infinity, child: RaisedButton( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(25.0), ), color: MyColors.blue, child: Text( "Next", style: Theme.of(context).textTheme.button, ), onPressed: () { if (onBoardingInstructions.length -1 == _pageController.page) { Navigator.pushReplacementNamed(context, 'home'); } else _pageController.nextPage( duration: Duration(milliseconds: 250), curve: Curves.easeIn); }, ), )

In between we have an Expanded PageView.Builder:

Expanded( child: PageView.builder( controller: _pageController, itemCount: onBoardingInstructions.length, itemBuilder: (context, i) => Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Flexible( child: Image.asset("${onBoardingInstructions[i].image}"), ), Row( mainAxisAlignment: MainAxisAlignment.center, children: List.generate( onBoardingInstructions.length, (f) => Container( margin: const EdgeInsets.symmetric(horizontal: 5.0), decoration: BoxDecoration( shape: f == i ? BoxShape.rectangle : BoxShape.circle, color: f == i ? Colors.blueAccent : Colors.grey, borderRadius: f == i ? BorderRadius.circular(5.0) : null, ), width: f == i ? 15 : 5, height: 5, ), ), ), SizedBox(height: 11.0), Text( "${onBoardingInstructions[i].title}", style: Theme.of(context).textTheme.title, textAlign: TextAlign.center, ), SizedBox(height: 5.0), Text( "${onBoardingInstructions[i].subtitle}", style: Theme.of(context) .textTheme .subtitle .copyWith(color: Colors.grey), textAlign: TextAlign.center, ) ], ), ), ),

Home Screen:

Contrary to the previous screen, the home screen lays it’s children inside a ListView which allows avoiding content overflowing in smaller sized devices.

As you can see the content on top has to be padded, while the bottom grey container shouldn’t. To achieve that I wrapped the top content in it’s own Column and wrapped that in a Padding widget.

Details Screen:

The Scaffold of this Screen is wrapped in a SafeArea widget and because we are going to use MediaQueries and a DraggableScrollableSheet.

First, the LayoutBuilder build function returns a Stack.

The first part of it is where the doctor shows a banner picture and at the top left corner we have a custom BACK button.

Positioned( left: 0, right: 0, top: 0, height: MediaQuery.of(context).size.height / 3, child: Container( alignment: Alignment.topLeft, padding: const EdgeInsets.all(15.0), decoration: BoxDecoration( image: DecorationImage( image: NetworkImage(doctorInfo[widget.id].image), fit: BoxFit.cover, ), ), child: Container( decoration: BoxDecoration( color: Colors.grey.withOpacity(.5), borderRadius: BorderRadius.circular(9.0), ), child: IconButton( icon: Icon( Icons.arrow_back, color: Colors.white, ), onPressed: () => Navigator.pop(context), ), ), ), ),

The second Positioned widget is the DraggableScrollableSheet itself.

We set it’s initialChildSize to be equal to the minChildSize in order to perfectly fit the banner image. It’s builder returns a neatly decorated Container that has rounded top corners as well as a clean looking shadow while its child is a ListView that takes the DraggableScrollableSheet scrollController as a controller.

Most of the children of this ListView are pretty simple, so I wanted to focus on the only Widget that actually does something.

Wrap( children: [ Text( "${doctorInfo[widget.id].about}", maxLines: _showMoreAbout ? null : 1, ), FlatButton( child: Text( _showMoreAbout ? "See Less" : "See More", style: Theme.of(context) .textTheme .button .copyWith(color: MyColors.blue), ), onPressed: () { setState(() { _showMoreAbout = !_showMoreAbout; }); }, ) ], ),

Using the Wrap widget we can lay a Text widget along with a “Show More | Show Less” flat button. Once clicked the value of _showMoreAbout is changed which either limits the maxLines of the Text widget to 1 or unlimited.

Final Thoughts:

Make sure to subscribe to my Newsletter, and follow me on Twitter at @cybdom to receive a notification once a new tutorial is out!

If you liked this tutorial please share it. And consider buying me a coffee at BuyMeACoffee.

Until the next, enjoy your coffee.