Upgrade to Pro — share decks privately, control downloads, hide ads and more …

進化したWidget

 進化したWidget

Dosukoi

July 20, 2022
Tweet

More Decks by Dosukoi

Other Decks in Programming

Transcript

  1. 進化したWidget
    naoki hidaka

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  7. 比較してみよう

    View Slide

  8. Viewの扱い

    View Slide

  9. これまでの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)
    )
    }

    View Slide

  10. これまでの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…?

    View Slide

  11. これまでの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で指定するの...?

    View Slide

  12. これまでの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で指定するの...?
    初見殺しすぎる!

    View Slide

  13. これからの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)
    }
    }

    View Slide

  14. これからの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で開発してる人なら
    慣れ親しんだ記法!

    View Slide

  15. リスト実装

    View Slide

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

    View Slide

  17. これまでの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 {
    }

    View Slide

  18. これまでの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する必要がある

    View Slide

  19. これまでの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をセット

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  24. イベントハンドリング

    View Slide

  25. これまでのイベントハンドリング
    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)
    }

    View Slide

  26. これまでのイベントハンドリング
    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を定義する必要がある

    View Slide

  27. これまでのイベントハンドリング
    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を紐づけてあげる

    View Slide

  28. これまでのイベントハンドリング
    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を紐づけてあげる

    View Slide

  29. これまでのイベントハンドリング
    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を登録

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  36. これからの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を型として定義

    View Slide

  37. これからの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を登録してあげれば良い

    View Slide

  38. Glanceでの状態の扱い

    View Slide

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

    View Slide

  40. 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()
    )
    }
    }
    }

    View Slide

  41. 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を定義

    View Slide

  42. 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を定義
    値を取得

    View Slide

  43. 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)
    }
    }

    View Slide

  44. 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を定義

    View Slide

  45. 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の中で値を更新

    View Slide

  46. 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を更新

    View Slide

  47. 独自のデータ型を扱う
    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;
    }

    View Slide

  48. 独自のデータ型を扱う
    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;
    }
    独自のデータ型を定義

    View Slide

  49. 独自のデータ型を扱う
    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)
    }

    View Slide

  50. 独自のデータ型を扱う
    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から取り出す方法を定義

    View Slide

  51. 独自のデータ型を扱う
    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する

    View Slide

  52. 独自のデータ型を扱う
    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
    )

    View Slide

  53. 独自のデータ型を扱う
    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
    )
    どうやってパースするか定義

    View Slide

  54. 独自のデータ型を扱う
    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の紐付け

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide