Flutter URL Scheme 跨平台跳转

提示词优化器V2.1.1-新增AI应用一键跳转功能

用户将优化后的提示词应用到目标 AI 应用时,传统流程需要:

  1. 复制优化结果
  2. 手动切换到目标 AI 应用
  3. 粘贴并发送

所以我新增了一个功能 URL Scheme 跨平台跳转 + 剪贴板联动实现一键直达的技术方案,将上述三步操作压缩为单次点击。

支持平台

平台 Scheme 跳转 包名兜底 应用商店引导
Android ✅(market:// + 豌豆荚)
iOS ✅(App Store)
Web/Desktop

展开关闭动画效果图

分层设计

采用 Clean Architecture 分层,确保业务逻辑与 Flutter 框架解耦:

scss 复制代码
┌─────────────────────────────────────────┐
│         Presentation Layer              │
│  ┌─────────────┐  ┌──────────────────┐ │
│  │ AIAppButton │  │ AIAppLauncher    │ │
│  │ Animated    │  │ Section          │ │
│  └─────────────┘  └──────────────────┘ │
│  ┌─────────────────────────────────────┐│
│  │ AIAppManager (Notifier)             ││
│  └─────────────────────────────────────┘│
└─────────────────────────────────────────┘
                    │
                    ▼
┌─────────────────────────────────────────┐
│           Domain Layer                  │
│  ┌─────────────────┐ ┌────────────────┐ │
│  │ LaunchAIAppUC   │ │ OpenAppStoreUC │ │
│  └─────────────────┘ └────────────────┘ │
└─────────────────────────────────────────┘
                    │
                    ▼
┌─────────────────────────────────────────┐
│            Data Layer                   │
│  ┌─────────────────────────────────────┐│
│  │ AIAppRepository (Drift + Hive)      ││
│  └─────────────────────────────────────┘│
│  ┌─────────────────────────────────────┐│
│  │ AIAppConfigModel (freezed)          ││
│  └─────────────────────────────────────┘│
└─────────────────────────────────────────┘

核心组件职责

组件 职责
AIAppLauncherSection 可折叠区域 UI,监听已启用应用列表
AIAppManager 状态管理:切换启用、排序、增删、跳转
LaunchAIAppUseCase 领域服务:剪贴板 + Scheme 跳转 + 降级处理
OpenAppStoreUseCase 领域服务:跨平台应用商店跳转
AIAppRepository 数据持久化:Drift 数据库 + 内置常量合并

技术实现详解

1. 跳转策略:优先 Scheme,降级包名

LaunchAIAppUseCase 实现了分层降级策略:

dart 复制代码
class LaunchAIAppUseCase {
  Future<bool> call({
    required String scheme,
    required String promptText,
    String? packageName,
  }) async {
    // 1. 复制提示词到剪贴板
    await Clipboard.setData(ClipboardData(text: promptText));

    // 2. 优先尝试 Scheme 跳转
    final schemeUrl = Uri.parse(scheme);
    final canLaunchScheme = await canLaunchUrl(schemeUrl);

    if (canLaunchScheme) {
      final launched = await launchUrl(
        schemeUrl,
        mode: LaunchMode.externalApplication,
      );
      if (launched) return true;
    }

    // 3. Scheme 失败,用包名兜底(仅 Android)
    if (Platform.isAndroid && packageName != null) {
      final packageUrl = Uri.parse('package:$packageName');
      final canLaunchPackage = await canLaunchUrl(packageUrl);

      if (canLaunchPackage) {
        return await launchUrl(packageUrl, mode: LaunchMode.externalApplication);
      }
    }

    return false;  // 跳转失败,触发应用未安装提示
  }
}

关键设计

  • 剪贴板优先于参数传递 :部分 AI 应用不支持通过 Scheme 参数传递文本(如 doubao://?prompt=xxx),统一使用剪贴板作为数据交换媒介
  • canLaunchUrl 预检查 :避免直接 launchUrl 抛出 PlatformException
  • 包名降级 :Android 端 package:com.bytedance.stable_diffusion 可直达应用详情页,比 Scheme 更可靠

2. 应用商店跨平台跳转

OpenAppStoreUseCase 处理平台差异:

dart 复制代码
class OpenAppStoreUseCase {
  Future<bool> call(String appName) async {
    Uri? url;

    if (Platform.isAndroid) {
      // 优先 market:// 协议(调用系统应用商店)
      url = Uri.parse('market://search?q=$appName');
      final canLaunch = await canLaunchUrl(url);

      if (!canLaunch) {
        // 降级到豌豆荚网页版
        url = Uri.parse('https://www.wandoujia.com/search?key=$appName');
      }
    } else if (Platform.isIOS) {
      // App Store 搜索页
      url = Uri.parse('https://apps.apple.com/cn/search?term=$appName');
    } else {
      return false;
    }

    return await launchUrl(url, mode: LaunchMode.externalApplication);
  }
}

异常处理

  • Android 设备无 Google Play 时,market:// 会失败,自动降级到豌豆荚
  • iOS 使用 HTTPS 链接而非 itms://,兼容性更好

3. 应用配置数据模型

内置应用与自定义应用采用统一模型:

dart 复制代码
@freezed
class AIAppConfigModel with _$AIAppConfigModel {
  const factory AIAppConfigModel({
    required String id,
    required String name,
    required String scheme,
    required String iconPath,
    @Default(true) bool isEnabled,
    @Default(0) int position,
    @Default(false) bool isBuiltin,
    required DateTime createdAt,
  }) = _AIAppConfigModel;

  // 内置应用从 AppConstants 获取实际配置
  Map<String, String>? get _builtinConfig {
    if (!isBuiltin) return null;
    return AppConstants.builtInAIApps.firstWhere(
      (app) => app['name'] == name,
    );
  }

  String get actualScheme => _builtinConfig?['scheme'] ?? scheme;
  String? get actualPackageName => _builtinConfig?['packageName'];
}

设计优势

  • 内置应用热更新 :修改 AppConstants.builtInAIApps 即可更新所有内置应用配置,无需数据库迁移
  • 自定义应用持久化:用户添加的应用存储到 Drift 数据库,独立于内置配置
  • 统一接口 :UI 层无需区分内置/自定义,通过 actualScheme 等 getter 透明访问

4. Riverpod 状态管理

dart 复制代码
/// AI 应用管理 Notifier
class AIAppManager extends AsyncNotifier<void> {
  late AIAppRepository _repository;
  late LaunchAIAppUseCase _launchUseCase;
  late OpenAppStoreUseCase _openStoreUseCase;

  @override
  Future<void> build() async {
    _repository = ref.watch(aiAppRepositoryProvider);
    _launchUseCase = LaunchAIAppUseCase();
    _openStoreUseCase = OpenAppStoreUseCase();
  }

  /// 启动 AI 应用
  Future<void> launchApp(String id, String promptText) async {
    final app = await _repository.getAppById(id);
    if (app == null) {
      _showErrorToast('应用不存在');
      return;
    }

    final success = await _launchUseCase(
      scheme: app.actualScheme,
      promptText: promptText,
      packageName: app.actualPackageName,
    );

    if (success) {
      _showSuccessToast('已复制到剪贴板并跳转');
    } else {
      _showAppNotInstalledToast(app.name);
    }
  }
}

状态监听模式

dart 复制代码
// 在 UI 中监听
final appsAsync = ref.watch(enabledAIAppListProvider);

appsAsync.when(
  data: (apps) => _buildAppButtons(apps),
  loading: () => const CircularProgressIndicator(),
  error: (e, s) => Text('加载失败:$e'),
)

用户体验优化

1. 应用未安装检测与引导

当跳转失败时,显示带操作按钮的 Toast:

dart 复制代码
void _showAppNotInstalledToast(String appName) {
  ref.read(toastProvider.notifier).showAction(
    message: '该应用未安装,是否前往应用商店下载?',
    type: ToastType.warning,
    primaryAction: ToastAction(
      label: '去下载',
      onPressed: () async {
        final success = await _openStoreUseCase(appName);
        if (!success) {
          _showErrorToast('打开应用商店失败');
        }
      },
    ),
    duration: const Duration(seconds: 5),
  );
}

应用未安装 Toast 截图-黄色警告背景,右侧"去下载"按钮为重点

2. 拖拽排序与持久化

已启用应用支持拖拽重排序,位置持久化到数据库:

dart 复制代码
class _EnabledAppsSectionState extends ConsumerState<_EnabledAppsSection> {
  late List _orderedApps;
  int? _draggingIndex;

  void _saveOrder() {
    final positions = <String, int>{};
    for (int i = 0; i < _orderedApps.length; i++) {
      positions[_orderedApps[i].id] = i;
    }
    ref.read(aIAppManagerProvider.notifier)
       .updateAppPositions(positions);
  }
}

实现细节

  • 使用 LongPressDraggable + DragTarget 实现拖拽
  • 拖拽时触发 HapticFeedback.mediumImpact() 提供触觉反馈
  • 松手时立即更新 UI,异步保存到数据库

拖拽排序 GIF 截图-手指长按豆包按钮拖动到通义千问左侧

边界处理

1. 异常捕获策略

dart 复制代码
try {
  // 跳转逻辑
} on PlatformException catch (_) {
  // 平台异常:应用未安装、系统拒绝打开
  return false;
} catch (_) {
  // 其他异常:链接格式错误、超时
  return false;
}

2. Scheme 验证

用户添加自定义应用时,前端验证 Scheme 格式:

dart 复制代码
validator: (value) {
  if (value == null || value.trim().isEmpty) {
    return '请输入 URL Scheme';
  }
  if (!value.contains('://')) {
    return 'URL Scheme 格式错误,应包含 ://';
  }
  return null;
}

3. 内置应用白名单

内置应用配置在 AppConstants 中预定义,防止恶意注入:

dart 复制代码
static const List<Map<String, String>> builtInAIApps = [
  {
    'name': '豆包',
    'scheme': 'doubao://',
    'packageName': 'com.ss.android.ugc.alive',
    'iconPath': 'assets/icon/doubao.svg',
  },
  {
    'name': '通义千问',
    'scheme': 'alibabacloudqwen://',
    'packageName': 'com.alibaba.cloud.qwen',
    'iconPath': 'assets/icon/qwen.svg',
  },
  // ...
];

开源地址

🌐Github开源地址:JIULANG9/PromptOptimizer:

如果这个项目对您有帮助,恳请点个 ⭐ Star 支持一下------您的认可是我持续维护和更新的重要动力。

相关推荐
ZFSS2 小时前
Kimi Chat Completion API 申请及使用
前端·人工智能
天翼云开发者社区3 小时前
春节复工福利就位!天翼云息壤2500万Tokens免费送,全品类大模型一键畅玩!
人工智能·算力服务·息壤
知识浅谈3 小时前
教你如何用 Gemini 将课本图片一键转为精美 PPT
人工智能
_squirrel3 小时前
记录一次 Flutter 升级遇到的问题
flutter
Ray Liang4 小时前
被低估的量化版模型,小身材也能干大事
人工智能·ai·ai助手·mindx
逛逛GitHub4 小时前
4 个热门的 GitHub 开源项目
github
Haha_bj4 小时前
Flutter——状态管理 Provider 详解
flutter·app
shengjk15 小时前
NanoClaw 深度剖析:一个"AI 原生"架构的个人助手是如何运转的?
人工智能