FLUTTER
Flutter 플러그인 만들어보기
2023.07.28
•
11 min read
![Flutter 플러그인 만들어보기](https://cdn.comento.kr/blog/developer/2023/11/flutter-plugin.webp)
플러그인을 만들어보자
이번에 플러그인을 직접 만들어보며 flutter plugin에 대해 정리해봤습니다.
이 글에서 만들어볼 예제 플러그인은 version을 리턴해주는 아주 간단한 기능 하나를 가지고 있고, 이름은 version입니다.
version 플러그인을 flutter에서 추천하는 Federated Plugin 구조를 사용하여 개발해보도록 하겠습니다.
Federated Plugin Structure란?
![](https://cdn.comento.kr/blog/developer/2023/07/image-9.png)
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
![](https://cdn.comento.kr/blog/developer/2023/07/image-8.png)
이제 example project를 실행해보면(없다면 flutter create . 를 이용해서 예제 프로젝트를 생성) 위와 같이 version을 확인할 수 있습니다. (version을 출력하는 example code가 자동으로 들어가 있습니다.)
배포
dart pub publish
- 배포를 하게 되면 되돌릴 수 없고, 삭제할 수도 없으므로 필요없는 파일을 제외하거나 세팅에 대해 다시 한번 확인합니다.
- LICENSE를 입력하고, CHANGELOG.md를 작성, 버전을 확인합니다.
맺으며
플러그인을 만들어서 배포하는 것은 생각보다 쉽습니다.
업무 상 필요한 기능이 아직 pub.dev 에 없다면, 만들어서 배포해보는 건 어떨까요?