Flutter 네이티브 연결
먼저 Platform Channel에 대해서 모르시는 분들을 위해 간단하게 살펴보자면, Flutter는 앱, 웹, 데스크탑, 리눅스 등의 환경을 개발할 수 있도록 하는 프레임워크라는 사실은 이미 알고 계실거다. 앱에서는 양대 플랫폼인 Android / IOS를 모두 개발하는 크로스플랫폼인데, 네이티브 고유의 기능에 접근해야 하는 등의 개발이 필요한 경우 네이티브 언어를 통해 Flutter와 통신을 할 수 있다. 물론 통신을 하지 않고도 Flutter의 Dart Package에 등록된 PlugIn을 사용하여 개발이 가능하긴 하다.
여기서 말한 PlugIn이 바로 네이티브 코드로 만들어진 것이다. Library라고 불리는거는 Flutter 프레임워크에서 만들어진거고, PlugIn은 네이티브로 만들어진 것이다.
네이티브 언어로는 안드로이드의 Kotlin, IOS의 Swift를 사용하여 개발을 하여야 하는데, 네이티브 언어를 사용하지 못하거나 플랫폼 개발이 어려운 분들에게는 어려울 수도 있다.
Flutter만으로 앱을 개발하는 경우가 가장 베스트이기는 하지만 저도 많은 부분에서 Platform 채널을 통한 네이티브 코드를 사용하는 경우가 상당히 많은 편이다.
Flutter <> Native 간의 Platform Channle에는 MethodChannel, EventChannel, BasicMessageChannel 이렇게 세 가지 방법으로 통신할 수 있는데, BasicMessageChannel은 가벼운 통신 방식이며, MethodChannel이 활발하게 사용되다 보니 잘 사용하지 않는 기능이다. 저도 간단한 로그 출력하는 용도로만 사용하고, 잘 사용하지는 않는다.
MethodChannel은 단기적 이벤트를 발생시키는 채널로, 쉽게 생각하면 Flutter의 Future 비동기라고 생각하면 된다. EventChannel은 Stream 비동기 방식으로, 변경이 발생할 때마다 Flutter에서 Native로 또는 Native에서 Flutter로 이벤트를 발생하게 해준다.
당연히 MethodChannel도 Flutter -> Native, Natieve -> Flutter로 양방향 통신이 가능하다.
Method Channel
먼저 가장 흔하게 사용되는 Platform Channel인 MethodChannel에 대해서 살펴보도록 하겠다.
dependencies
사용되는 예제를 Cubit을 통해서 만들었기에 필요하시면 flutter_bloc, equatable 라이브러리를 추가하여야 한다.
dependencies:
flutter_bloc: ^8.1.1
equatable: ^2.0.5
Count App
카운트 예제 앱에는 Cubit 상태 관리를 사용하여 개발하였으며, UI 코드는 설명하지 않고 공유한 Git에서 클론하여 사용해 볼 수 있도록 하겠다.
카운트 앱에는 숫자 카운트를 증가시키는 기능과 감소시키는 기능, 그리고 초기화하는 기능을 사용할 것인데, 기본 예제랑은 다르게 얼마만큼의 숫자를 증감할지에 대한 상태 하나를 더 추가한 예제이다.
아래 결과물이 있으니, 결과물을 먼저 보고 작업을 진행해보자.
여기서 카운트 값은 네이티브에서 받아올 것이고, 얼마만큼 증감할지에 대한 값은 Flutter에서 관리를 하다가 카운트를 증가시킬 때에 MethodChannel로 Native에 전달해 줄 것이다.
Native에서는 전달해 준 값을 받아서 해당하는 숫자만큼 카운트를 증가시킨 뒤 다시 Flutter로 카운트 값을 전달해주면서, 스낵바를 띄우는 이벤트를 같이 전달하는 예제이다.
Flutter
Flutter에 카운트 이벤트 전달에 사용되는 MehtodChannel과 스낵바 노출을 전달 받는 MethodChannel을 선언해주자.
final MethodChannel _countChannel = const MethodChannel("tyger/count/app");
final MethodChannel _toastChannel = const MethodChannel("tyger/count/toast");
얼마만큼 증감을 할지에 대한 숫자를 선택하는 로직이다. 이 부분은 그냥 Flutter에서 관리하고 있는 값이다.
Future<void> changedSelectCount(int count) async {
emit(state.copyWith(currentCount: count));
}
Increment
여기서 부터가 중요하다. 등록한 countChannel을 호출할 때에 invokeMethod를 사용할 수있다. invokeMethod 호출시 콜 사인과 arguments를 전달할 수 있는데, Object, Map 둘 다 가능하다.
콜 사인은 "increment"라고 해주고, count 값으로 얼마만큼 값을 증가시킬지에 대한 값인 state.currentCount를 전달해 주었다.
MethodChannel의 전달되는 count 값으로 state의 count 값을 변경해 주고 있다.
Future<void> increment() async {
int? _count = await _countChannel.invokeMethod("increment", {
"count": state.currentCount,
});
emit(state.copyWith(count: _count));
}
Decrement
감소도 증가와 동일하다. Flutter에서는 Native로 부터 받아온 카운트 값을 그냥 보여주기만 하면 된다.
Future<void> decrement() async {
int? _count = await _countChannel.invokeMethod("decrement", {
"count": state.currentCount,
});
emit(state.copyWith(count: _count));
}
Reset
카운트를 다시 0으로 변경하는 초기화 기능이다. 증가와 감소와 동일하게 Flutter에서는 Native에서 관리되는 카운트 값만 가지고 올 것이기 때문에 count 값만 변경해주면 된다.
Future<void> countReset() async {
int? _count = await _countChannel.invokeMethod("reset");
emit(state.copyWith(count: _count));
}
Listener
여기 부분은 toastChannel로 전달되는 값을 받는 부분이다. 여태까지 사용한 MethodChannel과는 다르다.
이유는 위에서 살펴본 증감/리셋 기능은 Flutter가 Native에 이벤트를 호출하는 거라면, 여기서는 Native가 호출하는 이벤트를 Flutter에서 수신을 받는 것이다 (Event Channel 과는 다른 개념이기 때문에 혼동하면 안됨).
setMethodCallHandler에서는 Object, Map 형태로 콜백이 오는데, 우리는 네이티브로부터 받은 콜백을 스낵바로 노출만 해주도록 하자.
void listener(BuildContext context) {
_toastChannel.setMethodCallHandler((call) async {
String _content = call.method;
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(_content)));
});
}
Kotlin
Android 플랫폼을 작성하도록 하자. Platform Channel 사용이 익숙치 않으신 분들이 많기 때문에 최대한 이해할 수 있는 코드를 제공하도록 하겠다.
Kotlin 파일을 열어보자. 안드로이드 플랫폼 개발은 Android Studio에서 작업하면 편하다.
Flutter 프로젝트를 생성하면 아래와 같이 MainActivity를 FlutterActivity로 사용하는데, 아래에 configureFlutterEngine 코드를 추가해주자.
class MainActivity: FlutterActivity() {
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine)
}
}
Kotlin에서 카운트의 상태를 가져야 하기에 count를 정수형 타입으로 선언해주고, countToastChannel을 등록해주자. MethodChannel 등록시 binaryMessenger와 채널명이 필요하다.
참고로 채널명은 패키지네임 + 이벤트 경로로 작성한다. 패키지 네임을 넣어서 만드는 이유는 타 PlugIn과의 충돌을 방지하기 위함이다.
class MainActivity: FlutterActivity() {
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine)
var count : Int = 0
val countToastChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "tyger/count/toast")
}
}
자 여기서는 MethodChannel을 선언하지 않고 바로 사용을 하였다. 여기는 이벤트 값을 수신받으면 되서 별도로 등록을 하지 않았다.
이제 MethodChannel의 setMethodCallHandler 부분을 보자.
익숙한 코드이다. 바로 Flutter에서도 스낵바 노출을 수신받는 부분에 똑같은 함수가 사용되었었다. 기능적으로 동일하다.
Kotlin 코드를 모르더라도 따라서 작성해보도록 하자.
when 절은 Flutter의 switch-case문이고 생각하면 된다. 자 call.method가 우리가 Flutter에서 호출한 콜사인이 된다.
해당 콜 사인이 reset인 경우 count를 0으로 변경해주고 result.success()를 호출해주고 있다.
위에 코드를 보면 result를 리턴하게 되어있는데, FlutterResult 객체를 리턴하여야 한다. 실패시 플랫폼 에러를 발생시키고 싶다면 result.notImplemented()를 리턴해주면 된다.
"increment", "decrement" 콜 사인이 호출될 때에는 arguments를 받아서 얼마만큼을 증감할지를 전달하고 있어서 Map의 count를 숫자형으로 파싱해서 count 변수를 변경시켜 주고 result를 호출한다.
자 여기서 보면 countToastChannel이 사용되는데, 카운트 값과 전달받은 값을 Flutter로 보내기 위해 invokeMethod에 값을 전달하고 있다. 콜 사인이 따로 사용되지 않아도 되서 간편하게 콜 사인에 호출하였다.
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "tyger/count/app").setMethodCallHandler {
call, result ->
when(call.method){
"reset" -> {
count = 0
result.success(count)
}
"increment" ->{
val args : Int? = call.argument<Int>("count")
count += args!!
result.success(count)
countToastChannel.invokeMethod("Count : $count Argument : $args", null)
}
"decrement" -> {
val args : Int? = call.argument<Int>("count")
count -= args!!
result.success(count)
countToastChannel.invokeMethod("Count : $count Argument : $args", null)
}
else -> {
result.success(null)
}
}
}
지금까지 개발한 Kotlin 코드의 전체 코드이다.
class MainActivity: FlutterActivity() {
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine)
var count : Int = 0
val countToastChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "tyger/count/toast")
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "tyger/count/app").setMethodCallHandler {
call, result ->
when(call.method){
"reset" -> {
count = 0
result.success(count)
}
"increment" ->{
val args : Int? = call.argument<Int>("count")
count += args!!
result.success(count)
countToastChannel.invokeMethod("Count : $count Argument : $args", null)
}
"decrement" -> {
val args : Int? = call.argument<Int>("count")
count -= args!!
result.success(count)
countToastChannel.invokeMethod("Count : $count Argument : $args", null)
}
else -> {
result.success(null)
}
}
}
}
}
Swift
이번에는 IOS 플랫폼 코드 작성을 위해 Xcode를 열어 Swift로 개발을 해보자. 로직은 Kotlin과 동일하다고 보면된다.
Flutter 프로젝트 생성시 Swift 코드가 아래와 같이 생성되어있다. Kotlin 구조와 도일하게 Delegate에 FlutterAppDelegate가 사용되고 있는 것을 알 수 있다.
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
우선 count 변수를 정수형으로 Kotlin과 동일하게 선언을 해주자. Swift에서는 Kotlin에서 작업한 방식과 조금다르게 selectCount라는 Flutter에서 전달해 줄 값을 미리 선언해 주었다.
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
var count : Int = 0
var selectCount : Int = 1
let countToastChannel = FlutterMethodChannel(name: "tyger/count/toast", binaryMessenger: (window?.rootViewController as! FlutterViewController).binaryMessenger)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
Kotlin과 동일하게 Swift에서도 MethodChannel을 따로 선언하지 않았다. 여기서도 switch-case 문을 통해서 call.mehtod인 콜 사인을 통해 분기 처리를 해주었고, 전달 받은 arguments 값을 selectCount 값으로 전달하고 있다.
Kotlin에서 설명한 것과 로직은 동일하며, 작업이 끝나고 나면 countToastChannel을 통해 값을 리턴하여 Flutter로 전달하고 있다.
다른 점은 Swift는 result 함수안에 바로 전달 값을 보내주면 된다. 물론 Platform 에러를 전달하고 싶다면 전달할 수 있다. result(FlutterError(...)) 이렇게 FlutterError를 발생시킬 수 있다.
FlutterMethodChannel(name: "tyger/count/app",binaryMessenger: (window?.rootViewController as! FlutterViewController).binaryMessenger).setMethodCallHandler({
[weak self] (call: FlutterMethodCall, result: FlutterResult) -> Void in
if let args = call.arguments as? Dictionary<String, Any>,
let param = args["count"] as? Int {
selectCount = param
}
switch call.method {
case "reset":
count = 0
result(count)
break
case "increment":
count += selectCount
result(count)
countToastChannel.invokeMethod("Count : \(count) Argument : \(selectCount)",arguments: nil)
break
case "decrement":
count -= selectCount
result(count)
countToastChannel.invokeMethod("Count : \(count) Argument : \(selectCount)",arguments: nil)
break
default:
break
}
})