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 ? '开始使用' : '下一步',
),
五、最终实现效果 📸
(此处附鸿蒙设备上成功运行的截图)
---

六、个人学习总结 📝
通过引导页的学习,我收获了很多:
- ✅ 学会了 PageView 的使用
- ✅ 学会了动画的组合使用
- ✅ 学会了状态持久化
一个好的引导页能给用户留下好印象,这个功能真的很重要!
💡 提示:完整代码已开源至 AtomGit,欢迎 Star 和 Fork!
🔗 仓库地址:https://atomgit.com
作者:上海某高校大一学生,Flutter 爱好者
发布时间:2026年4月