한국의 개발자들을 위한 Google for Developers 국문 블로그입니다.
Flutter 플랫폼 채널을 소개합니다.
2018년 10월 11일 목요일
<블로그 원문은
이곳
에서 확인하실 수 있으며 번역 리뷰에는 양찬석(Google)님이 참여해 주셨습니다.>
"멋진 UI군요. 그런데 Flutter는 어떻게 플랫폼별 API를 다루죠?”
Flutter
를 사용하면 Dart 프로그래밍 언어로 모바일 앱을 작성하고 안드로이드와 iOS 용 모바일 앱을 빌드할 수 있습니다. 하지만 Dart는 안드로이드의 Dalvik 바이트코드로 컴파일되지않고,, iOS에서 Dart/Objective-C 바인딩 기능을 제공하지도 않습니다. 즉, Dart 코드는 iOS Cocoa Touch와 안드로이드 SDK 플랫폼 API에 직접 액세스하지 않고 작성됩니다.
화면에 픽셀을 그리려고 Dart 코드를 작성한다면 큰 문제가 아닙니다. Flutter 프레임워크의 그래픽 엔진은 이 작업을 정말 잘 처리합니다. 화면을 그리는 것 외에 파일 또는 네트워크 I/O 작업이나, 비즈니스 로직을 처리하는 경우도 큰 문제는 아닙니다. Dart 언어, 런타임 및 라이브러리를 사용해 처리할 수 있습니다.
하지만 아주 간단한 앱이 아니라면, 플랫폼과 더 깊은 단계의 통합이 필요합니다.
알림, 앱 수명 주기, 딥 링크...
센서, 카메라, 배터리, 위치정보, 사운드, 연결...
다른 앱과의 정보 공유, 다른 앱 실행…
앱 설정 백업, 특수 폴더, 기기 정보...
그 밖에도 다양한 기능이 있으며, 플랫폼 새 버전이 출시될 때 마다 이 목록은 점점 더 늘어나는 것처럼 보입니다.
Flutter 프레임워크 자체에 이 모든 플랫폼 API를 포함할 수도 있습니다. 하지만 그러면 Flutter가 훨씬 더 방대해지고, 자주 변경되어야 합니다. 실제로 그렇게 되면 Flutter가 최신 플랫폼 릴리스에 뒤처질 가능성이 높습니다. 아니면, 불만족스러운 플랫폼 API의 '최소공통분모' 래퍼를 사용해야 하는 상황으로 이어질 수도 있겠죠. 또는 플랫폼의 차이를 미봉책으로 가리기 위해 다루기 어려운 추상화 기법을 사용하는 바람에 처음 접하는 개발자를 골치 아프게 할 수도 있습니다. 그것도 아니면 버전 단편화나 버그로 귀결될 수도 있습니다.
그러고 보니 위 모든 사항이 일어날 수도 있겠네요.
Flutter 팀은 다른 접근 방식을 택했습니다. 거창하진 않지만, 간단하고 다목적으로 활용할 수 있으며 이를 어떻게 활용하느냐는 전적으로 개발자 손에 달려 있습니다.
먼저, 안드로이드 혹은 iOS 앱이
Flutter를 호스트
합니다. Flutter 부분은 안드로이드의 View나 iOS의 UIViewController와 같은 표준 플랫폼 컴포넌트로 래핑됩니다. 기본적으로 Flutter에서는 Dart로 앱을 작성할 것을 권장 하지만, 플랫폼별 API 바로 위에서 동작하는 호스트 앱에서는 원하는 만큼 Java/Kotlin 또는 Objective-C/Swift로 코드를 작성할 수 있습니다.
둘째,
플랫폼 채널
은 호스트 앱의 Dart 코드와 플랫폼 특화 코드 사이 간단한 통신 메커니즘을 제공합니다. 즉, 호스트 앱 코드에 플랫폼 서비스를 노출하고 Dart에서 이를 호출할 수 있습니다. 또는 그 반대도 가능합니다.
셋째,
플러그인
을 사용하면 Java나 Kotlin으로 작성된 안드로이드 구현과 Objective-C나 Swift로 작성된 iOS 구현으로 뒷받침되는 Dart API를 만들 수 있습니다. 그런 다음 플랫폼 채널을 사용하여 Flutter/안드로이드/iOS 셋 모두를 하나의 패키지로 묶을 수 있습니다. 다시말해, Flutter에서 특정 플랫폼 API를 어떻게 사용해야 할지에 관한
여러분의 접근 방식
을 재사용하고 공유하고 배포할 수 있습니다.
이 글은 플랫폼 채널을 깊이있게 소개하는 글입니다. Flutter의 메시징 기반부터 시작해서 메시지/메서드/이벤트 채널 개념을 소개하고 API 설계 고려 사항을 설명하겠습니다. API 목록을 일일이 나열하지는 않겠지만, 복사 / 붙여넣기하여 재사용할 수 있는 짧은 코드 샘플을 제공해 드리겠습니다. Flutter 팀의 일원으로서
flutter/pl
ugins
GitHub 저장소에 기여한 경험을 바탕으로, 간략하게 몇 가지 사용 지침을 제공해 드리겠습니다. 마지막으로, DartDoc/JavaDoc/ObjcDoc 참조 API에 대한 링크를 비롯한 추가 자료 목록을 알려드리는 것으로 이 글을 마무리하도록 하겠습니다.
목차
플랫폼 채널 API
기반: 비동기 바이너리 메시징
메시지 채널: 이름 + 코덱
메서드 채널: 표준화된 봉투
이벤트 채널: 스트리밍
사용 지침
고유성 보장을 위해 도메인별로 채널 이름에 접두사 추가
플랫폼 채널은 모듈 내 통신을 위한 방법으로 고려
플랫폼 채널 모방(Mock) 금지
플랫폼 상호 작용을 위해 자동화된 테스트 고려
플랫폼 측을 동기 호출에 준비된 상태로 유지
참고 자료
플랫폼 채널 API
대부분의 사용 사례에서 아마 플랫폼 통신을 위해
메서드 채널
을 사용할 것입니다. 하지만 메서드 채널의 속성 중 다수가 더 간단한
메시지 채널
과 기본
바이너리 메시징
기반에서 파생되므로 거기서부터 시작하겠습니다.
기반: 비동기 바이너리 메시징
가장 기본적인 수준에서, Flutter는 바이너리 메시지 형태의 비동기 메시지를 통해 플랫폼 코드와 대화합니다. 즉, 메시지 페이로드(payload)는 바이트 버퍼입니다. 다양한 목적으로 사용되는 메시지를 구별하기 위해 각 메시지는 논리적 '채널'을 통해 전송됩니다. 이 ‘채널'은 단지 채널 이름을 나타내는 문자열입니다. 아래 예제에서는 ‘foo’라는 채널 이름을 사용합니다.
// Send a binary message from Dart to the platform.
final
WriteBuffer buffer = WriteBuffer()
..putFloat64(3.1415)
..putInt32(12345678);
final
ByteData message = buffer.done();
await
BinaryMessages.send('foo', message);
print('Message sent, reply ignored');
안드로이드에서는 Kotlin 코드를 사용하여 이러한 메시지를 java.nio.ByteBuffer로 수신할 수 있습니다.
// Receive binary messages from Dart on Android.
// This code can be added to a FlutterActivity subclass, typically
// in onCreate.
flutterView.setMessageHandler("foo") { message, reply
->
message.order(ByteOrder.nativeOrder())
val
x = message.double
val
n = message.int
Log.i("MSG", "Received: $x and $n")
reply.reply(null)
}
ByteBuffer API는 현재 읽기 위치를 자동으로 전진시키면서 값을 읽을 수 있는 기능을 지원합니다. iOS도 이와 비슷합니다. 필자가 약한 부분인 Swift 실력을 향상하는 데 도움이 되는 제안은 언제든 대환영입니다.
// Receive binary messages from Dart on iOS.
// This code can be added to a FlutterAppDelegate subclass,
// typically in application:didFinishLaunchingWithOptions:.
let
flutterView =
window?.rootViewController
as
! FlutterViewController;
flutterView.setMessageHandlerOnChannel("foo") {
(message: Data!, reply: FlutterBinaryReply) -> Void
in
let
x : Float64 = message.subdata(in: 0..<8)
.withUnsafeBytes { $0.pointee }
let
n : Int32 = message.subdata(in: 8..<12)
.withUnsafeBytes { $0.pointee }
os_log("Received %f and %d", x, n)
reply(nil)
}
통신은 양방향으로 이루어지므로, 반대 방향인 Java/Kotlin이나 Objective-C/Swift에서 Dart로 메시지를 보낼 수도 있습니다. 위 설정의 방향을 반대로 하면 다음과 같이 됩니다.
// Send a binary message from Android.
val
message = ByteBuffer.allocateDirect(12)
message.putDouble(3.1415)
message.putInt(123456789)
flutterView.send("foo", message) { _ ->
Log.i("MSG", "Message sent, reply ignored")
}
// Send a binary message from iOS.
var
message = Data(capacity: 12)
var
x : Float64 = 3.1415
var
n : Int32 = 12345678
message.append(UnsafeBufferPointer(start: &x, count: 1))
message.append(UnsafeBufferPointer(start: &n, count: 1))
flutterView.send(onChannel: "foo", message: message) {(_) -> Void
in
os_log("Message sent, reply ignored")
}
// Receive binary messages from the platform.
BinaryMessages.setMessageHandler('foo', (ByteData message)
async
{
final
ReadBuffer readBuffer = ReadBuffer(message);
final
double x = readBuffer.getFloat64();
final
int n = readBuffer.getInt32();
print('Received $x and $n');
return
null;
});
세부 사항.
필수 회신.
메시지를 전송 할 때 마다, 송신자는 비동기적으로 수신자의 응답을 기다립니다. 위 예제에서는 회신 메세지에 별다른 의미가 없지만, Dart future가 완료되고 두 플랫폼 콜백이 실행되기 위해서는 반드시 회신 메시지가 필요합니다. 이런 경우 null 회신을 사용합니다.
스레드.
메시지는 반드시 플랫폼의 메인 UI 스레드로 보내고, 회신은 메인 UI 스레드에서 받아야 합니다. Dart에서는 Dart isolate당(즉, Flutter 뷰당) 하나의 스레드만 있으므로, 어떤 스레드를 사용할지 고민할 필요가 없습니다.
예외.
Dart 또는 안드로이드 메시지 핸들러에서 발생했는데 포착되지 않은 예외는 모두 프레임워크에서 포착되고 로그에 기록된 후 발신자에게 null 회신을 보냅니다. 회신 핸들러(Reply Handler)에서 발생했는데 포착되지 않은 예외는 로그에 기록됩니다.
핸들러 수명.
등록된 메시지 핸들러는 Flutter 뷰(Dart isolate, 안드로이드 FlutterView 인스턴스, iOS FlutterViewController를 의미함)가 살아있는한 활성 상태로 계속 유지됩니다. 필요한 경우, 메시지 핸들러 등록을 취소해 핸들러의 수명을 끝내거나 단축시킬 수 있습니다. 같은 채널 이름을 사용하는 null 핸들러를 설정하거나 같은 채널 이름의 다른 핸들러를 설정해 기존 핸들러 등록을 취소할 수 있습니다.
핸들러의 고유성.
핸들러는 채널 이름을 키로 사용하는 해시 맵에 저장됩니다. 따라서, 채널 당 한 개의 핸들러만 존재합니다. 등록된 메시지 핸들러가 없는 채널을 통해 전송된 메시지는 자동으로 null 회신으로 응답됩니다.
동기 통신.
플랫폼 통신은 비동기 모드에서만 사용할 수 있습니다. 이를 통해 여러 스레드를 거쳐 블로킹 호출이 일어나고, 시스템 수준의 문제(성능 불량, 교착 상태에 빠질 위험)가 발생하는 것을 방지합니다. 이 글을 쓰는 시점을 기준으로, Flutter에서 동기 통신이 정말 필요한지 분명치 않고, 정말 필요하다면 어떤 방법을 취해야 할지도 확실치 않습니다.
바이너리 메시지 수준에서 작업할 때는 바이트 순서(Endianness) 문제나 바이트로 맵과 문자열처럼 좀 더 높은 수준의 메시지를 표시하는 방법 같은 세부 사항을 잘 생각해야합니다. 또한 메시지를 보내거나 핸들러를 등록하고 싶을 때는 항상 올바른 채널 이름을 지정해야 합니다. 이런 부분을 조금 더 쉽게 만들면 플랫폼 채널로 이어지게 됩니다.
플랫폼 채널은 메시지를 바이너리 양식으로, 또는 바이너리 양식을 메시지로 직렬화/역직렬화하기 위한 코덱과 채널 이름이 합쳐져 있는 객체입니다
.
메시지 채널: 이름 + 코덱
바이트 버퍼 대신 문자열 메시지를 주고받고 싶다면, 메시지 채널을 사용할 수 있습니다. 메시지 채널은 일종의 문자열 코덱을 갖고 있는 간단한 플랫폼 채널 입니다. 아래 코드를 통해 Dart, 안드로이드 및 iOS에서 양방향으로 메시지 채널을 사용하는 방법을 알 수 있습니다.
// String messages
// Dart side
const
channel = BasicMessageChannel<String>('foo', StringCodec());
// Send message to platform and receive reply.
final
String reply =
await
channel.send('Hello, world');
print(reply);
// Receive messages from platform and send replies.
channel.setMessageHandler((String message)
async
{
print('Received: $message');
return
'Hi from Dart';
});
// Android side
val
channel = BasicMessageChannel<String>(
flutterView, "foo", StringCodec.INSTANCE)
// Send message to Dart and receive reply.
channel.send("Hello, world") { reply ->
Log.i("MSG", reply)
}
// Receive messages from Dart and send replies.
channel.setMessageHandler { message, reply ->
Log.i("MSG", "Received: $message")
reply.reply("Hi from Android")
}
// iOS side
let
channel = FlutterBasicMessageChannel(
name: "foo",
binaryMessenger: controller,
codec: FlutterStringCodec.sharedInstance())
// Send message to Dart and receive reply.
channel.sendMessage("Hello, world") {(reply: Any?) -> Void
in
os_log("%@", type: .info, reply
as
! String)
}
// Receive messages from Dart and send replies.
channel.setMessageHandler {
(message: Any?, reply: FlutterReply) -> Void
in
os_log("Received: %@", type: .info, message
as
! String)
reply("Hi from iOS")
}
채널 이름은 채널 생성 시에만 지정됩니다. 그 후에는 채널 이름을 반복하지 않고도 메시지를 보내거나 메시지 핸들러를 설정할 수 있습니다. 바이트 버퍼를 문자열로 해석하거나 반대로 문자열을 바이트 버퍼로 해석하는 문제는 문자열 코덱 클래스의 몫입니다.
이러한 특징은 분명한 장점이지만, BasicMessageChannel 구현에 별다를게 없다는 점에 아마 동의하실 겁니다. 이는 의도된 부분입니다. 위의 Dart 코드는 바이너리 메시징을 사용해 다음과 같이 작성할 수 있습니다.
const
codec = StringCodec();
// Send message to platform and receive reply.
final
String reply = codec.decodeMessage(
await
BinaryMessages.send(
'foo',
codec.encodeMessage('Hello, world'),
),
);
print(reply);
// Receive messages from platform and send replies.
BinaryMessages.setMessageHandler('foo', (ByteData message)
async
{
print('Received: ${codec.decodeMessage(message)}');
return
codec.encodeMessage('Hi from Dart');
});
이는
안드로이드
와
iOS
에서 메시지 채널을 구현할 때도 똑같이 적용됩니다.
이를 위한 마법같은 방법은 없습니다
.
메시지 채널은 바이너리 메시징 계층에 모든 통신을 위임합니다.
메시지 채널은 등록된 핸들러 자체를 추적하지 않습니다.
메시지 채널은 가볍고(light-weight) 상태 정보를 관리하지 않습니다(stateless).
같은 채널 이름과 코덱을 사용해 생성된 두 메시지 채널 인스턴스는 동등하며, 따라서 서로의 통신을 간섭할 수 있습니다.
다양한 역사적 이유로, Flutter 프레임워크는 다음 네가지 메시지 코덱을 지원합니다.
StringCodec
UTF-8을 사용하여 문자열을 인코딩합니다. 방금 살펴본 바와 같이, 이 코덱을 사용하는 메시지 채널은 Dart에서 BasicMessageChannel<String> 타입으로 정의됩니다
BinaryCodec
바이트 버퍼를 그대로 사용합니다. 인코딩/디코딩이 필요하지 않은 경우, 이 코덱을 사용하면 채널 객체의 편리성을 누릴 수 있습니다. 이 코덱을 사용하는 Dart 메시지 채널은 BasicMessageChannel<ByteData> 타입으로 정의됩니다.
JSONMessageCodec
JSON 형식의 값(문자열, 숫자, 부울, null, 값의 목록, 문자열 키를 갖는 맵등)을 처리합니다. 목록과 맵은 다른 타입의 값을 포함할 수 있고, 중첩될 수도 있습니다. 인코딩 중에 이러한 값은 JSON 문자열로 바뀐 다음에 UTF-8을 사용하여 바이트로 바뀝니다. 이 코덱을 사용하는 Dart 메시지 채널은 BasicMessageChannel<dynamic> 타입으로 정의됩니다.
StandardMessageCodec
JSON 코덱보다 좀 더 일반화된 값을 처리합니다. 데이터 버퍼(UInt8List, Int32List, Int64List, Float64List)와 문자열이 아닌 키도 지원합니다. 숫자 처리 방식이 JSON과는 다른데, Dart int가 크기에 따라 32비트나 64비트의 부호 있는 정수로 전달되고, 부동 소수점 숫자는 지원하지 않습니다. 값들은 적당히 간략하면서도 확장 가능한 커스텀 바이너리 포맷으로 인코딩됩니다. Flutter에서의 채널 통신을 위해 기본적으로 선택되는 형식은 StandardMessageCodec 코덱입니다. JSON과 마찬가지로, StandardMessageCodec을 사용하는 Dart 메시지 채널은 BasicMessageChannel<dynamic> 타입으로 정의됩니다
아마 짐작하셨다시피, 메시지 채널은 간단한 계약을 만족하는 그 어떤 메시지 코덱도 사용할 수 있습니다. 따라서 필요하다면 자체적으로 코덱을 만들어 연결할 수 있습니다. 이를 위해서는 Dart, Java/Kotlin, Objective-C/Swift에서 호환 가능한 인코딩 및 디코딩을 구현해야 합니다.
세부 사항.
코덱의 진화
. 메시지 코덱은 Flutter가 플랫폼의 일부로 Dart에서 사용될 수 있습니다. 또한, 안드로이드와 iOS 에서 라이브러리 형태로 노출해 사용할 수 있습니다. Flutter는 코덱을 앱 내 통신용으로만 사용하며, 영구적으로 저장될 데이타를 인코딩하고 디코딩하는데는 사용하지 않습니다. 다시 말해, Flutter 버전이 업데이트 되면, 아무런 경고도 없이 메시지의 바이너리 양식이 바뀔 수 있습니다. 물론, 이런 경우 Dart, 안드로이드 및 iOS 코덱은 함께 진화해 발신자가 인코딩한 정보를 수신자가 올바로 디코딩할 수 있도록 보장합니다.
null 메시지.
어떤 메시지 코덱이든 null 메시지를 지원하고 유지해야 합니다. 등록된 메시지 핸들러가 없는 채널로 전송된 메시지는 기본적으로 null 메시지로 회신되기 때문입니다.
Dart에서 정적으로 메시지 타입 선언.
표준 메시지 코덱으로 구성된 메시지 채널은 메시지와 메시지 회신에 dynamic 형식을 부여합니다. 그런데 일반적으로 다음과 같이 명시적으로 변수 타입을 선언하는 방법으로 예상되는 메시지의 타입을 표시하는 경우가 있습니다.
final
String reply1 =
await
channel.send(msg1);
final
int reply2 =
await
channel.send(msg2);
하지만 다음과 같이 제네릭 매개변수를 포함한 메시지를 다룰 때는 주의할 점이 있습니다.
final
List<String> reply3 =
await
channel.send(msg3);
// Fails.
final
List<dynamic> reply3 =
await
channel.send(msg3);
// Works.
첫 번째 행은 회신이 null이 아닐 경우 런타임 오류가 발생합니다. 표준 메시지 코덱은 dynamic 맵이나 리스트를 위해 작성되었습니다. Dart 측에서 보면, List<dynamic> 혹은 Map<dynamic, dynamic> 타입입니다. Dart 2 에서는 제네릭 타입의 변수가 더 구체적인 타입의 변수 (위의 예처럼 List<dynamic> 에서 List<String>으로 할당하는 경우)로 할당될 수 없으며, 런타임 오류가 발생합니다.
Future에서도 비슷한 문제가 생길 수 있습니다.
Future<String> greet() => channel.send('hello, world');
// Fails.
Future<String> greet()
async
{
// Works.
final
String reply =
await
channel.send('hello, world');
return
reply;
}
수신되는 회신이 문자열이더라도 첫 번째 메서드는 런타임에서 실패합니다. 채널 구현은 회신의 형식과는 무관하게 Future<dynamic>을 생성하고, 이 객체를 Future<String> 타입에 할당할 수 없습니다.
BasicMessageChannel이 'basic'인 이유는 무엇일까요?
메시지 채널은 동일한 데이터 타입을 송신자와 수신자가 미리 알고 주고받는 제한적 상황에서만 사용될 수 있을 것 같습니다. 키보드 이벤트를 전달하는 경우가 좋은 예가 될 수 있겠네요. 하지만, 플랫폼 채널을 사용하는 많은 경우, 메시지로 전달되는 값뿐만이 아니라 메시지를 통해 이루고자하는 구체적인 목적이나 수신자가 메시지를 어떻게 해석했으면 하는지 등의 메타 정보를 전달하고 싶은 경우가 있습니다. 한 가지 방법은 메시지가 전달할 값과 특정 메서드 호출을 모두 포함하도록 만드는 것 입니다. 이렇게 하려면, 메시지 내에서 호출 될 메서드 이름과 호출에 사용 할 매개변수를 구분해야합니다. 또한,메서드가 성공적으로 호출되었는지 아닌지를 구분하는 표준적인 회신 메시지를 정의할 필요도 있습니다. 이것이 바로
메서드
채널이 하는 역할입니다. BasicMessageChannel의 원래 이름은 MessageChannel이었지만 코드에서 MessageChannel과 MethodChannel의 혼동을 피하기 위해 이름을 바꿨습니다. 더 포괄적으로 적용할 수 있는 메서드 채널의 이름을 짧은 이름으로 유지했습니다.
메서드 채널: 표준화된 봉투
메서드 채널은 Dart와 Java/Kotlin 또는 Objective-C/Swift에서 이름 지어진 코드 조각을 호출하기 위해 고안된 플랫폼 채널입니다. 메서드 채널을 이용하면 표준화된 메시지 '봉투'를 사용하여 발신자로부터 수신자에게 메서드 이름과 인수를 전달하고, 그 결과 (성공 혹은 실패)를 받을 수 있습니다. 봉투와 지원되는 페이로드는 별개의 메서드 코덱 클래스로 정의됩니다. 메시지 채널이 메시지 코덱을 사용하는 방법과 유사합니다.
채널 이름을 코덱과 결합하는 것이 바로 메서드 채널이 하는 모든 일입니다.
사실 메서드 채널을 통해 메시지를 받은 수신 측이 어떤 코드를 실행하는지 대해 어떤 규칙도 없습니다. 메시지가 특정 메서드 이름을 포함하고 있다 하더라도, 해당 메서드를 호출할 필요는 없습니다. 메서드 이름을 받아 switch 분기문 처리를 한 후 몇 줄의 코드를 실행해도 좋습니다.
첨언.
이처럼 메서드와 메서드의 매개변수에 대한 암시적 또는 자동 바인딩이 없다는 점에 실망하실지 모르겠습니다. 괜찮습니다. 실망이 오히려 생산적인 결과를 가져올 수도 있으니까요. 여러분이 직접 어노테이션 프로세싱과 코드 생성 기능을 사용해 이와 같은 솔루션을 만들거나, 기존 RPC 프레임워크 중 일부를 재사용할 수 있으리라 생각합니다. Flutter는 오픈소스이므로
자유롭게 각자 빌드한 코드를 공개해 Flutter의 발전에 공헌해 보세요
! 메서드 채널은 조건만 맞으면 코드 생성의 대상이 될 수 있습니다. 한편, 메서드 채널은 '수작업 모드'에서도 나름 유용하게 사용될 수 있습니다.
메서드 채널은 플러그인 생태계가 막 시작되던 시절에, 유용하게 사용할 수 있는 통신 API를 정의하는 문제에 대해 Flutter 팀이 내놓은 해답입니다. 플러그인 작성자가 많은 상용구나 복잡한 빌드 설정 없이 곧바로 사용할 수 있는 뭔가를 원했습니다. 메서드 채널 개념은 꽤나 제대로 된 해답이라 생각하지만, 그것이
유일한
해답으로 계속 남는다면 더욱 놀랄 것입니다.
다음으로 Dart에서 플랫폼 코드를 호출하는 간단한 사례에서 메서드 채널을 어떻게 사용할지 알아봅시다. 이 예제에서는 ‘bar’ 라는 이름을 사용하고 있는데, ‘bar’는 실제 메서드의 이름일 수도 아닐 수도 있습니다. 이 코드가 하는 일은 오직 환영 인사 문자열을 생성하여 발신자에게 반환하는 것으로, 플랫폼 상에서 호출이 실패하지 않을 것이라는 합리적인 가정하에 코딩할 수 있습니다. 오류 처리에 관해서는 아래에서 추가로 살펴보겠습니다.
// Invocation of platform methods, simple case.
// Dart side.
const
channel = MethodChannel('foo');
final
String greeting
= await
channel.invokeMethod('bar', 'world');
print(greeting);
// Android side.
val
channel = MethodChannel(flutterView, "foo")
channel.setMethodCallHandler { call, result ->
when
(call.method) {
"bar" -> result.success("Hello, ${call.arguments}")
else
-> result.notImplemented()
}
}
// iOS side.
let
channel = FlutterMethodChannel(
name: "foo", binaryMessenger: flutterView)
channel.setMethodCallHandler {
(call: FlutterMethodCall, result: FlutterResult) -> Void
in
switch
(call.method) {
case
"bar": result("Hello, \(call.arguments
as
! String)")
default
: result(FlutterMethodNotImplemented)
}
}
switch 구문에 case를 추가하면 여러 메서드를 처리하도록 코드를 쉽게 확장할 수 있습니다. default 절은 알 수 없는 메서드가 호출되는 상황(프로그래밍 오류 때문일 가능성이 가장 높음)을 처리합니다.
위의 Dart 코드는 다음 코드와 같습니다.
const
codec = StandardMethodCodec();
final
ByteData reply =
await
BinaryMessages.
send
(
'foo',
codec.encodeMethodCall(MethodCall('bar', 'world')),
);
if
(reply ==
null
)
throw
MissingPluginException();
else
print(codec.decodeEnvelope(reply));
안드로이드
와
iOS
에서의 메서드 채널 구현은 바이너리 메시징 기반 API를 감싼 얇은 래퍼 입니다. null 회신은 '구현되지 않음(not implmented)' 결과를 나타내는 데 사용됩니다. 그 덕분에 호출이 switch의 default 구문으로 빠지거나, 채널에 등록된 메서드 핸들러가 아예 없는 경우 동일하게 예외 상황을 처리할 수 있습니다.
이 예제에서 인수 값은 단일 문자열 ‘world’ 입니다. 하지만 기본 메서드 코덱('표준 메서드 코덱' 이라고 불림)은 보이지 않는 곳에서 표준
메시지
코덱을 사용하여 페이로드 값을 인코딩합니다. 즉, 앞서 설명한 '일반화된 JSON 같은' 값들을 메서드 인수와 성공한 메서드 호출에 대한 결과 값으로 사용할 수 있습니다. 예를 들어, 리스트는 타입이 다른 여러 값을 담을 수 있고, 맵은 타입이 다른 여러 키-값 쌍을 담을 수 있습니다. 기본 인수 값은 null입니다. 몇 가지 예시를 살펴봅시다.
await
channel.invokeMethod('bar');
await
channel.invokeMethod('bar', <dynamic>['world', 42, pi]);
await
channel.invokeMethod('bar', <String, dynamic>{
name: 'world',
answer: 42,
math: pi,
}));
Flutter SDK는 다음 두 메서드 코덱을 포함합니다.
StandardMethodCodec
: 기본적으로 페이로드 값의 인코딩을 StandardMessageCodec에 위임합니다. 후자가 확장 가능하므로 전자도 확장 가능합니다.
JSONMethodCodec
: 페이로드 값의 인코딩을 JSONMessageCodec에 위임합니다.
사용자설정 메서드 코덱을 포함하여 원하는 메서드 코덱을 사용해 메서드 채널을 구성할 수 있습니다. 코덱 구현에 관련된 사항을 완전히 이해하기 위해, 위 예제에서 오류가 발생하는 baz 메서드를 추가해 메서드 채널 API 레벨에서 오류를 처리하는 방식을 살펴봅시다.
// Method calls with error handling.
// Dart side.
const
channel = MethodChannel('foo');
// Invoke a platform method.
const
name = 'bar';
// or 'baz', or 'unknown'
const
value = 'world';
try
{
print(
await
channel.invokeMethod(name, value));
}
on
PlatformException
catch
(e) {
print('$name failed: ${e.message}');
}
on
MissingPluginException {
print('$name not implemented');
}
// Receive method invocations from platform and return results.
channel.setMethodCallHandler((MethodCall call)
async
{
switch
(call.method) {
case
'bar':
return
'Hello, ${call.arguments}';
case
'baz':
throw
PlatformException(code: '400', message: 'This is bad');
default
:
throw
MissingPluginException();
}
});
// Android side.
val
channel = MethodChannel(flutterView, "foo")
// Invoke a Dart method.
val
name = "bar"
// or "baz", or "unknown"
val
value = "world"
channel.invokeMethod(name, value, object: MethodChannel.Result {
override fun
success(result: Any?) {
Log.i("MSG", "$result")
}
override fun
error(code: String?, msg: String?, details: Any?) {
Log.e("MSG", "$name failed: $msg")
}
override fun
notImplemented() {
Log.e("MSG", "$name not implemented")
}
})
// Receive method invocations from Dart and return results.
channel.setMethodCallHandler { call, result ->
when
(call.method) {
"bar" -> result.success("Hello, ${call.arguments}")
"baz" -> result.error("400", "This is bad", null)
else
-> result.notImplemented()
}
}
// iOS side.
let
channel = FlutterMethodChannel(
name: "foo", binaryMessenger: flutterView)
// Invoke a Dart method.
let
name = "bar"
// or "baz", or "unknown"
let
value = "world"
channel.invokeMethod(name, arguments: value) {
(result: Any?) -> Void
in
if
let
error = result
as
? FlutterError {
os_log("%@ failed: %@", type: .error, name, error.message!)
}
else if
FlutterMethodNotImplemented.isEqual(result) {
os_log("%@ not implemented", type: .error, name)
} else {
os_log("%@", type: .info, result
as
! NSObject)
}
}
// Receive method invocations from Dart and return results.
channel.setMethodCallHandler {
(call: FlutterMethodCall, result: FlutterResult) -> Void
in
switch
(call.method) {
case
"bar": result("Hello, \(call.arguments
as
! String)")
case
"baz": result(FlutterError(
code: "400", message: "This is bad", details: nil))
default
: result(FlutterMethodNotImplemented)
}
오류는 세 가지 항목(code, message, details)을 갖습니다. code와 message는 문자열입니다. message는 사람이 사용하는 용도이고, code는 코드용입니다. 오류 세부 정보(details)는 사용자설정 값으로 종종 null로 지정되는데, 코덱이 지원하는 값 중 하나를 사용합니다.
세부 사항.
예외.
Dart 또는 안드로이드 메서드 호출 핸들러에서 발생했는데 포착되지 않은 예외는 모두 채널 구현에 의해 포착되고 로그에 기록된 후 발신자에게 오류 결과가 반환됩니다. 메서드 호출 결과를 처리하는 핸들러에서 발생한 포착되지 않은 예외는 로그에 기록됩니다.
봉투 인코딩.
메서드 코덱이 봉투를 인코딩하는 방식은 메시지 코덱이 메시지를 바이트로 변환하는 방식과 마찬가지로 구현 세부 사항에 달렸습니다. 예를 들어, 메서드 코덱은 리스트를 사용할 수도 있습니다. 예를 들어, 메서드 호출은 요소가 2개[method name, arguments]인 리스트로, 성공 결과는 요소가 1개[result] 인 리스트로, 오류 결과는 요소가 3개[code, message, details] 있는 리스트로 인코딩될 수 있습니다. 이러한 메서드 코덱은 리스트, 문자열 및 null을 지원하는 기본
메시지
코덱에 많은 부분을 위임하여 간단히 구현할 수 있습니다. 메서드 호출 인수, 성공 결과 및 오류 세부 정보는 기본 메시지 코덱에서 지원하는 값을 가질 수 있습니다.
API 차이점.
위 코드 예시는 다음과 같이 메서드 채널이 결과를 전달할 때 Dart, 안드로이드 및 iOS 플랫폼에 따라 사뭇 다른 방식으로 결과를 전달하는 점을 잘 보여줍니다.
Dart에서는 future를 반환하는 메서드가 호출을 처리합니다. future는 성공 시에는 호출의 결과와 함께 완료되고 오류 발생 시에는 PlatformException과 함께 완료되며, 메서드가 구현되지 않은 경우에는 MissingPluginException과 함께 완료됩니다.
안드로이드에서는 콜백 인수를 취하는 메서드가 호출을 처리합니다. 콜백 인터페이스는 결과에 따라 호출 될 세 가지 메서드를 정의하고 있습니다. 클라이언트 코드는 성공 시, 오류 발생 시 그리고 구현되지 않은 경우에 어떻게 처리해야 할지 정의하는 콜백 인터페이스를 구현합니다.
마찬가지로, iOS에서도 콜백 인수를 취하는 메서드가 호출을 처리합니다. 하지만 iOS에서는 콜백이 FlutterError 인스턴스, FlutterMethodNotImplemented 상수, 혹은 호출이 성공한 경우 호출 결과를 받는 단일 인수 함수입니다. 콜백 함수를 구현 할 때, 각각의 케이스에 따라 다른 처리하도록 코드 블록에 조건부 로직이 추가됩니다.
메시지 호출 핸들러의 작성 방식에도 반영되는 이러한 차이점은 Flutter SDK 메서드 채널을 구현할 때, 사용되는 프로그래밍 언어(Dart, Java, Objective-C)의 스타일을 허용함에 따라 발생했습니다. Kotlin과 Swift 코드를 조금 더 수정하면, 차이점을 줄일 수 있겠지만, Java와 Objective-C에서 메서드 채널을 사용하기 더 어려워질 수 있음으로 주의해야 합니다.
이벤트 채널: 스트리밍
이벤트 채널은 플랫폼 이벤트를 Flutter에 Dart 스트림으로서 노출하기 위해 특화된 플랫폼 채널입니다. 필요하다면 만들 수 있겠지만, 현재 Flutter SDK에는 Dart 스트림을 플랫폼 코드에 노출하는 반대 경우를 지원하지 않습니다.
다음은 Dart 측에서 플랫폼 이벤트 스트림을 사용하는 방법을 보여줍니다.
// Consuming events on the Dart side.
const
channel = EventChannel('foo');
channel.receiveBroadcastStream().listen((dynamic event) {
print('Received event: $event');
}, onError: (dynamic error) {
print('Received error: ${error.message}');
});
아래 코드는 안드로이드 센서 이벤트를 기반으로 플랫폼에서 이벤트를 생성하는 방법을 보여줍니다. 여기서 주된 관심사는 플랫폼 소스(이 경우에는 sensor manager)로부터 이벤트를 수신하고, 정확히 1) Dart 측에 stream listener가 하나 이상 있고, 2) 호스트 엑티비티가 실행 중일 때, 이벤트 채널을 통해 이벤트를 전송하도록 보장하는 것입니다. 아래처럼 필요한 로직을 하나의 클래스에 패키징하면 실수를 줄일 수 있습니다.
// Producing sensor events on Android.
// SensorEventListener/EventChannel adapter.
class
SensorListener(
private val
sensorManager: SensorManager) :
EventChannel.StreamHandler, SensorEventListener {
private var
eventSink: EventChannel.EventSink? = null
// EventChannel.StreamHandler methods
override fun
onListen(
arguments: Any?, eventSink: EventChannel.EventSink?) {
this
.eventSink = eventSink
registerIfActive()
}
override fun
onCancel(arguments: Any?) {
unregisterIfActive()
eventSink = null
}
// SensorEventListener methods.
override fun
onSensorChanged(event: SensorEvent) {
eventSink?.success(event.values)
}
override fun
onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
if
(accuracy == SensorManager.SENSOR_STATUS_ACCURACY_LOW)
eventSink?.error("SENSOR", "Low accuracy detected", null)
}
// Lifecycle methods.
fun
registerIfActive() {
if
(eventSink == null)
return
sensorManager.registerListener(
this
,
sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE),
SensorManager.SENSOR_DELAY_NORMAL)
}
fun
unregisterIfActive() {
if
(eventSink == null)
return
sensorManager.unregisterListener(
this
)
}
}
// Use of the above class in an Activity.
class
MainActivity: FlutterActivity() {
var
sensorListener: SensorListener? = null
override fun
onCreate(savedInstanceState: Bundle?) {
super
.onCreate(savedInstanceState)
GeneratedPluginRegistrant.registerWith(
this
)
sensorListener = SensorListener(
getSystemService(Context.SENSOR_SERVICE) as SensorManager)
val
channel = EventChannel(flutterView, "foo")
channel.setStreamHandler(sensorListener)
}
override fun
onPause() {
sensorListener?.unregisterIfActive()
super
.onPause()
}
override fun
onResume() {
sensorListener?.registerIfActive()
super
.onResume()
}
}
앱에서 android.arch.lifecycle 패키지를 사용하는 경우 SensorListener를 LifecycleObserver로 만들어 SensorListener를 더 독립적으로 만들 수 있습니다.
세부 사항.
스트림 핸들러의 수명.
플랫폼 측 스트림 핸들러에는 onListen과 onCancel의 두 가지 메서드가 있습니다. 이 콜백은 Dart 스트림 리스너의 수가 각각 0에서 1로, 다시 1에서 0으로 바뀔 때마다 호출됩니다. 이런 일은 여러 차례 발생할 수 있습니다. 스트림 핸들러는 onListen이 호출될 때 이벤트 싱크로 이벤트를 쏟아붓기 시작하고 onCancel이 호출될 때 멈춥니다. 그 밖에도 호스트 앱 컴포넌트가 실행되고 있지 않을 때는 이벤트 푸시를 일시 중지해야 합니다. 위의 코드가 전형적인 예시입니다. 물론, 스트림 핸들러는 이벤트 채널 이름을 사용하여 Flutter 뷰에 등록된 바이너리 메시지 핸들러일 뿐입니다.
코덱.
이벤트 채널은 메서드 코덱으로 구성되므로, 메서드 채널이 성공적인 결과와 오류 결과를 구별할 수 있는 것과 똑같은 방식으로 성공 이벤트와 오류 이벤트를 구별할 수 있게 해줍니다.
스트림 핸들러 인수와 에러.
onListen 및 onCancel 스트림 핸들러 메서드는 메서드 채널 호출을 통해 호출됩니다. 따라서, 플랫폼에서 Dart로 전달되는 이벤트 뿐만아니라, 반대로 Dart에서 플랫폼 쪽으로 가는 제어 메서드 호출도 존재합니다. 이 양방향 통신은 동일한 논리적 채널 아래 이루어집니다. 이러한 설정 덕분에 제어 메서드에 인수를 전달하고, 오류를 보고 받을 수 있습니다. Dart 측에서는 receiveBroadcastStream 호출 시 (있는 경우) 인수를 지정할 수 있습니다. 즉, 인수는 onListen 및 onCancel 호출 횟수에 상관없이 한 번만 지정됩니다. 오류는 모두 보고되고 로그에 기록됩니다.
스트림 종료.
이벤트 싱크에는 추가로 보낼 성공 이벤트나 오류 이벤트가 없을 것임을 알리기 위해 호출하는 endOfStream 메서드가 있습니다. 이 목적으로 사용되는 것이 바로 null 바이너리 메시지입니다. Dart 측에서 이 메시지를 수신하면 스트림이 닫힙니다.
스트림의 수명
. Dart 스트림은 플랫폼 채널 메시지를 받는 스트림 컨트롤러와 같은 수명을 갖습니다. 바이너리 메시지 핸들러는 스트림에 리스너가 있는 동안에만 이벤트 채널의 이름을 사용해 등록되고 메시지를 수신합니다.
사용 지침
고유성 보장을 위해 도메인별로 채널 이름에 접두사 추가
채널 이름은 단지 문자열이지만, 앱에서 다양한 목적으로 사용되는 모든 채널 객체는 고유한 이름을 가져야합니다. 이를 위해 적합한 명명 체계가 필요합니다.
플러그인에 사용되는 채널에
권장되는 접근 방식은 도메인 이름 및 플러그인 이름 접두사를 사용하는 것입니다. 예를들어 example.com의 some.body가 개발한 sensors 플러그인에 사용되는 foo 채널에 대해 some.body.example.com/sensors/foo와 같은 이름을 지정할 수 있습니다. 그렇게 하면 플러그인 사용자가 채널 이름 충돌 위험 없이 원하는 만큼의 플러그인을 앱에 결합해 사용할 수 있습니다.
플랫폼 채널은 모듈 내 통신을 위한 방법으로 고려
분산 시스템에서 RPC(Remote Procedure Call) 구현을 위한 코드는 표면적으로는 메서드 채널을 사용하는 코드와 비슷해 보입니다. 문자열로 주어지는 메서드를 호출하고 인수와 결과를 직렬화합니다. 분산 시스템 컴포넌트는 독립적으로 개발되고 배포되는 일이 종종 있으므로 요청 및 회신을 잘 확인하는 것이 중요합니다. 보통은 네트워크의 양쪽에서 확인하고 로그에 기록하는 스타일로 처리됩니다.
반면에 플랫폼 채널은 함께 개발되고 배포되는 세 종류의 코드를 단일 컴포넌트에 붙입니다.
Java/Kotlin ↔ Dart ↔ Objective-C/Swift
세 부분을 Flutter 플러그인과 같은
단일 코드 모듈
에 패키지로 묶는 것이 타당한 경우가 무척 많습니다. 이는 곧 메서드 채널 호출 간에 인수 및 결과 확인의 필요성이 같은 모듈 내에서 이루어지는 일반적인 메서드 호출 간에 인수 및 결과 확인 필요성과 비슷하다는 의미입니다. 즉, RPC 구현 만큼 메서드 이름 및 인자를 철저하게 확인할 필요가 적습니다.
모듈 내에서 우리의 주된 관심사는 컴파일러의 정적 확인 과정에서 확인되지 않고, 결과에 큰 영향을 미칠 때까지 런타임 테스트 중에도 발견하지 못한 채로 넘어가는 프로그래밍 오류로부터 모듈을 보호하는 것입니다. 합리적인 코딩 스타일은 타입이나 어설션(assertions)을 사용해 명시적으로 가정을 세우고, 만일 가정에서 어긋나면, 예컨대 예외를 이용해 빠르고 분명하게 오류가 발생하도록 하는 것입니다. 세부 사항은 당연히 프로그래밍 언어마다 다릅니다. 예:
플랫폼 채널을 통해 수신되는 값이 특정 타입을 가질 것으로 예상되는 경우 즉시 그 값을 해당 타입의 변수에 할당합니다.
플랫폼 채널을 통해 수신되는 값이 null이 아닐 것으로 예상되는 경우, 즉시 그 값이 참조되지 않도록 null로 설정하거나, 다음에 사용하려고 저장하기 전에 값이 null이 아님을 어설션합니다. 프로그래밍 언어에 따라 null을 허용하지 않는 형식의 변수에 값을 할당할 수도 있습니다.
두 가지 간단한 예:
// Dart: we expect to receive a non-null List of integers.
for (final
int n
in
await
channel.invokeMethod('getFib', 100)) {
print(n * n);
}
// Android: we expect non-null name and age arguments for
// asynchronous processing, delivered in a string-keyed map.
channel.setMethodCallHandler { call, result ->
when
(call.method) {
"bar" -> {
val
name : String = call.argument("name")
val
age : Int = call.argument("age")
process(name, age, result)
}
else
-> result.notImplemented()
}
}
:
fun
process(name: String, age: Int, result: Result) { ... }
시실, 위 안드로이드 코드는 <T> T argument(String key) 시그니처를 갖는 argument 메소드를 오용하고 있습니다. argument 메서드는 키 값을 받아 해당 키로 조회되는 값을 generic 형태로 반환하지만, 이 코드에서는 메시지가 특정 포맷을 따르고 있다고 가정하고 키로 조회되는 값을 바로 String 이나 Int 형으로 변환해 사용하고 있습니다. 어떤 이유로든 코드 실행 중 문제가 발생하면 예외가 발생할 것 입니다. 메서드 호출 핸들러에서 발생하는 이런 예외는 로그에 기록되고 오류 결과가 Dart 측으로 전송됩니다.
플랫폼 채널 모방(mock) 금지
(가벼운 말장난입니다.) 플랫폼 채널을 사용하는 Dart 코드에 대한 단위 테스트를 작성할 때 네트워크 연결 시 그러하듯이 마치 무릎 반사처럼 채널 객체를 모방해 사용할 수도 있습니다.
하지만, 사실 단위 테스트를 깔끔하게 수행기 위해 모의 채널 객체(mock channel object)를 만들 필요가 없습니다. 대신 테스트 중, 모의 메시지 또는 메서드 핸들러를 등록하여 플랫폼 역할을 맡을 수 있습니다. 다음은 foo 채널을 통해 bar 메서드를 호출하는 함수 hello의 단위 테스트입니다.
test('gets greeting from platform', ()
async
{
const
channel = MethodChannel('foo');
channel.setMockMethodCallHandler((MethodCall call)
async
{
if
(call.method == 'bar')
return
'Hello, ${call.arguments}';
throw
MissingPluginException();
});
expect(
await
hello('world'), 'Platform says: Hello, world');
});
메시지 또는 메서드 핸들러가 올바르게 설정되었는지 테스트 하기위해 BinaryMessages.handlePlatformMessage 메서드를 사용해 수신할 메시지를 직접 만들어 테스트 할 수 있습니다. 현재 플랫폼 채널에서는 이 메서드를 제공하지 않지만, 아래 주석에서 언급한 것 처럼, 쉽게 추가할 수 있습니다. 아래 코드는 Hello 클래스의 단위 테스트입니다. Hello는 채널 foo를 통해 메서드 bar를 호출하는 메시지 인수를 모으고, 인사말을 반환하는 클래스입니다.
test('collects incoming arguments', ()
async
{
const
channel = MethodChannel('foo');
final
hello = Hello();
final
String result =
await
handleMockCall(
channel,
MethodCall('bar', 'world'),
);
expect(result, contains('Hello, world'));
expect(hello.collectedArguments, contains('world'));
});
// Could be made an instance method on class MethodChannel.
Future<
dynamic
> handleMockCall(
MethodChannel channel,
MethodCall call,
)
async
{
dynamic
result;
await
BinaryMessages.
handlePlatformMessage
(
channel.name,
channel.codec.encodeMethodCall(call),
(ByteData reply) {
if
(reply ==
null
)
throw
MissingPluginException();
result = channel.codec.decodeEnvelope(reply);
},
);
return
result;
}
위 두 가지 예제에서 모두 단위 테스트를 위해 채널 객체를 선언해 사용합니다. 채널 이름과 코덱을 중복해서 사용하는 것만 주의하면 이 코드는 잘 작동합니다. 같은 이름과 코덱을 가진 모든 채널 객체는 동등한 객체라는 점을 잊지 마세요. 프로덕션 코드와 테스트 코드 양쪽에서 접근 가능한 곳에 채널을
const
로 선언하면 중복을 방지할 수 있습니다.
지금까지 살펴본 것 처럼, 프로덕션 코드에 모의 채널을 주입할 방법을 제공하기 위해 걱정 할 필요가 없습니다.
플랫폼 상호 작용을 위해 자동화된 테스트 고려
플랫폼 채널은 충분히 단순하지만, Flutter UI에서 별개의 Java/Kotlin 및 Objective-C/Swift 구현으로 지원되는 커스텀 Dart API가 잘 동작하도록 만드는건 단순한 일이 아닙니다. 앱에 변경 사항이 있을 때도 설정이 그대로 동작하도록 보장하기 위해서는 회귀 테스트 (regression test)를 포함 자동화된 테스트가 필요합니다. 플랫폼 채널이 실제로 플랫폼과 통신하려면, 실행 중인 앱이 필요하므로 단위 테스트만으로는 이러한 목적을 달성할 수 없습니다.
Flutter에는 flutter_driver 통합 테스트 프레임워크가 제공됩니다. 이를 활용해 실제 기기와 에뮬레이터에서 실행 중인 Flutter 앱을 테스트할 수 있습니다. 그러나 flutter_driver는 현재 Flutter와 플랫폼 컴포넌트 전체에 걸쳐 테스트를 할 수 있도록 다른 프레임워크와 통합되어 있지 않습니다. 이는 Flutter가 앞으로 꼭 개선할 부분이라고 필자는 확신합니다.
상황에 따라 flutter_driver를 있는 그대로 사용하여 플랫폼 채널 테스트를 수행할 수 있습니다. 이를 위해서는 Flutter 사용자 인터페이스를 사용하여 플랫폼 상호 작용을 트리거할 수 있어야 하고, 그런 상호 작용의 결과를 확인할 수 있도록 Flutter 사용자 인터페이스가 충분한 세부 정보를 갖고 업데이트되어야 합니다.
그런 상황이 아니거나 플랫폼 채널 사용을 Flutter 플러그인으로 패키징한 경우라면 모듈 테스트를 하고 싶을 수 있습니다. 하지만, 그 대신 테스트 목적의 간단한 Flutter 앱을 작성할 수도 있습니다. 위에 언급한 특성을 갖춘 앱이어야 하며 flutter_driver를 사용하여 실행할 수 있습니다.
Flutter GitHub 저장소
에서 적당한 예를 찾아볼 수 있습니다.
플랫폼 측을 수신 동기 호출에 준비된 상태로 유지
플랫폼 채널은 비동기 전용입니다. 하지만 호스트 엑티비티를 통해 정보를 요청하거나, 도움을 구하거나, 그 외 필요한 작업을 수행하려고 할 때, 동기 호출이 필요한 플랫폼 API가 있습니다. 한 가지 예로 안드로이드의 Activity.onSaveInstanceState를 들 수 있습니다. 이 콜백 함수는 엑티비티가 종료되기 전에 필요한 상태정보를 저장할 수 있는 기회를 제공합니다. 동기화된다는 것은 API를 호출하기 전에 필요한 정보가 모두 준비되어야한다는 뜻 입니다. 동기 호출 중에 사용 할 데이터를 Dart 측에서 생성해 전달하고 싶을 수 있습니다. 하지만, 주 UI 스레드에서 이미 동기 호출이 시작되었다면, 비동기 메시지 발송을 시작하기엔 너무 늦습니다.
정보의 의미 체계 및 정보 접근성 관점에서 Flutter에서 사용하는 접근 방식은 Dart 측에서 정보가 바뀔 때마다 플랫폼 측으로 업데이트된 정보나 업데이트할 정보를 미리 보내는 것입니다. 그러면 동기 호출을 시작할 때 Dart 측으로부터 받은 정보가 이미 존재하여 플랫폼 측 코드에 사용할 수 있습니다.
참고 자료
Flutter API 문서
:
Dart 플랫폼 채널 형식이 포함된
서비스 라이브러리
에 대한 DartDoc.
안드로이드 플랫폼 채널 형식이 포함된
io.flutter.plugins.common 패키지
에 대한 JavaDoc.
iOS Flutter 라이브러리
에 대한 ObjcDoc.
가이드:
flutter.io 웹사이트
에
메서드 채널 사용 방법
과 표준 메서드 코덱 사용과 관련된 Dart/안드로이드/iOS 값 변환에 관한 문서가 있습니다.
Boring Flutter Development Show
, Episode 6:
Packages and plugins
는 플랫폼 채널을 사용하여 구현되는 Flutter 플러그인을 라이브로 보여주는 YouTube 동영상입니다.
코드 예:
flutter/flutter GitHub 저장소
에는 완성된 Flutter 앱으로 래핑 된, 기기 배터리 정보에 액세스하기 위해 메서드 및 이벤트 채널을 사용하는
기본 예제
가 수록되어 있습니다.
flutter/plugins GitHub 저장소
에는 Flutter 플러그인을 구현하기 위해 플랫폼 채널을 사용하는 여러 가지 예제가 수록되어 있습니다. 코드는 플러그인별로 구성된
packages
하위 폴더에 있습니다. 각 플러그인에는 완전한 예시 앱이 포함되어 있습니다.
Contents
ML/Tensorflow
Android
Flutter
Web/Chrome
Cloud
Google Play
Community
Game
Firebase
검색
Tag
인디게임페스티벌
정책 세미나
창구프로그램
AdMob
AI
Android
Android 12
Android 12L
Android 13
Android 14
Android Assistant
Android Auto
Android Games
Android Jetpack
Android Machine Learning
Android Privacy
Android Studio
Android TV
Android Wear
App Bundle
bootcamp
Business
Chrome
Cloud
Community
compose
Firebase
Flutter
Foldables
Game
gdg
GDSC
google
Google Developer Student Clubs
Google Play
Google Play Games
Interview
Jetpack
Jetpack Compose
kotlin
Large Screens
Library
ma
Material Design
Material You
ML/Tensorflow
mobile games
Now in Android
PC
Play Console
Policy
priva
wa
wear
Wearables
Web
Web/Chrome
Weeklyupdates
WorkManager
Archive
2024
12월
11월
10월
9월
8월
7월
6월
5월
4월
3월
2월
1월
2023
12월
11월
10월
9월
8월
7월
6월
5월
4월
3월
2월
1월
2022
12월
11월
10월
9월
8월
7월
6월
5월
4월
3월
2월
1월
2021
12월
11월
10월
9월
8월
7월
6월
5월
4월
3월
2월
1월
2020
12월
11월
10월
9월
8월
7월
6월
5월
4월
3월
2월
1월
2019
12월
11월
10월
9월
8월
7월
6월
5월
4월
3월
2월
1월
2018
12월
11월
10월
9월
8월
7월
6월
5월
4월
3월
2월
1월
2017
12월
11월
10월
9월
8월
7월
6월
5월
4월
3월
2월
1월
2016
12월
11월
10월
9월
8월
7월
6월
5월
4월
3월
2월
1월
2015
12월
11월
10월
9월
8월
7월
6월
5월
4월
3월
2월
1월
2014
12월
11월
10월
9월
8월
7월
6월
5월
4월
3월
2월
1월
2013
12월
11월
10월
9월
8월
7월
6월
5월
4월
3월
2월
1월
2012
12월
11월
10월
9월
8월
7월
6월
5월
3월
2월
1월
2011
12월
11월
Feed