Flutter 是移動端的跨平台開發,一套代碼可以分別運行在安卓和iOS系統上,能節省開發時間和效率,flutter現階段還處在不斷發展更新階段,不能夠完全適配多個系統,尤其是調用一些原生的功能,你如相冊相機通訊錄等等。這時就需要flutter和iOS或者安卓相互調用進行混合開發。flutter混合開發大致分為兩種場景一種是以Flutter為主項目去掉用原聲的功能,另一種是flutter作為某一個小模塊嵌入到以原生為主的項目中。
一. Flutter調用原生功能
以相機和電池為例介紹一下flutter調用原生功能
1.1. Camera
某些應用程序可能需要使用移動設備進行拍照或者選擇相冊中的照片,Flutter官方提供了插件:image_picker
1.1.1. 添加依賴
添加對image_picker的依賴:https://pub.dev/packages/image_picker
dependencies: image_picker: ^0.6.5
1.1.2. 平台配置
對iOS平台,想要訪問相冊或者相機,需要獲取用戶的允許:
- 依然是修改info.plist文件:/ios/Runner/Info.plist
- 添加對相冊的訪問權限:Privacy - Photo Library Usage Description
- 添加對相機的訪問權限:Privacy - Camera Usage Description
1.1.3. 代碼實現
image_picker的核心代碼是pickImage方法:
- 可以傳入數據源、圖片的大小、質量、前置后置攝像頭等
- 數據源是必傳參數:ImageSource枚舉類型
- camera:相機
- gallery:相冊
import 'package:flutter/material.dart'; import 'dart:io'; import 'package:image_picker/image_picker.dart'; class HYCameraScreen extends StatefulWidget { static const String routeName = "/camera"; @override _HYCameraScreenState createState() => _HYCameraScreenState(); } class _HYCameraScreenState extends State<HYCameraScreen> { File _image; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Camera"), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ _image == null ? Text("未選擇圖片"): Image.file(_image), RaisedButton( child: Text("選擇照片"), onPressed: _pickImage, ) ], ), ), ); } void _pickImage() async { File image = await ImagePicker.pickImage(source: ImageSource.gallery); setState(() { _image = image; }); } }
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
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( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, splashColor: Colors.transparent), home: HYBatteryScreen(), ); } } class HYBatteryScreen extends StatefulWidget { static const String routeName = "/battery"; @override _HYBatteryScreenState createState() => _HYBatteryScreenState(); } class _HYBatteryScreenState extends State<HYBatteryScreen> { // 核心代碼一: static const platform = const MethodChannel("dzq.com/battery"); int _result = 0; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Battery"), ), body: Center( child: Column( children: <Widget>[ Text("當前電池信息: $_result"), RaisedButton( child: Text("獲取電池信息"), onPressed: getBatteryInfo, ) ], ), ), ); } void getBatteryInfo() async { // 核心代碼二 final int result = await platform.invokeMethod("getBatteryInfo"); setState(() { _result = result; }); } }
當我們通過 platform.invokeMethod
調用對應平台方法時,需要在對應的平台實現其操作:
- iOS中可以通過Objective-C或Swift來實現
- Android中可以通過Java或者Kotlin來實現
1.2.4. 編寫iOS代碼
1.2.4.1. Swift代碼實現
代碼相關的操作步驟如下:
- 1.獲取FlutterViewController(是應用程序的默認Controller)
- 2.獲取MethodChannel(方法通道)
- 注意:這里需要根據我們創建時的名稱來獲取
- 3.監聽方法調用(會調用傳入的回調函數)
- iOS中獲取信息的方式
- 如果沒有獲取到,那么返回給Flutter端一個異常
- 通過result將結果回調給Flutter端
- 3.1.判斷是否是getBatteryInfo的調用,告知Flutter端沒有實現對應的方法
- 3.2.如果調用的是getBatteryInfo的方法, 那么通過封裝的另外一個方法實現回調
-
import UIKit import Flutter @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { // 1.獲取FlutterViewController(是應用程序的默認Controller) let controller : FlutterViewController = window?.rootViewController as! FlutterViewController // 2.獲取MethodChannel(方法通道) let batteryChannel = FlutterMethodChannel(name: "coderwhy.com/battery", binaryMessenger: controller.binaryMessenger) // 3.監聽方法調用(會調用傳入的回調函數) batteryChannel.setMethodCallHandler({ [weak self] (call: FlutterMethodCall, result: FlutterResult) -> Void in // 3.1.判斷是否是getBatteryInfo的調用,告知Flutter端沒有實現對應的方法 guard call.method == "getBatteryInfo" else { result(FlutterMethodNotImplemented) return } // 3.2.如果調用的是getBatteryInfo的方法, 那么通過封裝的另外一個方法實現回調 self?.receiveBatteryLevel(result: result) }) GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } private func receiveBatteryLevel(result: FlutterResult) { // 1.iOS中獲取信息的方式 let device = UIDevice.current device.isBatteryMonitoringEnabled = true // 2.如果沒有獲取到,那么返回給Flutter端一個異常 if device.batteryState == UIDevice.BatteryState.unknown { result(FlutterError(code: "UNAVAILABLE", message: "Battery info unavailable", details: nil)) } else { // 3.通過result將結果回調給Flutter端 result(Int(device.batteryLevel * 100)) } } }
- 找不到flutter的錯我解決辦法 Not found 'Flutter'
1、先清理 flutter clean 2、加載插件 flutter pub get 3、編譯ios flutter build ios 如果第三步還解決不了直接走下面的吧 4、把flutter項目->build->ios下面的兩個目錄拖拽到ios->Flutter目錄下面 具體步驟 as右鍵打開build->ios->App.framework目錄,在文件夾中把App.framework和Flutter.framework拖拽到xcode的Flutter目錄下面,並導入
1.2.5. 編寫Android代碼
1.2.5.1. Kotlin代碼實現
實現思路和上面是一致的,只是使用了Kotlin來實現:
- 可以參考注釋內容
import androidx.annotation.NonNull import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel import android.content.Context 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 class MainActivity: FlutterActivity() { private val CHANNEL = "coderwhy.com/battery" override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { // 1.創建MethodChannel對象 val methodChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL) // 2.添加調用方法的回調 methodChannel.setMethodCallHandler { // Note: this method is invoked on the main thread. call, result -> // 2.1.如果調用的方法是getBatteryInfo,那么正常執行 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 (VERSION.SDK_INT >= 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 } }
二. 嵌入原有項目
首先,我們先明確一點: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項目:
1.為了進行測試,我們這里創建一個默認的iOS項目:使用Xcode創建即可
2.將項目加入CocoaPods進行管理
- 電腦上需要已經安裝了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
重新執行安裝CocoaPods的依賴:
pod install
2.2.1. Swift代碼
為了在既有的iOS應用中展示Flutter頁面,需要啟動 Flutter Engine
和 FlutterViewController
。
通常建議為我們的應用預熱一個 長時間存活
的FlutterEngine:
- 我們將在應用啟動的 app delegate 中創建一個
FlutterEngine
,並作為屬性暴露給外界import UIKit import FlutterPluginRegistrant @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { // 1.創建一個FlutterEngine對象 lazy var flutterEngine = FlutterEngine(name: "my flutter engine") func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // 2.啟動flutterEngine flutterEngine.run() return true } }
在啟動的ViewController中,創建一個UIButton,並且點擊這個Button時,彈出FlutterViewController
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) let flutterEngine = (UIApplication.shared.delegate as! AppDelegate).flutterEngine; let flutterViewController = FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil); navigationController?.pushViewController(flutterViewController, animated: true); } }
我們也可以省略預先創建的
FlutterEngine
:不推薦這樣來做,因為在第一針圖像渲染完成之前,可能會出現明顯的延遲。
2.3.嵌入Android項目
嵌入到現有Android項目有多種方式:
- 編譯為AAR文件(Android Archive)
- 通過Flutter編譯為aar,添加相關的依賴
- 依賴模塊的源碼方式,在gradle進行配置
這里我們采用第二種方式
1.創建一個Android的測試項目使用Android Studio創建
2.添加相關的依賴
修改Android項目中的settings.gradle文件:
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.1. 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) ); } }
2.3.2. Kotlin代碼
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.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