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 Boostに学ぶ、AIにPHPを書かせる技術 〜OSSの実装から蒸留するエージェント制御の王道〜
kentaroutakeda
3
690
ここ半年くらいでAIに作らせたR用ツール
eitsupi
0
380
テーブルをDELETEした
yuzneri
0
140
freeeにおけるEvalsの実践例の紹介
freee
PRO
0
110
生成AIで帳票OCRが「簡単に」作れる時代になった?
kon_shou
0
880
Cloudflare is Agents
chimame
0
160
言葉の格闘技のススメ~紙とペンと言葉から始める、キャリアの描き方~
progresscicada
2
150
AI時代のPHPer生存戦略 ~「言語、もうなんでもよくない?」に本気で向き合う~
vivion
0
400
170k Jobs a Day on GKE: Scaling Mercari's CI Platform - and What's Next for AI-Native Development
junyaokabe
0
110
jsmini JavaScript Engine を作ってみた話
yosuke_furukawa
PRO
0
320
FDEが実現するAI駆動経営の現在地
gonta
2
280
Google Apps Script で Ruby を動かす
kawahara
0
220
Featured
See All Featured
A Guide to Academic Writing Using Generative AI - A Workshop
ks91
PRO
1
370
Creating an realtime collaboration tool: Agile Flush - .NET Oxford
marcduiker
35
2.5k
Winning Ecommerce Organic Search in an AI Era - #searchnstuff2025
aleyda
1
2.1k
What does AI have to do with Human Rights?
axbom
PRO
1
2.3k
Lightning Talk: Beautiful Slides for Beginners
inesmontani
PRO
2
630
Gemini Prompt Engineering: Practical Techniques for Tangible AI Outcomes
mfonobong
2
480
How to make the Groovebox
asonas
2
2.3k
Digital Ethics as a Driver of Design Innovation
axbom
PRO
1
360
Designing for Performance
lara
611
70k
How to build a perfect <img>
jonoalderson
1
5.9k
Testing 201, or: Great Expectations
jmmastey
46
8.2k
Building Better People: How to give real-time feedback that sticks.
wjessup
370
20k
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ライフを! ご清聴ありがとうございました!