当前位置:首页 > 技术 > 正文内容

Flutter 实现启动页、广告页与引导页集成方案

访客 技术 2026年7月17日 1

在 Flutter 应用开发中,常见的启动流程包括启动图展示、新手引导页以及广告页跳转功能。本文介绍如何通过状态管理与路由控制实现一个完整的 Splash 流程。

核心依赖组件

  • shared_preferences:用于本地存储是否已显示引导页或缓存广告内容。
  • cached_network_image:高效加载并缓存网络图片作为广告背景。
  • swipeable_page_routeswiper:实现滑动引导页轮播效果。
  • 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 页面再进主页,返回会回到广告页。正确做法是:

  1. 先跳转主页(替换当前页)
  2. 再压入 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);
}
    

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

linux screen 用法详情 (nohup 的替代方案)

一、screen 是什么?能干嘛?screen 是一个终端复用器,可以:在一个 SSH 会话中开多个“虚拟终端”SSH 断线后,程序仍然在后台运行随时重新连接到原来的会话特别适合:nohup 的替代方案跑脚本 / 爬虫 / 训练模型运维、远程开发二、安装 screen# CentOS / Rocky / Almayum install -y screen# Debian / Ubuntuapt i...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。