【maaath】Flutter for OpenHarmony 集成应用更新能力

Flutter for OpenHarmony 集成应用更新能力:热更新检测、增量更新、强制更新与更新弹窗

作者:maaath


社区引导

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


前言

在移动应用开发中,应用更新能力是保障用户体验和功能迭代的重要环节。Flutter 作为跨平台框架,在 OpenHarmony 上的应用越来越广泛。本文将详细介绍如何在 Flutter for OpenHarmony 项目中实现完整的应用更新能力,包括热更新检测、增量更新、强制更新以及美观的更新弹窗设计。


一、技术概述

1.1 什么是应用更新能力

应用更新能力是指应用在运行过程中,能够检测到新版本、提示用户下载安装的一系列功能。根据更新策略的不同,可以分为:

  • 热更新(Hotfix):小体积补丁,快速修复紧急问题
  • 增量更新(Incremental):差分包,只下载变化的部分
  • 全量更新(Full):完整安装包,适合大版本升级
  • 强制更新(Mandatory):用户必须更新,无法跳过

1.2 Flutter for OpenHarmony 的优势

Flutter 提供了跨平台的一致性体验,在 OpenHarmony 设备上,我们可以通过 flutter_banner 或原生插件机制实现应用更新功能。结合 ArkUI 的原生组件,可以打造既美观又高效的更新体验。


二、项目结构设计

在开始实现之前,我们需要设计清晰的项目结构:

复制代码
lib/
├── main.dart                    # 应用入口
├── model/
│   └── update_models.dart       # 更新相关数据模型
├── service/
│   └── update_service.dart     # 更新业务逻辑服务
├── widgets/
│   ├── update_dialog.dart      # 更新弹窗组件
│   └── download_progress.dart  # 下载进度组件
└── pages/
    └── update_page.dart        # 更新演示页面

三、核心数据模型实现

3.1 更新状态枚举

dart 复制代码
/// 更新类型枚举
enum UpdateType {
  hotfix,    // 热更新
  incremental, // 增量更新
  full       // 全量更新
}

/// 更新优先级枚举
enum UpdatePriority {
  optional,    // 可选更新
  recommended, // 推荐更新
  mandatory    // 强制更新
}

/// 下载状态枚举
enum DownloadState {
  idle,        // 空闲状态
  checking,    // 检查中
  available,   // 有可用更新
  downloading, // 下载中
  paused,      // 已暂停
  ready,       // 可安装
  installing,  // 安装中
  installed,   // 已安装
  failed       // 失败
}

3.2 更新信息模型

dart 复制代码
/// 更新信息数据类
class UpdateInfo {
  final int versionCode;
  final String versionName;
  final UpdateType type;
  final UpdatePriority priority;
  final int packageSize;       // 字节为单位
  final String description;
  final String downloadUrl;
  final bool isForcedUpdate;

  UpdateInfo({
    required this.versionCode,
    required this.versionName,
    required this.type,
    required this.priority,
    required this.packageSize,
    required this.description,
    required this.downloadUrl,
    this.isForcedUpdate = false,
  });

  /// 格式化包大小显示
  String get formattedSize {
    final mb = packageSize / (1024 * 1024);
    if (mb >= 1024) {
      return '${(mb / 1024).toStringAsFixed(2)} GB';
    }
    return '${mb.toStringAsFixed(2)} MB';
  }

  /// 判断是否为强制更新
  bool get isMandatory =>
      priority == UpdatePriority.mandatory || isForcedUpdate;
}

/// 下载进度数据类
class DownloadProgress {
  int downloadedBytes;
  int totalBytes;
  int speed;  // 字节/秒
  DownloadState state;
  int progress;  // 0-100
  String? errorMessage;

  DownloadProgress({
    this.downloadedBytes = 0,
    this.totalBytes = 0,
    this.speed = 0,
    this.state = DownloadState.idle,
    this.progress = 0,
    this.errorMessage,
  });

  /// 格式化进度文本
  String get formattedProgress =>
      '${_formatBytes(downloadedBytes)} / ${_formatBytes(totalBytes)}';

  /// 格式化速度文本
  String get formattedSpeed =>
      speed > 0 ? '${_formatBytes(speed)}/s' : '';

  String _formatBytes(int bytes) {
    if (bytes == 0) return '0 B';
    const k = 1024;
    final sizes = ['B', 'KB', 'MB', 'GB'];
    final i = (bytes == 0) ? 0 : (Math.log(bytes) / Math.log(k)).floor();
    return '${(bytes / Math.pow(k, i)).toStringAsFixed(2)} ${sizes[i]}';
  }
}

3.3 更新结果模型

dart 复制代码
/// 更新检查结果
class UpdateResult {
  final bool success;
  final String? errorCode;
  final String? errorMessage;
  final UpdateInfo? updateInfo;

  UpdateResult({
    required this.success,
    this.errorCode,
    this.errorMessage,
    this.updateInfo,
  });

  factory UpdateResult.success(UpdateInfo info) => UpdateResult(
        success: true,
        updateInfo: info,
      );

  factory UpdateResult.failure(String code, String message) => UpdateResult(
        success: false,
        errorCode: code,
        errorMessage: message,
      );
}

四、更新服务层实现

4.1 服务类设计

dart 复制代码
/// 应用更新管理服务(单例模式)
class UpdateService {
  static UpdateService? _instance;
  static UpdateService get instance => _instance ??= UpdateService._();

  UpdateService._();

  // 当前版本信息
  int _currentVersionCode = 101;
  String _currentVersionName = '1.0.1';

  // 最新更新信息
  UpdateInfo? _latestUpdate;
  DownloadProgress _downloadProgress = DownloadProgress();
  DownloadState _downloadState = DownloadState.idle;

  // 事件回调
  Function()? onCheckStart;
  Function(UpdateResult)? onCheckComplete;
  Function(UpdateInfo)? onUpdateAvailable;
  Function()? onDownloadStart;
  Function(DownloadProgress)? onDownloadProgress;
  Function()? onDownloadComplete;
  Function(String)? onDownloadFailed;
  Function()? onInstallComplete;

  /// 获取当前版本
  Future<Map<String, dynamic>> getCurrentVersion() async {
    // 实际项目中从包管理器获取
    return {
      'versionCode': _currentVersionCode,
      'versionName': _currentVersionName,
    };
  }

  /// 检查更新
  Future<UpdateResult> checkForUpdate() async {
    _downloadState = DownloadState.checking;
    onCheckStart?.call();

    try {
      // 模拟网络请求延迟
      await Future.delayed(const Duration(seconds: 1));

      // 模拟从服务器获取更新信息
      // 实际项目中替换为真实的接口调用
      final updateInfo = _fetchDemoUpdateInfo();

      if (updateInfo == null) {
        _downloadState = DownloadState.idle;
        final result = UpdateResult.failure('NO_UPDATE', '当前已是最新版本');
        onCheckComplete?.call(result);
        return result;
      }

      if (updateInfo.versionCode <= _currentVersionCode) {
        _downloadState = DownloadState.idle;
        final result = UpdateResult.failure('NO_UPDATE', '当前已是最新版本');
        onCheckComplete?.call(result);
        return result;
      }

      _latestUpdate = updateInfo;
      _downloadState = DownloadState.available;
      onUpdateAvailable?.call(updateInfo);
      final result = UpdateResult.success(updateInfo);
      onCheckComplete?.call(result);
      return result;
    } catch (e) {
      _downloadState = DownloadState.failed;
      final result = UpdateResult.failure('NETWORK_ERROR', e.toString());
      onCheckComplete?.call(result);
      return result;
    }
  }

  /// 模拟获取更新信息(演示用)
  UpdateInfo? _fetchDemoUpdateInfo() {
    // 模拟:如果当前版本小于指定版本,返回更新信息
    if (_currentVersionCode < 200) {
      return UpdateInfo(
        versionCode: 200,
        versionName: '2.0.0',
        type: UpdateType.full,
        priority: UpdatePriority.mandatory,
        packageSize: 120 * 1024 * 1024, // 120MB
        description: '全新UI设计,重构核心架构,支持更多设备类型',
        downloadUrl: 'https://example.com/update/200.hap',
        isForcedUpdate: true,
      );
    }
    return null;
  }

  /// 开始下载
  Future<bool> downloadUpdate() async {
    if (_latestUpdate == null) return false;

    _downloadState = DownloadState.downloading;
    _downloadProgress = DownloadProgress();
    _downloadProgress.state = DownloadState.downloading;
    _downloadProgress.totalBytes = _latestUpdate!.packageSize;
    onDownloadStart?.call();

    // 模拟下载过程
    return _simulateDownload();
  }

  /// 模拟下载进度
  Future<bool> _simulateDownload() async {
    int downloaded = 0;
    const total = 120 * 1024 * 1024;
    const baseSpeed = 2 * 1024 * 1024; // 2MB/s

    while (downloaded < total && _downloadState == DownloadState.downloading) {
      await Future.delayed(const Duration(milliseconds: 500));

      final increment = (baseSpeed * (0.8 + Random().nextDouble() * 0.4) * 0.5).toInt();
      downloaded = (downloaded + increment).clamp(0, total);

      _downloadProgress.downloadedBytes = downloaded;
      _downloadProgress.totalBytes = total;
      _downloadProgress.speed = increment * 2;
      _downloadProgress.progress = ((downloaded / total) * 100).toInt();
      onDownloadProgress?.call(_downloadProgress);
    }

    if (_downloadState == DownloadState.downloading) {
      _downloadState = DownloadState.ready;
      _downloadProgress.state = DownloadState.ready;
      _downloadProgress.progress = 100;
      onDownloadComplete?.call();
      return true;
    }
    return false;
  }

  /// 暂停下载
  void pauseDownload() {
    if (_downloadState == DownloadState.downloading) {
      _downloadState = DownloadState.paused;
      _downloadProgress.state = DownloadState.paused;
    }
  }

  /// 恢复下载
  Future<bool> resumeDownload() async {
    if (_downloadState == DownloadState.paused) {
      _downloadState = DownloadState.downloading;
      _downloadProgress.state = DownloadState.downloading;
      return _simulateDownload();
    }
    return false;
  }

  /// 取消下载
  void cancelDownload() {
    _downloadState = DownloadState.idle;
    _downloadProgress = DownloadProgress();
  }

  /// 安装更新
  Future<bool> installUpdate() async {
    if (_downloadState != DownloadState.ready) return false;

    _downloadState = DownloadState.installing;
    await Future.delayed(const Duration(seconds: 2));

    _downloadState = DownloadState.installed;
    onInstallComplete?.call();
    return true;
  }

  /// 获取下载状态
  DownloadState get downloadState => _downloadState;

  /// 获取下载进度
  DownloadProgress get downloadProgress => _downloadProgress;

  /// 获取最新更新信息
  UpdateInfo? get latestUpdate => _latestUpdate;
}

五、更新弹窗组件实现

5.1 强制更新弹窗

强制更新弹窗的设计要点是:不能被关闭,必须引导用户进行更新。

dart 复制代码
/// 强制更新弹窗
class ForcedUpdateDialog extends StatelessWidget {
  final UpdateInfo updateInfo;
  final VoidCallback onUpdate;

  const ForcedUpdateDialog({
    Key? key,
    required this.updateInfo,
    required this.onUpdate,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Dialog(
      backgroundColor: Colors.transparent,
      child: Container(
        width: 300,
        decoration: BoxDecoration(
          color: Colors.white,
          borderRadius: BorderRadius.circular(16),
        ),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            // 图标区域
            Container(
              width: double.infinity,
              padding: const EdgeInsets.only(top: 24),
              child: const Text(
                '🚀',
                textAlign: TextAlign.center,
                style: TextStyle(fontSize: 48),
              ),
            ),

            // 标题
            const Padding(
              padding: EdgeInsets.only(top: 16),
              child: Text(
                '发现新版本',
                style: TextStyle(
                  fontSize: 22,
                  fontWeight: FontWeight.bold,
                  color: Color(0xFF1A1A1A),
                ),
              ),
            ),

            // 版本号
            Container(
              margin: const EdgeInsets.only(top: 8),
              padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
              decoration: BoxDecoration(
                color: const Color(0xFFFFF3E0),
                borderRadius: BorderRadius.circular(12),
              ),
              child: Text(
                'V${updateInfo.versionName}',
                style: const TextStyle(
                  fontSize: 14,
                  fontWeight: FontWeight.w500,
                  color: Color(0xFFE65100),
                ),
              ),
            ),

            // 更新类型标签
            Container(
              margin: const EdgeInsets.only(top: 8),
              padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
              decoration: BoxDecoration(
                color: const Color(0xFFE65100),
                borderRadius: BorderRadius.circular(4),
              ),
              child: Text(
                _getUpdateTypeText(),
                style: const TextStyle(
                  fontSize: 12,
                  color: Colors.white,
                ),
              ),
            ),

            // 更新描述
            Container(
              height: 80,
              margin: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
              child: SingleChildScrollView(
                child: Text(
                  updateInfo.description,
                  style: const TextStyle(
                    fontSize: 14,
                    color: Color(0xFF666666),
                    height: 1.5,
                  ),
                  textAlign: TextAlign.center,
                ),
              ),
            ),

            // 包大小
            Text(
              '包大小: ${updateInfo.formattedSize}',
              style: const TextStyle(
                fontSize: 12,
                color: Color(0xFF999999),
              ),
            ),

            // 警告提示
            Container(
              margin: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
              padding: const EdgeInsets.all(12),
              decoration: BoxDecoration(
                color: const Color(0xFFFFF3E0),
                borderRadius: BorderRadius.circular(8),
              ),
              child: const Row(
                children: [
                  Text('⚠️', style: TextStyle(fontSize: 14)),
                  SizedBox(width: 8),
                  Expanded(
                    child: Text(
                      '此更新为强制更新,必须安装后才能继续使用应用',
                      style: TextStyle(
                        fontSize: 12,
                        color: Color(0xFFE65100),
                      ),
                    ),
                  ),
                ],
              ),
            ),

            // 更新按钮
            Container(
              margin: const EdgeInsets.only(left: 24, right: 24, bottom: 24),
              width: double.infinity,
              height: 48,
              child: ElevatedButton(
                onPressed: onUpdate,
                style: ElevatedButton.styleFrom(
                  backgroundColor: const Color(0xFFE65100),
                  foregroundColor: Colors.white,
                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(24),
                  ),
                ),
                child: const Text(
                  '立即更新',
                  style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }

  String _getUpdateTypeText() {
    switch (updateInfo.type) {
      case UpdateType.hotfix:
        return '热更新';
      case UpdateType.incremental:
        return '增量更新';
      case UpdateType.full:
        return '全量更新';
    }
  }
}

5.2 普通更新弹窗

普通更新弹窗允许用户跳过或稍后更新。

dart 复制代码
/// 普通更新弹窗
class NormalUpdateDialog extends StatefulWidget {
  final UpdateInfo updateInfo;
  final DownloadState downloadState;
  final DownloadProgress downloadProgress;
  final VoidCallback onUpdate;
  final VoidCallback onSkip;
  final VoidCallback? onPauseResume;

  const NormalUpdateDialog({
    Key? key,
    required this.updateInfo,
    required this.downloadState,
    required this.downloadProgress,
    required this.onUpdate,
    required this.onSkip,
    this.onPauseResume,
  }) : super(key: key);

  @override
  State<NormalUpdateDialog> createState() => _NormalUpdateDialogState();
}

class _NormalUpdateDialogState extends State<NormalUpdateDialog> {
  @override
  Widget build(BuildContext context) {
    return Dialog(
      backgroundColor: Colors.transparent,
      child: Container(
        width: 300,
        decoration: BoxDecoration(
          color: Colors.white,
          borderRadius: BorderRadius.circular(16),
        ),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            // 关闭按钮
            Align(
              alignment: Alignment.topRight,
              child: IconButton(
                icon: const Icon(Icons.close, color: Color(0xFF999999)),
                onPressed: () => Navigator.of(context).pop(),
              ),
            ),

            // 图标
            const Text('🚀', style: TextStyle(fontSize: 48)),

            // 标题
            const Padding(
              padding: EdgeInsets.only(top: 12),
              child: Text(
                '发现新版本',
                style: TextStyle(
                  fontSize: 20,
                  fontWeight: FontWeight.bold,
                ),
              ),
            ),

            // 版本号
            Text(
              'V${widget.updateInfo.versionName}',
              style: const TextStyle(
                fontSize: 14,
                color: Color(0xFF2196F3),
                fontWeight: FontWeight.w500,
              ),
            ),

            // 更新类型和大小
            Padding(
              padding: const EdgeInsets.only(top: 8),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Container(
                    padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
                    decoration: BoxDecoration(
                      color: const Color(0xFF2196F3),
                      borderRadius: BorderRadius.circular(4),
                    ),
                    child: Text(
                      _getUpdateTypeText(),
                      style: const TextStyle(fontSize: 12, color: Colors.white),
                    ),
                  ),
                  const SizedBox(width: 8),
                  Text(
                    widget.updateInfo.formattedSize,
                    style: const TextStyle(fontSize: 12, color: Color(0xFF999999)),
                  ),
                ],
              ),
            ),

            // 描述
            Container(
              height: 60,
              margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
              child: Text(
                widget.updateInfo.description,
                style: const TextStyle(fontSize: 14, color: Color(0xFF666666)),
              ),
            ),

            // 下载进度(如果正在下载)
            if (widget.downloadState == DownloadState.downloading ||
                widget.downloadState == DownloadState.paused)
              _buildProgressSection(),

            const SizedBox(height: 16),

            // 操作按钮
            if (widget.downloadState == DownloadState.idle ||
                widget.downloadState == DownloadState.available)
              _buildActionButtons(),

            if (widget.downloadState == DownloadState.ready)
              _buildInstallButton(),
          ],
        ),
      ),
    );
  }

  Widget _buildProgressSection() {
    return Container(
      margin: const EdgeInsets.symmetric(horizontal: 24),
      child: Column(
        children: [
          LinearProgressIndicator(
            value: widget.downloadProgress.progress / 100,
            backgroundColor: const Color(0xFFE0E0E0),
            valueColor: const AlwaysStoppedAnimation(Color(0xFF2196F3)),
          ),
          const SizedBox(height: 8),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              Text(
                widget.downloadProgress.formattedProgress,
                style: const TextStyle(fontSize: 12, color: Color(0xFF999999)),
              ),
              Text(
                widget.downloadProgress.formattedSpeed,
                style: const TextStyle(fontSize: 12, color: Color(0xFF2196F3)),
              ),
            ],
          ),
        ],
      ),
    );
  }

  Widget _buildActionButtons() {
    return Padding(
      padding: const EdgeInsets.only(left: 24, right: 24, bottom: 24),
      child: Row(
        children: [
          Expanded(
            child: OutlinedButton(
              onPressed: widget.onSkip,
              style: OutlinedButton.styleFrom(
                foregroundColor: const Color(0xFF999999),
                side: const BorderSide(color: Color(0xFFE0E0E0)),
                padding: const EdgeInsets.symmetric(vertical: 12),
              ),
              child: const Text('跳过此版本'),
            ),
          ),
          const SizedBox(width: 12),
          Expanded(
            child: ElevatedButton(
              onPressed: widget.onUpdate,
              style: ElevatedButton.styleFrom(
                backgroundColor: const Color(0xFF2196F3),
                foregroundColor: Colors.white,
                padding: const EdgeInsets.symmetric(vertical: 12),
              ),
              child: const Text('立即更新'),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildInstallButton() {
    return Container(
      margin: const EdgeInsets.only(left: 24, right: 24, bottom: 24),
      width: double.infinity,
      height: 48,
      child: ElevatedButton(
        onPressed: widget.onUpdate,
        style: ElevatedButton.styleFrom(
          backgroundColor: const Color(0xFF4CAF50),
          foregroundColor: Colors.white,
        ),
        child: const Text('安装更新'),
      ),
    );
  }

  String _getUpdateTypeText() {
    switch (widget.updateInfo.type) {
      case UpdateType.hotfix:
        return '热更新';
      case UpdateType.incremental:
        return '增量更新';
      case UpdateType.full:
        return '全量更新';
    }
  }
}

六、更新演示页面

6.1 页面完整实现

dart 复制代码
/// 更新演示页面
class UpdateDemoPage extends StatefulWidget {
  const UpdateDemoPage({Key? key}) : super(key: key);

  @override
  State<UpdateDemoPage> createState() => _UpdateDemoPageState();
}

class _UpdateDemoPageState extends State<UpdateDemoPage> {
  final UpdateService _updateService = UpdateService.instance;

  int _currentVersionCode = 101;
  String _currentVersionName = '1.0.1';
  bool _isChecking = false;
  UpdateInfo? _updateInfo;
  DownloadProgress _downloadProgress = DownloadProgress();
  DownloadState _downloadState = DownloadState.idle;
  String _checkResult = '';

  @override
  void initState() {
    super.initState();
    _loadCurrentVersion();
    _setupCallbacks();
  }

  void _loadCurrentVersion() async {
    final version = await _updateService.getCurrentVersion();
    setState(() {
      _currentVersionCode = version['versionCode'];
      _currentVersionName = version['versionName'];
    });
  }

  void _setupCallbacks() {
    _updateService
      ..onCheckStart = () => setState(() {
            _isChecking = true;
            _checkResult = '';
          })
      ..onCheckComplete = (result) => setState(() {
            _isChecking = false;
            if (!result.success) {
              _checkResult = result.errorMessage ?? '';
            }
          })
      ..onUpdateAvailable = (info) => setState(() {
            _updateInfo = info;
            _showUpdateDialog(info);
          })
      ..onDownloadProgress = (progress) => setState(() {
            _downloadProgress = progress;
          })
      ..onDownloadComplete = () => setState(() {
            _downloadState = DownloadState.ready;
          });
  }

  void _showUpdateDialog(UpdateInfo info) {
    if (info.isMandatory) {
      showDialog(
        context: context,
        barrierDismissible: false,
        builder: (ctx) => ForcedUpdateDialog(
          updateInfo: info,
          onUpdate: () {
            Navigator.of(ctx).pop();
            _startDownload();
          },
        ),
      );
    } else {
      showDialog(
        context: context,
        builder: (ctx) => NormalUpdateDialog(
          updateInfo: info,
          downloadState: _downloadState,
          downloadProgress: _downloadProgress,
          onUpdate: _startDownload,
          onSkip: () {
            Navigator.of(ctx).pop();
            setState(() {
              _checkResult = '已跳过此版本';
            });
          },
        ),
      );
    }
  }

  void _checkForUpdate() async {
    setState(() {
      _isChecking = true;
      _checkResult = '';
    });
    await _updateService.checkForUpdate();
    setState(() {
      _isChecking = false;
    });
  }

  void _startDownload() async {
    setState(() {
      _downloadState = DownloadState.downloading;
    });
    await _updateService.downloadUpdate();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFFF5F7FA),
      appBar: AppBar(
        title: const Text('应用更新演示'),
        backgroundColor: Colors.white,
        foregroundColor: Colors.black,
        elevation: 0,
      ),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          _buildVersionCard(),
          const SizedBox(height: 16),
          _buildCheckSection(),
          if (_downloadState != DownloadState.idle) ...[
            const SizedBox(height: 16),
            _buildDownloadSection(),
          ],
          const SizedBox(height: 16),
          _buildSimulationSection(),
        ],
      ),
    );
  }

  Widget _buildVersionCard() {
    return Container(
      padding: const EdgeInsets.all(20),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(16),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(0.05),
            blurRadius: 10,
            offset: const Offset(0, 2),
          ),
        ],
      ),
      child: Row(
        children: [
          Container(
            width: 64,
            height: 64,
            decoration: BoxDecoration(
              color: const Color(0xFFE3F2FD),
              borderRadius: BorderRadius.circular(16),
            ),
            child: const Center(child: Text('🚀', style: TextStyle(fontSize: 32))),
          ),
          const SizedBox(width: 16),
          Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              const Text(
                '当前版本',
                style: TextStyle(fontSize: 14, color: Color(0xFF999999)),
              ),
              const SizedBox(height: 4),
              Row(
                children: [
                  Text(
                    'V$_currentVersionName',
                    style: const TextStyle(
                      fontSize: 20,
                      fontWeight: FontWeight.bold,
                      color: Color(0xFF1A1A1A),
                    ),
                  ),
                  const SizedBox(width: 8),
                  Text(
                    '(Build $_currentVersionCode)',
                    style: const TextStyle(fontSize: 12, color: Color(0xFF999999)),
                  ),
                ],
              ),
            ],
          ),
        ],
      ),
    );
  }

  Widget _buildCheckSection() {
    return Container(
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(16),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Text(
            '版本检测',
            style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
          ),
          const SizedBox(height: 12),
          SizedBox(
            width: double.infinity,
            height: 48,
            child: ElevatedButton(
              onPressed: _isChecking ? null : _checkForUpdate,
              style: ElevatedButton.styleFrom(
                backgroundColor: const Color(0xFF2196F3),
                foregroundColor: Colors.white,
                shape: RoundedRectangleBorder(
                  borderRadius: BorderRadius.circular(24),
                ),
              ),
              child: _isChecking
                  ? const SizedBox(
                      width: 20,
                      height: 20,
                      child: CircularProgressIndicator(
                        strokeWidth: 2,
                        valueColor: AlwaysStoppedAnimation(Colors.white),
                      ),
                    )
                  : const Text('检查更新'),
            ),
          ),
          if (_checkResult.isNotEmpty)
            Padding(
              padding: const EdgeInsets.only(top: 12),
              child: Text(
                _checkResult,
                style: TextStyle(
                  fontSize: 14,
                  color: _checkResult.contains('失败')
                      ? const Color(0xFFF44336)
                      : const Color(0xFF4CAF50),
                ),
              ),
            ),
        ],
      ),
    );
  }

  Widget _buildDownloadSection() {
    return Container(
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(16),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Text(
            '下载进度',
            style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
          ),
          const SizedBox(height: 12),
          if (_downloadState == DownloadState.downloading ||
              _downloadState == DownloadState.paused) ...[
            LinearProgressIndicator(
              value: _downloadProgress.progress / 100,
              backgroundColor: const Color(0xFFE0E0E0),
              valueColor: const AlwaysStoppedAnimation(Color(0xFF2196F3)),
            ),
            const SizedBox(height: 8),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Text('${_downloadProgress.progress}%'),
                Text(_downloadProgress.formattedSpeed),
              ],
            ),
            const SizedBox(height: 8),
            Row(
              children: [
                TextButton(
                  onPressed: () {
                    if (_downloadState == DownloadState.paused) {
                      _updateService.resumeDownload();
                    } else {
                      _updateService.pauseDownload();
                    }
                  },
                  child: Text(_downloadState == DownloadState.paused ? '继续' : '暂停'),
                ),
                const Spacer(),
                TextButton(
                  onPressed: () {
                    _updateService.cancelDownload();
                    setState(() {
                      _downloadState = DownloadState.idle;
                    });
                  },
                  child: const Text('取消', style: TextStyle(color: Color(0xFFF44336))),
                ),
              ],
            ),
          ],
          if (_downloadState == DownloadState.ready)
            Column(
              children: [
                const Row(
                  children: [
                    Icon(Icons.check_circle, color: Color(0xFF4CAF50)),
                    SizedBox(width: 8),
                    Text('下载完成'),
                  ],
                ),
                const SizedBox(height: 12),
                SizedBox(
                  width: double.infinity,
                  height: 44,
                  child: ElevatedButton(
                    onPressed: () => _updateService.installUpdate(),
                    style: ElevatedButton.styleFrom(
                      backgroundColor: const Color(0xFF4CAF50),
                      foregroundColor: Colors.white,
                    ),
                    child: const Text('安装更新'),
                  ),
                ),
              ],
            ),
        ],
      ),
    );
  }

  Widget _buildSimulationSection() {
    return Container(
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(16),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Text(
            '模拟更新场景',
            style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
          ),
          const SizedBox(height: 8),
          const Text(
            '以下按钮用于模拟不同类型的更新弹窗',
            style: TextStyle(fontSize: 12, color: Color(0xFF999999)),
          ),
          const SizedBox(height: 12),
          Row(
            children: [
              Expanded(
                child: ElevatedButton(
                  onPressed: () => _simulateForcedUpdate(),
                  style: ElevatedButton.styleFrom(
                    backgroundColor: const Color(0xFFE65100),
                    foregroundColor: Colors.white,
                  ),
                  child: const Text('强制更新'),
                ),
              ),
              const SizedBox(width: 8),
              Expanded(
                child: ElevatedButton(
                  onPressed: () => _simulateNormalUpdate(),
                  style: ElevatedButton.styleFrom(
                    backgroundColor: const Color(0xFF2196F3),
                    foregroundColor: Colors.white,
                  ),
                  child: const Text('普通更新'),
                ),
              ),
              const SizedBox(width: 8),
              Expanded(
                child: ElevatedButton(
                  onPressed: () => _simulateHotfixUpdate(),
                  style: ElevatedButton.styleFrom(
                    backgroundColor: const Color(0xFF4CAF50),
                    foregroundColor: Colors.white,
                  ),
                  child: const Text('热更新'),
                ),
              ),
            ],
          ),
        ],
      ),
    );
  }

  void _simulateForcedUpdate() {
    final info = UpdateInfo(
      versionCode: 200,
      versionName: '2.0.0',
      type: UpdateType.full,
      priority: UpdatePriority.mandatory,
      packageSize: 120 * 1024 * 1024,
      description: '全新UI设计,重构核心架构,支持更多设备类型',
      downloadUrl: '',
      isForcedUpdate: true,
    );
    _showUpdateDialog(info);
  }

  void _simulateNormalUpdate() {
    final info = UpdateInfo(
      versionCode: 103,
      versionName: '1.0.3',
      type: UpdateType.incremental,
      priority: UpdatePriority.optional,
      packageSize: 15 * 1024 * 1024,
      description: '新增图片预览功能,优化列表加载性能',
      downloadUrl: '',
    );
    _showUpdateDialog(info);
  }

  void _simulateHotfixUpdate() {
    final info = UpdateInfo(
      versionCode: 102,
      versionName: '1.0.2',
      type: UpdateType.hotfix,
      priority: UpdatePriority.recommended,
      packageSize: 2 * 1024 * 1024,
      description: '修复已知问题,提升应用稳定性',
      downloadUrl: '',
    );
    _showUpdateDialog(info);
  }
}

七、代码托管

完整代码已托管至 AtomGit 仓库:

仓库地址https://atomgit.com/maaath/flutter-update-demo


八、截图运行验证

8.1 检查更新界面

运行应用后,点击"检查更新"按钮,系统将发起更新检查请求。

8.2 强制更新弹窗

当检测到强制更新时,弹出不可关闭的更新提示,用户必须完成更新才能继续使用应用。

8.3 普通更新弹窗

对于可选更新,用户可以选择"跳过此版本"或"立即更新"。

8.4 下载进度显示

下载过程中显示实时进度条、已下载大小和下载速度。

8.5 安装完成

下载完成后显示安装按钮,点击即可完成更新安装。


九、实际应用集成指南

9.1 与真实后端对接

在实际项目中,需要将 _fetchDemoUpdateInfo() 方法替换为真实的 API 调用:

dart 复制代码
Future<UpdateInfo?> _fetchFromServer() async {
  try {
    final response = await Dio().get('https://your-api.com/check-update');
    if (response.statusCode == 200) {
      final data = response.data;
      return UpdateInfo(
        versionCode: data['versionCode'],
        versionName: data['versionName'],
        type: UpdateType.values.firstWhere(
          (e) => e.name == data['type'],
          orElse: () => UpdateType.full,
        ),
        priority: UpdatePriority.values.firstWhere(
          (e) => e.name == data['priority'],
          orElse: () => UpdatePriority.optional,
        ),
        packageSize: data['packageSize'],
        description: data['description'],
        downloadUrl: data['downloadUrl'],
        isForcedUpdate: data['isForcedUpdate'] ?? false,
      );
    }
  } catch (e) {
    print('Failed to fetch update info: $e');
  }
  return null;
}

9.2 本地化存储

建议使用 SharedPreferences 或 Hive 存储已跳过的版本和用户偏好设置:

dart 复制代码
// 存储已跳过的版本
Future<void> saveSkippedVersion(int versionCode) async {
  final prefs = await SharedPreferences.getInstance();
  final skipped = prefs.getStringList('skipped_versions') ?? [];
  skipped.add(versionCode.toString());
  await prefs.setStringList('skipped_versions', skipped);
}

// 检查版本是否被跳过
Future<bool> isVersionSkipped(int versionCode) async {
  final prefs = await SharedPreferences.getInstance();
  final skipped = prefs.getStringList('skipped_versions') ?? [];
  return skipped.contains(versionCode.toString());
}

十、总结

本文详细介绍了在 Flutter for OpenHarmony 项目中实现完整应用更新能力的方法,包括:

  1. 清晰的数据模型设计:枚举定义、不可变数据类、进度追踪
  2. 单例模式的服务层:统一的更新管理逻辑、事件回调机制
  3. 多样化的弹窗设计:强制更新与普通更新的差异化处理
  4. 流畅的用户体验:下载进度实时反馈、暂停/恢复/取消功能

通过本文的指导,开发者可以快速在自己的 Flutter 项目中集成企业级的应用更新能力,为用户提供更好的使用体验。

感谢各位阅读!

相关推荐
key_3_feng11 小时前
鸿蒙6.0 Widget服务卡片落地方案
华为·harmonyos
maaath11 小时前
【maaath】 OpenHarmony 设备信息获取能力集成指南
flutter·华为·harmonyos
Hello__777712 小时前
开源鸿蒙 Flutter 实战|帮助中心功能全流程实现
flutter·开源·harmonyos
Hello__777712 小时前
开源鸿蒙 Flutter 实战|用户认证标识功能全流程实现
flutter·开源·harmonyos
Hello__777712 小时前
开源鸿蒙 Flutter 实战|用户详情页按钮布局溢出全流程修复与最佳实践
flutter·开源·harmonyos
Swift社区12 小时前
多端一致性:鸿蒙游戏如何避免状态漂移?
游戏·华为·harmonyos
恋猫de小郭12 小时前
Flutter 3.41.8 又双叒修复调试问题,草台班子日常 hotfix
android·前端·flutter
liulian091612 小时前
【Flutter for OpenHarmony第三方库】Flutter for OpenHarmony 离线模式实现:让你的应用无网也能萌萌哒~
开发语言·flutter·华为·php·学习方法·harmonyos
Lanren的编程日记12 小时前
Flutter 鸿蒙应用手势导航系统实战:自定义手势识别+手势导航+冲突处理,打造流畅交互体验
flutter·交互·harmonyos