Building a One-Time Onboarding Screen in Flutter with GetX & Localization


👋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

🧠Core Concepts Used:
  • 🚀GetX for routing, localization and reactive state
  • 📦SharedPreference to track first launch
  • 📄PageView for swiping between pages
  • 🔘SmoothPageIndicator for animated dot indicators
.
.

How it Works?

1. Onboarding Data Model

Create a simple model to hold title, description, and image data for each onboarding slide. 
These can be localized string to support multiple languages🌐.

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

We create a class to manage the onboarding pages. This allows the content to dynamically update whenever the locate changes
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

The main onboarding screen combines a PageView, skip button, Language toggle, and a final "Continue" button that appears on the last page. All transitions and logic are reactive with Obx. Make sure not to add padding to the whole Column.

The full OnboardingScreens widget includes:
  • 🌐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 

This onboarding system is designed to be:
  • 🔧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
Whether you're working on a startup MVP, client project, or scaling an existing app - a solid onboarding experience helps users understand your app faster, increasing retention and satisfaction🚀✨

🙌Let's Connect

Got questions? Need help with GetX, onboarding localization, or anything Flutter-related?
Drop a comment, shoot a DM, or tag me on your posts 🧑‍💻💬
I'd love to hear how you're using this, or help you build your next app idea!
  • 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
Let's build beautiful, scalable apps - one widget at a time. 💙 
#FlutterDev #MobileDevelopment #GetX
Previous Post Next Post