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
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
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
880
Other Decks in Programming
See All in Programming
Agentic Software Factoryに、すごく賢いIF文を。 / super smart IF statement into the Agentic Software Factory.
rkaga
8
2.1k
個人開発者よ、Honoを使え(おじさんver)
tanahiro2010
2
220
速く作れる。その次は、速く確かめられる開発へ 〜AIネイティブ開発を支える、Shift Down〜 / Can build fast. Next, moving to development where we can verify fast.
rkaga
8
5.6k
Starting & Sustaining Code-Based E2E Testing for Non-Coding QA Teams( #jasstniigata )
teyamagu
PRO
1
960
Augmenting AI with the Power of Jakarta EE
ivargrimstad
0
600
Everything will be SERVERLESS — 信じて運用した10年の経験値 / Everything Will be Serverless — Lessons Learned from 10 Years of Operational Experience
seike460
PRO
1
950
半永久的に提供し続けられるプライベートクラウドを目指して ― 利用者の認知負荷を抑えるAPI抽象化とハードウェア世代交代の基盤設計
tomokon
0
470
[2026-09-26]空論ジェネリックプロセス~テスト資産とAIで紡ぐ、再現可能なパフォーマンスチューニングの話~
tosite
0
260
Turning Architecture into Unit Tests in the AI Era (NSSpain XIV)
steliosf
PRO
1
140
When benchmarks go bad - what I learned from measuring performance wrong
hollycummins
0
200
sbt 2.0: takeoff
eed3si9n
0
120
UnityでSystem.Net.WebSocketsなWebSocketサーバが動かないのでUnity Monoのコードを覗いてみた / about implementing websocket server with unity mono
drumath2237
1
580
Featured
See All Featured
Joys of Absence: A Defence of Solitary Play
codingconduct
1
540
VelocityConf: Rendering Performance Case Studies
addyosmani
331
25k
Designing Experiences People Love
moore
143
24k
JAMstack: Web Apps at Ludicrous Speed - All Things Open 2022
reverentgeek
1
630
Tell your own story through comics
letsgokoyo
1
1.1k
A designer walks into a library…
pauljervisheath
211
25k
Become a Pro
speakerdeck
PRO
31
6.3k
ピンチをチャンスに:未来をつくるプロダクトロードマップ #pmconf2020
aki_iinuma
128
56k
YesSQL, Process and Tooling at Scale
rocio
174
15k
Leadership Guide Workshop - DevTernity 2021
reverentgeek
1
390
A Soul's Torment
seathinner
8
3.7k
ReactJS: Keep Simple. Everything can be a component!
pedronauck
666
130k
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ライフを! ご清聴ありがとうございました!