目次

目次

【Android】 Glanceを使ったアプリウィジェット

アバター画像
杉山裕哉
アバター画像
杉山裕哉
最終更新日2022/12/12 投稿日2022/12/12

今回の記事はレコチョク Advent Calendar 2022の12日目の記事となります。

はじめに

はじめまして!株式会社レコチョクでAndroid開発を担当しています、入社3年目の杉山です! アイドルが好きなのですが、今年は色々あったなぁと1年を振り返っているところです。 坂道グループが好きなのですがとてもいいライブがたくさんありました。(最近涙脆いのか泣いてばかりでした笑)

今回は自身の参加しているプロジェクトでアプリウィジェットを実装する際に、導入したGlanceについて記事をまとめたいと思います!

アプリウィジェットとは

ホーム画面上に表示されるアプリのショートカット機能のことを指します。 アプリを開くことをせず、ホーム画面を見るだけで情報を得ることができる便利さから使っている人も多いかと思います。 みなさんがよく利用しているものでは、時計、天気予報やバッテリー残量などでしょうか。

メリットとしては先程も言ったとおり、欲しい情報をひと目で確認できることや、そこから必要な画面に移動してくれることです。 そんなアプリウィジェットの実装方法の一つとして今回はGlanceを紹介します。

Glanceとは

近年Android開発で使われているJetpack Composeを用いてアプリウィジェットを開発できるようにしたものになります。 Jetpack Composeは2019年に発表されたAndroid用のUIツールキットになります。 Jetpack Composeに関しては今回の記事では詳しいことまで話しませんが、主な利点としては、コード量の削減、直感的な開発、ビルド速度の向上があります。 GlanceはそのJetpack Composeのランタイムをベースに作られたフレームワークになります。 そのため、ホーム画面などで利用できるアプリ ウィジェットをすばやく簡単に構築できるように設計されています。

内部の構造としてはGlanceを用いて作成したComposeをRemoteViewに変換している形になります。 利点としてはJetpack Compose同様、コード量の削減、直感的な開発、ができるようになります。

image-20221201055533210.png

実際に導入してみた

準備

Glanceを使うにはbuild.gradleに以下の依存関係や設定を追加する必要があります。

// すでにJetpack Composeを使用してる場合
dependencies {
    implementation("androidx.glance:glance-appwidget:1.0.0-alpha03")
}

// 新規にJetpack Composeを使用していない場合、追記する必要があります。
android {
    buildFeatures {
        compose = true
    }

    composeOptions {
        kotlinCompilerExtensionVersion = "1.1.0"
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }
}

Glanceは専用のComposeを使用するため、普通のComposeの依存関係は不要になります。 (※ Composeを使用するための設定は必要になります。)

各クラスの説明

  • GlanceAppWidget
    • ウィジェット本体のクラスになります。レイアウトはこのクラスで設定します。
    class MyGlanceAppWidget : GlanceAppWidget() {
      @Composable
      override fun Content() {
        // ここにレイアウトに関する実装をする
      }
    }
    
  • GlanceAppWidgetReceiver
    • ウィジェットの更新を扱うクラスになります。このクラスのonUpdateonReceiveを設定することで更新処理を変更することができます。
    class MyGlanceAppWidgetReceiver : GlanceAppWidgetReceiver() {
    
      override val glanceAppWidget: GlanceAppWidget
          get() = MyGlanceAppWidget()
        override fun onUpdate() {
        // ここに更新処理を実装する
      }
    }
    

使ってみた

実際に使ってレスポンシブなウィジェットを作成してみました。 ウィジェットのサイズに応じて表示する内容が変わるようになっています。

作ったウィジェットの動き.gif

Receiver

今回情報更新はないため、Receiverの構成は最小限にしています。

class GlanceAppWidgetReceiverSample : GlanceAppWidgetReceiver() {
    override val glanceAppWidget: GlanceAppWidget
        get() = GlanceAppWidgetSample()
}

ウィジェット側からの操作であったり、アプリ側から更新がある場合はこのクラスに実装していきます。

AndroidManifest

ウィジェットのReceiverを登録する必要があります。 AndroidManifestにReceiverを登録します。また、meta-dataとしてウィジェットの情報を記載します。

<receiver
    android:name=".GlanceAppWidgetReceiverSample"
    android:exported="false">
    <intent-filter>
        <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
    </intent-filter>
    <meta-data
        android:name="android.appwidget.provider"
        android:resource="@xml/glance_appwidget_sample_meta_data" />
</receiver>

meta-data

meta-dataとして定義するxmlファイルになります。

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:targetCellHeight="2"
    android:targetCellWidth="2"
    android:minWidth="130dp"
    android:minHeight="50dp"
    android:minResizeWidth="130dp"
    android:minResizeHeight="50dp"
    android:maxResizeHeight="130dp"
    android:previewImage="@drawable/henohenomoheji_mark"
    android:resizeMode="horizontal|vertical"
    android:widgetCategory="home_screen"
    android:updatePeriodMillis="0"
    android:initialLayout="@layout/glance_appwidget_sample" />

targetCellWidthtargetCellHeightはAndroid12(SDK31)以上で動作するものになります。 12以上の端末であれば minWidth,minHeightの代わりに使用されます。 previewImageにアプリウィジェット配置時のプレビュー画像を定義することができます。(今回は仮画像を入れています。) updatePeriodMillisは自動更新の際に設定する値で今回は不要であるので0にしています。 initialLayout はウィジェットの初回更新がされるまで表示されるレイアウトをになります。ここはまだComposeでの実装はできないようなので、xmlでレイアウトを記述する必要があります。

initialLayout

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/white">
    <ProgressBar
        android:id="@+id/widget_initial_loading"
        style="?android:attr/progressBarStyle"
        android:layout_width="24dp"
        android:layout_height="24dp" />

    <TextView
        android:id="@+id/widget_initial_loading_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:gravity="center"
        android:paddingVertical="8dp"
        android:text="loading"
        android:textColor="@color/black"
        android:textSize="16sp" />
</LinearLayout>

initialLayoutはRemoteViewの制約を受けてしまうので、以下のように使用できるレイアウトが限られます。 そのため今回はLinearLayoutで実装しました。

initialLayoutで使用可能なレイアウト

  • FrameLayout
  • LinearLayout
  • RelativeLayout
  • GridLayout

GlanceAppWidget

メインとなるウィジェットのレイアウトに関してです。 ウィジェットのサイズに応じて表示するレイアウトを変更するようにできます。

GlanceAppWidget()のクラス内に以下のように実装します。

class GlanceAppWidgetSample : GlanceAppWidget() {
   companion object {
        private val LANDSCAPE = DpSize(130.dp, 50.dp)
        private val PORTRAIT = DpSize(130.dp, 130.dp)
        private val ROW = DpSize(210.dp, 50.dp)
        private val MULTI_ROW = DpSize(210.dp, 130.dp)
    }
    override val sizeMode = SizeMode.Responsive(
        setOf(LANDSCAPE, PORTRAIT, ROW, MULTI_ROW)
    )

    @SuppressLint("ResourceType")
    @Composable
    override fun Content() {
        when (LocalSize.current) {
            LANDSCAPE -> SingleResponseLandScape()
            PORTRAIT -> SingleResponsePortrait()
            ROW -> SingleResponseRow()
            MULTI_ROW -> MultiResponseRow()
        }
    }
}

LocalSize.currentによって現在のウィジェットのサイズを取得することができます。

予めサイズ分けしたいものを SizeMode.ResponsiveとしてsizeModeに設定することでサイズに応じたレイアウトをレスポンシブに表示することができます。 動作としてはウィジェットのサイズを変更した際、 LocalSize.current から現在のウィジェットのサイズを取得し、when文から表示するComposeを変更するといった動作になります。 このときに、情報を送ることもできるため、表示する情報を変更することも可能です。

それぞれ表示するレイアウトは以下のように実装することができます。(今回4種類作成しましたが、そのうちの一つを以下に示します。)

SingleRow.png
import androidx.glance.GlanceModifier
import androidx.glance.Image
import androidx.glance.ImageProvider
import androidx.glance.layout.Box
import androidx.glance.layout.Column
import androidx.glance.layout.Row
import androidx.glance.text.Text
import androidx.glance.unit.ColorProvider

@Composable
fun SingleResponseRow() {
    Box(
        contentAlignment = Alignment.TopEnd,
        modifier = GlanceModifier
            .padding(5.dp)
            .fillMaxSize()
            .background(Gray)
            .clickable(actionStartActivity<MainActivity>())
    ) {
        Row(
            verticalAlignment = Alignment.CenterVertically,
            modifier = GlanceModifier
                .fillMaxSize()
                .padding(end = 32.dp)
        ) {
            Image(
                provider = ImageProvider(R.drawable.entertainment_music),
                contentDescription = null,
                modifier = GlanceModifier.size(90.dp)
            )
            Column {
                Text(
                    text = "タイトル名",
                    style = TextStyle(
                        fontWeight = FontWeight.Bold,
                        fontSize = 13.sp,
                        color = ColorProvider(R.color.black)
                    ),
                    maxLines = 2,
                    modifier = GlanceModifier
                        .padding(start = 10.dp)
                )
                Text(
                    text = "アーティスト名",
                    style = TextStyle(
                        fontSize = 11.sp,
                        color = ColorProvider(R.color.black)
                    ),
                    maxLines = 1,
                    modifier = GlanceModifier
                        .padding(start = 10.dp)
                )
            }
        }
        Image(
            ImageProvider(R.drawable.henohenomoheji_mark),
            contentDescription = "App Icon",
            modifier = GlanceModifier
                .size(32.dp, 32.dp)
                .padding(4.dp)
        )
    }
}

RowやBoxなどJetpack Composeと同様に実装することができますが、1点注意点があります。 Glanceで使用するComposeは従来のものと異なり、 Glance専用のもの を使用します。 Box,Row,Text,Image等の同名のComposeがありますが、それぞれJetpack Composeのものとは異なるものになっているため、注意が必要です。 使う際はGlanceのものは、 import androidx.glance.xxxとなります。(上記ソースコードには一部を抜粋して記載してあります。)

主な違いとしては、Modifierが GlanceModifier であるという点です。ウィジェット用に変更されているためか、中で設定できることも異なっています。 それ以外は大きな変化はないもののGlance自体が、アルファ版であるため、細かい部分で対応していないものがあったりします。

僕自身も実装する際に、Jetpack Composeのものを使用していてエラーが発生してしまい、躓いたりしました。

GlanceModifierにタップ時の動作を設定することで、アプリを開く事ができます。

@Composable
fun SingleResponseRow() {
    Box(
        contentAlignment = Alignment.TopEnd,
        modifier = GlanceModifier
            .padding(5.dp)
            .fillMaxSize()
            .background(Gray)
            .clickable(actionStartActivity<MainActivity>())

.clickable(actionStartActivity<MainActivity>()) でアプリを開くことができます。 開く画面を別のActivityやIntentにすることで任意の画面を開くことができます。

まとめ

今回はGlanceを用いてアプリのウィジェットを作成したので、紹介しました。 レイアウトを簡潔に書くことができるので、Jetpack Compose同様に今後も業務で使用していきたいと思います。 Receiverを活用することでもっとたくさんの用途を持ったウィジェットを作ることができると思うので今後のアップデートが非常に楽しみです。

最後まで読んでいただきありがとうございました。 明日の レコチョク Advent Calendar 2022は13日目 S3上のMP4ファイルを切り抜いて高速にレスポンスするWeb APIを作る となります。お楽しみに!

この記事はレコチョクのエンジニアブログの記事を転載したものとなります。

https://blog.tools.product.recochoku.net/8700

アバター画像

杉山裕哉

目次