Flutter 引导页 Onboarding 第三方库 的鸿蒙化适配与实战指南

Flutter 引导页 Onboarding 的鸿蒙化适配与实战指南

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net


各位小伙伴们好呀!👋 我是那个上海某高校的大一计算机学生,继续来给大家分享 Flutter for OpenHarmony 开发的学习心得!

今天要聊的是 引导页(Onboarding)!🎉

大家第一次打开很多 App 的时候,都会看到几页介绍 App 功能的页面,比如:

  • "欢迎使用 xxx App"
  • "海量商品等你来选"
  • "便捷支付安全可靠"
  • ...

这就是引导页!好的引导页能让新用户快速了解 App 的核心功能,留住用户的第一步!

今天就给大家详细分享一下如何实现一个漂亮的引导页~


一、功能引入介绍 📱

1.1 引导页的作用

  • 🌟 第一印象:给新用户留下好印象
  • 📖 功能介绍:让用户快速了解核心功能
  • 🎯 引导操作:告诉用户可以做什么
  • 💫 品牌展示:展现 App 的设计风格

1.2 引导页设计要点

要点 说明
简洁 3-5 页为宜,不要太长
重点突出 每页只讲一个核心功能
图文并茂 大图标 + 简洁文案
CTA 明确 最后一页要有明确操作指引

二、环境与依赖配置 🔧

2.1 pubspec.yaml 依赖

yaml 复制代码
dependencies:
  flutter:
    sdk: flutter
  
  # ========== 引导页 ==========
  smooth_page_indicator: ^1.2.0+3
  
  # ========== 动画 ==========
  flutter_animate: ^4.5.0
  
  # ========== 本地存储(保存引导页状态)==========
  shared_preferences: ^2.3.5

三、分步实现完整代码 🚀

3.1 引导页模型

dart 复制代码
import 'package:flutter/material.dart';

/// 引导页数据模型
class OnboardingItem {
  /// 标题
  final String title;
  
  /// 描述文字
  final String description;
  
  /// 图标
  final IconData icon;
  
  /// 背景颜色
  final Color backgroundColor;

  const OnboardingItem({
    required this.title,
    required this.description,
    required this.icon,
    required this.backgroundColor,
  });
}

/// 预设的引导页数据
class OnboardingData {
  static const List<OnboardingItem> items = [
    OnboardingItem(
      title: '欢迎使用 My Ohos App',
      description: '一款基于 Flutter 和 OpenHarmony 的跨平台应用',
      icon: Icons.shopping_bag_outlined,
      backgroundColor: Color(0xFF6366F1),
    ),
    OnboardingItem(
      title: '海量商品',
      description: '精选优质商品,实时更新,价格优惠',
      icon: Icons.inventory_2_outlined,
      backgroundColor: Color(0xFF8B5CF6),
    ),
    OnboardingItem(
      title: '便捷购物',
      description: '轻松下单,快速配送,售后无忧',
      icon: Icons.shopping_cart_outlined,
      backgroundColor: Color(0xFFEC4899),
    ),
    OnboardingItem(
      title: '社交聊天',
      description: '与好友实时聊天,分享购物心得',
      icon: Icons.chat_bubble_outline,
      backgroundColor: Color(0xFF10B981),
    ),
  ];
}

3.2 引导页完整实现

dart 复制代码
import 'package:flutter/material.dart';
import 'package:smooth_page_indicator/smooth_page_indicator.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:go_router/go_router.dart';

/// 引导页
/// 
/// 首次打开 App 时显示的功能介绍页面
/// 支持跳过、滑动翻页、动画效果
class OnboardingPage extends StatefulWidget {
  const OnboardingPage({super.key});

  @override
  State<OnboardingPage> createState() => _OnboardingPageState();
}

class _OnboardingPageState extends State<OnboardingPage> {
  /// PageView 控制器
  final PageController _pageController = PageController();
  
  /// 当前页码
  int _currentPage = 0;
  
  /// 引导页数据
  final List<OnboardingItem> _items = OnboardingData.items;

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

  /// 下一页
  void _nextPage() {
    if (_currentPage < _items.length - 1) {
      _pageController.nextPage(
        duration: const Duration(milliseconds: 300),
        curve: Curves.easeInOut,
      );
    } else {
      // 最后一页,完成引导
      _completeOnboarding();
    }
  }

  /// 完成引导页
  void _completeOnboarding() async {
    // 标记引导页已完成
    final prefs = await SharedPreferences.getInstance();
    await prefs.setBool('onboarding_completed', true);
    
    if (mounted) {
      // 跳转到首页
      context.go('/');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          // ============ PageView 页面 ============
          PageView.builder(
            controller: _pageController,
            itemCount: _items.length,
            onPageChanged: (index) {
              setState(() => _currentPage = index);
            },
            itemBuilder: (context, index) {
              return _OnboardingPage(item: _items[index], index: index);
            },
          ),
          
          // ============ 跳过按钮 ============
          Positioned(
            top: MediaQuery.of(context).padding.top + 20,
            right: 20,
            child: TextButton(
              onPressed: _completeOnboarding,
              child: const Text(
                '跳过',
                style: TextStyle(
                  color: Colors.white,
                  fontSize: 16,
                  fontWeight: FontWeight.w500,
                ),
              ),
            ),
          ),
          
          // ============ 页面指示器 ============
          Positioned(
            bottom: MediaQuery.of(context).padding.bottom + 100,
            left: 0,
            right: 0,
            child: Center(
              child: SmoothPageIndicator(
                controller: _pageController,
                count: _items.length,
                effect: WormEffect(
                  dotWidth: 10,
                  dotHeight: 10,
                  spacing: 8,
                  dotColor: Colors.white.withValues(alpha: 0.3),  // 未选中
                  activeDotColor: Colors.white,                   // 选中
                ),
              ),
            ),
          ),
          
          // ============ 底部按钮 ============
          Positioned(
            bottom: MediaQuery.of(context).padding.bottom + 30,
            left: 20,
            right: 20,
            child: AnimatedOpacity(
              opacity: 1.0,
              duration: const Duration(milliseconds: 200),
              child: SizedBox(
                width: double.infinity,
                height: 56,
                child: ElevatedButton(
                  onPressed: _nextPage,
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.white,
                    foregroundColor: _items[_currentPage].backgroundColor,
                    shape: RoundedRectangleBorder(
                      borderRadius: BorderRadius.circular(28),
                    ),
                    elevation: 4,
                  ),
                  child: Text(
                    _currentPage == _items.length - 1 ? '开始使用' : '下一步',
                    style: const TextStyle(
                      fontSize: 18,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

/// 单个引导页组件
class _OnboardingPage extends StatelessWidget {
  final OnboardingItem item;
  final int index;

  const _OnboardingPage({
    required this.item,
    required this.index,
  });

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        gradient: LinearGradient(
          begin: Alignment.topCenter,
          end: Alignment.bottomCenter,
          colors: [
            item.backgroundColor,
            item.backgroundColor.withValues(alpha: 0.8),
          ],
        ),
      ),
      child: SafeArea(
        child: Padding(
          padding: const EdgeInsets.all(40),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              const Spacer(),
              
              // 图标(带动画)
              _buildIcon(),
              
              const SizedBox(height: 60),
              
              // 标题
              _buildTitle(),
              
              const SizedBox(height: 20),
              
              // 描述
              _buildDescription(),
              
              const Spacer(),
            ],
          ),
        ),
      ),
    );
  }

  /// 构建图标
  Widget _buildIcon() {
    return Container(
      width: 160,
      height: 160,
      decoration: BoxDecoration(
        color: Colors.white.withValues(alpha: 0.2),
        shape: BoxShape.circle,
      ),
      child: Icon(
        item.icon,
        size: 80,
        color: Colors.white,
      ),
    )
        // 延迟 200ms 执行
        .animate(delay: 200.ms)
        // 弹性放大动画
        .scale(
          begin: const Offset(0.5, 0.5),
          end: const Offset(1, 1),
          duration: 600.ms,
          curve: Curves.elasticOut,
        )
        // 淡入
        .fadeIn(duration: 400.ms);
  }

  /// 构建标题
  Widget _buildTitle() {
    return Text(
      item.title,
      style: const TextStyle(
        fontSize: 32,
        fontWeight: FontWeight.bold,
        color: Colors.white,
        letterSpacing: 1.2,
      ),
      textAlign: TextAlign.center,
    )
        .animate(delay: 400.ms)
        .fadeIn(duration: 400.ms)
        .slideY(begin: 0.3, end: 0, curve: Curves.easeOutCubic);
  }

  /// 构建描述
  Widget _buildDescription() {
    return Text(
      item.description,
      style: TextStyle(
        fontSize: 16,
        color: Colors.white.withValues(alpha: 0.9),
        height: 1.5,
      ),
      textAlign: TextAlign.center,
    )
        .animate(delay: 500.ms)
        .fadeIn(duration: 400.ms)
        .slideY(begin: 0.3, end: 0, curve: Curves.easeOutCubic);
  }
}

3.3 引导页状态管理

dart 复制代码
/// 引导页状态检查器
/// 
/// 用于判断用户是否已完成引导
class OnboardingChecker {
  /// 存储键名
  static const String _key = 'onboarding_completed';

  /// 检查是否应该显示引导页
  /// 
  /// 返回 true 表示需要显示引导页
  /// 返回 false 表示已完成引导
  static Future<bool> shouldShowOnboarding() async {
    final prefs = await SharedPreferences.getInstance();
    // 如果没有保存过状态,说明是首次打开
    return !(prefs.getBool(_key) ?? false);
  }

  /// 标记引导页已完成
  static Future<void> markCompleted() async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setBool(_key, true);
  }

  /// 重置引导状态(用于测试)
  static Future<void> reset() async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setBool(_key, false);
  }
}

3.4 在 main.dart 中集成

dart 复制代码
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'pages/onboarding_page.dart';
import 'pages/home_page.dart';
import 'services/onboarding_checker.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  // 检查是否显示引导页
  final showOnboarding = await OnboardingChecker.shouldShowOnboarding();
  
  runApp(MyApp(showOnboarding: showOnboarding));
}

class MyApp extends StatelessWidget {
  final bool showOnboarding;
  
  const MyApp({super.key, required this.showOnboarding});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'My Ohos App',
      debugShowCheckedModeBanner: false,
      
      // 初始路由
      initialRoute: showOnboarding ? '/onboarding' : '/',
      
      // 路由表
      routes: {
        '/': (context) => const HomePage(),
        '/onboarding': (context) => const OnboardingPage(),
      },
    );
  }
}

3.5 带进度指示的引导页

如果你想让引导页有进度指示:

dart 复制代码
class ProgressOnboardingPage extends StatefulWidget {
  const ProgressOnboardingPage({super.key});

  @override
  State<ProgressOnboardingPage> createState() => _ProgressOnboardingPageState();
}

class _ProgressOnboardingPageState extends State<ProgressOnboardingPage> {
  final PageController _pageController = PageController();
  int _currentPage = 0;
  final int _totalPages = 4;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          // ============ 顶部进度条 ============
          Container(
            height: 4,
            margin: EdgeInsets.only(
              top: MediaQuery.of(context).padding.top,
            ),
            child: Row(
              children: List.generate(_totalPages, (index) {
                return Expanded(
                  child: Container(
                    margin: const EdgeInsets.symmetric(horizontal: 2),
                    decoration: BoxDecoration(
                      color: index <= _currentPage
                          ? Colors.blue  // 已完成
                          : Colors.grey[300],  // 未完成
                      borderRadius: BorderRadius.circular(2),
                    ),
                    child: AnimatedContainer(
                      duration: const Duration(milliseconds: 300),
                    ),
                  ),
                );
              }),
            ),
          ),
          
          // ============ PageView ============
          Expanded(
            child: PageView.builder(
              controller: _pageController,
              itemCount: _totalPages,
              onPageChanged: (index) {
                setState(() => _currentPage = index);
              },
              itemBuilder: (context, index) {
                // ... 引导页内容
                return Container(
                  color: Colors.primaries[index],
                  child: Center(
                    child: Text(
                      'Page ${index + 1}',
                      style: const TextStyle(color: Colors.white, fontSize: 32),
                    ),
                  ),
                );
              },
            ),
          ),
          
          // ============ 底部按钮 ============
          Padding(
            padding: const EdgeInsets.all(20),
            child: Row(
              children: [
                if (_currentPage > 0)
                  TextButton(
                    onPressed: () {
                      _pageController.previousPage(
                        duration: const Duration(milliseconds: 300),
                        curve: Curves.easeInOut,
                      );
                    },
                    child: const Text('上一步'),
                  ),
                const Spacer(),
                ElevatedButton(
                  onPressed: () {
                    if (_currentPage < _totalPages - 1) {
                      _pageController.nextPage(
                        duration: const Duration(milliseconds: 300),
                        curve: Curves.easeInOut,
                      );
                    } else {
                      // 完成
                    }
                  },
                  child: Text(
                    _currentPage < _totalPages - 1 ? '下一步' : '完成',
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

四,开发踩坑与挫折 😤

4.1 踩坑一:引导页显示时机不对

问题描述

用户每次打开 App 都显示引导页。

原因分析

没有正确保存引导页完成状态。

解决方案

dart 复制代码
// ✅ 在 main() 中检查
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final showOnboarding = await OnboardingChecker.shouldShowOnboarding();
  runApp(MyApp(showOnboarding: showOnboarding));
}

4.2 踩坑二:动画效果不流畅

问题描述

动画看起来很卡。

解决方案

  • 使用 RepaintBoundary 包裹动画区域
  • 避免在动画中使用复杂布局
  • 使用 flutter_animate 的预设动画(性能更好)

4.3 踩坑三:最后一页按钮文字

问题描述

最后一页按钮文字应该是"开始使用"而不是"下一步"。

解决方案

dart 复制代码
child: Text(
  _currentPage == _items.length - 1 ? '开始使用' : '下一步',
),

五、最终实现效果 📸

(此处附鸿蒙设备上成功运行的截图)

---

六、个人学习总结 📝

通过引导页的学习,我收获了很多:

  1. ✅ 学会了 PageView 的使用
  2. ✅ 学会了动画的组合使用
  3. ✅ 学会了状态持久化

一个好的引导页能给用户留下好印象,这个功能真的很重要!


💡 提示:完整代码已开源至 AtomGit,欢迎 Star 和 Fork!

🔗 仓库地址https://atomgit.com

作者:上海某高校大一学生,Flutter 爱好者
发布时间:2026年4月

相关推荐
IntMainJhy2 小时前
Flutter WebView 第三方库 内嵌 H5 页面的鸿蒙化适配与实战指南
flutter·华为·harmonyos
南村群童欺我老无力.2 小时前
鸿蒙pc aboutToAppear与onPageShow的执行时机差异
华为·harmonyos
liulian09162 小时前
Flutter for OpenHarmony 用户登录与身份认证功能实现指南
flutter·华为·学习方法·harmonyos
jiejiejiejie_2 小时前
Flutter for OpenHarmony 登录认证小指南:用 Flutter 给鸿蒙 App 安上 “安全小锁”✨
安全·flutter·华为·harmonyos
前端不太难2 小时前
鸿蒙游戏:设备不再是边界
游戏·状态模式·harmonyos
liulian09162 小时前
Flutter Hero 共享元素转场与 animated_text_kit 文字动画库的 OpenHarmony 适配总结
flutter·华为·学习方法·harmonyos
liulian09162 小时前
Flutter for OpenHarmony 跨平台技术实战:lottie 动画库与 flutter_view 页面转场库的鸿蒙化适配指南
flutter·华为·学习方法·harmonyos
南村群童欺我老无力.2 小时前
鸿蒙中暗黑模式的颜色适配体系
华为·harmonyos
木斯佳3 小时前
HarmonyOS 数据可视化实战:封装一个可复用的 3D 热点词球卡片组件
3d·信息可视化·harmonyos