Slide 1

Slide 1 text

進化したWidget naoki hidaka

Slide 2

Slide 2 text

自己紹介 ● Naoki Hidaka(@dosukoi_android) ● KyashでAndroidアプリ開発 ● Flutterもやってます

Slide 3

Slide 3 text

Widget作ってますか 最近KyashはWidgetをリリースしました

Slide 4

Slide 4 text

Widget作るのは結構大変 https://www.youtube.com/watch?v=15Q7xqxBGG0

Slide 5

Slide 5 text

Glanceの登場 ● Android Dev Summit 2021で発表 ● ComposeでWidgetを作れるライブラリ ● 今はalpha版だけど今後に期待 ● そんなに便利になるの?

Slide 6

Slide 6 text

宣伝 KyashがWidgetを作って辛かった話はこちらに書いてあります https://speakerdeck.com/maiyama18/creating-kyash-widget

Slide 7

Slide 7 text

比較してみよう

Slide 8

Slide 8 text

Viewの扱い

Slide 9

Slide 9 text

これまでのWidget override fun onUpdate( context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray ) { val views = RemoteViews( context.packageName, R.layout.app_widget ) views.setTextViewText(R.id.text, "text") views.setImageViewResource(R.id.image, R.drawable.icon) views.setInt( R.id.background, "setBackgroundColor", context.getColor(R.color.white) ) }

Slide 10

Slide 10 text

これまでのWidget override fun onUpdate( context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray ) { val views = RemoteViews( context.packageName, R.layout.app_widget ) views.setTextViewText(R.id.text, "text") views.setImageViewResource(R.id.image, R.drawable.icon) views.setInt( R.id.background, "setBackgroundColor", context.getColor(R.color.white) ) } RemoteView…?

Slide 11

Slide 11 text

これまでのWidget override fun onUpdate( context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray ) { val views = RemoteViews( context.packageName, R.layout.app_widget ) views.setTextViewText(R.id.text, "text") views.setImageViewResource(R.id.image, R.drawable.icon) views.setInt( R.id.background, "setBackgroundColor", context.getColor(R.color.white) ) } RemoteView…? Stringで指定するの...?

Slide 12

Slide 12 text

これまでのWidget override fun onUpdate( context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray ) { val views = RemoteViews( context.packageName, R.layout.app_widget ) views.setTextViewText(R.id.text, "text") views.setImageViewResource(R.id.image, R.drawable.icon) views.setInt( R.id.background, "setBackgroundColor", context.getColor(R.color.white) ) } RemoteView…? Stringで指定するの...? 初見殺しすぎる!

Slide 13

Slide 13 text

これからのWidget @Composable private fun MainContent( @ColorRes backgroundColorResId: Int, @DrawableRes iconResId: Int, text: String, modifier: GlanceModifier = GlanceModifier ) { Row( modifier = GlanceModifier .background(backgroundColorResId) .then(modifier) ) { Image( provider = AndroidResourceImageProvider(resId = iconResId), contentDescription = null ) Spacer(modifier = GlanceModifier.width(8.dp)) Text(text = text) } }

Slide 14

Slide 14 text

これからのWidget @Composable private fun MainContent( @ColorRes backgroundColorResId: Int, @DrawableRes iconResId: Int, text: String, modifier: GlanceModifier = GlanceModifier ) { Row( modifier = GlanceModifier .background(backgroundColorResId) .then(modifier) ) { Image( provider = AndroidResourceImageProvider(resId = iconResId), contentDescription = null ) Spacer(modifier = GlanceModifier.width(8.dp)) Text(text = text) } } 普段Composeで開発してる人なら 慣れ親しんだ記法!

Slide 15

Slide 15 text

リスト実装

Slide 16

Slide 16 text

これまでのWidget ● RecyclerViewは使えない ● ListViewを使う必要があるが、ListAdapterが使えるわけではない ● RemoteViewsServiceとRemoteViewsService.RemoteViewsFactoryのそれぞれを継承した クラスを作る必要がある ● RemoteViewsService.RemoteViewsFactoryを継承したクラスは9つのメソッドを継承する必 要がある

Slide 17

Slide 17 text

これまでのWidget // AppWidgetProvider override fun onUpdate(…) { val adapterIntent = Intent(context, ListViewService::class.java) views.setRemoteAdapter(R.id.list, adapterIntent) } class ListViewService : RemoteViewsService() { override fun onGetViewFactory(intent: Intent?): RemoteViewsFactory { return ListViewFactory() } } class ListViewFactory : RemoteViewsService.RemoteViewsFactory { }

Slide 18

Slide 18 text

これまでのWidget // AppWidgetProvider override fun onUpdate(…) { val adapterIntent = Intent(context, ListViewService::class.java) views.setRemoteAdapter(R.id.list, adapterIntent) } class ListViewService : RemoteViewsService() { override fun onGetViewFactory(intent: Intent?): RemoteViewsFactory { return ListViewFactory() } } class ListViewFactory : RemoteViewsService.RemoteViewsFactory { } 9つのメソッドをoverrideする必要がある

Slide 19

Slide 19 text

これまでのWidget // AppWidgetProvider override fun onUpdate(…) { val adapterIntent = Intent(context, ListViewService::class.java) views.setRemoteAdapter(R.id.list, adapterIntent) } class ListViewService : RemoteViewsService() { override fun onGetViewFactory(intent: Intent?): RemoteViewsFactory { return ListViewFactory() } } class ListViewFactory : RemoteViewsService.RemoteViewsFactory { } 9つのメソッドをoverrideする必要がある ListViewに対してAdapterをセット

Slide 20

Slide 20 text

これからのWidget @Composable private fun MainContent(items: List) { LazyColumn { items(items = items) { item -> Text(text = item) } } }

Slide 21

Slide 21 text

これからのWidget @Composable private fun MainContent(items: List) { LazyColumn { items(items = items) { item -> Text(text = item) } } } LazyColumnでリスト実装

Slide 22

Slide 22 text

これからのWidget @Composable private fun MainContent(items: List) { LazyColumn { items(items = items) { item -> Text(text = item) } } } LazyColumnでリスト実装 クリックイベントもitemに設定してあげれば良い

Slide 23

Slide 23 text

これからのWidget @Composable private fun MainContent(items: List) { LazyColumn { items(items = items) { item -> Text(text = item) } } } LazyColumnでリスト実装 クリックイベントもitemに設定してあげれば良い ※LazyRowはまだないので注意

Slide 24

Slide 24 text

イベントハンドリング

Slide 25

Slide 25 text

これまでのイベントハンドリング companion object { private const val CLICK_INCREMENT_ACTION = "com.example.glance_project.widget.HogeWidgetProvider.CLICK_INCREMENT_ACTION” } override fun onUpdate(...) { val intent = Intent(context, AppWidgetProviderSample::class.java).apply { action = CLICK_INCREMENT_ACTION putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds) } val pendingIntent = PendingIntent.getBroadcast( context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) views.setOnClickPendingIntent(R.id.increment_button, pendingIntent) }

Slide 26

Slide 26 text

これまでのイベントハンドリング companion object { private const val CLICK_INCREMENT_ACTION = "com.example.glance_project.widget.HogeWidgetProvider.CLICK_INCREMENT_ACTION” } override fun onUpdate(...) { val intent = Intent(context, AppWidgetProviderSample::class.java).apply { action = CLICK_INCREMENT_ACTION putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds) } val pendingIntent = PendingIntent.getBroadcast( context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) views.setOnClickPendingIntent(R.id.increment_button, pendingIntent) } StringでActionを定義する必要がある

Slide 27

Slide 27 text

これまでのイベントハンドリング companion object { private const val CLICK_INCREMENT_ACTION = "com.example.glance_project.widget.HogeWidgetProvider.CLICK_INCREMENT_ACTION” } override fun onUpdate(...) { val intent = Intent(context, AppWidgetProviderSample::class.java).apply { action = CLICK_INCREMENT_ACTION putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds) } val pendingIntent = PendingIntent.getBroadcast( context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) views.setOnClickPendingIntent(R.id.increment_button, pendingIntent) } StringでActionを定義する必要がある IntentとActionを紐づけてあげる

Slide 28

Slide 28 text

これまでのイベントハンドリング companion object { private const val CLICK_INCREMENT_ACTION = "com.example.glance_project.widget.HogeWidgetProvider.CLICK_INCREMENT_ACTION” } override fun onUpdate(...) { val intent = Intent(context, AppWidgetProviderSample::class.java).apply { action = CLICK_INCREMENT_ACTION putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds) } val pendingIntent = PendingIntent.getBroadcast( context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) views.setOnClickPendingIntent(R.id.increment_button, pendingIntent) } StringでActionを定義する必要がある PendingIntentを発行 IntentとActionを紐づけてあげる

Slide 29

Slide 29 text

これまでのイベントハンドリング companion object { private const val CLICK_INCREMENT_ACTION = "com.example.glance_project.widget.HogeWidgetProvider.CLICK_INCREMENT_ACTION” } override fun onUpdate(...) { val intent = Intent(context, AppWidgetProviderSample::class.java).apply { action = CLICK_INCREMENT_ACTION putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds) } val pendingIntent = PendingIntent.getBroadcast( context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) views.setOnClickPendingIntent(R.id.increment_button, pendingIntent) } StringでActionを定義する必要がある PendingIntentを発行 Viewに対してPendingIntentを登録

Slide 30

Slide 30 text

これまでのWidget override fun onReceive(context: Context, intent: Intent) { super.onReceive(context, intent) when (intent.action) { CLICK_INCREMENT_ACTION -> { // ここにクリック時の処理を書く // Activityを開いたり、Serviceを起動したり、Broadcastを送ったり、Toastを表示したり、なんでもござれ } }

Slide 31

Slide 31 text

これまでのWidget override fun onReceive(context: Context, intent: Intent) { super.onReceive(context, intent) when (intent.action) { CLICK_INCREMENT_ACTION -> { // ここにクリック時の処理を書く // Activityを開いたり、Serviceを起動したり、Broadcastを送ったり、Toastを表示したり、なんでもござれ } } イベントは全てonReceiveに飛んでくる

Slide 32

Slide 32 text

これまでのWidget override fun onReceive(context: Context, intent: Intent) { super.onReceive(context, intent) when (intent.action) { CLICK_INCREMENT_ACTION -> { // ここにクリック時の処理を書く // Activityを開いたり、Serviceを起動したり、Broadcastを送ったり、Toastを表示したり、なんでもござれ } } イベントは全てonReceiveに飛んでくる 定義したActionごとにハンドリング

Slide 33

Slide 33 text

これからのWidget ● Activityを開きたい時 Box(modifier = GlanceModifier.clickable(onClick = actionStartActivity())) ● Broadcastを送りたい時 Box(modifier = GlanceModifier.clickable(onClick = actionSendBroadcast())) ● Serviceを開始したい時 Box(modifier = GlanceModifier.clickable(onClick = actionStartService()))

Slide 34

Slide 34 text

これからのWidget ● Activityを開きたい時 Box(modifier = GlanceModifier.clickable(onClick = actionStartActivity())) ● Broadcastを送りたい時 Box(modifier = GlanceModifier.clickable(onClick = actionSendBroadcast())) ● Serviceを開始したい時 Box(modifier = GlanceModifier.clickable(onClick = actionStartService())) 初めから用意されている関数を使えば良い

Slide 35

Slide 35 text

これからのWidget ● 独自Actionの定義 Box(modifier = GlanceModifier.clickable(onClick = actionRunCallBack())) class ClickAction : ActionCallback { override suspend fun onRun(context: Context, glanceId: GlanceId, parameters: ActionParameters) { // DataStoreの更新やWorkManagerを起動してAPIを叩いたり、なんでもござれ } }

Slide 36

Slide 36 text

これからのWidget ● 独自Actionの定義 Box(modifier = GlanceModifier.clickable(onClick = actionRunCallBack())) class ClickAction : ActionCallback { override suspend fun onRun(context: Context, glanceId: GlanceId, parameters: ActionParameters) { // DataStoreの更新やWorkManagerを起動してAPIを叩いたり、なんでもござれ } } 独自Actionを型として定義

Slide 37

Slide 37 text

これからのWidget ● 独自Actionの定義 Box(modifier = GlanceModifier.clickable(onClick = actionRunCallBack())) class ClickAction : ActionCallback { override suspend fun onRun(context: Context, glanceId: GlanceId, parameters: ActionParameters) { // DataStoreの更新やWorkManagerを起動してAPIを叩いたり、なんでもござれ } } 独自Actionを型として定義 Modifierで独自Actionを登録してあげれば良い

Slide 38

Slide 38 text

Glanceでの状態の扱い

Slide 39

Slide 39 text

DataStoreを使う ● DataStoreで状態を管理する ● Primitive型の扱いはとても簡単 ● 独自のデータ型もprotocol bufferで定義できる

Slide 40

Slide 40 text

Primitive型を扱う private val countKey = intPreferencesKey("count") class PrimitiveWidget : GlanceAppWidget() { @Composable override fun Content() { val count = currentState(countKey) ?: 0 Column { Text(text = count.toString()) Button( text = "increment", onClick = actionRunCallback() ) } } }

Slide 41

Slide 41 text

Primitive型を扱う private val countKey = intPreferencesKey("count") class PrimitiveWidget : GlanceAppWidget() { @Composable override fun Content() { val count = currentState(countKey) ?: 0 Column { Text(text = count.toString()) Button( text = "increment", onClick = actionRunCallback() ) } } } DataStoreのKeyを定義

Slide 42

Slide 42 text

Primitive型を扱う private val countKey = intPreferencesKey("count") class PrimitiveWidget : GlanceAppWidget() { @Composable override fun Content() { val count = currentState(countKey) ?: 0 Column { Text(text = count.toString()) Button( text = "increment", onClick = actionRunCallback() ) } } } DataStoreのKeyを定義 値を取得

Slide 43

Slide 43 text

Primitive型を扱う private class IncrementAction : ActionCallback { override suspend fun onRun( context: Context, glanceId: GlanceId, parameters: ActionParameters ) { updateAppWidgetState(context, glanceId) { state -> state[countKey] = (state[countKey] ?: 0) + 1 } PrimitiveWidget().update(context, glanceId) } }

Slide 44

Slide 44 text

Primitive型を扱う private class IncrementAction : ActionCallback { override suspend fun onRun( context: Context, glanceId: GlanceId, parameters: ActionParameters ) { updateAppWidgetState(context, glanceId) { state -> state[countKey] = (state[countKey] ?: 0) + 1 } PrimitiveWidget().update(context, glanceId) } } 独自Actionを定義

Slide 45

Slide 45 text

Primitive型を扱う private class IncrementAction : ActionCallback { override suspend fun onRun( context: Context, glanceId: GlanceId, parameters: ActionParameters ) { updateAppWidgetState(context, glanceId) { state -> state[countKey] = (state[countKey] ?: 0) + 1 } PrimitiveWidget().update(context, glanceId) } } 独自Actionを定義 updateAppWidgetStateの中で値を更新

Slide 46

Slide 46 text

Primitive型を扱う private class IncrementAction : ActionCallback { override suspend fun onRun( context: Context, glanceId: GlanceId, parameters: ActionParameters ) { updateAppWidgetState(context, glanceId) { state -> state[countKey] = (state[countKey] ?: 0) + 1 } PrimitiveWidget().update(context, glanceId) } } 独自Actionを定義 updateAppWidgetStateの中で値を更新 Widgetを更新

Slide 47

Slide 47 text

独自のデータ型を扱う custom_data.proto syntax = "proto3"; option java_package = "com.example.glance_project"; option java_multiple_files = true; message CustomData { int64 id = 1; string title = 2; string description = 3; }

Slide 48

Slide 48 text

独自のデータ型を扱う custom_data.proto syntax = "proto3"; option java_package = "com.example.glance_project"; option java_multiple_files = true; message CustomData { int64 id = 1; string title = 2; string description = 3; } 独自のデータ型を定義

Slide 49

Slide 49 text

独自のデータ型を扱う class CustomDataWidget @Inject constructor() : GlanceAppWidget() { override val stateDefinition: GlanceStateDefinition = customDataStateDefinition @Composable override fun Content() { val state = currentState() } } val customDataStateDefinition = object : GlanceStateDefinition { override suspend fun getDataStore( context: Context, fileKey: String ): DataStore = context.customDataStore override fun getLocation(context: Context, fileKey: String): File = context.preferencesDataStoreFile(fileKey) }

Slide 50

Slide 50 text

独自のデータ型を扱う class CustomDataWidget @Inject constructor() : GlanceAppWidget() { override val stateDefinition: GlanceStateDefinition = customDataStateDefinition @Composable override fun Content() { val state = currentState() } } val customDataStateDefinition = object : GlanceStateDefinition { override suspend fun getDataStore( context: Context, fileKey: String ): DataStore = context.customDataStore override fun getLocation(context: Context, fileKey: String): File = context.preferencesDataStoreFile(fileKey) } DataStoreから取り出す方法を定義

Slide 51

Slide 51 text

独自のデータ型を扱う class CustomDataWidget @Inject constructor() : GlanceAppWidget() { override val stateDefinition: GlanceStateDefinition = customDataStateDefinition @Composable override fun Content() { val state = currentState() } } val customDataStateDefinition = object : GlanceStateDefinition { override suspend fun getDataStore( context: Context, fileKey: String ): DataStore = context.customDataStore override fun getLocation(context: Context, fileKey: String): File = context.preferencesDataStoreFile(fileKey) } DataStoreから取り出す方法を定義 stateDefinitionをoverrideする

Slide 52

Slide 52 text

独自のデータ型を扱う object CustomDataSerializer : Serializer { override val defaultValue: CustomData = CustomData.getDefaultInstance() override suspend fun readFrom(input: InputStream): CustomData { try { return CustomData.parseFrom(input) } catch (throwable: Throwable) { throw CorruptionException("Cannot read proto", throwable) } } override suspend fun writeTo(t: CustomData, output: OutputStream) { t.writeTo(output) } } val Context.customDataStore: DataStore by dataStore( fileName = "custom_data.proto", serializer = CustomDataSerializer )

Slide 53

Slide 53 text

独自のデータ型を扱う object CustomDataSerializer : Serializer { override val defaultValue: CustomData = CustomData.getDefaultInstance() override suspend fun readFrom(input: InputStream): CustomData { try { return CustomData.parseFrom(input) } catch (throwable: Throwable) { throw CorruptionException("Cannot read proto", throwable) } } override suspend fun writeTo(t: CustomData, output: OutputStream) { t.writeTo(output) } } val Context.customDataStore: DataStore by dataStore( fileName = "custom_data.proto", serializer = CustomDataSerializer ) どうやってパースするか定義

Slide 54

Slide 54 text

独自のデータ型を扱う object CustomDataSerializer : Serializer { override val defaultValue: CustomData = CustomData.getDefaultInstance() override suspend fun readFrom(input: InputStream): CustomData { try { return CustomData.parseFrom(input) } catch (throwable: Throwable) { throw CorruptionException("Cannot read proto", throwable) } } override suspend fun writeTo(t: CustomData, output: OutputStream) { t.writeTo(output) } } val Context.customDataStore: DataStore by dataStore( fileName = "custom_data.proto", serializer = CustomDataSerializer ) どうやってパースするか定義 protoファイルとSerializerの紐付け

Slide 55

Slide 55 text

まとめ ● 普段Composeで開発してる人にとってはWidgetは開発しやすいものになりそう ● 自分はイベントハンドリングで結構困ってたので、イベントハンドリングが楽になるのもとても助 かる ● これからWidgetを開発するのであれば、Glance一択になりそう

Slide 56

Slide 56 text

注意点 ● Glanceはまだalpha版 ○ 破壊的変更の可能性は十分ある ● Composeで書けるけど、差分更新ができるわけではない ○ 内部は今まで通りRemoteView ○ 更新の仕組みが変わったわけではない

Slide 57

Slide 57 text

Glanceで快適なWidgetライフを! ご清聴ありがとうございました!