안녕하세요. 이번에는 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
https://docs.flutter.dev/development/packages-and-plugins/developing-packages