前言
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的跨平台开发虽然能共享大部分代码,但平台差异仍需要认真处理。建议:
- 尽早在目标平台上测试
- 封装平台差异代码,保持业务逻辑统一
- 关注各平台的设计规范
- 及时跟进平台API变化