Simple Onboarding
Loading preview
Starting the Flutter app
A Flutter onboarding screen with swipeable intro slides, page dots, and a get-started button.
import 'package:flutter/material.dart';
class OnboardingPage1 extends StatelessWidget {
const OnboardingPage1({super.key});
// Replace with navigation to your first real screen.
void _exit(BuildContext context, String message) {
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(SnackBar(content: Text(message)));
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: OnboardingPagePresenter(
onSkip: () => _exit(context, 'Onboarding skipped'),
onFinish: () => _exit(context, 'Onboarding finished'),
pages: [
OnboardingPageModel(
title: 'Fast, Fluid and Secure',
description:
'Enjoy the best of the world in the palm of your hands.',
imageUrl: 'https://i.ibb.co/cJqsPSB/scooter.png',
bgColor: Colors.indigo,
),
OnboardingPageModel(
title: 'Connect with your friends.',
description: 'Connect with your friends anytime anywhere.',
imageUrl: 'https://i.ibb.co/LvmZypG/storefront-illustration-2.png',
bgColor: const Color(0xff1eb090),
),
OnboardingPageModel(
title: 'Bookmark your favourites',
description:
'Bookmark your favourite quotes to read at a leisure time.',
imageUrl: 'https://i.ibb.co/420D7VP/building.png',
bgColor: const Color(0xfffeae4f),
),
OnboardingPageModel(
title: 'Follow creators',
description: 'Follow your favourite creators to stay in the loop.',
imageUrl: 'https://i.ibb.co/cJqsPSB/scooter.png',
bgColor: Colors.purple,
),
],
),
);
}
}
class OnboardingPagePresenter extends StatefulWidget {
final List<OnboardingPageModel> pages;
final VoidCallback? onSkip;
final VoidCallback? onFinish;
const OnboardingPagePresenter({
super.key,
required this.pages,
this.onSkip,
this.onFinish,
});
@override
State<OnboardingPagePresenter> createState() => _OnboardingPageState();
}
class _OnboardingPageState extends State<OnboardingPagePresenter> {
// Store the currently visible page
int _currentPage = 0;
// Define a controller for the pageview
final PageController _pageController = PageController(initialPage: 0);
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: AnimatedContainer(
duration: const Duration(milliseconds: 250),
color: widget.pages[_currentPage].bgColor,
child: SafeArea(
child: Column(
children: [
Expanded(
// Pageview to render each page
child: PageView.builder(
controller: _pageController,
itemCount: widget.pages.length,
onPageChanged: (idx) {
// Change current page when pageview changes
setState(() {
_currentPage = idx;
});
},
itemBuilder: (context, idx) {
final item = widget.pages[idx];
return Column(
children: [
Expanded(
flex: 3,
child: Padding(
padding: const EdgeInsets.all(32.0),
child: Image.network(
item.imageUrl,
errorBuilder: (context, error, stackTrace) =>
Icon(
Icons.image_not_supported_outlined,
size: 64,
color: item.textColor,
),
),
),
),
Expanded(
flex: 1,
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
item.title,
style: Theme.of(context).textTheme.titleMedium
?.copyWith(
fontWeight: FontWeight.bold,
color: item.textColor,
),
),
),
Container(
constraints: const BoxConstraints(
maxWidth: 280,
),
padding: const EdgeInsets.symmetric(
horizontal: 24.0,
vertical: 8.0,
),
child: Text(
item.description,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium
?.copyWith(color: item.textColor),
),
),
],
),
),
],
);
},
),
),
// Current page indicator
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: widget.pages
.map(
(item) => AnimatedContainer(
duration: const Duration(milliseconds: 250),
width: _currentPage == widget.pages.indexOf(item)
? 30
: 8,
height: 8,
margin: const EdgeInsets.all(2.0),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10.0),
),
),
)
.toList(),
),
// Bottom buttons
SizedBox(
height: 100,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
TextButton(
style: TextButton.styleFrom(
visualDensity: VisualDensity.comfortable,
foregroundColor: Colors.white,
textStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
onPressed: () {
widget.onSkip?.call();
},
child: const Text("Skip"),
),
TextButton(
style: TextButton.styleFrom(
visualDensity: VisualDensity.comfortable,
foregroundColor: Colors.white,
textStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
onPressed: () {
if (_currentPage == widget.pages.length - 1) {
widget.onFinish?.call();
} else {
_pageController.animateToPage(
_currentPage + 1,
curve: Curves.easeInOutCubic,
duration: const Duration(milliseconds: 250),
);
}
},
child: Row(
children: [
Text(
_currentPage == widget.pages.length - 1
? "Finish"
: "Next",
),
const SizedBox(width: 8),
Icon(
_currentPage == widget.pages.length - 1
? Icons.done
: Icons.arrow_forward,
),
],
),
),
],
),
),
],
),
),
),
);
}
}
class OnboardingPageModel {
final String title;
final String description;
final String imageUrl;
final Color bgColor;
final Color textColor;
OnboardingPageModel({
required this.title,
required this.description,
required this.imageUrl,
this.bgColor = Colors.blue,
this.textColor = Colors.white,
});
}
Add it to your project
Copies the Dart source into your app, so you own it. Flutter SDK only, no extra packages.
First time? Run dart pub global activate fluttertemplates_dev, or read the CLI guide.
How it works
OnboardingPagePresenter is reusable: it takes a list of OnboardingPageModel and optional onSkip and onFinish callbacks. An AnimatedContainer fades the background to bgColor as _currentPage changes, and the page dots animate width. The demo OnboardingPage1 wires both callbacks to _exit, which only shows a SnackBar. _pageController is disposed, and images load from third-party i.ibb.co links, with an errorBuilder icon if they fail.
Adapting it
- Replace
_exitwith navigation into your app. - Save a “seen” flag so returning users skip the flow.
- Bundle the art and use
Image.assetin place ofImage.network.