Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
進化した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
870
Other Decks in Programming
See All in Programming
Family mrubyの進捗
kishima
1
100
Swift愛好会と私(ウホーイ) / Swift Fan Club and Uhooi
uhooi
0
140
AIと壁打ちしながら進めるコスト管理
fufuhu
2
2k
【DroidKaigi 2026】「アクセシビリティを利用するとき、 アクセシビリティもまたこちらを利用している」 〜マルウェアによる攻撃と防衛について〜
halunoyo
0
410
WebMCP Challenge に星空観察アプリで参加した話
okajun35
0
100
マイコン向けの軽量Ruby「PicoRuby」で各種デバイスを制御するネイティブアプリの実現手法
bash0c7
0
130
型解析で実現する Go の言語内 DSL / Conference に Go! タイムテーブルの歩き方 for Gophers
mazrean
0
130
Claude Codeを組織的に動かして月400PRを実現した話
happy_ryo
0
260
GKE アップグレード前に知っておきたい Blue/Green と PDB の関係
stkk
0
160
標準パッケージに uuid が追加された 背景から見る Go らしい意思決定 / go_127_uuid_decision
convto
4
3.2k
DroidKaigi 2026 「個人開発という実験場: Android エンジニアが手にする4つの自由」
slashnephy
0
220
GraphRAGのKnowledge Graphを 直接!見る/View-GraphRAG's-KnowledgeGraph-directly!
tyumugi1113
0
250
Featured
See All Featured
[RailsConf 2023 Opening Keynote] The Magic of Rails
eileencodes
31
10k
Code Review Best Practice
trishagee
74
20k
I Don’t Have Time: Getting Over the Fear to Launch Your Podcast
jcasabona
35
2.8k
RailsConf 2023
tenderlove
30
1.5k
How to Talk to Developers About Accessibility
jct
2
540
YesSQL, Process and Tooling at Scale
rocio
174
15k
My Coaching Mixtape
mlcsv
0
310
Ten Tips & Tricks for a 🌱 transition
stuffmc
0
210
How to audit for AI Accessibility on your Front & Back End
davetheseo
0
530
Into the Great Unknown - MozCon
thekraken
41
2.7k
The Cost Of JavaScript in 2023
addyosmani
55
10k
CSS Pre-Processors: Stylus, Less & Sass
bermonpainter
360
30k
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ライフを! ご清聴ありがとうございました!