Flutter/Basic

[Flutter] Plugin을 만들고 Android Native 코드 접근하기(ft.MethodChannel, EventChannel, RxJava 사용)

seunghwaan 2022. 7. 21. 17:16
반응형

안녕하세요. 이번에는 Flutter Plugin을 만들어서 Android Native 코드를 호출하는 법을 알아보겠습니다.

 

Flutter Plugin이란?

Flutter Plugin은 Android(Kotlin 또는 Java) 및 iOS(swift 또는 objective c)와 같은 네이티브 코드의 Wrapper입니다. 그러므로 Flutter는 Flutter Plugin을 통해서 platform channels와 메시지 전달을 통해 네이티브 애플리케이션에서 할 수 있는 모든 것을 할 수 있습니다. 동작은 Flutter에서 기본 iOS/Android 코드에 작업을 수행하고 결과를 Dart코드에 Return 하도록 지시합니다.

Flutter Platform Architectural overview: platform channels

클라이언트(UI)와 Android, iOS 각각의 Platform들이 대화하는 방법은 아래의 다이어그램에 그려져 있습니다. 

 

클라이언트(UI)에서 MethodChannel에 메시지를 보내서 각 플랫폼의 메소드를 통해서 메시지를 받고 응답할 수 있게 해 줍니다. 플랫폼 단에서는 Android는 MethodChannel, iOS는 FlutterMethodChannel이 메시지를 받고 응답할 수 있습니다.

 

* Flutter가 Dart와 메시지를 비동기로 주고 받음에도 불구하고, Channel 메소드를 호출할 때 메인 스레드에서 호출해야 합니다.

 

플러그인 생성하기

명령어

 flutter create --org com.example --template=plugin --platforms=android,ios,linux,macos,windows -a kotlin hello

--platforms:  어떤 platform을 support하는 plugin을 만들 것 인지 나열해줍니다. 콤마로 separator를 사용할 수 있으며 android, ios, web, linux, macos, winodws가 있습니다.

--org: 어떤 organization인지 reverse domain name을 이용하여 정합니다. 

-a: Android에서 어떤 언어를 사용할 것인지 정합니다. kotlin과 java을 사용할 수 있습니다.

-i: iOS에서 어떤 언어를 사용할 것인지 정합니다. swift와 objc를 사용할 수 있습니다.

 

이번 포스트에서는 코틀린 언어를 사용해서 BatteryPlugin을 만들어 설명할 예정이므로 아래 명령어를 사용하여 프로젝트를 생성하겠습니다.

 flutter create --org com.seosh817 --template=plugin --platforms=android,ios -a kotlin -i swift battery_plugin

 

프로젝트를 생성하면 아래와 같은 플러그인 모듈이 생성됩니다.

 

BatteryPlugin.kt

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

/** BatteryPlugin */
class BatteryPlugin: FlutterPlugin, MethodCallHandler {
  /// The MethodChannel that will the communication between Flutter and native Android
  ///
  /// This local reference serves to register the plugin with the Flutter Engine and unregister it
  /// when the Flutter Engine is detached from the Activity
  private lateinit var channel : MethodChannel

  override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
    // TODO: your plugin is now attached to a Flutter experience.
  }

  override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
    // TODO: your plugin is no longer attached to a Flutter experience.
  }
  
  override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
  }
}

플러그인 메소드에 대해 하나씩 알아보겠습니다.

onAttachedToEngine

FlutterPlugin이 PluginRegistry.add(Class)를 통해 FlutterEngine에 attached 되면 onAttachedToEngine 메소드가 호출합니다. FlutterPluginBinding을 통해 플러그인의 코드에 접근하는 것이 허용됩니다.

override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {

}

onDetachedToEngine

FlutterPlugin이 PluginRegistry.remove(Class)를 통해 FlutterEngine이 제거되거나 FlutterEngine이 파괴되면 FlutterEngine은 FlutterPlugin에서 onDetachedFromEngine(FlutterPluginBinding)을 호출합니다.

override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
    methodChannel.setMethodCallHandler(null)
    eventChannel.setStreamHandler(null)
}

onMethodCall

FlutterPlugin의 MethodChannel에 대한 콜백입니다. 플러터의 Dart 코드에서 MethodChannel을 통해 invokeMethod를 호출하면 onMethodCall을 통해 받고 응답할 수 있습니다.

override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
}

 

어떤 경우에는 Flutter Plugin에서 Activity에 액세스해야 할 수도 있습니다. (예를들면, Activity 생명주기)

그럴 경우에는 ActivityAware을 상속받아서 사용해주면 됩니다.

BatteryPlugin 구현

플러터 앱을 만들다보면 Dart코드 만으로는 할 수 없는 것들이 있고, 네이티브 플랫폼과 대화해야만 할 수 있는 기능들이 있습니다.

이에 대해 우리는 크게 두 가지의 Platform Channels를 통해 구현할 수 있습니다. 바로 Method Channel과 Event Channel입니다.

 

Method channel: Dart코드와 각 Platform(Android, iOS...)이 비동기적 호출을 통해 메시지와 응답을 주고받을 수 있는 채널.

Event channel: Dart코드와 각 Platform(Android, iOS...)이 이벤트 스트림을 통해 커뮤니케이팅할 수 있는 채널.

Method Channel

Dart코드의MethodChannel의 invokeMethod로 호출을 하면 코틀린 코드에서 onMethodCall()에서 콜백을 받게됩니다.

그러면 MethodCall을 통해 arguments를 받을 수 있고, MethodChannel.Result 클래스를 통해 success() 혹은 error()로 결과를 날려주면 Dart 코드에 결과를 반환할 수 있습니다.

 

아래의 코드로 예를 들면,battery_plugin_method_channel이라는 이름을 가진 MethodChannel에서 invokeMethod로 getBatteryLevel라는 이름으로 호출한다면 코틀린 코드의 onMethodCall()에서 call.method가 "getBatteryLevel"로 오게 되고, switch문을 통해 메소드를 분리해 주고 결과를 반환하도록 처리해 주면 되는 것입니다.

 

Event Channel

Dart코드에서 EventChannel을 통해 recevie() 해주고, 코틀린코드의 EventChannel에 StreamHandler를 등록해 주면 onListen()에서 arguments를 받을 수 있고 EventChannel.EventSink를 통해 Dart의 Stream에 결과를 반환할 수 있습니다.

 

아래의 코드로 예를 들면,Dart코드에서 battery_charging_state_event_channel_name이라는 이름을 가진 EventChannel에 데이터를 흘려주면 코틀린 코드의 StreamHandler의 onListen()에서 브로드캐스트 리시버를 등록해 주고 배터리의 충전변화 때마다 eventSink에 데이터를 보내주면 Dart의 스트림에서 데이터를 받을 수 있게 됩니다.

 

이 채널들을 이용해서 어떻게 대화할 수 있는지 공부하기 위해 배터리에 관련한 기능을 제공해 주는 BatteryPlugin을 만들 예정이고 플러터 플러그인 예제 깃허브를 조금 참고하여 만들어보겠습니다. 또한, iOS는 다루지 않고 Android 쪽 코드만 다루어보도록 하겠습니다.

 

첫 번째로, Method Channel을 이용해서 버튼을 클릭하면 안드로이드로부터 배터리가 몇 프로 채워져 있는지 Android Platform으로부터 받아오는 기능 

두 번째로, Event Channel을 이용해서 Event Stream을 통해 안드로이드로부터 주기적으로 Battery가 충전 중인지 아닌지를 받아오는 기능을 구현해 보겠습니다.

 

먼저 플러터 쪽 코드를 작성해 보도록 하겠습니다.

battery_plugin_platform_interface.dart

import 'package:plugin_platform_interface/plugin_platform_interface.dart';

import 'battery_plugin_method_channel.dart';

abstract class BatteryPluginPlatform extends PlatformInterface {
  /// Constructs a BatteryPluginPlatform.
  BatteryPluginPlatform() : super(token: _token);

  static final Object _token = Object();

  static BatteryPluginPlatform _instance = MethodChannelBatteryPlugin();

  /// The default instance of [BatteryPluginPlatform] to use.
  ///
  /// Defaults to [MethodChannelBatteryPlugin].
  static BatteryPluginPlatform get instance => _instance;

  /// Platform-specific implementations should set this with their own
  /// platform-specific class that extends [BatteryPluginPlatform] when
  /// they register themselves.
  static set instance(BatteryPluginPlatform instance) {
    PlatformInterface.verifyToken(instance, _token);
    _instance = instance;
  }

  Future<String?> getPlatformVersion() {
    throw UnimplementedError('platformVersion() has not been implemented.');
  }

  Future<int> getBatteryLevel() {
    throw UnimplementedError('getBatteryLevel() has not been implemented.');
  }
}

battery_plugin_method_channel.dart

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

import 'battery_plugin_platform_interface.dart';

/// An implementation of [BatteryPluginPlatform] that uses method channels.
class MethodChannelBatteryPlugin extends BatteryPluginPlatform {
  /// The method channel used to interact with the native platform.
  @visibleForTesting
  final methodChannel = const MethodChannel('battery_plugin_method_channel');

  @override
  Future<String?> getPlatformVersion() async {
    final version = await methodChannel.invokeMethod<String>('getPlatformVersion');
    return version;
  }

  @override
  Future<int> getBatteryLevel() async {
    return await methodChannel.invokeMethod("getBatteryLevel");
  }
}

BatteryPlugin의 MethodChannel을 담당하는 클래스입니다.

MethodChannel로 호출하는 함수들은 비동기로 호출되므로 getBatteryLevel()을 Future<Int> 타입을 return 하는 메소드를 작성해 주겠습니다.

 

battery_charging_state.dart

클래스에서 EventChannel을 만들어서 battery_charging_state_event_channel_name이라는 채널이름으로 받을 수 있게 해 줍니다.

그리고, enum클래스 BatteryState를 만들었습니다.

getChargingStateStream()은 String 데이터를 받으면 BatteryState로 파싱 하게 하여 Stream<BatteryState>를 return하게 해 줍니다. 

import 'package:flutter/services.dart';

enum BatteryState { full, charging, disCharging }

class BatteryChargingState {

  static final BatteryChargingState _instance = BatteryChargingState._internal();

  factory BatteryChargingState() {
    return _instance;
  }

  BatteryChargingState._internal();

  final EventChannel _eventChannel = EventChannel('battery_charging_state_event_channel_name');

  Stream<BatteryState> getChargingStateStream() {
    return _eventChannel
        .receiveBroadcastStream()
        .map((dynamic event) => _parseBatteryChargingState(event));
  }
}

BatteryState _parseBatteryChargingState(String state) {
  switch (state) {
    case 'full':
      return BatteryState.full;
    case 'charging':
      return BatteryState.charging;
    case 'disCharging':
      return BatteryState.disCharging;
    default:
      throw ArgumentError('$state is not a valid BatteryState.');
  }
}

battery_plugin.dart

위에서 작성한 기능들을 모두 호출할 수 있는 BatteryPlugin을 만들어주었습니다.

import 'package:battery_plugin/battery_charging_state.dart';
import 'package:battery_plugin/battery_level.dart';

import 'battery_plugin_platform_interface.dart';

class BatteryPlugin {

  Future<String?> getPlatformVersion() {
    return BatteryPluginPlatform.instance.getPlatformVersion();
  }

  Future<int> getBatteryLevel() async {
    return BatteryPluginPlatform.instance.getBatteryLevel();
  }

  Stream<BatteryState> getBatteryChargingState() {
    return BatteryChargingState().getChargingStateStream();
  }
}

BatteryChargeStatusReceiver

먼저, 충전 중인지 아닌지를 받아올 수 있는 BroadCastReceiver를 만들어줍니다. 

interface BatteryChargeStatusListener {
    fun onBatteryStatusChanged(status: String)
}

class BatteryChargeStatusReceiver : BroadcastReceiver() {

    private var callback: BatteryChargeStatusListener? = null

    fun setListener(callback: BatteryChargeStatusListener) {
        this.callback = callback;
    }

    fun unregisterListener() {
        this.callback = null
    }

    override fun onReceive(context: Context?, intent: Intent?) {
        val status = intent?.getIntExtra(BatteryManager.EXTRA_STATUS, -1)
        when (status) {
            BatteryManager.BATTERY_STATUS_FULL -> callback?.onBatteryStatusChanged("full")
            BatteryManager.BATTERY_STATUS_CHARGING -> callback?.onBatteryStatusChanged("charging")
            BatteryManager.BATTERY_STATUS_DISCHARGING -> callback?.onBatteryStatusChanged("disCharging")
            else -> callback?.onBatteryStatusChanged("error")
        }
    }
}

BatteryPlugin.kt

/** BatteryPlugin */
class BatteryPlugin : FlutterPlugin, MethodCallHandler {
    /// The MethodChannel that will the communication between Flutter and native Android
    ///
    /// This local reference serves to register the plugin with the Flutter Engine and unregister it
    /// when the Flutter Engine is detached from the Activity
    private lateinit var methodChannel: MethodChannel
    private lateinit var batteryChargingStateEventChannel: EventChannel

    private lateinit var context: Context
    private var batteryChargeStateReceiver: BatteryChargingStateReceiver? = null

    override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
        context = flutterPluginBinding.applicationContext

        methodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, METHOD_CHANNEL_NAME)
        methodChannel.setMethodCallHandler(this)

        batteryChargingStateEventChannel = EventChannel(flutterPluginBinding.binaryMessenger, BATTERY_CHARGING_STATE_EVENT_CHANNEL_NAME)
        batteryChargingStateEventChannel.setStreamHandler(object : EventChannel.StreamHandler {
            override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
                registerBatteryChargingStateReceiver(events)
            }

            override fun onCancel(arguments: Any?) {
                unRegisterBatteryChargingStateReceiver()
            }
        })
    }

    override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
        methodChannel.setMethodCallHandler(null)
        batteryChargingStateEventChannel.setStreamHandler(null)
    }

    override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
        when (call.method) {
            "getPlatformVersion" -> result.success("Android ${android.os.Build.VERSION.RELEASE}")
            "getBatteryLevel" -> getBatteryLevel(result)
            else -> result.notImplemented()
        }
    }

    private fun getBatteryLevel(result: MethodChannel.Result) {
        try {
            val batteryLevel = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                val batteryManager = context.getSystemService(Context.BATTERY_SERVICE) as BatteryManager
                batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
            } else {
                val intent = ContextWrapper(context).registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
                intent!!.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100 / intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
            }
            result.success(batteryLevel)
        } catch (e: Exception) {
            result.error(e.javaClass.simpleName, e.message, null)
        }
    }

    private fun registerBatteryChargingStateReceiver(events: EventChannel.EventSink?) {
        batteryChargeStateReceiver = BatteryChargingStateReceiver()

        batteryChargeStateReceiver?.setListener(object : BatteryChargeStatusListener {
            override fun onBatteryStatusChanged(status: String) {
                events?.success(status)
            }
        })
        context.registerReceiver(batteryChargeStateReceiver, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
    }

    private fun unRegisterBatteryChargingStateReceiver() {
        batteryChargeStateReceiver?.unregisterListener()
        context.unregisterReceiver(batteryChargeStateReceiver)
        batteryChargeStateReceiver = null
    }
    
    companion object {
        private val TAG: String = BatteryPlugin::class.java.simpleName

        private const val METHOD_CHANNEL_NAME = "battery_plugin_method_channel"
        private const val BATTERY_CHARGING_STATE_EVENT_CHANNEL_NAME = "battery_charging_state_event_channel_name"
    }
}

battery_plugin_screen.dart

이제 위에서 만들어준 플러그인을 통해서 배터리 퍼센트충전상태를 가져오겠습니다.

 

'getBatteryLevel' 텍스트를 누르면 BatteryPlugin()의 getBatteryLevel()메소드가 호출되어 누른 시점의 배터리 퍼센트를 가져올 수 있습니다.

battery status 아래에 있는 'subscribe' 텍스트를 누르면 BatteryPlugin()의 getBatteryChargingState()메소드를 통해 Stream을 구독하게 되어서 배터리의 충전상태가 바뀔 때마다 데이터를 받을 수 있습니다.

 

참고로 onCancel() 메소드는 rxdart에 있는 메소드이므로 라이브러리를 추가해야 합니다.

import 'dart:async';

import 'package:battery_plugin/battery_plugin.dart';
import 'package:flutter/material.dart';
import 'package:flutter_widgets/colors.dart';
import 'package:flutter_widgets/text_style.dart';
import 'package:rxdart/rxdart.dart';

class BatteryPluginScreen extends StatefulWidget {
  const BatteryPluginScreen({Key? key}) : super(key: key);

  static const String routeName = "/battery_plugin_screen";

  @override
  State<BatteryPluginScreen> createState() => _BatteryPluginScreenState();
}

class _BatteryPluginScreenState extends State<BatteryPluginScreen> {
  int _batteryPercent = -1;
  String _batteryStatus = 'none';
  String _batteryChargingStateSubscribeText = 'subscribe';

  StreamSubscription? _batteryStateSubscription;

  void _getBatteryLevel() async {
    _batteryPercent = await BatteryPlugin().getBatteryLevel();
    setState(() {});
  }

  void _subscribeBatteryChargingStatus() {
    if (_batteryStateSubscription != null) {
      _batteryStateSubscription?.cancel();
      setState(() {
        _batteryStatus = 'none';
        _batteryChargingStateSubscribeText = 'subscribe';
      });
      return;
    }

    _batteryStateSubscription = BatteryPlugin().getBatteryChargingState().doOnCancel(() {
      _batteryStateSubscription = null;
    }).listen((event) {
      setState(() {
        _batteryStatus = event.name;
        _batteryChargingStateSubscribeText = 'stop';
      });
    }, onError: (error) {
      setState(() {
        _batteryStatus = 'subscribe onError: $error';
        _batteryChargingStateSubscribeText = 'subscribe';
      });
    }, onDone: () {
      _batteryChargingStateSubscribeText = 'subscribe';
    }, cancelOnError: true);
  }

  @override
  void dispose() {
    _batteryStateSubscription?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      resizeToAvoidBottomInset: false,
      appBar: AppBar(
        leading: IconButton(
          onPressed: () {},
          icon: Icon(Icons.arrow_back),
        ),
        title: Text(
          'Battery Plugin Screen',
          style: kNotoSansMedium16.copyWith(color: Colors.white),
        ),
      ),
      body: SafeArea(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            SizedBox(height: 8.0),
            Text(
              'battery percent: $_batteryPercent',
              style: kNotoSansBold14.copyWith(color: AppColors.white),
            ),
            TextButton(
                onPressed: _getBatteryLevel,
                child: Text(
                  'getBatteryLevel',
                  style: kNotoSansBold14.copyWith(color: AppColors.white),
                )),
            Text(
              'battery status: $_batteryStatus',
              style: kNotoSansBold14.copyWith(color: AppColors.white),
            ),
            TextButton(
                onPressed: _subscribeBatteryChargingStatus,
                child: Text(
                  _batteryChargingStateSubscribeText,
                  style: kNotoSansBold14.copyWith(color: AppColors.white),
                )),
          ],
        ),
      ),
    );
  }
}

실행결과

 

플러그인에서 RxJava 사용하기

위에서 버튼을 클릭하면 배터리 퍼센트를 받아올 수 있는 기능을 추가하였습니다.

이번에는 일회성으로 받아오지 않고 RxJava를 이용하여 구독을 시작하면 실시간으로 배터리를 가져오는 기능으로 변형해 보겠습니다.

플러그인에 RxJava의 Battery Level 퍼센트 데이터를 방출하는 Observable을 추가하겠습니다.

그래서 Stream을 구독하면 플러그인의 Observable을 구독하게 구성하여 구독하는 동안 Observable의 데이터를 받을 수 있도록 합니다.

 

battery_level.dart

EventChannel의 이름을 'battery_level_event_channel_name'으로 지정해 주고 receive()해주도록 합니다.

import 'package:flutter/services.dart';

class BatteryLevel {

  static final BatteryLevel _instance = BatteryLevel._internal();

  factory BatteryLevel() {
    return _instance;
  }

  final EventChannel _eventChannel = EventChannel('battery_level_event_channel_name');

  BatteryLevel._internal();

  Stream<int> getBatteryLevelStream() {
    return _eventChannel
        .receiveBroadcastStream()
        .map((dynamic event) => event as int);
  }
}

 

battery_plugin.dart

import 'package:battery_plugin/battery_level.dart';

class BatteryPlugin {

  // ...

  Stream<int> getBatteryLevelStream() {
    return BatteryLevel().getBatteryLevelStream();
  }
}

BatteryPlugin.kt

Observable.interval을 이용하여 1초마다 Stream에 데이터를 넣어주게 해 줍니다.

또한, 스레드 관리는 채널 메소드는 메인스레드에서 실행하게 해주어야 하므로 메인스레드에서 실행하게 해 주었습니다.

disposable 관리는 onListen(), onCancel()시점에 메모리 할당, 해제해주도록 해주었습니다.

/** BatteryPlugin */
class BatteryPlugin : FlutterPlugin, MethodCallHandler {

    private lateinit var batteryLevelEventChannel: EventChannel
    private var batteryLevelDisposable: Disposable? = null

    override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {

        batteryLevelEventChannel = EventChannel(flutterPluginBinding.binaryMessenger, BATTERY_LEVEL_EVENT_CHANNEL_NAME)
        batteryLevelEventChannel.setStreamHandler(object : EventChannel.StreamHandler {
            override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
                observeBatteryLevel(events)
            }

            override fun onCancel(arguments: Any?) {
                batteryLevelDisposable?.dispose()
            }
        })
    }

    override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
        batteryLevelEventChannel.setStreamHandler(null)
        batteryLevelDisposable?.dispose()
    }

    private fun getBatteryLevel(): Int {
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            val batteryManager = context.getSystemService(Context.BATTERY_SERVICE) as BatteryManager
            batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
        } else {
            val intent = ContextWrapper(context).registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
            intent!!.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100 / intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
        }
    }

    private fun observeBatteryLevel(events: EventChannel.EventSink?) {
        if (batteryLevelDisposable == null) {
            batteryLevelDisposable = Observable
                .interval(0L, 1000L, TimeUnit.MILLISECONDS)
                .flatMapSingle {
                    Single.create<Int> {
                        try {
                            val batteryLevel = getBatteryLevel()
                            it.onSuccess(batteryLevel)
                        } catch (e: Exception) {
                            it.onError(e)
                        }
                    }
                }
                .doFinally {
                    batteryLevelDisposable = null
                }
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe({
                    events?.success(it)
                }, {
                    events?.error(it.javaClass.simpleName, it.message, null)
                })
        }
    }

    companion object {
        private val TAG: String = BatteryPlugin::class.java.simpleName

        private const val BATTERY_LEVEL_EVENT_CHANNEL_NAME = "battery_level_event_channel_name"
    }
}

battery_plugin_screen.dart

import 'dart:async';

import 'package:battery_plugin/battery_plugin.dart';
import 'package:flutter/material.dart';
import 'package:flutter_widgets/colors.dart';
import 'package:flutter_widgets/text_style.dart';
import 'package:rxdart/rxdart.dart';

class BatteryPluginScreen extends StatefulWidget {
  const BatteryPluginScreen({Key? key}) : super(key: key);

  static const String routeName = "/battery_plugin_screen";

  @override
  State<BatteryPluginScreen> createState() => _BatteryPluginScreenState();
}

class _BatteryPluginScreenState extends State<BatteryPluginScreen> {

  // ...
  
  int _batteryLevelStreamPercent = -1;
  String _batteryLevelStreamSubscribeText = 'subscribe';

  StreamSubscription? _batteryLevelSubscription;

  // ...
  
  void _subscribeBatteryLevel() {
    if (_batteryLevelSubscription != null) {
      _batteryLevelSubscription?.cancel();
      setState(() {
        _batteryLevelStreamPercent = -1;
        _batteryLevelStreamSubscribeText = 'subscribe';
      });
      return;
    }

    _batteryLevelSubscription = BatteryPlugin().getBatteryLevelStream().doOnCancel(() {
      _batteryLevelSubscription = null;
    }).listen((event) {
      setState(() {
        _batteryLevelStreamPercent = event;
        _batteryLevelStreamSubscribeText = 'stop';
      });
    }, onError: (error) {
      setState(() {
        _batteryLevelStreamPercent = -1;
        _batteryLevelStreamSubscribeText = 'subscribe';
      });
    }, onDone: () {
      _batteryLevelStreamSubscribeText = 'subscribe';
    }, cancelOnError: true);
  }

  @override
  void dispose() {
  	// ...
    _batteryLevelSubscription?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      resizeToAvoidBottomInset: false,
      appBar: AppBar(
        leading: IconButton(
          onPressed: () {},
          icon: Icon(Icons.arrow_back),
        ),
        title: Text(
          'Battery Plugin Screen',
          style: kNotoSansMedium16.copyWith(color: Colors.white),
        ),
      ),
      body: SafeArea(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
			// ...
            Text(
              'battery stream percent: $_batteryLevelStreamPercent',
              style: kNotoSansBold14.copyWith(color: AppColors.black),
            ),
            TextButton(
                onPressed: _subscribeBatteryLevel,
                child: Text(
                  _batteryLevelStreamSubscribeText,
                  style: kNotoSansBold14.copyWith(color: AppColors.black),
                )),
          ],
        ),
      ),
    );
  }
}

실행결과

맨 아래에 있는 subscribe 버튼을 누르면 batteryLevelStream을 구독하게 되고

배터리 퍼센트가 84 -> 85로 증가하는 것을 실시간으로 받을 수 있습니다.

 

↑ 동영상

 

감사합니다!

 

References

https://github.com/flutter/flutter/wiki/Experimental:-Create-Flutter-Plugin

 

GitHub - flutter/flutter: Flutter makes it easy and fast to build beautiful apps for mobile and beyond

Flutter makes it easy and fast to build beautiful apps for mobile and beyond - GitHub - flutter/flutter: Flutter makes it easy and fast to build beautiful apps for mobile and beyond

github.com

https://docs.flutter.dev/development/packages-and-plugins/developing-packages

 

Developing packages & plugins

How to write packages and plugins for Flutter.

docs.flutter.dev

반응형