본문 바로가기

프로그래밍/Flutter

[flutter] 플러그인 프로젝트 구현 절차

플러그인 프로젝트 생성

flutter create --org com.example --template=plugin --platforms=android,ios -i swift my_plugin

 

/pubspec.yaml 

 

 


 

인터페이스 만들기

/lib 하위에 dart 클래스를 구성


네이티브 인터페이스

네이티브 인터페이스 클래스

native와 연결될 메쏘드들을 정의할 인터페이스 클래스 생성.

abstract class MyPluginPlatform extends PlatformInterface {
   MyPluginPlatform() : super(token: _token);
   
   static final Object _token = Object();
   static MyPluginPlatform _instance = MyPluginMethodCall();
   static MyPluginPlatform get instance => _instance;
   
   static set instance(MyPluginPlatform instance) {
      PlatformInterface.verifyToken(instance, _token);
      _instance = instance
   }
}

 

네이티브 인터페이스 구현 클래스

메쏘드 채널을 포함하는 클래스로 인터페이스에 정의된 api들의 구현부가 들어간다. 

class MyPluginMethodCall extends MyPluginPlatform {
   @visibleForTesting
   final methodChannel = const MethodChannel('MyPlugin.Method');
}

 

Dart 인터페이스 클래스

dart 에서 plugin 패키지를 import 해 네이티브 호출을 위한 메인 dart 클래스.
dart 객체들은 이 클래스를 통해 네이티브 호출을 수행하게 되며, 네이티브로부터의 이벤트를 전달받기 위해 이벤트 채널을 생성한다.

class MyPluginClass {   

}

 

 

인터페이스 구현

각 인터페이스와 객체 설정이 완료되면, 실제 네이티브 메쏘드를 호출하는 인터페이스들을 구현한다.

네이티브 인터페이스 추가

예를 들어 'nativeMethod' 라는 이름으로 메쏘드 채널을 통해 네이티브를 호출한다. 두개의 String 인자를 전달하며, 결과는 Map<Object?, Object?>? 으로 반환한다. 일반적인 경우 <String, Object?> 정도를 사용하는데, 테스트 코드에서 사용하는 setMockMethodCallHandler의 경우 Map<Object?, Object?> 로 Map 결과를 반환하기에 테스트가 항상 실패하게 된다. 

// 인터페이스(프로토콜) 정의
abstract class MyPluginPlatform extends PlatformInterface {
   .
   .
   .
   .



   // 여기에 native와 연결할 메쏘드를 정의
   Future<Map<Object?, Object?>?> callNative(String arg1, String arg2) {
      throw UnimplementedError('error');
   }
}


// 인터페이스(프로토콜) 구현
class MyPluginMethodCall extends MyPluginPlatform {
    .
    .
    .
    .
    
   @override
   Future<Map<Object?, Object?>?> callNative(String arg1, String arg2) async {
      var args = <String, String>{ "arg1": arg1, "arg2": arg2 };
      final result = await methodChannel.invokeMethod<String?>('nativeMethod', args);
      return result;
   }
}

 

메쏘드 채널 테스트 코드

메쏘드 채널의 호출과 mock 데이터 리턴을 통해 네이티브 인터페이스의 정상 동작여부를 확인한다. 메쏘드 채널의 setMockMethodCallHandler() 를 통해 테스트를 위한 핸들러를 등록한다. 이 핸들러에 네이티브 호출과 리턴을 위한 테스트 코드를 추가한다.

void main() {
   MyPluginMethodChannel platform = MyPluginMethodChannel();
   const MethodChannel channel = MethodChannel('MyPlugin.Method');
   TestWidgetsFlutterBinding.ensureInitialized();
   
   
   setUp( () {
      channel.setMockMethodCallHandler( (methodCall) async {
         if (methodCall.method == 'nativeMethod') {
            return { "result": true };
         }
         
         return null;
      });
   });
   
   
   tearDown( () {
      channel.setMockMethodCallHandler(null);
   });
   
   
   test('callNative', () async {
      var result = await platform.callNative("arg1", "arg2");
      if (result != null) {
         expect(result['result'], true);
      }
   });
}

 

Dart api 추가

네이티브 메쏘드 호출 인터페이스 구성 및 구현, 테스트가 완료되면, dart 에서 사용할 api 를 구성한다. 

class MyPluginClass {
   .
   .
   Future<bool> callNativeMethod(String arg1, String arg2) async {
      var result = await MyPluginPlatform.instance.callNative(arg1, arg2);
      if (result != null) {
         if( result['result'] == true ) {
            return true;
         }
      }
      
      return false;
   }
   .
   .
}

 

Dart api 테스트

메인 api만 테스트하기 위한 코드로 실제 메쏘드 채널을 거치지 않은 플랫폼 인터페이스를 테스트용 인터페이스로 교체한다.

// 테스트를 위한 플랫폼 인터페이스 구현
class MockMyPluginPlatform with MockPlatformInterfaceMixin implements MyPluginPlatform {
   @override
   Future<Map<Object?, Object?>?> callNative(String arg1, String arg2) async {
      return { "result": true };
   }
}


void main() {
   final MyPluginPlatform initialPlatform = MyPluginPlatform.instance;
   
   
   test('callNative', () async {
      MyPlugin plugin = MyPlugin();
      
      // 테스트용 플랫폼 인스턴스로 교체
      MockMyPluginPlatform fakePlatform = MockMyPluginPlatform();
      MyPluginPlatform.instance = fackePlatform;
      
      // api 호출
      var result = await plugin.callNative("arg1", "arg2");
      if (result != null) {
         expect(result['result'], true);
      }
   });
}

 

 

이벤트 채널 구현

메인 api 클래스는 네이티브에서 전달할 이벤트를 수신하기 위해 이벤트 채널을 사용하는데, 해당 이벤트 채널 생성시 지정한 리스너를 구현한다.

class MyPluginClass {
   EventChannel eventChannel = const EventChannel('MyPlugin.Event')
   StreamSubscription<dynamic>? streamSubscription;
   
   
   void startListener() {
      streamSubscription = eventChannel.receiveBroadcastStream().listen(eventListener, onError: errorListener);
   }
   
   void stopListener() {
      streamSubscription?.cancel()
   }

   void eventListener(dynamic event) {
      final Map<dynamic, dynamic> map = event;
   
      switch (map['event']) {
         case 'nativeEvent':
      
         break;
      }
   }
   
   
   void errorListener(Object obj) {
   
   
   }
   
}

네이티브 구현

샘플빌드

ios 개발은 xcode에서 진행해야 하므로, plugin 프로젝트 생성시 함께 생성되는 Runner 프로젝트에서 네이티브 구현을 진행한다. 프로젝트 생성시에는 아직 Podfile 등이 존재하지 않으므로, 빌드해 준다.

cd example
flutter build ios

 

Runner.xcworkspace 를 열어보면 아래처럼 프로젝트가 구성되며, iOS 측 네이티브 코드는 xcode 로 작성한다.

Pods > Development Pods > my_plugin > .. > .. > example > ios > .symlinks > plugins > my_plugin > ios > Classes

 

 

등록 메쏘드에서 메쏘드 채널과 이벤트 채널 생성

FlutterAppDelegate 를 상속받은 앱에서 호출하는 인스턴스 생성 루틴을 포함하는 네이티브 메인 클래스

public class MyPlugin: NSObject, FlutterPlugin {
   public static func register(with registrar: FlutterPluginRegistrar) {
      let channel = FlutterMethodChannel(name: "MyClass.Method", binaryMessenger: registrar messenger())
      let eventChannel = FlutterEventChannel(name: "MyClass.Event", binaryMessaenger: messenger)
      let instance = MyPlugin(messenger: messenger)
      
      registrar.addMethodCallDelegate(instance, channel: channel)
      eventChannel.setStreamHandler(instance)
   }
   
   var eventSink: FlutterEventSink
}

extension MyPlugin: FlutterStreamHandler {

}

 

메쏘드채널 delegate

메쏘드 채널

public fund handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
   if call.method == "nativeMethod" {
      guard let args = call.arguments as? [String: String] else {
         result(FlutterError(code: "ArgError", message: "ArgError", details: nil))
      }
      
      // native 처리
         .
         .
      
      // 결과를 원하는 타입으로 전달
      result( [ "result": true] )
   } else {
      result(FlutterMethodNotImplemented)
   }
}

 

스트림 핸들러

이벤트 채널 리스터가 등록되거나 제거 될때 호출되는 콜백 메쏘드 

extension MyPlugin: FlutterStreamHandler {
   func onListen(withArguemnts arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
      self.eventSink = sink
      return nil
   }

   func onCancel(withArguments arguments: Any?) -> FlutterError? {
      self.eventSink = nil
      return nil
   }
}

 

이벤트 전달

dart쪽으로 네이티브의 이벤트를 전달할때 사용

// 비동기적으로 호출되는 상태 변경이 일어나면 이벤트를 전달
func someStateChanged() {
   eventSink(["event":"nativeEvent","value":"0"] )
}