概述
- 调用原生功能
- 嵌入原有项目
- Flutter模块调试
一、调用原生功能
-
1.1、Camera
某些应用程序可能需要使用移动设备进行拍照或者选择相册中的照片,Flutter官方提供了插件:image_picker-
1.1.1、添加依赖
添加对image_picker的依赖:https://pub.dev/packages/image_picker,在项目的pubspec.ymal
里面添加下面的依赖即可,然后执行右上角的Pub get
dependencies: image_picker: ^0.6.7+1
-
1.1.2、平台配置
对iOS平台,想要访问相册或者相机,需要获取用户的允许:
依然是修改info.plist
文件:/ios/Runner/Info.plist
添加对相册的访问权限:Privacy - Photo Library Usage Description
添加对相机的访问权限:Privacy - Camera Usage Description
-
拓展
:其他的权限 - 相机权限:
Privacy - Camera Usage Description
是否允许此App使用你的相机? - 相册权限:
Privacy - Photo Library Usage Description
是否允许此App访问你的媒体资料库? - 通讯录权限:
Privacy - Contacts Usage Description
是否允许此App访问你的通讯录? - 蓝牙权限:
Privacy - Bluetooth Peripheral Usage Description
是否许允此App使用蓝牙? - 使用期间定位权限:
Privacy - Location When In Use Usage Description
是否允许此App使用定位服务? - 始终定位权限:
Privacy - Location Always Usage Description
是否允许此App始终使用定位服务? - 语音转文字权限:
Privacy - Speech Recognition Usage Description
是否允许此App使用语音识别? - 日历权限:
Privacy - Calendars Usage Description
是否允许此App使用日历? - 健康—读取数据:
Privacy - Health Share Usage Description
是否允许此App读取健康数据? - 健康—写入数据:
Privacy - Health Share Usage Description
是否允许此App写入健康数据? - 读取HomeKit:
Privacy - HomeKit Usage Description
是否允许此App访问HomeKit? - 麦克风:
Privacy - Microphone Usage Description
是否允许此App访问麦克风? - 提醒事项:
Privacy - Reminders Usage Description
是否允许此App访问提醒事项? - 运动与健身:
Privacy - Motion Usage Description
是否允许此App访问运动与健身? - 面部ID权限:
Privacy - Face ID Usage Description
是否允许此App访问Face ID?
之后选择相册或者访问相机时,会弹出如下的提示框:
-
-
1.1.3、代码实现
image_picker
的核心代码是getImage
方法:
可以传入数据源、图片的大小、质量、前置后置摄像头等
数据源是必传参数:ImageSource
枚举类型:camera:相机
、gallery:相册
Future
getImage({ @required ImageSource source, double maxWidth, double maxHeight, int imageQuality, // 默认后置摄像头 CameraDevice preferredCameraDevice = CameraDevice.rear, }) { return platform.pickImage( source: source, maxWidth: maxWidth, maxHeight: maxHeight, imageQuality: imageQuality, preferredCameraDevice: preferredCameraDevice, ); } 案例演练:
import 'dart:io'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; class JKCameraScreen extends StatefulWidget { @override _JKCameraScreenState createState() => _JKCameraScreenState(); } class _JKCameraScreenState extends State
{ PickedFile _imageFile; final ImagePicker _picker = ImagePicker(); @override Widget build(BuildContext context) { return Center( child: Column( children: [ RaisedButton( child: Text('选择一个相册'), onPressed: _pickImage ), _imageFile == null ? Text('请选择一张照片') : Image.file(File(_imageFile.path)) ], ), ); } void _pickImage() async { print('选择相册'); PickedFile pickedFile = await _picker.getImage(source: ImageSource.gallery); setState(() { _imageFile = pickedFile; }); } }
-
-
1.2、电池信息
某些原生的信息,如果没有很好的插件,我们可以通过、platform channels(平台通道)
来获取信息。-
1.2.1、平台通过介绍
平台通过是如何工作的呢?- 消息使用platform channels(平台通道)在客户端(UI)和宿主(平台)之间传递;
- 消息和响应以异步的形式进行传递,以确保用户界面能够保持响应;
调用过程大致如下:
- 1.客户端(Flutter端)发送与方法调用相对应的消息
- 2.平台端(iOS、Android端)接收方法,并返回结果;
- iOS端通过FlutterMethodChannel做出响应;
- Android端通过MethodChannel做出响应;
Flutter、iOS、Android端数据类型的对应关系:
-
1.2.2、创建测试项目
我们这里创建一个获取电池电量信息的项目,分别通过iOS和Android原生代码来获取对应的信息:-
创建方式一:默认创建方式,目前默认创建的Flutter项目,对应iOS的编程语言是Swift,对应Android的编程语言是kotlin
flutter create batterylevel
-
创建方式二:指定编程语言,如果我们希望指定编程语言,比如iOS编程语言为Objective-C,Android的编程语言为Java
flutter create -i objc -a java batterylevel2
-
提示:
i
代表iOS
,a
代表android
-
-
-
1.2.3、写Dart代码
在Dart代码中,我们需要创建一个MethodChannel对象:- 创建该对象时,需要传入一个name,该name是区分多个通信的名称
- 可以通过调用该对象的invokeMethod来给对应的平台发送消息进行通信
-
该调用是异步操作,需要通过await获取then回调来获取结果
import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( // 启动要显示的界面 home: HomePage(), ); } } class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("原生电池的调用"), ), body: JKBatteryLevel(), ); } } class JKBatteryLevel extends StatefulWidget { @override _JKBatteryLevelState createState() => _JKBatteryLevelState(); } class _JKBatteryLevelState extends State
{ // 定义一个平台通道 static const platform1 = const MethodChannel('com.jk/battery'); int _batterylevel = 0; @override Widget build(BuildContext context) { return Center( child: Column( children: [ RaisedButton( child: Text('获取剩余电量'), onPressed: _buildLevelInfo ), Text('电量:${_batterylevel}') ], ), ); } void _buildLevelInfo() async { // 调用原生的电池信息 final result = await platform1.invokeMethod('getBatteryInfo'); setState(() { _batterylevel = result; }); } } 当我们通过
platform.invokeMethod
调用对应平台方法时,需要在对应的平台实现其操作:
iOS
中可以通过Objective-C
或Swift
来实现
Android
中可以通过Java
或者Kotlin
来实现
-
-
1.2.4、编写iOS代码
-
<1>、Swift 代码,在
AppDelegate.swift
里面写代码import UIKit import Flutter @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { // 实现获取电量信息的功能 // 1、获取FlutterViewController let flutterController: FlutterViewController = window.rootViewController as! FlutterViewController // 2、创建 FlutterMethodChannel /** name:static const platform1 = const MethodChannel('com.jk/battery'); 的 com.jk/battery,名字自己定义: 域名/名字 binaryMessenger: 二进制消息 */ let channel = FlutterMethodChannel(name: "com.jk/battery", binaryMessenger: flutterController.binaryMessenger); // 3.监听channnel方法 channel.setMethodCallHandler { (call: FlutterMethodCall, result: @escaping FlutterResult) in guard call.method == "getBatteryInfo" else { // 找不到该方法 result(FlutterMethodNotImplemented) return; } let device = UIDevice.current // 电池电量的探测,设置为true,才能更好的获取电量 device.isBatteryMonitoringEnabled = true if device.batteryState == .unknown { result(FlutterError(code: "Unknown", message: "Battery is unknown", details: nil)) } else { result(Int(device.batteryLevel * 100)) } } GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } }
-
<2>、OC 代码
#import "AppDelegate.h" #import "GeneratedPluginRegistrant.h" @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // 1.获取FlutterViewController(是应用程序的默认Controller) FlutterViewController *flutterController = (FlutterViewController *)self.window.rootViewController; // 2.获取MethodChannel(方法通道) FlutterMethodChannel *batteryChannel = [FlutterMethodChannel methodChannelWithName:@"com.jk/battery" binaryMessenger:flutterController.binaryMessenger]; // 3.监听方法调用(会调用传入的回调函数) __weak typeof(self) weakSelf = self; [batteryChannel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult _Nonnull result) { // 3.1.判断是否是getBatteryInfo的调用 if ([@"getBatteryInfo" isEqualToString:call.method]) { // 1.iOS中获取信息的方式 int batteryLevel = [weakSelf getBatteryLevel]; // 2.如果没有获取到,那么返回给Flutter端一个异常 if (batteryLevel == -1) { result([FlutterError errorWithCode:@"UNAVAILABLE" message:@"Battery info unavailable" details:nil]); } else { // 3.通过result将结果回调给Flutter端 result(@(batteryLevel)); } } else { // 3.2.如果调用的是getBatteryInfo的方法, 那么通过封装的另外一个方法实现回调 result(FlutterMethodNotImplemented); } }]; [GeneratedPluginRegistrant registerWithRegistry:self]; // Override point for customization after application launch. return [super application:application didFinishLaunchingWithOptions:launchOptions]; } - (int)getBatteryLevel { // 获取信息的方法 UIDevice* device = UIDevice.currentDevice; device.batteryMonitoringEnabled = YES; if (device.batteryState == UIDeviceBatteryStateUnknown) { return -1; } else { return (int)(device.batteryLevel * 100); } } @end
-
-
1.2.5、编写 Android 代码
-
<1>、Ktolin 代码
package com.example.batterylevel import android.content.Context import android.content.ContextWrapper import android.content.Intent import android.content.IntentFilter import android.os.BatteryManager import android.os.Build import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel class MainActivity: FlutterActivity() { private val CHANNEL = "com.jk/battery" override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) // 1.创建MethodChannel对象 val methodChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL) // 2.添加调用方法的回调 methodChannel.setMethodCallHandler { call, result -> if (call.method == "getBatteryInfo") { // 2.1.1.调用另外一个自定义方法回去电量信息 val batteryLevel = getBatteryLevel() // 2.1.2. 判断是否正常获取到 if (batteryLevel != -1) { // 获取到返回结果 result.success(batteryLevel) } else { // 获取不到抛出异常 result.error("UNAVAILABLE", "Battery level not available.", null) } } else { // 2.2.如果调用的方法是getBatteryInfo,那么正常执行 result.notImplemented() } } } private fun getBatteryLevel(): Int { val batteryLevel: Int if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) { val batteryManager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY) } else { val intent = ContextWrapper(applicationContext).registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED)) batteryLevel = intent!!.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100 / intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1) } return batteryLevel } }
-
<2>、Java 代码
实现思路和上面是一致的,只是使用了Java来实现:package com.example.batterylevel2; import androidx.annotation.NonNull; import io.flutter.embedding.android.FlutterActivity; import io.flutter.embedding.engine.FlutterEngine; import io.flutter.plugins.GeneratedPluginRegistrant; import io.flutter.plugin.common.MethodChannel; import android.content.ContextWrapper; import android.content.Intent; import android.content.IntentFilter; import android.os.BatteryManager; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import android.os.Bundle; public class MainActivity extends FlutterActivity { private static final String CHANNEL = "com.jk/battery"; @Override public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { // 1.创建MethodChannel对象 MethodChannel methodChannel = new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL); // 2.添加调用方法的回调 methodChannel.setMethodCallHandler( (call, result) -> { // 2.1.如果调用的方法是getBatteryInfo,那么正常执行 if (call.method.equals("getBatteryInfo")) { // 2.1.1.调用另外一个自定义方法回去电量信息 int batteryLevel = getBatteryLevel(); // 2.1.2. 判断是否正常获取到 if (batteryLevel != -1) { // 获取到返回结果 result.success(batteryLevel); } else { // 获取不到抛出异常 result.error("UNAVAILABLE", "Battery level not available.", null); } } else { // 2.2.如果调用的方法是getBatteryInfo,那么正常执行 result.notImplemented(); } } ); } private int getBatteryLevel() { int batteryLevel = -1; if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { BatteryManager batteryManager = (BatteryManager) getSystemService(BATTERY_SERVICE); batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY); } else { Intent intent = new ContextWrapper(getApplicationContext()). registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); batteryLevel = (intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100) / intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1); } return batteryLevel; } }
-
-
二、嵌入原有项目
首先,我们先明确一点:Flutter设计初衷并不是为了和其它平台进行混合开发,它的目的是为了打造一个完整的跨平台应用程序。
但是,实际开发中,原有项目完全使用Flutter进行重构并不现实,对于原有项目我们更多可能采用混合开发的方式。
-
2.1、创建Flutter模块
-
对于需要进行混合开发的原有项目,Flutter可以作为一个库或者模块,继承进现有项目中。
- 模块引入到你的Android或iOS应用中,以使用Flutter渲染一部分的UI,或者共享的Dart代码。
- 在Flutter v1.12中,添加到现有应用的基本场景已经被支持,每个应用在同一时间可以集成一个全屏幕的Flutter实例。
-
但是,目前一些场景依然是有限制的:
- 运行多个Flutter实例,或在屏幕局部上运行Flutter可能会导致不可以预测的行为;
- 在后台模式使用Flutter的能力还在开发中(目前不支持);
- 将Flutter库打包到另一个可共享的库或将多个Flutter库打包到同一个应用中,都不支持;
- 添加到应用在Android平台的实现基于 FlutterPlugin 的 API,一些不支持 FlutterPlugin 的插件可能会有不可预知的行为。
-
创建 Flutter Module
flutter create --template module my_flutter
创建完成后,该模块和普通的Flutter项目一直,可以通过Android Studio或VSCode打开、开发、运行;
- 目录结构如下:
和之前项目不同的iOS和Android项目是一个隐藏文件,并且我们通常不会单独打开它们再来运行;
-
它们的作用是将Flutter Module进行编译,之后继承到现有的项目中;
my_flutter/ ├── .iOS/ ├── .android/ ├── lib/ │ └── main.dart ├── test/ └── pubspec.yaml
- 目录结构如下:
-
-
2.2、嵌入iOS项目
-
嵌入到现有iOS项目有多种方式:
- 可以使用 CocoaPods 依赖管理和已安装的 Flutter SDK ;
- 也可以通过手动编译 Flutter engine 、你的 dart 代码和所有 Flutter plugin 成 framework ,用 Xcode 手动集成到你的应用中,并更新编译设置;
目前iOS项目几乎都已经使用Cocoapods进行管理,所以推荐使用第一种CocoaPods方式;
-
我们按照如下的方式,搭建一个需要继承的iOS项目:我们暂且起名字:
testdemoios
-
1、为了进行测试,我们这里创建一个默认的iOS项目:使用Xcode创建即可
-
2、将项目加入CocoaPods进行管理,电脑上需要已经安装了CocoaPods,直接百度输入 CocoaPods即可搜到很多的教程,按着教程来就好
初始化CocoaPods:
cd 进入刚才创建的 testdemoios pod init
编译Podfile文件:
# platform :ios, '9.0' # 添加模块所在路径,记得 `command + s` 保存 flutter_application_path = '../../my_flutter/my_flutter' load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb') target 'testdemoios' do use_frameworks! # 安装Flutter模块 install_all_flutter_pods(flutter_application_path) end
提示:
flutter_application_path = '../../my_flutter/my_flutter'
后面的路径,我们可以放一个统一的位置,方便团队开发
安装CocoaPods的依赖
pod install
-
-
2.2.1、Swift代码里面嵌入 上面 my_flutter 包
为了在既有的iOS应用中展示Flutter页面,需要启动 Flutter Engine和 FlutterViewController。
通常建议为我们的应用预热一个 长时间存活 的FlutterEngine:
我们将在应用启动的AppDelegate.swift
中创建一个FlutterEngine
,并作为属性暴露给外界。import UIKit import Flutter @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? lazy var flutterEngine = FlutterEngine(name: "my flutter engine") func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // 开启引擎 flutterEngine.run() return true } }
在启动的ViewController中,创建一个UIButton,并且点击这个Button时,弹出FlutterViewController
import UIKit import Flutter class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .green let button = UIButton(frame: CGRect(x: 0, y: 0, width: 200, height: 50)) button .setTitle("进入 Flutter 界面", for: .normal) button.backgroundColor = .brown button.center = view.center button.addTarget(self, action: #selector(click), for: .touchUpInside) view.addSubview(button) } @objc func click() { let flutterVC = FlutterViewController(engine: (UIApplication.shared.delegate as! AppDelegate).flutterEngine, nibName: nibName, bundle: nil) self .present(flutterVC, animated: true, completion: nil) } }
提示:我当时运行代码报错:
framework not found FlutterPluginRegistrant
,我进行了一下pod update
就好了\-
我们也可以省略预先创建的 FlutterEngine :不推荐这样来做,因为在第一针图像渲染完成之前,可能会出现明显的延迟。
func showFlutter() { let flutterViewController = FlutterViewController(project: nil, nibName: nil, bundle: nil) present(flutterViewController, animated: true, completion: nil) }
-
2.2.1、Objective-C代码
如果上面的代码希望使用Objective-C也是可以实现的:代码的逻辑是完成一致的-
AppDelegate.h代码:
@import UIKit; @import Flutter; @interface AppDelegate : FlutterAppDelegate @property (nonatomic,strong) FlutterEngine *flutterEngine; @end
-
AppDelegate.m代码:
#import
// Used to connect plugins. #import "AppDelegate.h" @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { self.flutterEngine = [[FlutterEngine alloc] initWithName:@"my flutter engine"]; [self.flutterEngine run]; [GeneratedPluginRegistrant registerWithRegistry:self.flutterEngine]; return [super application:application didFinishLaunchingWithOptions:launchOptions]; } @end -
ViewController.m代码
@import Flutter; #import "AppDelegate.h" #import "ViewController.h" @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom]; [button addTarget:self action:@selector(showFlutter) forControlEvents:UIControlEventTouchUpInside]; [button setTitle:@"Show Flutter!" forState:UIControlStateNormal]; button.backgroundColor = UIColor.blueColor; button.frame = CGRectMake(80.0, 210.0, 160.0, 40.0); [self.view addSubview:button]; } - (void)showFlutter { FlutterEngine *flutterEngine = ((AppDelegate *)UIApplication.sharedApplication.delegate).flutterEngine; FlutterViewController *flutterViewController = [[FlutterViewController alloc] initWithEngine:flutterEngine nibName:nil bundle:nil]; [self presentViewController:flutterViewController animated:YES completion:nil]; } @end
-
-
-
2.3.嵌入Android项目
嵌入到现有Android项目有多种方式:- 编译为AAR文件(Android Archive):通过Flutter编译为aar,添加相关的依赖
- 依赖模块的源码方式,在gradle进行配置
这里我们采用第二种方式
-
1>、创建一个Android的测试项目,使用Android Studio创建
-
2>、添加相关的依赖
-
修改Android项目中的settings.gradle文件:
include ':app' rootProject.name = "testdemoandroid" setBinding(new Binding([gradle: this])) // new evaluate(new File( // new settingsDir.parentFile, // new '../my_flutter/my_flutter/.android/include_flutter.groovy' // new ))
提示:
File()
后面的路径是my_flutter
项目的路径,我放置的和上面iOS那个图一样 -
我们需要在Android项目工程的build.gradle中添加依赖:
dependencies { implementation project(':flutter') }
编译代码,可能会出现如下错误: 1、这是因为从Java8开始才支持接口方法;2、Flutter Android引擎使用了该Java8的新特性
解决办法:通过设置Android项目工程的build.gradle配置使用Java8编译:
compileOptions { sourceCompatibility 1.8 targetCompatibility 1.8 }
接下来,我们这里尝试添加一个Flutter的screen到Android应用程序中
Flutter提供了一个FlutterActivity来展示Flutter界面在Android应用程序中,我们需要先对FlutterActivity进行注册:-
在AndroidManifest.xml中进行注册
-
-
2.3.1、Java代码
package com.jk.testandroid; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import io.flutter.embedding.android.FlutterActivity; public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // setContentView(R.layout.activity_main); startActivity( FlutterActivity.createDefaultIntent(this) ); } }
也可以在创建时,传入默认的路由:
package com.jk.testandroid; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import io.flutter.embedding.android.FlutterActivity; public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // setContentView(R.layout.activity_main); startActivity( FlutterActivity .withNewEngine() .initialRoute("/my_route") .build(currentActivity) ); } }
-
2.3.2、Kotlin代码
package com.jk.test_demo_a_k import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import io.flutter.embedding.android.FlutterActivity class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // setContentView(R.layout.activity_main) startActivity( FlutterActivity.createDefaultIntent(this) ) } }
也可以在创建时指定路由:
package com.coderwhy.test_demo_a_k import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import io.flutter.embedding.android.FlutterActivity class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // setContentView(R.layout.activity_main) startActivity( FlutterActivity .withNewEngine() .initialRoute("/my_route") .build(this) ); } }
-
三、Flutter模块调试
一旦将Flutter模块继承到你的项目中,并且使用Flutter平台的API运行Flutter引擎或UI,那么就可以先普通的Android或者iOS一样来构建自己的Android或者iOS项目了
但是Flutter的有一个非常大的优势是其快速开发,也就是hot reload。
那么对应Flutter模块,我们如何使用hot reload加速我们的调试速度呢?
-
可以使用
flutter attach
# --app-id是指定哪一个应用程序 # -d是指定连接哪一个设备 flutter attach --app-id com.coderwhy.ios-my-test -d 3D7A877C-B0DD-4871-8D6E-0C5263B986CD