FLUTTER
Flutter 플러그인 만들어보기
2023.07.28
•
11 min read
플러그인을 만들어보자
이번에 플러그인을 직접 만들어보며 flutter plugin에 대해 정리해봤습니다.
이 글에서 만들어볼 예제 플러그인은 version을 리턴해주는 아주 간단한 기능 하나를 가지고 있고, 이름은 version입니다.
version 플러그인을 flutter에서 추천하는 Federated Plugin 구조를 사용하여 개발해보도록 하겠습니다.
Federated Plugin Structure란?
Federated plugin structure가 적용된 위 url_launcher 패키지의 경우 아래 세 가지 패키지로 구성되어있습니다.
url_launcher
app-facing package
라고 합니다.- 플러그인 사용자들이 사용하는 패키지입니다.
url_launcher_platform_interface
platform interface package
라고 합니다.- platform 패키지의 인터페이스이며, 기능을 명세합니다. 이를 구현한 platform 패키지는 각자 플랫폼이 달라도 동일한 형태로 동일한 기능을 제공합니다.
- app-facing과 platform이 의존성을 가지고 있고, 서로를 연결해주는 패키지입니다.
url_launcher_web
platform package
라고 합니다.- 플랫폼 별 구현체 중 web 패키지입니다. platform interface package를
extends
합니다.
Federated plugin structure 구조의 장점은 다음과 같습니다.
- 플러그인 작성자가 모든 플랫폼에 대해 알고 있지 않아도 됩니다.
- 플러그인 작성자가 코드를 검토하고 가져올 필요 없이 새 플랫폼에 대한 지원을 추가할 수 있습니다.
- 플랫폼 패키지들은 각각 유지보수되고 테스트될 수 있습니다.
폴더 구조
version/
├─ version/
├─ version_platform_interface/
├─ version_ios/
├─ version_android/
.gitignore
README.md
package, plugin 생성 명령어는 root 폴더(최상위 version)에서 실행합니다.
Platform Interface Package
생성
flutter create --template=package version_platform_interface
Pubspec.yaml
name: version_platform_interface
#...
dependencies:
flutter:
sdk: flutter
plugin_platform_interface: ^2.1.4
VersionPlatformInterface.dart
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
abstract class VersionPlatform extends PlatformInterface {
VersionPlatform() : super(token: _token);
static final Object _token = Object();
static VersionPlatform? _instance;
static VersionPlatform get instance => _instance ?? VersionDefault();
static set instance(VersionPlatform instance) {
PlatformInterface.verify(instance, _token);
_instance = instance;
}
Future<String?> getPlatformVersion() {
throw UnimplementedError('platformVersion() has not been implemented.');
}
}
이 인터페이스는 아래 두 가지 역할을 수행하고 있습니다.
instance set/get
set
- 실제 플랫폼 별 구현체 인스턴스를 등록하기 위한 instance setter가 있고, setter 안에서는 verify 메서드를 사용하여 해당 인스턴스가
VersionPlatformInterface
를 구현하지 않고 올바르게 상속 하였는지 검증 후 등록합니다.
만약 해당 클래스가 extend가 아닌 implement를 한다면 이후
VersionPlatformInterface
에 새롭게 추가되는 메서드가 있을 때 breaking change가 발생하기 때문입니다.get
- 인스턴스가 등록되어있다면 해당 인스턴스를 가져오고, 아니라면 Default 인스턴스를 가져옵니다.
기능 명세
플랫폼 버전을 가져오는 getPlatformVersion
메서드를 명세합니다. 실제 구현은 각 플랫폼 플러그인에서 할테니 안에서는 UnimplementError
를 던집니다.
VersionDefault.dart
class VersionDefault extends VersionPlatform {}
등록된 인스턴스가 없을 때 넣어줄 기본 구현체입니다. VersionPlatform
클래스를 extend하고 있고, 아무 일도 하지 않기 때문에 부모의 UnimplementError
를 그대로 던집니다.
배포
app-facing package와 구현체 패키지들이 이 패키지를 참조해야하므로 가장 먼저 배포합니다.
dart pub publish
- 배포를 하게 되면 되돌릴 수 없고, 삭제할 수도 없으므로 필요없는 파일을 제외하거나 세팅에 대해 다시 한번 확인합니다.
- LICENSE를 입력하고, CHANGELOG.md를 작성, 버전을 확인합니다.
Platform Package
이 글에서 구현하는 platform package는 각 플랫폼(android, iOS) 별로 MethodChannel을 이용해 해당 플랫폼 별 버전 데이터를 받는 역할을 합니다. MethodChannel을 이용해 주고 받는 데이터 형식은 공식문서에서 확인할 수 있습니다.
Platform Package - Android
생성
flutter create --template=plugin version_android --platform android --org com.example
Pubspec.yaml
name: version_android
#...
flutter:
plugin:
implements: version
platforms:
android:
package: com.example.version_android
dartPluginClass: VersionAndroid
pluginClass: VersionAndroidPlugin
dependencies:
flutter:
sdk: flutter
version_platform_interface: ^0.0.1
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
- package를 명시합니다.
- implements를 통해 이 구현체가 어떤 패키지를 implement하고 있는지 명시합니다.
- platforms에서 dartPluginClass 항목에
VersionPlatform
을 구현할 클래스VersionAndroid
를 넣습니다. 이를 통해VersionAndroid
의registerWith
가 실행됩니다. - dependencies에
version_platform_interface
를 추가합니다.
VersionAndroid.dart
import 'package:flutter/services.dart';
import 'package:version_platform_interface/version_platform.dart';
const MethodChannel _channel = MethodChannel('example.com/version_android');
class VersionAndroid extends VersionPlatform {
static void registerWith() {
VersionPlatform.instance = VersionAndroid();
}
@override
Future<String?> getPlatformVersion() {
return _channel
.invokeMethod<String>('getPlatformVersion');
}
}
위에서는 아래 두 가지 역할을 수행합니다.
instance 등록
registerWith
메서드가 실행되면 PlatformInterface
를 extend하고 있는 VersionAndroid
인스턴스를 등록합니다.
기능 구현
Platform Interface Package
에서 명세해놓은 getPlatformVersion
메서드를 override하여 실제로 구현합니다.
VersionAndroidPlugin.kt
package com.example.version_android
import androidx.annotation.NonNull
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
class VersionAndroidPlugin: FlutterPlugin, MethodCallHandler {
private lateinit var channel : MethodChannel
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "example.com/version_android")
channel.setMethodCallHandler(this)
}
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
if (call.method == "getPlatformVersion") {
result.success("Android ${android.os.Build.VERSION.RELEASE}")
} else {
result.notImplemented()
}
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
}
VersionAndroidPlugin
은 아래 역할들을 수행합니다.
플러그인 등록
FlutterPlugin
interface를 implement합니다. 그러면 이 플러그인을 사용하는 프로젝트 빌드시 자동 생성되는 GeneratedPluginRegistrant.java
라는 파일 안에서 다음과 같이 플러그인이 등록됩니다.
//...
flutterEngine.getPlugins().add(new com.example.version_android.VersionAndroidPlugin());
//...
또한, 플러그인이 플러터 엔진에 attach(onAttachedToEngine
), detach(onDetachedFromEngine
) 되었을 때의 Lifecycle을 통해 plugin에 필요한 작업을 할 수 있습니다. (리소스 등록, 해제)
MethodChannel 생성, Handler 등록
flutter와 데이터를 주고받을 수 있는 channel을 생성하고, handler를 등록합니다. MethodCallHandler
를 implement하고 onMethodCall
을 override하고 있으므로 channel.setMethodCallHandler에 this를 전달하면 channel을 통해 신호가 왔을 때 onMethodCall
함수가 실행됩니다.
기능 구현
result에 현재 시스템 버전을 담아 return합니다. (플러그인 생성 시 기본적으로 들어가있는 코드)
Activity
만약 플러그인이 현재 플러터의 Activity에 접근해야하거나, Android activity Lifecycle(e.g, onCreate, onStart, onDestroy)에 따라 필요한 작업이 있을 경우, 다음과 같이 ActivityAware
를 implement 해줍니다.
class VersionAndroidPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
private lateinit var binaryMessenger: BinaryMessenger
private lateinit var channel: MethodChannel
private lateinit var activity: Activity
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
binaryMessenger = flutterPluginBinding.binaryMessenger
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding.activity
channel = MethodChannel(binaryMessenger, "example.com/version_android")
channel.setMethodCallHandler(this)
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
onAttachedToActivity(binding)
}
override fun onDetachedFromActivityForConfigChanges() {}
override fun onDetachedFromActivity() {}
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result)
{
if (call.method == "getPlatformVersion") {
result.success("Android ${android.os.Build.VERSION.RELEASE}")
} else {
result.notImplemented()
}
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
}
위와 같이 onAttachedToActivity를 통해 activity를 저장하고, activity를 이용한 여러 작업을 할 수 있습니다. (e.g, runOnUiThread
를 통해 UiThread에 작업을 실행)
MethodChannel
을 등록하는 작업을 onAttachedToActivity
에서 진행하도록 변경했는데, onAttachedToEngine
에서 channel을 등록했을 때 invokeMethod
를 통해 flutter 쪽으로 신호가 가지 않는 예외사항을 확인했습니다. 추측하기로는, 아직 activity가 없는 onAttachedToEngine
에서 channel을 등록한 것이 문제가 되지 않았나 싶습니다.배포
dart pub publish
- 배포를 하게 되면 되돌릴 수 없고, 삭제할 수도 없으므로 필요없는 파일을 제외하거나 세팅에 대해 다시 한번 확인합니다.
- LICENSE를 입력하고, CHANGELOG.md를 작성, 버전을 확인합니다.
Platform Package - ios
생성
flutter create --template=plugin version_ios --platform ios --org com.example
Pubspec.yaml
name: version_ios
#...
flutter:
plugin:
implements: version
platforms:
ios:
dartPluginClass: VersionIOS
pluginClass: VersionIosPlugin
dependencies:
flutter:
sdk: flutter
version_platform_interface: ^0.0.1
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
- implements를 통해 이 플러그인이 어떤 플러그인을 implement하고 있는지 명시합니다.
- platforms에서 dartPluginClass 항목에
VersionPlatform
을 구현할 클래스VersionIOS
를 넣습니다. 이를 통해VersionIOS
의registerWith
가 실행됩니다. - dependencies에
version_platform_interface
를 추가합니다.
VersionIOS.dart
import 'package:flutter/services.dart';
import 'package:version_platform_interface/version_platform.dart';
const MethodChannel _channel = MethodChannel('example.com/version_ios');
class VersionIOS extends VersionPlatform {
static void registerWith() {
VersionPlatform.instance = VersionIOS();
}
@override
Future<String?> getPlatformVersion() {
return _channel
.invokeMethod<String>('getPlatformVersion');
}
}
위에서는 아래 두 가지 역할을 수행합니다.
instance 등록
registerWith
메서드가 실행되면 PlatformInterface
를 extend하고 있는 VersionIOS
인스턴스를 등록합니다.
기능 구현
Platform Interface Package
에서 명세해놓은 getPlatformVersion
메서드를 override하여 실제로 구현합니다.
SwiftVersionIosPlugin.swift
import Flutter
import UIKit
public class SwiftVersionIosPlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "example.com/version_ios", binaryMessenger: registrar.messenger())
let instance = SwiftVersionIosPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
result("iOS " + UIDevice.current.systemVersion)
}
}
ios 폴더 안의 SwiftVersionIosPlugin.swift
파일을 열어보면 위와 같이 두 가지 역할을 하는 코드가 있습니다.
instance, MethodChannel 등록
SwiftVersionIosPlugin
과 methodChannel
을 생성해서 FlutterPluginRegistrar
에 등록합니다. methodChannel의 이름은 위 VersionIOS
클래스에 선언한 이름과 동일해야합니다.
기능 구현
result에 현재 시스템 버전을 담아 return합니다. (플러그인 생성 시 기본적으로 들어가있는 코드)
배포
dart pub publish
- 배포를 하게 되면 되돌릴 수 없고, 삭제할 수도 없으므로 필요없는 파일을 제외하거나 세팅에 대해 다시 한번 확인합니다.
- LICENSE를 입력하고, CHANGELOG.md를 작성, 버전을 확인합니다.
App-Facing Package
마지막으로 유저들이 실제로 사용하게 될 facing package입니다.
생성
flutter create --template=plugin version
Pubspec.yaml
name: version
#...
flutter:
plugin:
platforms:
android:
default_package: version_android
ios:
default_package: version_ios
dependencies:
flutter:
sdk: flutter
version_platform_interface: ^0.0.1
version_android: ^0.0.1
version_ios: ^0.0.1
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: 2.0.1
plugin_platform_interface: ^2.1.4
- platforms에 구현하려고 하는 플랫폼의 plugin을 명시합니다.
- dependencies에 platform_interface와 각 플랫폼 plugin을 넣습니다.
- dev_dependencies에 plugin_platform_interface를 넣습니다.
PlatformInterface 사용
import 'package:version_platform_interface/version_platform.dart';
//...
Future<String?> getPlatformVersion() {
return VersionPlatform.instance.getPlatformVersion();
}
PlatformInterface
의 메서드를 포팅합니다. 실제 돌아가고 있는 instance가 어떤 것인지 정확히 알지 못해도 각 플랫폼에서 구현한 기능을 제공받습니다.
App-Facing Package Example
이제 example project를 실행해보면(없다면 flutter create . 를 이용해서 예제 프로젝트를 생성) 위와 같이 version을 확인할 수 있습니다. (version을 출력하는 example code가 자동으로 들어가 있습니다.)
배포
dart pub publish
- 배포를 하게 되면 되돌릴 수 없고, 삭제할 수도 없으므로 필요없는 파일을 제외하거나 세팅에 대해 다시 한번 확인합니다.
- LICENSE를 입력하고, CHANGELOG.md를 작성, 버전을 확인합니다.
맺으며
플러그인을 만들어서 배포하는 것은 생각보다 쉽습니다.
업무 상 필요한 기능이 아직 pub.dev 에 없다면, 만들어서 배포해보는 건 어떨까요?