Flutter应用多平台适配经验

记录将Flutter应用适配iOS、Android、macOS等多平台的开发经验与踩坑记录。

前言

Flutter的跨平台能力是其最大的优势之一。「回留」最初只是一个iOS应用,后来逐步扩展到Android和macOS平台。这个过程中遇到了不少平台差异问题,本文记录这些经验。

平台检测与条件编译

Flutter提供了多种方式检测当前平台:

import 'dart:io';
import 'package:flutter/foundation.dart';

// 运行时检测
if (Platform.isIOS) {
  // iOS特定逻辑
} else if (Platform.isAndroid) {
  // Android特定逻辑
} else if (Platform.isMacOS) {
  // macOS特定逻辑
}

// 检测是否为移动端
final isMobile = Platform.isIOS || Platform.isAndroid;

// 检测是否为桌面端
final isDesktop = Platform.isMacOS || Platform.isWindows || Platform.isLinux;

// Web平台检测
if (kIsWeb) {
  // Web特定逻辑
}

UI适配策略

1. 响应式布局

根据屏幕尺寸调整布局:

class ResponsiveLayout extends StatelessWidget {
  final Widget mobile;
  final Widget? tablet;
  final Widget? desktop;
  
  const ResponsiveLayout({
    super.key,
    required this.mobile,
    this.tablet,
    this.desktop,
  });
  
  @override
  Widget build(BuildContext context) {
    final width = MediaQuery.of(context).size.width;
    
    if (width >= 1200 && desktop != null) {
      return desktop!;
    } else if (width >= 768 && tablet != null) {
      return tablet!;
    }
    return mobile;
  }
}

2. 平台特定组件

某些组件在不同平台有不同的最佳实践:

Widget buildBackButton(BuildContext context) {
  if (Platform.isIOS || Platform.isMacOS) {
    return CupertinoButton(
      padding: EdgeInsets.zero,
      child: const Icon(CupertinoIcons.back),
      onPressed: () => Navigator.pop(context),
    );
  }
  return IconButton(
    icon: const Icon(Icons.arrow_back),
    onPressed: () => Navigator.pop(context),
  );
}

权限处理差异

不同平台的权限申请方式不同:

iOS权限配置(Info.plist)

<key>NSPhotoLibraryUsageDescription</key>
<string>需要访问相册以整理您的照片</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>需要保存照片到相册</string>

Android权限配置(AndroidManifest.xml)

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" 
    android:maxSdkVersion="32"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>

macOS权限配置(entitlements)

<key>com.apple.security.personal-information.photos-library</key>
<true/>

文件系统差异

不同平台的文件路径和访问方式不同:

import 'package:path_provider/path_provider.dart';

Future<String> getAppDataPath() async {
  if (Platform.isIOS || Platform.isMacOS) {
    final dir = await getApplicationSupportDirectory();
    return dir.path;
  } else if (Platform.isAndroid) {
    final dir = await getApplicationDocumentsDirectory();
    return dir.path;
  }
  throw UnsupportedError('Unsupported platform');
}

手势交互差异

移动端和桌面端的交互方式有本质区别:

class AdaptiveGestureDetector extends StatelessWidget {
  final Widget child;
  final VoidCallback? onTap;
  final VoidCallback? onDoubleTap;
  
  const AdaptiveGestureDetector({
    super.key,
    required this.child,
    this.onTap,
    this.onDoubleTap,
  });
  
  @override
  Widget build(BuildContext context) {
    final isDesktop = Platform.isMacOS || 
                      Platform.isWindows || 
                      Platform.isLinux;
    
    if (isDesktop) {
      // 桌面端支持鼠标悬停效果
      return MouseRegion(
        cursor: SystemMouseCursors.click,
        child: GestureDetector(
          onTap: onTap,
          onDoubleTap: onDoubleTap,
          child: child,
        ),
      );
    }
    
    return GestureDetector(
      onTap: onTap,
      onDoubleTap: onDoubleTap,
      child: child,
    );
  }
}

键盘快捷键支持

桌面端需要支持键盘操作:

class KeyboardShortcuts extends StatelessWidget {
  final Widget child;
  
  const KeyboardShortcuts({super.key, required this.child});
  
  @override
  Widget build(BuildContext context) {
    if (!Platform.isMacOS && !Platform.isWindows && !Platform.isLinux) {
      return child;
    }
    
    return Shortcuts(
      shortcuts: {
        LogicalKeySet(LogicalKeyboardKey.meta, LogicalKeyboardKey.keyZ): 
            const UndoIntent(),
        LogicalKeySet(LogicalKeyboardKey.delete): 
            const DeleteIntent(),
      },
      child: Actions(
        actions: {
          UndoIntent: CallbackAction<UndoIntent>(
            onInvoke: (_) => _handleUndo(),
          ),
          DeleteIntent: CallbackAction<DeleteIntent>(
            onInvoke: (_) => _handleDelete(),
          ),
        },
        child: child,
      ),
    );
  }
}

平台特定插件

有些功能需要使用平台特定的插件:

// pubspec.yaml
dependencies:
  photo_manager: ^3.0.0  # 支持iOS/Android/macOS
  
  # 平台特定依赖
  cupertino_icons: ^1.0.6  # iOS风格图标
  
// 条件导入
import 'photo_service_stub.dart'
    if (dart.library.io) 'photo_service_mobile.dart'
    if (dart.library.html) 'photo_service_web.dart';

踩坑记录

1. Android 13+ 权限变化

Android 13开始,READ_EXTERNAL_STORAGE被细分为READ_MEDIA_IMAGES等权限,需要分别申请。

2. macOS沙盒限制

macOS应用默认运行在沙盒中,访问用户文件需要配置entitlements并获取用户授权。

3. iOS模拟器与真机差异

某些功能(如相册访问)在模拟器上的行为与真机不同,务必在真机上测试。

总结

Flutter的跨平台开发虽然能共享大部分代码,但平台差异仍需要认真处理。建议: