flutter-onboarding-screen/

Crafting a Stunning Flutter Onboarding Screen with Advanced Animations

As a Senior Flutter UI designer, I’m excited to share a step-by-step guide on creating a unique and engaging Flutter onboarding screen for your Flutter app. This tutorial will walk you through building a modern onboarding experience with smooth animations, including Lottie animations, a parallax effect, a custom page indicator, and haptic feedback. By the end, you’ll have a polished, professional onboarding screen that captivates users and sets the tone for your app. Let’s dive in!

Why Flutter Onboarding Screen Matters

Onboarding screens are the first impression users have of your app. A well-designed onboarding experience can boost user retention by introducing key features, setting expectations, and creating a memorable interaction. Our goal is to craft a screen that’s visually appealing, intuitive, and interactive, using Flutter’s powerful animation capabilities to elevate the user experience.

Features of Our Onboarding Screen

Our onboarding screen will include:

  • Three swipeable slides highlighting key app features.
  • Lottie animations for dynamic, engaging visuals.
  • Parallax effect to add depth to animations during swipes.
  • Custom animated page indicator for a unique navigation experience.
  • Haptic feedback for tactile button interactions.
  • Animated gradient background and fade-in text for a polished look.
  • Interactive buttons with scale animations and a “Skip” option.

Step 1: Setting Up the Flutter Project

Start by creating a new Flutter project:

flutter create onboarding_ui
cd onboarding_ui

Add the necessary dependencies in pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  lottie: ^2.3.2
  haptic_feedback: ^0.7.0

flutter:
  assets:
    - assets/animations/welcome.json
    - assets/animations/features.json
    - assets/animations/get_started.json

Run flutter pub get to install dependencies. Download three Lottie animations from LottieFiles and save them as welcome.json, features.json, and get_started.json in the assets/animations folder.

Step 2: Building the Onboarding Screen

The main OnboardingScreen widget uses a PageView for swipeable slides, an AnimatedContainer for gradient background transitions, and a custom page indicator. Here’s the core structure:

import 'package:flutter/material.dart';
import 'package:haptic_feedback/haptic_feedback.dart';
import 'package:lottie/lottie.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      initialRoute: '/',
      routes: {
        '/': (context) => OnboardingScreen(),
        '/home': (context) => Scaffold(
              body: Center(child: Text('Welcome to the Home Screen!')),
            ),
      },
    );
  }
}

class OnboardingScreen extends StatefulWidget {
  @override
  _OnboardingScreenState createState() => _OnboardingScreenState();
}

class _OnboardingScreenState extends State<OnboardingScreen> {
  final PageController _pageController = PageController();
  double _currentPage = 0.0;

  final List<Map<String, String>> onboardingData = [
    {
      'title': 'Welcome to the App',
      'description': 'Discover a world of possibilities with our app.',
      'animation': 'assets/animations/welcome.json',
    },
    {
      'title': 'Explore Features',
      'description': 'Unlock powerful tools to enhance your experience.',
      'animation': 'assets/animations/features.json',
    },
    {
      'title': 'Get Started',
      'description': 'Join now and start your journey with us!',
      'animation': 'assets/animations/get_started.json',
    },
  ];

  @override
  void initState() {
    super.initState();
    _pageController.addListener(() {
      setState(() {
        _currentPage = _pageController.page ?? 0.0;
      });
    });
  }

  @override
  void dispose() {
    _pageController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          AnimatedContainer(
            duration: Duration(milliseconds: 500),
            decoration: BoxDecoration(
              gradient: LinearGradient(
                colors: _currentPage < 1
                    ? [Colors.blueAccent, Colors.purpleAccent]
                    : _currentPage < 2
                        ? [Colors.greenAccent, Colors.blueAccent]
                        : [Colors.orangeAccent, Colors.redAccent],
                begin: Alignment.topLeft,
                end: Alignment.bottomRight,
              ),
            ),
          ),
          PageView.builder(
            controller: _pageController,
            itemCount: onboardingData.length,
            itemBuilder: (context, index) {
              return OnboardingPage(
                title: onboardingData[index]['title']!,
                description: onboardingData[index]['description']!,
                animation: onboardingData[index]['animation']!,
                isLastPage: index == onboardingData.length - 1,
                pageOffset: _currentPage - index,
                onNext: () async {
                  await Haptics.vibrate(HapticsType.light);
                  if (index < onboardingData.length - 1) {
                    _pageController.nextPage(
                      duration: Duration(milliseconds: 300),
                      curve: Curves.easeInOut,
                    );
                  } else {
                    Navigator.pushReplacementNamed(context, '/home');
                  }
                },
              );
            },
          ),
          Positioned(
            bottom: 50,
            left: 0,
            right: 0,
            child: CustomPageIndicator(
              pageCount: onboardingData.length,
              currentPage: _currentPage,
            ),
          ),
          Positioned(
            top: 40,
            right: 20,
            child: TextButton(
              onPressed: () async {
                await Haptics.vibrate(HapticsType.medium);
                Navigator.pushReplacementNamed(context, '/home');
              },
              child: Text(
                'Skip',
                style: TextStyle(color: Colors.white, fontSize: 18),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

Step 3: Creating the Onboarding Page

The OnboardingPage widget displays each slide’s content, including a Lottie animation with a parallax effect, animated text, and a button with haptic feedback.

class OnboardingPage extends StatelessWidget {
  final String title;
  final String description;
  final String animation;
  final bool isLastPage;
  final double pageOffset;
  final VoidCallback onNext;

  OnboardingPage({
    required this.title,
    required this.description,
    required this.animation,
    required this.isLastPage,
    required this.pageOffset,
    required this.onNext,
  });

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Transform.translate(
          offset: Offset(pageOffset * 50, 0), // Parallax effect
          child: LottieAnimationWidget(animation: animation),
        ),
        SizedBox(height: 40),
        AnimatedText(
          text: title,
          style: TextStyle(
            fontSize: 28,
            fontWeight: FontWeight.bold,
            color: Colors.white,
          ),
        ),
        SizedBox(height: 20),
        AnimatedText(
          text: description,
          style: TextStyle(
            fontSize: 16,
            color: Colors.white.withOpacity(0.8),
          ),
          delay: Duration(milliseconds: 200),
        ),
        SizedBox(height: 40),
        if (isLastPage)
          ScaleAnimationButton(
            text: 'Get Started',
            onPressed: onNext,
          ),
        if (!isLastPage)
          ElevatedButton(
            onPressed: onNext,
            style: ElevatedButton.styleFrom(
              backgroundColor: Colors.white,
              padding: EdgeInsets.symmetric(horizontal: 40, vertical: 15),
              shape: RoundedRectangleBorder(
                borderRadius: BorderRadius.circular(30),
              ),
            ),
            child: Text(
              'Next',
              style: TextStyle(fontSize: 18, color: Colors.blueAccent),
            ),
          ),
      ],
    );
  }
}

Step 4: Adding Lottie Animations

Lottie animations bring life to the onboarding screen. The LottieAnimationWidget handles playback with a smooth fade-in effect.

class LottieAnimationWidget extends StatefulWidget {
  final String animation;

  LottieAnimationWidget({required this.animation});

  @override
  _LottieAnimationWidgetState createState() => _LottieAnimationWidgetState();
}

class _LottieAnimationWidgetState extends State<LottieAnimationWidget>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: Duration(seconds: 2),
      vsync: this,
    )..forward();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Lottie.asset(
      widget.animation,
      controller: _controller,
      height: MediaQuery.of(context).size.height * 0.3,
      fit: BoxFit.contain,
    );
  }
}

Step 5: Implementing Animated Text

The AnimatedText widget adds a fade-in effect for titles and descriptions, staggered for a polished look.

class AnimatedText extends StatefulWidget {
  final String text;
  final TextStyle style;
  final Duration delay;

  AnimatedText({required this.text, required this.style, this.delay = Duration.zero});

  @override
  _AnimatedTextState createState() => _AnimatedTextState();
}

class _AnimatedTextState extends State<AnimatedText>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _fadeAnimation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: Duration(milliseconds: 800),
      vsync: this,
    );
    _fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
      CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
    );
    Future.delayed(widget.delay, () {
      if (mounted) _controller.forward();
    });
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return FadeTransition(
      opacity: _fadeAnimation,
      child: Text(
        widget.text,
        style: widget.style,
        textAlign: TextAlign.center,
      ),
    );
  }
}

Step 6: Creating a Scaling Button with Haptic Feedback

The ScaleAnimationButton adds a pulsating scale effect to the “Get Started” button, with haptic feedback for a tactile experience.

class ScaleAnimationButton extends StatefulWidget {
  final String text;
  final VoidCallback onPressed;

  ScaleAnimationButton({required this.text, required this.onPressed});

  @override
  _ScaleAnimationButtonState createState() => _ScaleAnimationButtonState();
}

class _ScaleAnimationButtonState extends State<ScaleAnimationButton>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _scaleAnimation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: Duration(milliseconds: 600),
      vsync: this,
    );
    _scaleAnimation = Tween<double>(begin: 0.8, end: 1.0).animate(
      CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
    );
    _controller.repeat(reverse: true);
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return ScaleTransition(
      scale: _scaleAnimation,
      child: ElevatedButton(
        onPressed: () async {
          await Haptics.vibrate(HapticsType.selection);
          widget.onPressed();
        },
        style: ElevatedButton.styleFrom(
          backgroundColor: Colors.white,
          padding: EdgeInsets.symmetric(horizontal: 40, vertical: 15),
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(30),
          ),
        ),
        child: Text(
          widget.text,
          style: TextStyle(fontSize: 18, color: Colors.blueAccent),
        ),
      ),
    );
  }
}

Step 7: Designing a Custom Page Indicator

A custom page indicator, built with CustomPainter, provides a unique, animated dot transition.

class CustomPageIndicator extends StatelessWidget {
  final int pageCount;
  final double currentPage;

  CustomPageIndicator({required this.pageCount, required this.currentPage});

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      painter: DotPainter(
        pageCount: pageCount,
        currentPage: currentPage,
      ),
      size: Size(pageCount * 30.0, 10.0),
    );
  }
}

class DotPainter extends CustomPainter {
  final int pageCount;
  final double currentPage;

  DotPainter({required this.pageCount, required this.currentPage});

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..style = PaintingStyle.fill
      ..color = Colors.white.withOpacity(0.5);

    final activePaint = Paint()
      ..style = PaintingStyle.fill
      ..color = Colors.white;

    for (int i = 0; i < pageCount; i++) {
      final isActive = i >= currentPage.floor() && i < currentPage.ceil() + 1;
      final t = currentPage - currentPage.floor();
      final scale = isActive ? (1 - t) * 0.4 + 0.8 : 0.6;

      canvas.drawCircle(
        Offset(i * 30.0 + 15, 5),
        isActive ? 5 * scale : 4,
        isActive ? activePaint : paint,
      );
    }
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}

Step 8: Enhancing with Advanced Features

Lottie Animations

Lottie animations replace static images, adding a dynamic, professional touch. Choose animations that align with your app’s theme from LottieFiles and optimize them for performance using their editor.

Parallax Effect

The parallax effect shifts Lottie animations horizontally based on the PageView scroll offset, creating a sense of depth. The pageOffset parameter in OnboardingPage applies a Transform.translate to achieve this.

Custom Page Indicator

The custom indicator animates dot sizes and opacity, providing a smoother and more unique navigation cue than standard packages like dots_indicator.

Haptic Feedback

Haptic feedback enhances interactivity by providing tactile responses on button presses. We use Haptics.vibrate with different intensities (light, medium, selection) for “Next,” “Skip,” and “Get Started” actions.

Step 9: Design Tips for a Polished Look

  • Typography: Use bold fonts like Poppins or Roboto for titles and clean sans-serif fonts for descriptions. Ensure high contrast (e.g., white text on gradient backgrounds).
  • Color Palette: Create a vibrant gradient palette using tools like Coolors. Our example uses blue-to-purple, green-to-blue, and orange-to-red transitions.
  • Accessibility: Ensure text sizes are at least 16px for readability and buttons are 48×48 dp for touch targets.
  • Responsiveness: Adjust animation heights using MediaQuery to scale properly across devices.
  • Micro-Interactions: The parallax effect, scaling button, and animated dots add subtle interactivity that delights users.

Step 10: Testing and Optimization

  • Test on Devices: Run the app on iOS and Android to verify smooth animations and haptic feedback.
  • Performance: Optimize Lottie files and dispose of AnimationControllers to prevent memory leaks.
  • Responsiveness: Test on various screen sizes and adjust padding or animation heights as needed.
  • Animation Timing: Fine-tune durations (Lottie: 2s, text: 800ms, button: 600ms) for a balanced experience.

Final Result

The onboarding screen now features:

  • Dynamic Lottie animations with a parallax effect for depth.
  • Smooth gradient transitions for a modern aesthetic.
  • Animated text with staggered fade-ins.
  • Custom page indicator with scaling dots.
  • Haptic feedback for tactile button interactions.
  • Interactive buttons with a pulsating “Get Started” effect.

To see it in action, add your Lottie files, run flutter run, and swipe through the screens. The result is a professional, engaging onboarding experience that sets your app apart.

Conclusion

Building a unique onboarding screen in Flutter is a fantastic way to showcase your app’s personality and functionality. By combining Lottie animations, a parallax effect, a custom page indicator, and haptic feedback, you create an immersive experience that captivates users. Feel free to customize the colors, fonts, or animations to match your app’s brand, and explore additional micro-interactions to further enhance the UI.

For the full code and assets, check out the GitHub repository (replace with your repo link). Have questions or want to add more features? Drop a comment below or reach out on X for more Flutter tips!

Happy coding, and let’s create amazing user experiences with Flutter!

Leave a Comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.