📑 目录

  1. 📖 概述
  2. 📦 引入三方库步骤
  3. 🎨 具体页面实现功能
  4. ⚠️ 常见错误及解决方案
  5. 🔧 进阶功能

📖 概述

本教程将详细介绍如何在 Flutter 应用中集成 flutter_splash_screen 三方库,实现一个美观的启动页,支持静态和动态(天气动效)展示品牌 logo。

✨ 功能特性

  • ✅ 支持静态 Logo 展示
  • 🎬 支持动态动画效果(淡入、缩放、旋转)
  • 🌈 天气主题渐变背景
  • ⏱️ 可配置的显示时长
  • 🎯 平滑的页面过渡

📦 引入三方库步骤

📝 步骤 1:添加依赖

pubspec.yaml 文件的 dependencies 部分添加 flutter_splash_screen

dependencies:
  flutter:
    sdk: flutter
  flutter_splash_screen: ^3.0.0

📁 步骤 2:配置资源文件

pubspec.yamlflutter 部分添加资源路径:

flutter:
  uses-material-design: true
  
  assets:
    - assets/images/
    - assets/animations/

📂 步骤 3:创建资源文件夹

在项目根目录创建以下文件夹结构:

项目根目录/
├── assets/
│   ├── images/
│   │   └── logo.png          # 品牌 Logo 图片
│   └── animations/           # 动画资源(可选)

⬇️ 步骤 4:安装依赖

在终端执行以下命令:

flutter pub get

🎨 具体页面实现功能

1️⃣ 创建启动页组件

创建文件 lib/screens/splash_screen.dart

// 导入 Flutter Material 设计库,提供 UI 组件和主题
import 'package:flutter/material.dart';
// 导入异步操作库,用于定时器等异步功能
import 'dart:async';

/// 启动页组件
/// 支持静态和动态(天气动效)展示品牌 logo
class SplashScreen extends StatefulWidget {
  /// 启动页显示时长(毫秒),默认值为 2000,即 2 秒
  final int duration;
  
  /// 是否启用动态效果(天气动效)
  /// true: 启用淡入、缩放、旋转等动画效果
  /// false: 静态显示,无动画
  final bool enableAnimation;
  
  /// Logo 图片路径
  /// 相对于项目根目录的资源路径,默认 'assets/images/logo.png'
  final String logoPath;
  
  /// 启动完成后的回调函数
  /// 当启动页显示完成后会调用此回调,用于跳转到主页面
  final VoidCallback onComplete;

  /// 构造函数
  /// [key] - Widget 的唯一标识符,用于 Widget 树中的定位
  /// [duration] - 启动页显示时长,单位毫秒,默认 2000ms
  /// [enableAnimation] - 是否启用动画,默认 false
  /// [logoPath] - Logo 图片路径,默认 'assets/images/logo.png'
  /// [onComplete] - 启动完成回调,必须提供
  const SplashScreen({
    super.key,  // 调用父类构造函数,传递 key
    this.duration = 2000,  // 默认显示 2 秒
    this.enableAnimation = false,  // 默认禁用动画
    this.logoPath = 'assets/images/logo.png',  // 默认 Logo 路径
    required this.onComplete,  // 必须提供完成回调
  });

  /// 创建对应的 State 对象
  /// Flutter 框架会调用此方法创建状态管理对象
  
  State<SplashScreen> createState() => _SplashScreenState();
}
📝 代码解读

类定义说明:

  • SplashScreen extends StatefulWidget:继承自 StatefulWidget,因为启动页需要管理状态(动画、定时器等)
  • duration:启动页显示的时长,单位毫秒,默认 2000ms(2秒)
  • enableAnimation:布尔值,控制是否启用动画效果
  • logoPath:Logo 图片的资源路径,使用相对路径
  • onComplete:回调函数,当启动页显示完成后调用,用于跳转到主页面
  • super.key:传递给父类的 key,用于 Widget 树中的唯一标识

为什么使用 StatefulWidget?

启动页需要:

  1. 🔄 管理动画控制器的生命周期
  2. ⏰ 控制定时器的启动和取消
  3. 🎛️ 根据 enableAnimation 参数动态切换静态/动态模式
  4. 🧹 在 Widget 销毁时释放资源(防止内存泄漏)

2️⃣ 实现静态 Logo 展示

静态模式直接显示 Logo,无动画效果:

/// 构建静态 Logo Widget
/// 静态模式:直接显示 Logo 和文字,无动画效果
Widget _buildStaticLogo() {
  // Column: 垂直布局容器,用于垂直排列子 Widget
  return Column(
    // mainAxisAlignment: 主轴对齐方式(Column 的主轴是垂直方向)
    // MainAxisAlignment.center: 垂直居中对齐
    mainAxisAlignment: MainAxisAlignment.center,
    // children: 子 Widget 列表
    children: [
      // Image.asset: 加载资源文件中的图片
      Image.asset(
        widget.logoPath,  // 图片路径,从 widget 属性中获取
        width: 150,        // 图片宽度:150 像素
        height: 150,       // 图片高度:150 像素
        // errorBuilder: 错误处理回调
        // 当图片加载失败(文件不存在、格式错误等)时调用
        errorBuilder: (context, error, stackTrace) {
          // 如果图片不存在,显示占位符
          // 返回一个美观的占位符 Widget,避免应用崩溃
          return Container(
            width: 150,   // 占位符宽度:150 像素(与图片尺寸一致)
            height: 150,  // 占位符高度:150 像素
            // decoration: 容器装饰
            decoration: BoxDecoration(
              color: Colors.white,  // 背景颜色:白色
              // borderRadius: 圆角半径
              borderRadius: BorderRadius.circular(20),  // 20 像素圆角
              // boxShadow: 阴影效果列表
              boxShadow: [
                BoxShadow(
                  color: Colors.black.withOpacity(0.1),  // 阴影颜色:黑色,10% 透明度
                  blurRadius: 10,  // 模糊半径:10 像素
                  spreadRadius: 2,  // 扩散半径:2 像素
                ),
              ],
            ),
            // child: 占位符内容
            child: const Icon(
              Icons.wb_sunny,  // Material Design 图标:晴天图标
              size: 80,        // 图标大小:80 像素
              color: Colors.orange,  // 图标颜色:橙色
            ),
          );
        },
      ),
      // SizedBox: 固定尺寸的空白 Widget,用于创建间距
      const SizedBox(height: 30),  // 高度 30 像素的空白,作为 Logo 和文字之间的间距
      // Text: 文本 Widget,显示应用名称
      const Text(
        '天气预报',  // 文本内容
        style: TextStyle(
          fontSize: 28,              // 字体大小:28
          fontWeight: FontWeight.bold,  // 字体粗细:粗体
          color: Colors.white,       // 文字颜色:白色
          letterSpacing: 2,          // 字符间距:2 像素
        ),
      ),
    ],
  );
}

image-20260124210659131

代码解读

📐 布局结构:

  1. Column 组件

    • 垂直布局容器,用于排列 Logo 和文字
    • mainAxisAlignment: MainAxisAlignment.center:垂直居中,使内容在屏幕中央显示
  2. Image.asset() 组件

    • 加载资源文件中的图片
    • widget.logoPath:通过 widget. 访问 StatefulWidget 的属性
    • width: 150, height: 150:固定尺寸,保持 Logo 比例
    • errorBuilder 参数:关键的错误处理机制
      • 当图片文件不存在或加载失败时触发
      • 返回一个占位符 Widget,避免应用崩溃
      • 使用 Container + Icon 创建一个美观的占位符
  3. SizedBox(height: 30)

    • 间距组件,在 Logo 和文字之间创建 30 像素的垂直间距
    • const 关键字优化性能,因为高度值在编译时已知
  4. Text 组件

    • 显示应用名称
    • letterSpacing: 2:字符间距,增加视觉美感
    • const 关键字:因为文本内容不变,使用 const 可以避免不必要的重建

⚠️ 错误处理的重要性:

errorBuilder 是 Flutter 中处理资源加载失败的标准方式。如果不提供 errorBuilder,当图片不存在时会抛出异常,导致应用崩溃。使用占位符可以:

  • 👥 提升用户体验(即使资源缺失也能正常显示)
  • 🐛 便于开发调试(快速发现资源问题)
  • 🛡️ 增强应用健壮性
🔍 2.1 build 方法详解

Widget build(BuildContext context) {
  return Scaffold(
    body: Container(
      decoration: BoxDecoration(
        // 渐变背景,模拟天气主题
        gradient: LinearGradient(
          begin: Alignment.topLeft,
          end: Alignment.bottomRight,
          colors: [
            Colors.blue.shade300,
            Colors.blue.shade100,
            Colors.white,
          ],
        ),
      ),
      child: Center(
        child: widget.enableAnimation
            ? _buildAnimatedLogo()
            : _buildStaticLogo(),
      ),
    ),
  );
}

代码解读 - build 方法:

build() 方法:

  • 调用时机:Widget 首次创建时,以及状态改变时(通过 setState()
  • 返回值:必须返回一个 Widget,描述当前 UI 状态
  • 性能优化:Flutter 框架会智能地只重建变化的部分

Widget 树结构:

enableAnimation = true

enableAnimation = false

Scaffold
页面骨架

Container
容器,用于设置背景

BoxDecoration
装饰,定义渐变背景

Center
居中布局

条件渲染

_buildAnimatedLogo
动态Logo

_buildStaticLogo
静态Logo

渐变背景详解:

gradient: LinearGradient(
  begin: Alignment.topLeft,      // 渐变起始点:左上角
  end: Alignment.bottomRight,     // 渐变结束点:右下角
  colors: [
    Colors.blue.shade300,  // 起始颜色:浅蓝色
    Colors.blue.shade100,  // 中间颜色:更浅的蓝色
    Colors.white,         // 结束颜色:白色
  ],
)
  • LinearGradient:线性渐变,从一种颜色平滑过渡到另一种
  • begin/end:定义渐变方向,从左上到右下
  • colors:颜色列表,至少需要 2 个颜色,可以有多个中间色
  • 视觉效果:模拟天空渐变,符合天气应用主题

条件渲染:

child: widget.enableAnimation
    ? _buildAnimatedLogo()
    : _buildStaticLogo(),
  • 三元运算符:根据 enableAnimation 的值选择不同的 Widget
  • widget.:访问 StatefulWidget 的属性
  • 性能考虑:只构建需要的 Widget,不构建不需要的部分
  • 代码组织:将不同模式的 UI 分离到不同方法,提高可读性

3️⃣ 实现动态 Logo 展示(天气动效)

动态模式包含以下动画效果:

  • 🌅 淡入动画:Logo 从透明到不透明
  • 📏 缩放动画:Logo 从 0.5 倍缩放到 1.0 倍(弹性效果)
  • 🔄 旋转动画:太阳图标旋转
🎬 3.1 动画控制器初始化
/// 启动页状态类
/// 管理启动页的状态、动画和定时器
class _SplashScreenState extends State<SplashScreen>
    with SingleTickerProviderStateMixin {  // 混入 SingleTickerProviderStateMixin,提供 vsync 信号给动画控制器
  /// 动画控制器
  /// 控制动画的播放、暂停、停止等操作
  /// late 关键字表示延迟初始化,在 initState 中赋值
  late AnimationController _controller;
  
  /// 淡入动画对象
  /// Animation<double> 类型,值范围从 0.0(透明)到 1.0(不透明)
  late Animation<double> _fadeAnimation;
  
  /// 缩放动画对象
  /// Animation<double> 类型,值范围从 0.5(缩小到50%)到 1.0(原始大小)
  late Animation<double> _scaleAnimation;
  
  /// 定时器对象
  /// Timer? 表示可空类型,用于在指定时间后触发回调
  Timer? _timer;

  /// Widget 初始化方法
  /// 在 Widget 插入到 Widget 树后立即调用,只执行一次
  
  void initState() {
    super.initState();  // 调用父类 initState,确保父类初始化完成
    
    // 如果启用了动画,初始化动画相关对象
    if (widget.enableAnimation) {
      // 初始化动画控制器
      // duration: 动画持续时间,1500ms = 1.5 秒
      // vsync: 垂直同步信号,this 由 SingleTickerProviderStateMixin 提供
      // vsync 确保动画与屏幕刷新率同步(通常 60fps),提升性能
      _controller = AnimationController(
        duration: const Duration(milliseconds: 1500),  // 动画持续 1.5 秒
        vsync: this,  // 使用混入的 SingleTickerProviderStateMixin 提供的 vsync
      );

      // 创建淡入动画
      // Tween: 定义动画的起始值和结束值
      // begin: 0.0 表示完全透明
      // end: 1.0 表示完全不透明
      // CurvedAnimation: 为动画添加缓动曲线
      // Curves.easeIn: 缓动曲线,开始慢,逐渐加速
      _fadeAnimation = Tween<double>(
        begin: 0.0,  // 起始值:完全透明
        end: 1.0,    // 结束值:完全不透明
      ).animate(CurvedAnimation(
        parent: _controller,  // 绑定到动画控制器
        curve: Curves.easeIn,  // 使用 easeIn 缓动曲线(先慢后快)
      ));

      // 创建缩放动画
      // begin: 0.5 表示缩小到原始大小的 50%
      // end: 1.0 表示恢复到原始大小
      // Curves.elasticOut: 弹性曲线,有回弹效果,更生动
      _scaleAnimation = Tween<double>(
        begin: 0.5,  // 起始值:缩小到 50%
        end: 1.0,    // 结束值:原始大小(100%)
      ).animate(CurvedAnimation(
        parent: _controller,  // 绑定到同一个动画控制器
        curve: Curves.elasticOut,  // 使用弹性曲线(有回弹效果)
      ));

      // 启动动画
      // forward() 方法让动画从 0.0 播放到 1.0
      // 动画过程中会触发 AnimatedBuilder 的 builder 方法重建 Widget
      _controller.forward();
    }

    // 设置定时器,在指定时间后完成启动
    // Timer: 异步定时器,不会阻塞 UI 线程
    // Duration: 延迟时间,使用 widget.duration 的值(默认 2000ms)
    // 回调函数:当时间到达后执行,调用 onComplete 通知外部启动页已完成
    _timer = Timer(Duration(milliseconds: widget.duration), () {
      widget.onComplete();  // 调用完成回调,触发页面跳转
    });
  }
}

image-20260124210820549

📝 代码解读 - 动画初始化

1️⃣ SingleTickerProviderStateMixin

  • 作用:提供 vsync(垂直同步)信号给 AnimationController
  • 为什么需要:动画控制器需要知道何时刷新画面,vsync 确保动画与屏幕刷新率同步(通常 60fps)
  • 性能优化:当 Widget 不可见时自动暂停动画,节省资源

2️⃣ AnimationController

  • duration:动画持续时间,1500ms = 1.5 秒
  • vsync: this:使用 SingleTickerProviderStateMixin 提供的同步信号
  • 生命周期:必须在 dispose() 中释放,否则会造成内存泄漏

3️⃣ TweenAnimation

  • Tween<double>:定义动画的起始值和结束值
    • begin: 0.0end: 1.0:表示从 0% 到 100%
  • CurvedAnimation:为动画添加缓动曲线
    • Curves.easeIn:淡入效果,开始慢,逐渐加速
    • Curves.elasticOut:弹性效果,有回弹,更生动

4️⃣ _controller.forward()

  • 启动动画,从 0.0 播放到 1.0
  • 动画过程中会触发 AnimatedBuilderbuilder 方法重建 Widget

5️⃣ Timer

  • 异步定时器,在指定时间后执行回调
  • widget.onComplete():启动页显示完成后调用,触发页面跳转
  • 使用 Timer? 可空类型,便于在 dispose() 中安全取消
🎨 3.2 动态 Logo 构建
Widget _buildAnimatedLogo() {
  return AnimatedBuilder(
    animation: _controller,
    builder: (context, child) {
      return Opacity(
        opacity: _fadeAnimation.value,
        child: Transform.scale(
          scale: _scaleAnimation.value,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Image.asset(widget.logoPath, width: 150, height: 150),
              const SizedBox(height: 30),
              const Text('天气预报', style: TextStyle(...)),
              const SizedBox(height: 20),
              // 旋转的太阳图标
              RotationTransition(
                turns: AlwaysStoppedAnimation(_controller.value * 2),
                child: const Icon(Icons.wb_sunny, size: 40, color: Colors.orange),
              ),
            ],
          ),
        ),
      );
    },
  );
}
📝 代码解读 - 动画 Widget

1️⃣ AnimatedBuilder

  • 作用:监听动画值变化,自动重建子 Widget
  • animation: _controller:监听动画控制器的值变化
  • builder 方法:每次动画值变化时调用,返回新的 Widget 树
  • 性能优化:只重建 builder 返回的部分,而不是整个 Widget 树

2️⃣ Opacity Widget:

  • 作用:控制 Widget 的透明度
  • opacity: _fadeAnimation.value
    • 动画开始时:value = 0.0(完全透明)
    • 动画结束时:value = 1.0(完全不透明)
    • 中间过程:平滑过渡

3️⃣ Transform.scale Widget:

  • 作用:缩放变换
  • scale: _scaleAnimation.value
    • 动画开始时:value = 0.5(缩小到 50%)
    • 动画结束时:value = 1.0(原始大小)
    • 配合 Curves.elasticOut:有弹性回弹效果

4️⃣ RotationTransition Widget:

  • 作用:旋转动画
  • turns: AlwaysStoppedAnimation(_controller.value * 2)
    • _controller.value:范围 0.0 到 1.0
    • * 2:乘以 2,范围变为 0.0 到 2.0
    • turns:表示旋转圈数,2.0 = 旋转 2 圈(720度)
    • AlwaysStoppedAnimation:将普通值包装成动画对象

🎯 动画组合原理:

AnimatedBuilder
监听动画

Opacity
淡入效果

Transform.scale
缩放效果

Column
布局容器

Image
Logo

Text
应用名称

RotationTransition
旋转图标

多个动画效果通过 Widget 嵌套实现,每个 Widget 负责一种变换效果。

性能考虑:

  • AnimatedBuilder 只重建必要的部分,性能优于 setState()
  • 使用 const 标记不变的 Widget(如 TextIcon),减少重建
  • 动画结束后,可以考虑停止动画控制器以节省资源
3.3 资源释放(dispose 方法)

void dispose() {
  _timer?.cancel();  // 取消定时器
  if (widget.enableAnimation) {
    _controller.dispose();  // 释放动画控制器
  }
  super.dispose();  // 调用父类方法
}

📝 代码解读 - 资源管理:

为什么需要 dispose()

  1. 🛡️ 防止内存泄漏

    • AnimationController 持有 vsync 引用
    • Timer 持有回调函数引用
    • 如果不释放,这些对象会一直占用内存
  2. 🛑 停止后台操作

    • Timer 可能在 Widget 销毁后触发回调
    • 调用 cancel() 防止执行已销毁 Widget 的回调
  3. 💾 释放系统资源

    • AnimationController 会占用 GPU 资源
    • 及时释放可以提升应用性能

📊 执行顺序:

dispose 被调用

启用动画?

_timer?.cancel
取消定时器

启用动画?

_controller.dispose
释放动画控制器

super.dispose
调用父类方法

完成清理

常见错误:

// ❌ 错误:忘记释放资源

void dispose() {
  super.dispose();  // 只调用父类方法
  // AnimationController 和 Timer 没有被释放!
}

// ✅ 正确:完整释放所有资源

void dispose() {
  _timer?.cancel();
  if (widget.enableAnimation) {
    _controller.dispose();
  }
  super.dispose();
}

最佳实践:

  • initState() 中创建的资源,必须在 dispose() 中释放
  • 使用 ?. 安全调用,避免空指针异常
  • 先释放子资源,再调用 super.dispose()
  • 条件释放:只在需要时释放(如 if (widget.enableAnimation)

4. 集成到主应用

修改 lib/main.dart

// 导入 Flutter Material 设计库,提供 UI 组件和主题
import 'package:flutter/material.dart';
// 导入 flutter_splash_screen 三方库,用于隐藏原生启动屏
import 'package:flutter_splash_screen/flutter_splash_screen.dart';
// 导入自定义的启动页组件
import 'screens/splash_screen.dart';

/// 应用程序入口点
/// Flutter 应用启动时,首先执行此函数
void main() {
  // runApp: 启动 Flutter 应用
  // MyApp 是应用的根 Widget,Flutter 会将其渲染到屏幕上
  runApp(const MyApp());
}

/// 应用的根 Widget
/// StatelessWidget: 无状态 Widget,不需要管理状态
class MyApp extends StatelessWidget {
  /// 构造函数
  const MyApp({super.key});

  /// 构建 Widget 树
  /// MaterialApp: Material Design 风格的应用容器
  
  Widget build(BuildContext context) {
    return MaterialApp(
      // title: 应用标题,用于系统任务管理器显示
      title: '天气预报',
      // theme: 应用主题配置
      theme: ThemeData(
        // colorScheme: 颜色方案,从种子颜色生成
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        // useMaterial3: 是否使用 Material Design 3
        useMaterial3: true,
      ),
      // home: 应用的首页 Widget
      home: const SplashWrapper(),
      // debugShowCheckedModeBanner: 是否显示调试横幅
      debugShowCheckedModeBanner: false,
    );
  }
}

/// 启动页包装器 Widget
/// 负责显示启动页并处理跳转逻辑
class SplashWrapper extends StatefulWidget {
  /// 构造函数
  const SplashWrapper({super.key});

  /// 创建对应的 State 对象
  
  State<SplashWrapper> createState() => _SplashWrapperState();
}

/// 启动页包装器的状态类
/// 管理启动页的显示状态和生命周期
class _SplashWrapperState extends State<SplashWrapper> {
  /// 是否显示启动页的标志
  /// true: 显示启动页,false: 显示主页面
  bool _showSplash = true;

  /// Widget 初始化方法
  
  void initState() {
    super.initState();  // 调用父类 initState
    // 隐藏原生启动屏
    _hideNativeSplash();
  }

  /// 隐藏原生启动屏
  /// async: 异步方法,不会阻塞 UI 线程
  Future<void> _hideNativeSplash() async {
    // 延迟 100ms,确保 Flutter 引擎完全初始化
    await Future.delayed(const Duration(milliseconds: 100));
    // mounted: 检查 Widget 是否还在 Widget 树中
    if (mounted) {
      // 调用三方库方法,隐藏原生启动屏
      await FlutterSplashScreen.hide();
    }
  }

  /// 启动页完成回调函数
  /// 当 SplashScreen 显示完成后会调用此方法
  void _onSplashComplete() {
    // mounted: 安全检查,确保 Widget 仍然存在
    if (mounted) {
      // setState: 通知 Flutter 框架状态已改变
      setState(() {
        _showSplash = false;  // 切换到主页面
      });
    }
  }

  /// 构建 Widget 树
  /// 根据 _showSplash 的值返回不同的 Widget
  
  Widget build(BuildContext context) {
    // 如果 _showSplash 为 true,显示启动页
    if (_showSplash) {
      return SplashScreen(
        duration: 2000,  // 启动页显示时长:2 秒
        enableAnimation: true,  // 启用动态效果(天气动效)
        logoPath: 'assets/images/logo.png',  // Logo 图片路径
        onComplete: _onSplashComplete,  // 启动完成回调
      );
    }
    
    // 如果 _showSplash 为 false,显示主页面
    return const MyHomePage(title: '天气预报');
  }
}

image-20260124210912410

📝 代码解读 - 主应用集成

1️⃣ main() 函数:

  • Flutter 应用的入口点
  • runApp(const MyApp()):启动应用,MyApp 是根 Widget
  • const 关键字:优化性能,因为 MyApp 实例不会改变

2️⃣ MyApp Widget:

  • MaterialApp:Material Design 风格的应用容器
    • title:应用标题,用于系统任务管理器显示
    • theme:应用主题,定义颜色、字体等
    • home:应用的首页,设置为 SplashWrapper
    • debugShowCheckedModeBanner: false:隐藏右上角的调试横幅

3️⃣ SplashWrapper - 状态管理核心:

状态变量:

bool _showSplash = true;
  • 控制是否显示启动页
  • true:显示启动页
  • false:显示主页面

生命周期方法:

initState()


void initState() {
  super.initState();
  _hideNativeSplash();
}
  • Widget 初始化时调用,只执行一次
  • 调用 _hideNativeSplash() 隐藏原生启动屏

_hideNativeSplash() 方法:

Future<void> _hideNativeSplash() async {
  await Future.delayed(const Duration(milliseconds: 100));
  if (mounted) {
    await FlutterSplashScreen.hide();
  }
}

📝 详细解读:

  1. async/await:异步方法,不阻塞 UI 线程
  2. Future.delayed():延迟 100ms 执行
    • 为什么需要延迟?:确保 Flutter 引擎完全初始化后再隐藏原生启动屏
    • 如果立即调用可能失败,因为 Flutter 还未准备好
  3. if (mounted):安全检查
    • mounted:Widget 是否还在 Widget 树中
    • 为什么重要?:异步操作可能在 Widget 销毁后完成
    • 如果 Widget 已销毁,调用 setState() 会报错
    • 这是 Flutter 开发的最佳实践
  4. FlutterSplashScreen.hide()
    • 调用三方库的方法,隐藏 Android/iOS 的原生启动屏
    • 让自定义的 Flutter 启动页显示出来

🔄 _onSplashComplete() 方法:

void _onSplashComplete() {
  if (mounted) {
    setState(() {
      _showSplash = false;
    });
  }
}

📝 详细解读:

  1. ⏰ 回调时机:当 SplashScreen 显示完成后调用

  2. mounted 检查:确保 Widget 仍然存在

  3. 🔄 setState():触发 Widget 重建

    • 改变 _showSplash 的值
    • Flutter 框架检测到状态变化,调用 build() 方法
  4. 📊 状态变化流程

    _showSplash = true
    显示 SplashScreen

    _onSplashComplete
    回调触发

    设置 _showSplash = false

    setState
    触发 rebuild

    build 方法
    返回 MyHomePage

build() 方法:


Widget build(BuildContext context) {
  if (_showSplash) {
    return SplashScreen(...);
  }
  return const MyHomePage(title: '天气预报');
}

📝 详细解读:

  1. 🔀 条件渲染:根据 _showSplash 的值返回不同的 Widget
  2. 🎯 状态驱动 UI:这是 Flutter 的核心思想
    • 状态改变 → UI 自动更新
    • 不需要手动操作 DOM 或 View
  3. ✨ 平滑过渡:Flutter 会自动处理页面切换动画

📊 完整流程:

应用启动

MyApp.build
返回 SplashWrapper

SplashWrapper.initState
隐藏原生启动屏

SplashWrapper.build
_showSplash = true
返回 SplashScreen

SplashScreen 显示
2秒

Timer 触发
调用 onComplete

_onSplashComplete
setState _showSplash = false

SplashWrapper.build
_showSplash = false
返回 MyHomePage

显示主页面

🎯 关键设计模式:

  1. 📤 状态提升:将状态管理放在 SplashWrapper,而不是 SplashScreen
  2. 🔗 回调函数:通过 onComplete 回调实现组件间通信
  3. 🔀 条件渲染:使用 if 语句实现页面切换
  4. 🔄 生命周期管理:正确处理异步操作和资源释放

5️⃣ 使用方式

📌 5.1 静态模式
SplashScreen(
  duration: 2000,
  enableAnimation: false, // 禁用动画
  logoPath: 'assets/images/logo.png',
  onComplete: () {
    // 跳转到主页面
  },
)
🎬 5.2 动态模式
SplashScreen(
  duration: 2000,
  enableAnimation: true, // 启用动画
  logoPath: 'assets/images/logo.png',
  onComplete: () {
    // 跳转到主页面
  },
)

常见错误及解决方案

错误 1:图片资源找不到

错误信息:

Unable to load asset: assets/images/logo.png

✅ 解决方案:

  1. 📁 检查文件路径:确保图片文件存在于 assets/images/logo.png

  2. 📝 检查 pubspec.yaml:确保已正确配置资源路径:

    flutter:
      assets:
        - assets/images/
    
  3. 🔄 重新运行:执行 flutter clean 然后 flutter pub get

  4. 🛡️ 使用错误处理:在代码中添加 errorBuilder

    Image.asset(
      widget.logoPath,
      errorBuilder: (context, error, stackTrace) {
        return Icon(Icons.error); // 显示占位符
      },
    )
    

❌ 错误 2:依赖安装失败

错误信息:

Could not find a file named "pubspec.yaml" in ...

✅ 解决方案:

  1. 📂 确保在项目根目录执行命令
  2. 📝 检查 pubspec.yaml 文件格式是否正确(YAML 缩进)
  3. 🌐 检查网络连接,可能需要配置国内镜像源

❌ 错误 3:动画控制器未初始化

错误信息:

AnimationController not initialized

✅ 解决方案:

确保在 initState 中初始化动画控制器:


void initState() {
  super.initState();
  if (widget.enableAnimation) {
    _controller = AnimationController(
      duration: const Duration(milliseconds: 1500),
      vsync: this,  // 必须提供 vsync,需要混入 SingleTickerProviderStateMixin
    );
  }
}

代码解读:

  • vsync: this:需要 Widget 混入 SingleTickerProviderStateMixin
  • 条件初始化:只在 enableAnimation = true 时初始化,节省资源
  • 必须在 initState 中初始化:因为此时 Widget 已创建,this 引用有效

错误 4:内存泄漏警告

错误信息:

A AnimationController was used after being disposed

解决方案:

确保在 dispose 方法中正确释放资源:


void dispose() {
  _timer?.cancel();  // 取消定时器,防止回调执行
  if (widget.enableAnimation) {
    _controller.dispose();  // 释放动画控制器
  }
  super.dispose();  // 调用父类 dispose
}

📝 代码解读:

  • dispose():Widget 销毁时调用,用于清理资源
  • _timer?.cancel():使用 ?. 安全调用,如果 _timer 为 null 则不执行
  • _controller.dispose():释放动画控制器,停止动画并清理资源
  • super.dispose():最后调用父类方法,确保完整的清理流程
  • 顺序很重要:先取消定时器,再释放控制器,最后调用父类方法

❌ 错误 5:原生启动屏未隐藏

错误信息:
启动页显示后,原生启动屏仍然可见

✅ 解决方案:

  1. ✅ 确保在 initState 中调用 FlutterSplashScreen.hide()

  2. ⏱️ 添加适当的延迟:

    Future<void> _hideNativeSplash() async {
      await Future.delayed(const Duration(milliseconds: 100));
      if (mounted) {
        await FlutterSplashScreen.hide();
      }
    }
    
  3. 🔧 检查 Android/iOS 平台配置是否正确

📝 代码解读:

  • 为什么需要延迟?:Flutter 引擎需要时间初始化,立即调用可能失败
  • 延迟时间选择:100ms 是经验值,太短可能失败,太长影响用户体验
  • mounted 检查:异步操作可能在 Widget 销毁后完成,必须检查
  • await 关键字:等待操作完成,确保原生启动屏已隐藏

❌ 错误 6:页面跳转异常

错误信息:
启动页完成后无法跳转到主页面

✅ 解决方案:

  1. ✅ 使用 mounted 检查确保 Widget 仍然存在:

    void _onSplashComplete() {
      if (mounted) {
        setState(() {
          _showSplash = false;
        });
      }
    }
    
  2. 🔀 确保使用 Navigator 或状态管理正确跳转

📝 代码解读:

  • mounted 的重要性
    • 当 Widget 从树中移除后,mounted = false
    • 此时调用 setState() 会抛出异常
    • 异步回调中必须检查 mounted
  • 状态驱动的跳转
    • 不直接使用 Navigator.push()
    • 通过改变状态(_showSplash)触发 build() 重建
    • Flutter 自动处理页面切换,更符合框架设计
  • 为什么不用 Navigator?
    • 启动页是应用的一部分,不是独立路由
    • 使用状态管理更简洁,无需管理路由栈

❌ 错误 7:YAML 格式错误

错误信息:

YAMLException: mapping values are not allowed here

✅ 解决方案:

检查 pubspec.yaml 文件的缩进,YAML 使用 2 个空格缩进:

# ✅ 正确
flutter:
  assets:
    - assets/images/

# ❌ 错误(使用了 Tab 或 4 个空格)
flutter:
    assets:
        - assets/images/

🔧 进阶功能

🎨 1. 自定义动画效果

可以扩展动画效果,例如添加平移、旋转等:

// 添加平移动画
Animation<Offset> _slideAnimation = Tween<Offset>(
  begin: const Offset(0, -0.5),  // 起始位置:向上偏移 50%
  end: Offset.zero,              // 结束位置:原始位置
).animate(CurvedAnimation(
  parent: _controller,
  curve: Curves.easeOut,  // 缓动曲线:先快后慢
));

// 在 Widget 中使用
SlideTransition(
  position: _slideAnimation,
  child: Image.asset(widget.logoPath),
)
代码解读

Offset 类型:

  • Offset(x, y):表示二维坐标偏移量
  • Offset(0, -0.5):x=0(水平不偏移),y=-0.5(向上偏移 50%)
  • Offset.zeroOffset(0, 0),无偏移

SlideTransition Widget:

  • 实现平移动画效果
  • position:接收 Animation<Offset> 类型
  • 动画过程中,Widget 会从起始位置平滑移动到结束位置

组合多个动画:

// 可以同时应用多个动画效果
Opacity(
  opacity: _fadeAnimation.value,
  child: Transform.scale(
    scale: _scaleAnimation.value,
    child: SlideTransition(
      position: _slideAnimation,
      child: Image.asset(widget.logoPath),
    ),
  ),
)

这样 Logo 会同时具有:淡入 + 缩放 + 平移 三种效果。

📥 2. 加载数据时显示启动页

可以在启动页显示期间加载必要的数据:


void initState() {
  super.initState();
  _loadData();
}

Future<void> _loadData() async {
  // 加载用户数据、配置等
  await Future.wait([
    _loadUserData(),
    _loadAppConfig(),
    Future.delayed(Duration(milliseconds: widget.duration)),
  ]);
  
  widget.onComplete();
}
📝 代码解读

Future.wait() 方法:

  • 作用:并行执行多个异步操作,等待所有操作完成
  • 优势:比顺序执行(await _loadUserData(); await _loadAppConfig())更快
  • 返回值:所有 Future 的结果列表

📊 执行流程:

启动页显示

并行执行

_loadUserData
从服务器获取用户信息

_loadAppConfig
读取本地配置文件

Future.delayed 2000ms
确保至少显示 2 秒

所有操作完成

调用 onComplete
跳转到主页面

💡 实际应用示例:

Future<void> _loadData() async {
  try {
    await Future.wait([
      // 加载用户数据
      _loadUserData(),
      // 加载应用配置
      _loadAppConfig(),
      // 初始化数据库
      _initDatabase(),
      // 确保启动页至少显示 2 秒
      Future.delayed(Duration(milliseconds: widget.duration)),
    ]);
  } catch (e) {
    // 处理错误,例如显示错误提示
    print('数据加载失败: $e');
  } finally {
    // 无论成功或失败,都跳转到主页面
    widget.onComplete();
  }
}

⚠️ 注意事项:

  • 如果某个操作失败,Future.wait() 会抛出异常
  • 建议使用 try-catch 处理错误
  • 确保即使加载失败也能进入主页面,避免用户卡在启动页

🎛️ 3. 根据条件选择静态/动态模式

可以根据设备性能或用户设置选择模式:

SplashScreen(
  enableAnimation: _shouldUseAnimation(), // 根据条件决定
  // ...
)

bool _shouldUseAnimation() {
  // 可以根据设备性能、用户设置等决定
  return true;
}
📝 代码解读

💡 实际应用场景:

bool _shouldUseAnimation() {
  // 方式1:根据用户设置
  final prefs = SharedPreferences.getInstance();
  final enableAnimations = prefs.getBool('enable_animations') ?? true;
  if (!enableAnimations) return false;
  
  // 方式2:根据设备性能(需要导入 package_info_plus)
  // 低端设备禁用动画以提升性能
  // final deviceInfo = await DeviceInfoPlugin().androidInfo;
  // if (deviceInfo.systemFeatures.contains('low_ram')) {
  //   return false;
  // }
  
  // 方式3:根据电池电量
  // final batteryLevel = await Battery().batteryLevel;
  // if (batteryLevel < 20) {
  //   return false;  // 低电量时禁用动画节省电量
  // }
  
  return true;
}

⚡ 性能优化考虑:

  • 低端设备:禁用复杂动画,提升启动速度
  • 低电量模式:减少动画,节省电池
  • 用户偏好:允许用户在设置中关闭动画
  • 网络环境:弱网环境下可以禁用动画,加快加载

📊 4. 添加加载进度指示器

在启动页显示加载进度:

Column(
  children: [
    Image.asset(widget.logoPath),
    const SizedBox(height: 30),
    const CircularProgressIndicator(
      valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
    ),
  ],
)
📝 代码解读

CircularProgressIndicator Widget:

  • 作用:显示圆形进度指示器(加载动画)
  • valueColor:设置进度条颜色
  • AlwaysStoppedAnimation:将固定值包装成动画对象
    • 这里用于设置颜色,不是动画值
    • Colors.white:白色进度条,在深色背景上更明显

📈 显示实际进度:

// 如果有实际的加载进度值(0.0 到 1.0)
CircularProgressIndicator(
  value: _loadingProgress,  // 0.0 = 0%, 1.0 = 100%
  valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
  backgroundColor: Colors.white.withOpacity(0.3),  // 背景色
)

// 在加载数据时更新进度
void _updateProgress(double progress) {
  setState(() {
    _loadingProgress = progress;
  });
}

📏 线性进度条:

LinearProgressIndicator(
  value: _loadingProgress,
  valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
  backgroundColor: Colors.white.withOpacity(0.3),
)

🔄 无进度值(无限旋转):

// 不设置 value,显示无限旋转动画
const CircularProgressIndicator(
  valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
)

🌍 5. 支持多语言

添加多语言支持:

Text(
  Localizations.of(context).appName, // 使用本地化字符串
  style: TextStyle(...),
)
📝 代码解读

🌍 Flutter 国际化(i18n):

  1. 📦 添加依赖
dependencies:
  flutter_localizations:
    sdk: flutter
  intl: ^0.18.0
  1. ⚙️ 配置 MaterialApp
MaterialApp(
  localizationsDelegates: [
    GlobalMaterialLocalizations.delegate,
    GlobalWidgetsLocalizations.delegate,
    GlobalCupertinoLocalizations.delegate,
  ],
  supportedLocales: [
    Locale('zh', 'CN'),  // 中文
    Locale('en', 'US'),  // 英文
  ],
)
  1. 💬 使用本地化字符串
// 方式1:使用 Localizations
Text(
  Localizations.of(context).appName,
)

// 方式2:使用自定义本地化类(推荐)
Text(
  AppLocalizations.of(context)!.appName,
)

实际应用:

// 在启动页中使用
Text(
  AppLocalizations.of(context)!.splashTitle,  // '天气预报' 或 'Weather Forecast'
  style: TextStyle(...),
)

✨ 优势:

  • 🌐 根据系统语言自动切换
  • 🔄 支持动态切换语言
  • 🛠️ 代码更易维护
  • 📋 符合国际化标准

📚 总结

通过本教程,你已经学会了:

  1. ✅ 如何引入 flutter_splash_screen 三方库
  2. ✅ 如何创建支持静态和动态展示的启动页
  3. ✅ 如何集成启动页到主应用
  4. ✅ 如何处理常见错误
  5. ✅ 如何实现进阶功能

💡 最佳实践

  1. 📦 资源优化:使用适当大小的图片,避免启动页过大影响性能
  2. 动画性能:避免过于复杂的动画,保持流畅度
  3. 👥 用户体验:启动页时长建议 1-3 秒,过长会影响用户体验
  4. 🛡️ 错误处理:始终添加错误处理,避免因资源缺失导致应用崩溃
  5. 🔄 代码复用:将启动页组件化,便于在不同项目中使用

📖 相关资源


🎉 祝你开发顺利! 🚀

欢迎加入开源鸿蒙跨平台社区

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐