👋Hey Flutter Fam, Welcome back!
With evolving UX trends, many modern startups are skipping traditional
onboarding screens or merging them with sign-up flows. While simplicity is
key, completely ditching onboarding screens can sometimes do more harm than
good --especially when introducing unique features or workflows.
If you're part of company or team that still values the onboarding experience, I've got your back💪. Let's talk about why onboarding screens still matter and what you might risk by skipping them.
.
.
🎯With onboarding screens, there's more room for
- Personalization✨
- Theme setup 🎨
- Highlighting features 🌟
- Educating users on "how it works" 🧠
🧩 It's a chance to reinforce your brand voice, values, and the "why" behind your product.
And let's not forget -- many users actually like the onboarding experience. So why not just add it in... with a simple "Skip" button? 🆓
.
🛠️ Now, let's get started with coding:
🔑Key Features:
- ✅ One-time onboarding flow(doesn't show again once completed)
- 🌍In-app language switcher (To know how to implement this click here)
- ⏩ Skip option to jump directly into the app
- 📱Fully responsive UI
- 💨Smooth page indicators & transitions
- 🚀GetX for routing, localization and reactive state
- 📦SharedPreference to track first launch
- 📄PageView for swiping between pages
- 🔘SmoothPageIndicator for animated dot indicators
.
1. Onboarding Data Model
class OnboardingInfo {
final String title;
final String description;
final String image;
OnboardingInfo({
required this.title,
required this.description,
required this.image,
});
}
2. Dynamic Onboarding Items Loader
class OnboardingItems {
RxList<OnboardingInfo> items = <OnboardingInfo>[].obs;
OnboardingItems() {
refreshItems();
}
void refreshItems() {
items.value = [
OnboardingInfo(
title: 'onboarding_title_1'.tr,
description: 'onboarding_body_1'.tr,
image: 'assets/images/onboarding1.png',
),
// Add more pages as needed
];
}
}
3. UI with Language Switcher and Skip
- 🌐Language toggle or routing that update locale and refresh page content
- ⏭️"Skip" and "Next" buttons for flexible navigation.
- 👉A final "Continue" button to enter the app and prevent onboarding from showing again.
⚠ Pro tip: Store a bool in SharedPreferences like "onboarding": true once completed, and check it on app start to decide whether to show onboarding again.
Code for the screen:
class OnboardingScreens extends StatelessWidget {
final String page;
OnboardingScreens({super.key, required this.page});
final controller = OnboardingItems().obs;
final pageController = PageController();
var isLastPage = false.obs;
static late double screenWidth;
static late double screenHeight;
@override
Widget build(BuildContext context) {
screenWidth = MediaQuery.of(context).size.width;
screenHeight = MediaQuery.of(context).size.height;
// Refresh onboarding items if language was changed
if (Get.arguments == true) {
controller.value.refreshItems();
}
LocalizationController localizationController =
Get.find<LocalizationController>();
Locale myLocale = localizationController.locale;
return Scaffold(
body: SafeArea(
child: Padding(
padding: EdgeInsets.only(
top: 24,
bottom: 24,
),
child: Column(
children: [
Padding(
padding: EdgeInsets.only(
left: 16,
right: 16,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
textDirection: localizationController.isLtr
? TextDirection.ltr
: TextDirection.rtl,
children: [
//Language naviagtion system
InkWell(
onTap: () async {
HapticFeedback.lightImpact();
var result = await Get.toNamed(
RouteHelper.getLanguageRoute());
// Check if language was changed and refresh items
if (result == true) {
controller.value.refreshItems();
}
},
child: Container(
height: 30,
width: 84,
decoration: BoxDecoration(
border:
Border.all(color: CustomColor.primaryMain),
borderRadius: BorderRadius.circular(12),
boxShadow: const [
BoxShadow(
color: CustomColor.greyMain,
spreadRadius: 0.2,
blurRadius: 8,
offset: Offset(0, 0),
),
],
),
child: Row(
textDirection: localizationController.isLtr
? TextDirection.ltr
: TextDirection.ltr,
children: [
Container(
width: 42,
height: 30,
alignment: Alignment.center,
decoration: BoxDecoration(
color: myLocale.languageCode == 'en'
? CustomColor.primaryMain
: CustomColor.white,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(12),
bottomLeft: Radius.circular(12),
),
),
child: Text(
"EN",
style: nunitoSansBold.copyWith(
fontSize: 16,
color: myLocale.languageCode == 'en'
? CustomColor.white
: CustomColor.primaryMain,
),
),
),
Container(
width: 40,
height: 30,
decoration: BoxDecoration(
color: myLocale.languageCode == 'ar'
? CustomColor.primaryMain
: CustomColor.white,
borderRadius: const BorderRadius.only(
topRight: Radius.circular(12),
bottomRight: Radius.circular(12),
),
),
alignment: Alignment.center,
child: Text(
"AR",
style: nunitoSansBold.copyWith(
fontSize: 16,
color: myLocale.languageCode == 'en'
? CustomColor.primaryMain
: CustomColor.white,
),
),
),
],
),
),
),
TextButton(
onPressed: () async {
HapticFeedback.lightImpact();
final prefs = await SharedPreferences.getInstance();
prefs.setBool(
"onboarding",
true,
);
Get.toNamed(page);
},
child: Row(
children: [
Text(
"skip".tr,
style: nunitoSansSemiBold.copyWith(
fontSize: Dimensions.fontSizeBodyMedium,
color: CustomColor.textTertiary),
),
],
),
),
],
),
),
SizedBox(
height: screenHeight * 0.65,
child: PageView.builder(
onPageChanged: (index) => isLastPage.value =
controller.value.items.length - 1 == index,
itemCount: controller.value.items.length,
controller: pageController,
itemBuilder: (context, index) {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Image.asset(
controller.value.items[index].image,
height: screenHeight * 0.4,
width: screenWidth * 0.8,
),
SizedBox(height: screenHeight * 0.06),
Padding(
padding: EdgeInsets.only(
left: Dimensions.spacingLarge,
right: Dimensions.spacingLarge,
),
child: Text(
controller.value.items[index].title,
textAlign: TextAlign.center,
style: nunitoSansExtraBold.copyWith(
fontSize: Dimensions.fontSizeHeadingLarge,
color: CustomColor.textPrimary),
),
),
Padding(
padding: EdgeInsets.only(
left: 24
right: 24
),
child: Text(
controller.value.items[index].description,
style: nunitoSansRegular.copyWith(
fontSize: Dimensions.fontSizeBodyMedium,
color: CustomColor.textSecondary),
textAlign: TextAlign.center,
),
),
],
);
},
),
),
SmoothPageIndicator(
controller: pageController,
count: controller.value.items.length,
effect: const SlideEffect(
dotHeight: 8,
dotWidth: 8,
spacing: 4,
activeDotColor: CustomColor.primaryMain,
dotColor: Color.fromARGB(
255,
228,
228,
228,
),
),
),
const Spacer(),
Padding(
padding: EdgeInsets.only(
left: Dimensions.spacingLarge,
right: Dimensions.spacingLarge,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Image.asset(
Images.typeMark,
),
Obx(
() => isLastPage.value
? Container(
height: 48,
width: 164,
decoration: BoxDecoration(
color: CustomColor.primaryMain,
borderRadius: BorderRadius.circular(
12,
),
boxShadow: const [
BoxShadow(
color: CustomColor.blackLighter,
spreadRadius: 0.2,
blurRadius: 8,
offset: Offset(
0,
0,
),
),
],
),
child: TextButton(
onPressed: () async {
HapticFeedback.lightImpact();
final prefs =
await SharedPreferences.getInstance();
prefs.setBool(
"onboarding",
true,
);
Get.toNamed(
page,
);
},
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Text(
"continue".tr,
style: nunitoSansBold.copyWith(
fontSize: 16,
color: Colors.white,
),
),
const SizedBox(
width: 8,
),
],
),
),
)
: Container(
height:
Dimensions.screenWidth > 450 ? 64 : 48,
width:
Dimensions.screenWidth > 450 ? 200 : 164,
decoration: BoxDecoration(
color: CustomColor.primaryMain,
borderRadius: BorderRadius.circular(12),
boxShadow: const [
BoxShadow(
color: CustomColor.blackLighter,
spreadRadius: 0.2,
blurRadius: 8,
offset: Offset(0, 0),
),
],
),
child: TextButton(
onPressed: () {
HapticFeedback.lightImpact();
pageController.nextPage(
duration:
const Duration(milliseconds: 300),
curve: Curves.ease);
},
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Text(
'next'.tr,
style: nunitoSansBold.copyWith(
fontSize:
Dimensions.fontSizeBodyMedium,
color: Colors.white,
),
),
const SizedBox(
width: 8,
),
],
),
),
),
),
],
),
)
],
),
),
),
);
}
}
🌐 Want to Add Language Support?
If you're curious about how the in-app language system works --including how to swtich language dynamically with GetX -- click here to learn the full setup. 🌍🧑💻🧠Final Thoughts
- 🔧Modular - Add, remove, update screens easily without touching core logic
- 🌍Localization-ready - No hardcoded string! Every word is translatable for global reach
- 📱User-friendly - Smooth transitions, responsive layouts, and skip/next controls that make sense
🙌Let's Connect
- Save this post for later
- Try it out in your project
- Follow me for more Flutter tips, real world UI breakdowns, and startup deb insights. LinkedIn