플러터를 사용해 앱을 개발하다보면 네이티브 기능을 호출할 수 없는 경우가 찾아오게 된다.
이 때, 두가지 선택지를 고를 수 있는데 하나는 pub.dev
에서 원하는 기능에 해당하는 서드파티 패키지를 찾아 활용하는 방법이고, 다른 하나는 오늘 소개할 MethodChannel을 이용하는 방법이다.
메소드 채널은 플러터에서 네이티브에 작성된 함수를 호출하고 그 결과를 플러터 코드로 받을 수 있는 기능인데, 이를 통해 플러터만으로는 구현할 수 없는 다양한 기능을 구현할 수 있다.
실제로 서드파티 패키지들 중 네이티브 기능을 호출해야하는 패키지의 경우(특히 하드웨어 접근과 관련된 기능이 대표적이다)메소드 채널을 활용해 네이티브로 구현되어 있는 함수를 플러터에서 호출하고 그 결과를 활용한다.
아래의 코드는 메소드 채널을 구현한 예제이다.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return const MaterialApp(
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final MethodChannel _channel =
const MethodChannel("com.example.native_connection_study");
String _resultData = "";
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Method Channel')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(_resultData),
const SizedBox(height: 32.0),
ElevatedButton(
// 메소드 채널의 결과는 Future타입으로 반환되므로 async 키워드를 이용한다.
onPressed: () async {
// 메소드 채널을 호출할 때는 invokeMethod를 통해 호출한다.
final result = await _channel
.invokeMethod("getNativeData", ["Raon", "29"]);
setState(() {
_resultData = result.toString();
});
},
child: const Text("Call Native Method"),
),
],
),
),
);
}
}
MethodChannel객체는 네이티브와 통신할 수 있는 채널을 연결한다. 이 때 "com.example.native_connection_study"
와 같이 패키지 이름으로 채널을 연결하는데, 굳이 패키지 이름으로 할 필요는 없지만 구분이 확실한 이름으로 채널명을 지정하는 것이 좋다.
channel.invokeMethod()
를 보면 두 개의 파라미터를 전달하는데, 이는 각각 네이티브 함수 호출 이름, 네이티브로 전달하는 값을 의미한다.
여기서는 getNativeData
라는 이름으로 네이티브 함수를 호출하는데, 이 때, 전달되는 값은 "Raon"
과 "29"
라는 값이다.
import UIKit
import Flutter
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
// 작성된 코드 영역 시작
// MethodChannel 연결
let controller = window?.rootViewController as! FlutterViewController
let channel: FlutterMethodChannel = FlutterMethodChannel(
name: "com.example.native_connection_study",
binaryMessenger: controller.binaryMessenger)
// 핸들러를 이용한 네이티브 코드 실행
channel.setMethodCallHandler({
[weak self] (methodCall: FlutterMethodCall, result: FlutterResult) -> Void in
// Flutter코드에서 명시된 호출명 구분
if(methodCall.method == "getNativeData") {
result(self?.getNativeData(arguments: methodCall.arguments))
return
}
result(FlutterMethodNotImplemented)
})
// 작성된 코드 영역 끝
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
// 새로 작성한 함수
private func getNativeData(arguments:Any?) -> Any {
// Flutter에서 arguments가 전달될 때는 어차피 배열로 전달되므로 Array<Any>?로 캐스팅한다.
let args = arguments as? Array<Any>
// Flutter로 전달해줄 예제 데이터 생성
let jsonData : [String: Any] = [
"name": args?[0] ?? "None",
"age": args?[1] ?? "None"
] as Dictionary
// Json으로 변환
var jsonObj : String = ""
do {
let jsonCreate = try JSONSerialization.data(withJSONObject: jsonData, options: .prettyPrinted)
jsonObj = String(data: jsonCreate, encoding: .utf8) ?? ""
}catch{
jsonObj = ""
}
return jsonObj
}
}
이 코드는 flutter 프로젝트를 처음 생성하면 자동으로 만들어지는 AppDelegate.swift파일에 추가로 작성된 코드이다.
Swift를 경험해보지 못한 사람이 보면 장황하게 길어 보여 다소 복잡하게 느낄수 있는데, 크게 3부분으로 나눌 수 있다.
1. MethodChannel 연결
let controller = window?.rootViewController as! FlutterViewController
let channel: FlutterMethodChannel = FlutterMethodChannel(
name: "com.example.native_connection_study",
binaryMessenger: controller.binaryMessenger)
이 부분이 MethodChannel을 연결하는 부분인데, 플러터 코드의
final channel = MethodChannel("com.example.native_connection_study");
에 대응하는 코드로 볼 수 있다.
2. Handler 설정
channel.setMethodCallHandler({
[weak self] (methodCall: FlutterMethodCall, result: FlutterResult) -> Void in
// Flutter코드에서 명시된 호출명 구분
if(methodCall.method == "getNativeData") {
result(self?.getNativeData(arguments: methodCall.arguments))
return
}
result(FlutterMethodNotImplemented)
})
핸들러는 FlutterMethodChannel.setMethodCallHandler()
로 작성할 수 있다. 핸들러는 기본적으로 FlutterMethodCall
, FlutterResult
를 전달받는데, 각각에 대한 설명은 아래와 같다.
FlutterMethodCall
: Flutter에서 invokeMethod가 수행되면서 전달된 객체로, method, arguments 변수를 가지고 있다.
- method : invokeMethod에서 정의된 호출 명
- arguments : invokeMehtod의 arguments
FlutterResult
: 네이티브코드에서 수행된 로직의 결과를 Flutter코드로 전달하기 위한 객체.
3. 필요한 로직 작성
private func getNativeData(arguments:Any?) -> Any {
// Flutter에서 arguments가 전달될 때는 어차피 배열로 전달되므로 Array<Any>?로 캐스팅한다.
let args = arguments as? Array<Any>
// Flutter로 전달해줄 예제 데이터 생성
let jsonData : [String: Any] = [
"name": args?[0] ?? "None",
"age": args?[1] ?? "None"
] as Dictionary
// Json으로 변환
var jsonObj : String = ""
do {
let jsonCreate = try JSONSerialization.data(withJSONObject: jsonData, options: .prettyPrinted)
jsonObj = String(data: jsonCreate, encoding: .utf8) ?? ""
}catch{
jsonObj = ""
}
return jsonObj
}
이 예제에서는 Json으로 결과를 반환하는 함수를 작성했다. 굳이 Json이 아니더라도 필요에 따라 원하는 결과를 반환하면 된다.
package com.example.native_connection_study
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugins.GeneratedPluginRegistrant
import org.json.JSONObject
class MainActivity: FlutterActivity() {
// 작성된 코드 영역 시작
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
GeneratedPluginRegistrant.registerWith(flutterEngine)
val channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "com.example.native_connection_study")
channel.setMethodCallHandler { methodCall, result ->
if(methodCall.method == "getNativeData"){
result.success(getNativeData(methodCall.arguments))
}else{
result.notImplemented()
}
}
}
private fun getNativeData(arguments: Any?): Any {
val args: List<Any> = (arguments as List<Any>?) ?: mutableListOf()
val json = JSONObject(mapOf("name" to args[0], "age" to args[1]).toString())
return json.toString()
}
// 작성된 코드 영역 끝
}
Swift와 거의 동일한 구조로 작성되었다. 다른 점이 있다면 binaryMessenger를 호출하기 위해 참조해야할 객체나, kotlin과 swift라는 언어적 차이에서 발생하는 문법 정도가 다를 수 있다.
이렇게 Flutter에서 네이티브 코드를 호출하는 방법에 대해 알아보았다.