Flutter에서 Native로 작성한 Android/iOS View 사용하기

2020. 10. 12. 17:59Programming/Flutter

반응형

Flutter에서 Native(Android/iOS)로 작성한 UI Component 사용하기

Flutter를 사용해서 하이브리드 앱을 만들 때, 성능상 한계로 인해 네이티브를 사용해 UI를 작성해야 할 때가 있다. 혹은 이미 Kotlin/Swift를 사용해서 만들어진 컴포넌트 UI가 있어서, Flutter로 코드를 재작성하지 않고 네이티브로 작성된 UI를 불러와야 할 때가 있다. 이러한 경우 어떻게 하면 되는지 살펴보도록 하자.

현재 테스트는 안드로이드만 해봤기 때문에, 이 글은 안드로이드 기준으로 작성한다. 안드로이드는 v2로 업데이트 되면서 사용법이 좀 달라졌기 때문에 삽질을 했지만, iOS는 아마 별 문제 없으리라 생각한다.

사실 공식 문서 Hosting native Android and iOS views in your Flutter app with Platform Views 항목을 보면, v2 기준으로 잘 기술되어있다. 2020년 10월 기준으로 구글링하면 가뭄에 콩나듯 v1 기준으로 작성된 예제가 검색되는데, io.flutter.app.FlutterActivity는 당장은 아니더라도 Deprecated될 예정이기 때문에 공식 문서를 참조하여 v2 기준으로 작성하도록 하자.

Hybrid Composition과 Virtual Display

Native로 작성한 코드를 불러오기 위해서는, 먼저 Dart파일을 사용하여 위젯을 생성해야한다. Dart로 코드를 작성할 때 Hybrid Composition를 사용하는 방법과 Virtual Display 를 사용하는 방법으로 나뉘게되는데, 두 가지 방법의 차이는 아래와 같다. 적절히 골라서 사용하도록하자.

Hybrid Composition

Flutter 1.22 버전 이상에서 사용 가능하며, 네이티브로 작성된 android.view.View를 View 계층(View Hierachy)에 추가한다. 따라서 키보드와 같은 동작이 UI 범위 박스 바깥쪽(UI 바깥쪽)에서 동작한다. 안드로이드10 이전 버전에서는 프레임(FPS)이 조금 떨어질 수 있다. 성능은 링크에서 설명한다.

Virtual Display

android.view.View를 텍스쳐로 렌더링하기 때문에, 안드로이드 View 계층(View Hierachy)에 포함되지 않는다. 따라서 키보드나 접근성 관련 조작이 동작하지 않을 수 있다.

Dart로 코드 작성

Hybrid Composition

import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';

위의 패키지를 import하도록 하자. 사실 아래의 코드를 첨부한 뒤, 에러가 나는 부분에서 퀵 픽스(cmd+.)를 누르면 자동으로 import되는 항목들이다.

적당히 NativeView라는 이름의 StatelessWidget을 만들어준 뒤, build 함수의 내용을 다음과 같이 수정해주자.

Widget build(BuildContext context) {
  // viewType은 네이티브 코드에서 뷰를 등록할 때 사용한다.
  final String viewType = '<platform-view-type>';
  // 파라메터를 네이티브로 전달할 때 사용한다.
  final Map<String, dynamic> creationParams = <String, dynamic>{};

  return PlatformViewLink(
    viewType: viewType,
    surfaceFactory:
        (BuildContext context, PlatformViewController controller) {
      return AndroidViewSurface(
        controller: controller,
        gestureRecognizers: const <Factory<OneSequenceGestureRecognizer>>{},
        hitTestBehavior: PlatformViewHitTestBehavior.opaque,
      );
    },
    onCreatePlatformView: (PlatformViewCreationParams params) {
      return PlatformViewsService.initSurfaceAndroidView(
        id: params.id,
        viewType: viewType,
        layoutDirection: TextDirection.ltr,
        creationParams: creationParams,
        creationParamsCodec: StandardMessageCodec(),
      )
        ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated)
        ..create();
    },
  );
}

이걸로 Hybrid Composition를 위한 Dart 코드는 끝이다. 자세한 사항은 PlatformViewLink, AndroidViewService, PlatformViewsService API 문서를 참조하도록 하자.

Virtual Device

import 'package:flutter/widget.dart';

위의 패키지를 import하도록 하자.

Widget build(BuildContext context) {
  // viewType은 네이티브 코드에서 뷰를 등록할 때 사용한다.
  final String viewType = 'hybrid-view-type';
  // 파라메터를 네이티브로 전달할 때 사용한다.
  final Map<String, dynamic> creationParams = <String, dynamic>{};

  return AndroidView(
    viewType: viewType,
    layoutDirection: TextDirection.ltr,
    creationParams: creationParams,
    creationParamsCodec: const StandardMessageCodec(),
  );
}

Hybrid Composition에 비해 코드가 간단한 편이다. 자세한 사항은 AndroidView API 문서를 참조하도록 하자.

Native로 코드 작성 (Android)

이제 Native로 코드를 작성해보자.
PlatformView를 상속받아서 실제 View를 저장하는 함수와, PlatformViewFactory를 상속받아서 View를 생성하는 함수를 만든 뒤 FlutterEngine에 등록해주기만 하면 된다.

PlatformView

TextView를 생성해서 저장하고 있다가, 요청이 오면 저장하고 있던 TextView를 반환하는 NativeView.kt를 생성해보자.

import android.content.Context
import android.graphics.Color
import android.view.View
import android.widget.TextView
import io.flutter.plugin.platform.PlatformView

internal class NativeView(context: Context, id: Int, creationParams: Map<String?, Any?>?) : PlatformView {
    private val textView: TextView

    override fun getView(): View {
        return textView
    }

    override fun dispose() {}

    init { // textview을 생성한 후 속성을 변경해준다.
        textView = TextView(context)
        textView.textSize = 72f
        textView.setBackgroundColor(Color.rgb(255, 255, 255))
        textView.text = "Rendered on a native Android view (id: $id)"
    }
}

NativeView 클래스는 TextView의 객체를 생성해서 저장하고 있다가, getView()가 호출되면 저장하고 있던 TextView의 레퍼런스를 반환해주는 역할을 한다.

PlatformViewFactory

이제 PlatformViewFactory를 상속받는 NativeViewFactory를 생성하도록하자. NativeViewFactory는 위에서 작성한 NativeView의 인스턴스를 생성하는 역할을 한다.

import android.content.Context
import android.view.View
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.StandardMessageCodec
import io.flutter.plugin.platform.PlatformView
import io.flutter.plugin.platform.PlatformViewFactory

internal class NativeViewFactory(private val messenger: BinaryMessenger, private val containerView: View) : PlatformViewFactory(StandardMessageCodec.INSTANCE) {
    override fun create(context: Context, id: Int, args: Any?): PlatformView {
        val creationParams = args as Map<String?, Any?>?
        return NativeView(context, id, creationParams)
    }
}

FlutterEngine에 등록

이제 위에서 만든 NativeViewFactoryMainActivity에서,configureFlutterEngine를 호출하여 FlutterEngine에 등록해주기만 하면 된다.

import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine

class MainActivity : FlutterActivity() {
    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        flutterEngine
                .platformViewsController
                .registry
                .registerViewFactory("<platform-view-type>", NativeViewFactory())
    }
}

플러그인을 생성하는 중이라면, 플러그인의 메인 클래스를 다음과 같이 수정하여 등록해주도록 하자.

import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding

class PlatformViewPlugin : FlutterPlugin {
    override fun onAttachedToEngine(binding: FlutterPluginBinding) {
        binding
                .platformViewRegistry
                .registerViewFactory("<platform-view-type>", NativeViewFactory())
    }

    override fun onDetachedFromEngine(binding: FlutterPluginBinding) {}
}

여기까지 작성한 후에 코드를 실행해보면 화면에 Rendered on a native Android view (id: 0)라는 문구가 출력되는 것을 확인할 수 있다. 위의 코드는 안드로이드에만 해당되는 코드이므로, 다른 플랫폼에 대해서 예외처리가 필요하다. 이러한 플랫폼 별 예외처리에 대해서는 공식문서의 Putting it together를 참조하도록 하자.

반응형