Flutter 实现启动页、广告页与引导页集成方案
在 Flutter 应用开发中,常见的启动流程包括启动图展示、新手引导页以及广告页跳转功能。本文介绍如何通过状态管理与路由控制实现一个完整的 Splash 流程。
核心依赖组件
shared_preferences:用于本地存储是否已显示引导页或缓存广告内容。cached_network_image:高效加载并缓存网络图片作为广告背景。swipeable_page_route或swiper:实现滑动引导页轮播效果。rxdart:利用响应式编程处理延时逻辑(可选)。
整体结构设计
应用入口设置 SplashScreen 为首页,主页面注册在路由表中。当初始化完成后,使用 pushReplacementNamed 替换当前页面,避免用户返回到启动页。
MaterialApp(
home: SplashScreen(),
routes: {
'/home': (context) => HomePage(),
},
)
跳转至主界面方法:
void navigateToHome(BuildContext context) {
Navigator.of(context).pushReplacementNamed('/home');
}
多阶段页面状态控制
通过整型变量 _pageState 控制当前显示内容:
- 0:默认启动图(原生层 + Dart 层过渡)
- 1:广告页展示 + 倒计时跳过按钮
- 2:引导页轮播(仅首次安装显示)
消除白屏:Android 启动背景配置
为提升用户体验,应在原生端预先设置启动图,防止 Flutter 引擎加载时出现白屏。编辑 android/app/src/main/res/drawable/launch_background.xml:
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- 启动图全屏铺展 -->
<item>
<bitmap
android:src="@mipmap/splash_bg"
android:gravity="fill" />
</item>
</layer-list>
广告页点击跳转 H5 并正确返回主界面
关键在于导航栈的管理。若先跳转 Web 页面再进主页,返回会回到广告页。正确做法是:
- 先跳转主页(替换当前页)
- 再压入 Web 页面
// 先进入主界面
Navigator.of(context).pushReplacementNamed('/home');
// 随后打开广告链接(在新的上下文中)
Future.delayed(Duration(milliseconds: 100), () {
Navigator.of(context).push(MaterialPageRoute(
builder: (_) => WebviewPage(url: adUrl, title: adTitle)
));
});
引导页实现逻辑
仅在首次安装应用时显示引导页。通过 SharedPreferences 判断标记:
Future<void> checkFirstLaunch() async {
final prefs = await SharedPreferences.getInstance();
final hasSeenOnboard = prefs.getBool('has_seen_onboard') ?? false;
if (!hasSeenOnboard) {
// 显示引导页
setState(() { _pageState = 2; });
prefs.setBool('has_seen_onboard', true);
} else {
loadAdContent(); // 加载广告数据
}
}
倒计时跳过功能
使用 Timer 实现 3 秒倒计时,并支持点击"跳过"立即进入主界面:
startCountdown() {
const duration = Duration(seconds: 1);
int remaining = 3;
final timer = Timer.periodic(duration, (Timer t) {
if (remaining <= 1) {
t.cancel();
navigateToHome(context);
} else {
setState(() { remaining--; });
}
});
// 用户手动跳过
GestureDetector(
onTap: () {
timer.cancel();
navigateToHome(context);
},
child: Text('跳过 $remaining', style: TextStyle(color: Colors.white)),
);
}
完整 Splash 页面结构示例
class SplashScreen extends StatefulWidget {
@override
_SplashScreenState createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen>> {
int _pageState = 0;
Timer _countdownTimer;
List<Widget> _onboardingPages;
@override
void initState() {
super.initState();
initializeFlow();
}
void initializeFlow() async {
await Future.delayed(Duration(milliseconds: 800)); // 模拟资源预加载
final seen = await SharedPreferences.getInstance()
.then((s) => s.getBool('has_seen_onboard') ?? false);
if (!seen && buildOnboardingPages().isNotEmpty) {
setState(() { _pageState = 2; });
} else {
loadAdAndShow();
}
}
void loadAdAndShow() {
// 模拟获取广告数据
fetchAdData().then((ad) {
if (ad != null && ad.isValid) {
setState(() { _pageState = 1; });
startCountdown();
} else {
navigateToHome(context);
}
}).catchError((_) => navigateToHome(context));
}
List<Widget> buildOnboardingPages() {
final images = ['guide_1', 'guide_2', 'guide_3'].map(Utils.getAssetImage).toList();
return images.asMap().entries.map((entry) {
final index = entry.key;
final url = entry.value;
return Stack(
children: [
Image.asset(url, fit: BoxFit.cover, width: double.infinity, height: double.infinity),
if (index == images.length - 1)
Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: EdgeInsets.only(bottom: 120),
child: ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: Colors.blue),
onPressed: navigateToHome,
child: Text('开始体验', style: TextStyle(color: Colors.white)),
),
),
)
],
);
}).toList();
}
void startCountdown() {
int seconds = 3;
_countdownTimer = Timer.periodic(Duration(seconds: 1), (t) {
if (seconds <= 1) {
t.cancel();
navigateToHome(context);
} else {
setState(() { seconds--; });
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
// 默认启动图
Offstage(offstage: _pageState != 0, child: DefaultSplashBackground()),
// 引导页
Offstage(
offstage: _pageState != 2,
child: PageView.builder(
itemCount: _onboardingPages?.length ?? 0,
itemBuilder: (ctx, i) => _onboardingPages[i],
),
),
// 广告页
Offstage(
offstage: _pageState != 1,
child: GestureDetector(
onTap: () {
navigateToHome(context);
Navigator.of(context).push(createWebRoute(adUrl, '广告'));
},
child: CachedNetworkImage(
imageUrl: 'https://example.com/ad.jpg',
fit: BoxFit.fill,
placeholder: (ctx, url) => DefaultSplashBackground(),
),
),
),
// 跳过按钮(仅广告页显示)
Offstage(
offstage: _pageState != 1,
child: Align(
alignment: Alignment.topRight,
child: Container(
margin: EdgeInsets.all(16),
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Color.fromARGB(100, 0, 0, 0),
borderRadius: BorderRadius.circular(20),
),
child: GestureDetector(
onTap: () => navigateToHome(context),
child: Text('跳过 $_count', style: TextStyle(color: Colors.white, fontSize: 14)),
),
),
),
)
],
),
);
}
@override
void dispose() {
_countdownTimer?.cancel();
super.dispose();
}
}
其中 DefaultSplashBackground 可封装为通用本地图片组件:
Widget DefaultSplashBackground() {
return Image.asset(Utils.getImgPath('splash_bg'), fit: BoxFit.fill);
}