Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
進化したWidget
Search
Dosukoi
July 20, 2022
Programming
340
1
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
進化したWidget
Dosukoi
July 20, 2022
More Decks by Dosukoi
See All by Dosukoi
FlutterとMaterial3
dosukoi_android
0
860
Other Decks in Programming
See All in Programming
Laravelで学ぶ Webアプリケーションチューニング入門/web_application_tuning_101
hanhan1978
4
1.8k
Japan Community Day at Kubecon + CloudNativeCon Japan 2026: Learning Container Privilege Control by Building My Own Low-Level Container Runtime
ternbusty
1
150
仕様駆動開発へのトライを機に チームに適合する手法を模索し続けている話
freee
PRO
0
500
2年かけて Deno に DOMMatrix を実装した話 / How I implemented DOMMatrix in Deno over two years
petamoriken
0
210
今さら聞けない .NET CLI
htkym
0
190
S3 を使うアプリケーションをローカル完結で動かすことに全力を注いでみた / Running S3 Apps Offline
contour_gara
0
470
php-fpmのプロセスが枯渇した日-調査・対処・そして本当にやるべきだったこと-
shibuchaaaan
0
290
Apache Hive: Toward a Cloud Native Lakehouse
okumin
0
190
複数の Claude Code が"放置"されてしまう問題をCLI ダッシュボードを自作して解決した話
sumihiro3
1
680
ドリフトを絶対に許さない(?)CDK運用 / CDK Ops with Zero Tolerance for Drifts (?)
akihisaikeda
1
180
Laravel Boostに学ぶ、AIにPHPを書かせる技術 〜OSSの実装から蒸留するエージェント制御の王道〜
kentaroutakeda
3
690
Foundation Models frameworkで画像分析
ryodeveloper
1
610
Featured
See All Featured
Stop Working from a Prison Cell
hatefulcrawdad
274
21k
Principles of Awesome APIs and How to Build Them.
keavy
128
18k
RailsConf 2023
tenderlove
30
1.5k
Connecting the Dots Between Site Speed, User Experience & Your Business [WebExpo 2025]
tammyeverts
11
990
Odyssey Design
rkendrick25
PRO
2
750
Put a Button on it: Removing Barriers to Going Fast.
kastner
60
4.5k
Designing Dashboards & Data Visualisations in Web Apps
destraynor
232
55k
A Modern Web Designer's Workflow
chriscoyier
698
190k
Paper Plane
katiecoart
PRO
2
53k
Rebuilding a faster, lazier Slack
samanthasiow
85
9.6k
Max Prin - Stacking Signals: How International SEO Comes Together (And Falls Apart)
techseoconnect
PRO
0
410
Designing for Performance
lara
611
70k
Transcript
進化したWidget naoki hidaka
自己紹介 • Naoki Hidaka(@dosukoi_android) • KyashでAndroidアプリ開発 • Flutterもやってます
Widget作ってますか 最近KyashはWidgetをリリースしました
Widget作るのは結構大変 https://www.youtube.com/watch?v=15Q7xqxBGG0
Glanceの登場 • Android Dev Summit 2021で発表 • ComposeでWidgetを作れるライブラリ • 今はalpha版だけど今後に期待
• そんなに便利になるの?
宣伝 KyashがWidgetを作って辛かった話はこちらに書いてあります https://speakerdeck.com/maiyama18/creating-kyash-widget
比較してみよう
Viewの扱い
これまでの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) ) }
これまでの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…?
これまでの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で指定するの...?
これまでの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で指定するの...? 初見殺しすぎる!
これからの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) } }
これからの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で開発してる人なら 慣れ親しんだ記法!
リスト実装
これまでのWidget • RecyclerViewは使えない • ListViewを使う必要があるが、ListAdapterが使えるわけではない • RemoteViewsServiceとRemoteViewsService.RemoteViewsFactoryのそれぞれを継承した クラスを作る必要がある • RemoteViewsService.RemoteViewsFactoryを継承したクラスは9つのメソッドを継承する必
要がある
これまでの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 { }
これまでの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する必要がある
これまでの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をセット
これからのWidget @Composable private fun MainContent(items: List<String>) { LazyColumn { items(items
= items) { item -> Text(text = item) } } }
これからのWidget @Composable private fun MainContent(items: List<String>) { LazyColumn { items(items
= items) { item -> Text(text = item) } } } LazyColumnでリスト実装
これからのWidget @Composable private fun MainContent(items: List<String>) { LazyColumn { items(items
= items) { item -> Text(text = item) } } } LazyColumnでリスト実装 クリックイベントもitemに設定してあげれば良い
これからのWidget @Composable private fun MainContent(items: List<String>) { LazyColumn { items(items
= items) { item -> Text(text = item) } } } LazyColumnでリスト実装 クリックイベントもitemに設定してあげれば良い ※LazyRowはまだないので注意
イベントハンドリング
これまでのイベントハンドリング 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) }
これまでのイベントハンドリング 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を定義する必要がある
これまでのイベントハンドリング 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を紐づけてあげる
これまでのイベントハンドリング 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を紐づけてあげる
これまでのイベントハンドリング 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を登録
これまでのWidget override fun onReceive(context: Context, intent: Intent) { super.onReceive(context, intent)
when (intent.action) { CLICK_INCREMENT_ACTION -> { // ここにクリック時の処理を書く // Activityを開いたり、Serviceを起動したり、Broadcastを送ったり、Toastを表示したり、なんでもござれ } }
これまでのWidget override fun onReceive(context: Context, intent: Intent) { super.onReceive(context, intent)
when (intent.action) { CLICK_INCREMENT_ACTION -> { // ここにクリック時の処理を書く // Activityを開いたり、Serviceを起動したり、Broadcastを送ったり、Toastを表示したり、なんでもござれ } } イベントは全てonReceiveに飛んでくる
これまでのWidget override fun onReceive(context: Context, intent: Intent) { super.onReceive(context, intent)
when (intent.action) { CLICK_INCREMENT_ACTION -> { // ここにクリック時の処理を書く // Activityを開いたり、Serviceを起動したり、Broadcastを送ったり、Toastを表示したり、なんでもござれ } } イベントは全てonReceiveに飛んでくる 定義したActionごとにハンドリング
これからのWidget • Activityを開きたい時 Box(modifier = GlanceModifier.clickable(onClick = actionStartActivity<MainActivity>())) • Broadcastを送りたい時
Box(modifier = GlanceModifier.clickable(onClick = actionSendBroadcast<HogeBroadcastReceiver>())) • Serviceを開始したい時 Box(modifier = GlanceModifier.clickable(onClick = actionStartService<HogeService>()))
これからのWidget • Activityを開きたい時 Box(modifier = GlanceModifier.clickable(onClick = actionStartActivity<MainActivity>())) • Broadcastを送りたい時
Box(modifier = GlanceModifier.clickable(onClick = actionSendBroadcast<HogeBroadcastReceiver>())) • Serviceを開始したい時 Box(modifier = GlanceModifier.clickable(onClick = actionStartService<HogeService>())) 初めから用意されている関数を使えば良い
これからのWidget • 独自Actionの定義 Box(modifier = GlanceModifier.clickable(onClick = actionRunCallBack<ClickAction>())) class ClickAction
: ActionCallback { override suspend fun onRun(context: Context, glanceId: GlanceId, parameters: ActionParameters) { // DataStoreの更新やWorkManagerを起動してAPIを叩いたり、なんでもござれ } }
これからのWidget • 独自Actionの定義 Box(modifier = GlanceModifier.clickable(onClick = actionRunCallBack<ClickAction>())) class ClickAction
: ActionCallback { override suspend fun onRun(context: Context, glanceId: GlanceId, parameters: ActionParameters) { // DataStoreの更新やWorkManagerを起動してAPIを叩いたり、なんでもござれ } } 独自Actionを型として定義
これからのWidget • 独自Actionの定義 Box(modifier = GlanceModifier.clickable(onClick = actionRunCallBack<ClickAction>())) class ClickAction
: ActionCallback { override suspend fun onRun(context: Context, glanceId: GlanceId, parameters: ActionParameters) { // DataStoreの更新やWorkManagerを起動してAPIを叩いたり、なんでもござれ } } 独自Actionを型として定義 Modifierで独自Actionを登録してあげれば良い
Glanceでの状態の扱い
DataStoreを使う • DataStoreで状態を管理する • Primitive型の扱いはとても簡単 • 独自のデータ型もprotocol bufferで定義できる
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<IncrementAction>() ) } } }
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<IncrementAction>() ) } } } DataStoreのKeyを定義
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<IncrementAction>() ) } } } DataStoreのKeyを定義 値を取得
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) } }
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を定義
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の中で値を更新
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を更新
独自のデータ型を扱う 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; }
独自のデータ型を扱う 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; } 独自のデータ型を定義
独自のデータ型を扱う class CustomDataWidget @Inject constructor() : GlanceAppWidget() { override val
stateDefinition: GlanceStateDefinition<CustomData> = customDataStateDefinition @Composable override fun Content() { val state = currentState<CustomData>() } } val customDataStateDefinition = object : GlanceStateDefinition<CustomData> { override suspend fun getDataStore( context: Context, fileKey: String ): DataStore<CustomData> = context.customDataStore override fun getLocation(context: Context, fileKey: String): File = context.preferencesDataStoreFile(fileKey) }
独自のデータ型を扱う class CustomDataWidget @Inject constructor() : GlanceAppWidget() { override val
stateDefinition: GlanceStateDefinition<CustomData> = customDataStateDefinition @Composable override fun Content() { val state = currentState<CustomData>() } } val customDataStateDefinition = object : GlanceStateDefinition<CustomData> { override suspend fun getDataStore( context: Context, fileKey: String ): DataStore<CustomData> = context.customDataStore override fun getLocation(context: Context, fileKey: String): File = context.preferencesDataStoreFile(fileKey) } DataStoreから取り出す方法を定義
独自のデータ型を扱う class CustomDataWidget @Inject constructor() : GlanceAppWidget() { override val
stateDefinition: GlanceStateDefinition<CustomData> = customDataStateDefinition @Composable override fun Content() { val state = currentState<CustomData>() } } val customDataStateDefinition = object : GlanceStateDefinition<CustomData> { override suspend fun getDataStore( context: Context, fileKey: String ): DataStore<CustomData> = context.customDataStore override fun getLocation(context: Context, fileKey: String): File = context.preferencesDataStoreFile(fileKey) } DataStoreから取り出す方法を定義 stateDefinitionをoverrideする
独自のデータ型を扱う object CustomDataSerializer : Serializer<CustomData> { 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<CustomData> by dataStore( fileName = "custom_data.proto", serializer = CustomDataSerializer )
独自のデータ型を扱う object CustomDataSerializer : Serializer<CustomData> { 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<CustomData> by dataStore( fileName = "custom_data.proto", serializer = CustomDataSerializer ) どうやってパースするか定義
独自のデータ型を扱う object CustomDataSerializer : Serializer<CustomData> { 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<CustomData> by dataStore( fileName = "custom_data.proto", serializer = CustomDataSerializer ) どうやってパースするか定義 protoファイルとSerializerの紐付け
まとめ • 普段Composeで開発してる人にとってはWidgetは開発しやすいものになりそう • 自分はイベントハンドリングで結構困ってたので、イベントハンドリングが楽になるのもとても助 かる • これからWidgetを開発するのであれば、Glance一択になりそう
注意点 • Glanceはまだalpha版 ◦ 破壊的変更の可能性は十分ある • Composeで書けるけど、差分更新ができるわけではない ◦ 内部は今まで通りRemoteView ◦
更新の仕組みが変わったわけではない
Glanceで快適なWidgetライフを! ご清聴ありがとうございました!