Flutter学习之混合开发
1. 调用原生功能
有些时候,Flutter想要调用一些原生的能力,比如相机、相册、Map,这个时候我们就需要用到一些Flutter的一些插件了,在pub.dev
上,官方给我们提供了许多的插件。
想要调用原生的能力,但是确实没有对应的插件,这个时候就可以需要我们去写一部分原生的代码实现这个功能,然后再使用Dart调用原生的代码,来达到调用原生能力的目的
还有一种是公司已经有原生的APP(Android,IOS)但是新开发一个模块,或则重构模块功能的时候,我们希望使用Flutter来开发或则重构新的功能, 然后再把Flutter开发的代码集成到原生的项目中。
以上几点问题就造成了 混合开发
1.1 Camera(已有三方插件支持直接调用)
某些应用程序可能需要使用移动设备进行拍照或者选择相册中的照片,Flutter官方提供了插件:image_picker
1.1.1 添加依赖
首先我们添加image_picker
的依赖:
dependencies:
image_picker: ^0.8.4+4
1.1.2 平台配置
对IOS平台,想要访问相机和相册,对于用户来说这是隐私,需要获取用户的允许:
- 修改
info.plist
文件/ios/Runner/Info.plist
- 添加对相册的访问权限:
Privacy - Photo Library Usage Description
- 添加对相机的访问权限:
Privacy - Camera Usage Description
如果没有配置对应的权限,点击获取的话,APP是会崩溃的,所以这里我们需要使用Xcode打开项目 ,然后配置对应的权限
配置完对应的权限之后, 在运行demo,点击选择照片,会弹出如下提示框:
1.1.3 代码实现
image_picker
的核心方法是pickerImage
方法
Future<XFile?> pickImage({
required ImageSource source,
double? maxWidth,
double? maxHeight,
int? imageQuality,
CameraDevice preferredCameraDevice = CameraDevice.rear,
}) {
return platform.getImage(
source: source,
maxWidth: maxWidth,
maxHeight: maxHeight,
imageQuality: imageQuality,
preferredCameraDevice: preferredCameraDevice,
);
}
- 可以传入数据源
ImageSource
、图片的大小、质量、前后摄像头等 - 数据源是必传参数:
ImageSource
是一个枚举类型:
enum ImageSource {
/// Opens up the device camera, letting the user to take a new picture.
camera, //相机
/// Opens the user's photo gallery.
gallery,//相册
}
- 案例演练:
import 'package:flutter/material.dart';
import 'dart:io';
import 'package:image_picker/image_picker.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
XFile? imageFile;
final ImagePicker _picker = ImagePicker();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'访问原生功能相册',
),
ElevatedButton(onPressed: _pickImage, child: Text("请选择一张图片")),
imageFile == null ? Text("请您先选择一张照片") : Image.file(File(imageFile!.path)),
],
),
),
);
}
/* 获取相册图片是异步的, 访问相册*/
void _pickImage() async {
//访问用户相册
XFile? file = await _picker.pickImage(source: ImageSource.gallery) ;
setState(() {
imageFile = file;
});
}
}
选择照片后效果:
1.2 电池信息(没有三方插件支持,需要编写原生代码)
某些原生的信息,如果没有很好的插件,我们可以通过platform channels(平台通道)来获取信息。
1.2.1 平台通过介绍
官方文档介绍
平台通过是如何工作的?
- 消息使用
platform channels(平台通道)
在客户端(UI)和宿主(平台)之间传递 - 消息和响应已
异步
的形式传递,以确保用户界面能够保持响应
调用过程大致如下:
- 客户端(Flutter端)发送与方法调用对应的消息
- 平台(IOS、Android端)接收方法,并返回结果
- IOS端通过
FlutterMethodChannel
做出响应 - Android端通过
MethodChannel
做出响应
- IOS端通过
Flutter、IOS、Android端数据类型的对应关系:
1.2.2 创建测试项目
这里我们创建一个获取电池信息的demo,分别通过IOS和Android来获取对应的信息
创建方式一:默认创建方式
- Flutter默认的创建方式创建出来的项目,IOS对应是swift语言,Android对应的kotlin语言
flutter create xxx
- 创建方式二:指定编程语言
如果我们希望创建项目,IOS对应的Objetive-C语言,Android创建对应的Java语言,那么我们可以指定编程语言创建
flutter create -i objc -a java xxx
1.2.3 编写Dart代码
在Dart代码中,我们需要创建一个MethodChannel
对象
- 创建对象时,需要传入一个
name
,该name
是区分多个通信的名称 - 可以通过调用该对象的
invokeMethod
来对应的平台发送消息进行通信- 该调用是异步操作,需要通过await获取then回调来获取结果
class _GYHomePageState extends State<GYHomePage> {
//核心代码一:
static const platform = MethodChannel("gy.com/battery");
int _result = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title:const Text("battery")),
body: Center(
child: Column(
children: [
ElevatedButton(onPressed: (){
}, child: const Text("获取手机电量")),
Text("手机电量值==$_result"),
],
),
),
);
}
void getPhoneBattery() async {
//核心代码二:
final int result = await platform.invokeMethod("getBatteryInfo");
setState(() {
_result = result;
});
}
}
当我们通过platform.invokeMethod
,调用对应平台的方法时,需要再对应平台实现其操作
- IOS中可以通过Objective-C或swift来实现
- Android可以通过Kotlin或Java来实现
1.2.4 编写IOS平台代码
1.2.4.1 swift代码实现
- 代码相关操作步骤如下:
- 获取FlutterViewController(是应用程序的默认Controller)
- 获取MthodChannel(方法通道)(注意:这里需要根据我们创建名称来获取)
- 监听方法调用(会调用传入的回调函数)
- IOS中获取信息的方式
- 如果没有获取到,那么返回给Flutter端一个异常
- 通过result将结果回调给Flutter端
- 判断是否是
getBatteryInfo
的调用,告知Flutter端没有实现对应的方法 - 如果调用的是
getBatteryInfo
方法,那么通过封装的另外一个方法实现回调
import UIKit
import Flutter
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
//1.获取应用程序的FlutterViewController(是应用程序的默认Controller)
if let controller: FlutterViewController = window.rootViewController as? FlutterViewController {
//2.获取MethodChannel(方法通道) name: 是和flutter端定义好的通道名称
let batteryChannel = FlutterMethodChannel(name: "gy.com/battery",
binaryMessenger: controller.binaryMessenger)
//3.监听方法调用(会调用传入的回调函数)
batteryChannel.setMethodCallHandler { [weak self] (call: FlutterMethodCall, result :@escaping FlutterResult) in
guard let strongSelf = self else {return}
//3.1判断回调方法是否是getBatteryInfo获取电量的方法
guard call.method == "getBatteryInfo" else {
result(FlutterMethodNotImplemented)
return
}
//3.2如果调用的是getBatteryInfo方法,那么封装另外一个方法实现回调
strongSelf.receiveBatteryLevel(result: result)
}
}
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
/// 获取手机电量并回调
/// - Parameter result: <#result description#>
private func receiveBatteryLevel(result: FlutterResult) {
//1.IOS中获取设备信息的方式
let device = UIDevice.current
//是否开启电池监控
device.isBatteryMonitoringEnabled = true
//2.如果没有获取到电池信息,那么返回给Flutter一个异常, IOS中模拟器是无法获取到电池电量的,只有真机才可以获取
if device.batteryState == UIDevice.BatteryState.unknown {
result(FlutterError(code: "UNAVAILABLE",
message: "无法获取到手机电池信息",
details: nil))
} else {
//3. 通过result将结果回调给Flutter端
result(Int(device.batteryLevel))
}
}
}
1.2.4.2 Objective-C代码实现
实现思路和上面是一样的,只是使用Objective-C来实现:
#import <Flutter/Flutter.h>
#import "AppDelegate.h"
#import "GeneratedPluginRegistrant.h"
@implementation AppDelegate
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
// 1.获取FlutterViewController(是应用程序的默认Controller)
FlutterViewController* controller = (FlutterViewController*)self.window.rootViewController;
// 2.获取MethodChannel(方法通道)
FlutterMethodChannel* batteryChannel = [FlutterMethodChannel
methodChannelWithName:@"gy.com/battery"
binaryMessenger:controller.binaryMessenger];
// 3.监听方法调用(会调用传入的回调函数)
__weak typeof(self) weakSelf = self;
[batteryChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult 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:@"无法获取到手机电池信息"
details:nil]);
} else {
// 3.通过result将结果回调给Flutter端
result(@(batteryLevel));
}
} else {
// 3.2.如果调用的是getBatteryInfo的方法, 那么通过封装的另外一个方法实现回调
result(FlutterMethodNotImplemented);
}
}];
[GeneratedPluginRegistrant registerWithRegistry:self];
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代码
思路和上面是一致是 ,只是使用Kotlin来实现的,
编写Android代码,我们建议使用IDEA重新打开Android的项目, 不要在flutter工程里面的android文件夹下面直接写代码。因为使用IDEA重新打开Android项目, 工具会优化项目的目录结构。
1.2.5.1 kotlin 实现代码
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() {
//定义一个约定的渠道名称val: 声明一个只读变量
private val channel_name = "gy.com/battery";
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
//1.创建MethodChannel对象
val methodChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, channel_name);
//2.添加方法的调用
methodChannel.setMethodCallHandler { call, result ->
//判断调用的方法是否是getBatteryInfo
if (call.method == "getBatteryInfo") {
//调用方法获取电量
val batteryLevel = getBatteryLevel()
if (batteryLevel != -1) {
//获取到返回结果
result.success(batteryLevel)
} else {
//获取不到抛出异常
result.error("UNAVAILABLE","获取不到电量信息",null)
}
} else {
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
}
}
1.2.5.2 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 = "coderwhy.com/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;
}
}
2. 嵌入原有项目
首先我们明确一点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进行编译,之后继承到现有的项目中;
2.2 嵌入IOS项目
官方资料:https://docs.flutter.dev/development/add-to-app/ios/project-setup
嵌入到IOS项目有多重方式:
- 可以使用CocoaPods依赖和管理Flutter SDK
- 也可以通过手动编译
Flutter engine
、你的dart
代码和所有Flutter plugin
编译成framework
,用 Xcode 手动集成到你的应用中,并更新编译设置;
我们按照如下方式,首先需要搭建一个IOS的项目,关于创建IOS项目这里不做过多的描述。
在给项目添加CocoaPods依赖:
- 初始化CocoaPods:
pod init
- 安装CocoaPods的依赖:
pod install
- 编译
Podfile
文件
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'
# 添加模块所在路径
flutter_application_path = '../my_flutter'
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')
target 'ios_my_test' do
# Comment the next line if you don't want to use dynamic frameworks
use_frameworks!
# 安装Flutter模块
install_all_flutter_pods(flutter_application_path)
# Pods for ios_my_test
end
修改完Podfile
文件之后需要执行pod install
2.2.1 swift代码
为了在已有的IOS项目中展示Flutter页面,需要启动Flutter Engine
和 FlutterViewController
。
通常建议为我们的应用预热一个 长时间存活 的FlutterEngine:
- 官方建议我们尽早的初始化 flutter的引擎,所以我们在AppDelegate类中创建一个
FlutterEngine
,并作为属性暴露给外界
import UIKit
import FlutterPluginRegistrant
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
//1. 创建一个flutterEngine对象(flutter引擎对象)
lazy var flutterEngine = FlutterEngine(name: "my flutter engine")
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
//启动flutterEngine , 启动之后,我们在其他地方如果有需要用到flutter模块就不在需要当场创建flutterEngine对象了 官方建议我们提前把flutter引擎启动起来
flutterEngine.run()
// Override point for customization after application launch.
return true
}
}
接下来我们在启动的页面中创建一个按钮,点击按钮,然后跳转到flutter的页面。
import UIKit
import Flutter
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// 1.创建一个按钮
let button = UIButton(type: UIButton.ButtonType.custom)
button.addTarget(self, action: #selector(showFlutter), for: .touchUpInside)
button.setTitle("Show Flutter", for: .normal)
button.frame = CGRect(x: 80, y: 210, width: 160, height: 40)
button.backgroundColor = UIColor.blue
self.view.addSubview(button)
}
@objc func showFlutter() {
// 2.创建FlutterViewController对象(需要先获取flutterEngine)
if let flutterEngine = (UIApplication.shared.delegate as? AppDelegate)?.flutterEngine {
let flutterViewController = FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil)
flutterViewController.view.backgroundColor = UIColor.white //不设置 背景色跳转 会有明显卡顿和黑屏的问题,
flutterViewController.modalPresentationStyle = .fullScreen
present(flutterViewController, animated: true, completion: nil)
//navigationController?.pushViewController(flutterViewController, animated: true)
}
}
}
我们也可以省略预先创建的 FlutterEngine
:
- 不推荐这样来做,因为在第一针图像渲染完成之前,可能会出现明显的延迟
func showFlutter() {
//这里不传flutterEngine对象会默认创建一个该对象
let flutterViewController = FlutterViewController(project: nil, nibName: nil, bundle: nil)
present(flutterViewController, animated: true, completion: nil)
}
关于OC代码我这边 就不做演示了, 思路一样,只是把swift的代码用OC翻译一遍
2.3 嵌入Android项目
嵌入到现有Android项目有多种方式:
- 编译为AAR文件(Android Archive):通过Flutter编译为aar,添加相关的依赖
- 依赖模块的源码方式,在gradle进行配置
这里我们采用第二种方式来实现
创建一个Android的测试项目,使用Android Studio 来创建
2.3.1 添加相关依赖
- 修改Android项目中的settings.gradle文件:
// Include the host app project.
include ':app' // assumed existing content
setBinding(new Binding([gradle: this])) // new
evaluate(new File( // new
settingsDir.parentFile, // new
'my_flutter/.android/include_flutter.groovy' // new
))
- 另外,我们需要在Android项目工程的build.gradle中添加依赖:
dependencies {
implementation project(':flutter')
}
编译代码,可能会出现如下错误:
- 这是因为从Java8开始才支持接口方法
- Flutter Android引擎使用了该Java8的新特性
解决办法:通过设置Android项目工程的build.gradle配置使用Java8编译: `
compileOptions {
sourceCompatibility 1.8
targetCompatibility 1.8
}
接下来,我们这里尝试添加一个Flutter的screen到Android应用程序中
Flutter提供了一个FlutterActivity来展示Flutter界面在Android应用程序中,我们需要先对FlutterActivity进行注册:
在AndroidManifest.xml中进行注册:
<activity
android:name="io.flutter.embedding.android.FlutterActivity"
android:theme="@style/AppTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"
/>
2.3.2 java代码
package com.coderwhy.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.coderwhy.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)
);
}
}
3. 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 axzq.mixed-demo-ios-d EA45D4FC-2890-44DB-B60E-47F1B84E89BD
,也可以如下图操作 可以使用选择设备