🔰

AndroidアプリでWidgetをつくるまでの備忘録

2023/11/29に公開

備忘録も兼ねておいておきます

目標

  • Widgetを持つAndroidAppを作る(公開はしない)

環境

  • Windows11 Home

筆者の環境

  • Visual Studio Codeユーザー
  • 普段はWebのフロントエンドを触っている
  • .NetFramework WinFormを中学生の時少しだけ触ったが、UIの作り方についてはreactやvueなどの仮想DOMを使ったやり方に慣れている

本題

環境構築

環境構築:インストール

環境構築:Android Studio

参照元: https://developer.android.com/studio/install?hl=ja

  1. https://developer.android.com/studioからダウンロード
  2. Installer実行

Android Studioの日本語化(任意)

参照元: https://neet-rookie.hatenablog.com/entry/AndroidStudio日本語化

  1. Android Studioを立ち上げて、Help > Aboutからバージョンを確認
  2. そのバージョンのプラグインを(JetBrainsマーケットプレース)[https://plugins.jetbrains.com/plugin/13964-japanese-language-pack------/versions]からダウンロード(Not compatible with the version of your running IDE (Android Studio Giraffe | 2022.3.1 Patch 2)といわれるのは、Getを押してダウンロードしてもそのバージョンは違うから別のバージョンを自分で選んでねという意味)
  3. File > Settingsをクリック
  4. そのなかからPluginsをクリック
  5. 上の歯車アイコンをクリック
  6. Install Plugin from Diskをクリック
  7. ダウンロードしたzipファイルを選択 -> Okをクリック
  8. Restart IDEをクリックして、IDEを再起動する

Android Studioのキーマップ設定(任意)

VSCodeのキーマップを追加

既存のIDEやエディタのキーマップを読み込めます。すごい。
Visual Studio Codeは選択肢にはないですが、別途追加することができます。

ファイル > 設定 > キーマップ で以下のウィンドウを開きます。

以上の画像を参考にプラグインマネージャを開いてください。

開いたプラグインマネージャから「VSCode Keymap」を追加してください

キーマップの変更

ファイル > 設定 > キーマップ でまた設定項目を開いてこんどは今現在「Windows」と表示されている場所からVSCodeを選択してください

個別でキーマップ変更

ファイル > 設定 > キーマップから変更します。
おすすめは「選択範囲の拡大」を「Ctrl+l」あたりに割り当てると便利です。
またここで変更したキーマップは、別途保存されるため困った場合はもう一度「Keyマップの設定」からVSCodeを選択してください

学習

Kotlin

とりあえずとほほのKotlin入門を見る。とほほさん森羅万象への入門があるので助かる。

Android App & Kotlin Compose

https://developer.android.com/courses/pathways/android-basics-compose-unit-1-pathway-3?index=..%2F..android-kotlin-fundamentals&hl=ja#codelab-https://developer.android.com/codelabs/basic-android-kotlin-compose-add-images

公式のチュートリアルを少しだけする

Widget

compose glanceでトライ

https://developer.android.com/jetpack/compose/glance

文章内でimplementation "androidx.glance:glance-material3:1.0.0"を追加しろと言われているが、追加先はGradle Scripts/build.gradle.kts(Module :app)に入れる

ただしimplementation("androidx.glance:glance-material3:1.0.0")とする必要がある。

bingさんいわくこういうことらしい

implementation "androidx.glance:glance-material3:1.0.0"implementation("androidx.glance:glance-material3:1.0.0")の両方は、Gradleの依存関係を追加するための異なる記法ですが、どちらも同じ結果をもたらします。

implementation "androidx.glance:glance-material3:1.0.0"はGroovy記法で、Gradleの初期バージョンで使用されていました。
implementation("androidx.glance:glance-material3:1.0.0")はKotlin DSL記法で、Kotlinを使用したGradleスクリプトでよく使われます。
どちらの記法を使用しても、同じライブラリがプロジェクトに追加されます。ただし、プロジェクトがKotlinで書かれている場合や、Kotlin DSLを使用している場合は、Kotlin DSL記法(つまり括弧を使用する方法)を使用することをお勧めします。これにより、一貫性が保たれ、混乱が避けられます。

Create an app widget with Glanceまでやったとこで、glanceがないといわれてしまう

Unresolved package 'glance'
Unresolved class 'MyReceiver'
...

ちょくちょく「GlanceSnippets.ktに書けって言われるけどそのファイルどこに追加すればいいんだよ」っていう基礎知識不足が見えるのと、どうやらglaceを使わないwidget作成方法があることを知ったので、そっちに手を出してみる。

try AppWidgetProvider

Glaceを使わないタイプ(古いタイプ?)のやり方でやってみる

まずAppWidgetHostを作れと言われているので、これはまた別の公式チュートリアルを参考に作る。
多分Host側とWidgetが、それぞれwebでいうサーバーとクライアント見たいな感じになるのかな?
ただ、ちゃんと読んだら(最初からちゃんと読め)、どうやらアプリウィジェットをホストってのはbingさんに聞いた感じ必ずはしなくていいみたい。
というわけで、もういちどGlaceを使わないタイプ(古いタイプ?)のやり方に戻ってやってみる。

基本情報
アプリウィジェットの構成要素
  • AppWidgetProvider: ハンドラーをここに書くイメージ
  • AppWidgetProviderInfo: xmlで定義するウィジェットのメタデータ
  • ビューのレイアウト: 初期状態

設定画面(Activity)も作れるよ。

AppWidgetProvider

AndroidManifest.xmlに以下のコードを追加

android12以降はreceiverandroid:exportedが必須みたいなのでつけてあげる。

   <receiver android:name="ExampleAppWidgetProvider" 
         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/example_appwidget_info" />
   </receiver>

<action>とかはproviderに提供するイベントを追加できるみたい。ここで追加しなくてもデフォルトで受け取れるイベントはいくつかある。

        <meta-data android:name="android.appwidget.provider"
                   android:resource="@xml/example_appwidget_info" />

この部分でmeta-data、つまりAppWidgetProviderInfoを指定している。

AppWidgetProviderInfo メタデータを追加する

さっき作ったAndroidManifest.xmlandroid:resource=...と指定している部分で参照しているように、以下の内容をres/xml/example_app_widget_info.xmlに追加する。
android:previewImage="@drawable/preview"ってのはwidget選択画面で表示される画像。任意なので、今回はdrawableに追加するのがめんどくさいのでここでは追加しない。
@layout/example_appwidgetandroid.ExampleAppWidgetConfigureでエラーが出るけど気にしない。

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
        android:minWidth="40dp"
        android:minHeight="40dp"
        android:updatePeriodMillis="86400000"
        android:initialLayout="@layout/example_appwidget"
        android:configure="com.example.プロジェクト名.ExampleAppWidgetConfigure"
        android:resizeMode="horizontal|vertical"
        android:widgetCategory="home_screen">
</appwidget-provider>
widget用のレイアウトを追加する

レイアウトを追加する際はres/layoutに追加する

とりあえず今回はこんな感じの単純なレイアウトを追加してみる

res/layout/example_appwidget.xml

<?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">

    <TextView
        android:id="@+id/textView"
        android:layout_width="match_parent"
        android:layout_height="136dp"
        android:text="はじめてのwidget" />
</LinearLayout>
AppWidgetProvider クラスを使用する

いろいろ使い方が書いてあるけど、どこにファイルを追加すればいいかがわからない。とりえあずbingさんに聞いてみる。

AppWidgetProviderを継承したクラスは、通常はプロジェクトのソースコードディレクトリに保存します。具体的な場所はプロジェクトの構造によりますが、一般的にはsrc/main/java/ディレクトリ以下の適切なパッケージ内に配置します。

例えば、com.example.myapp.widgetsというパッケージを作成し、その中にAppWidgetProviderを継承したクラスを作成することができます。その場合、ファイルの場所はsrc/main/java/com/example/myapp/widgets/MyAppWidgetProvider.javaとなります。

ということなので、javaディレクトリを右クリック、新規>パッケージからcom.example.trywidget2.widgetsというパッケージを作り、その中にExampleAppWidgetProvider.ktを作成すればいいのかと思ったが、どうやら違いそう。
AndroidManifest.xmlExampleAppWidgetProviderを指定しているので、正しくは**MainActivity.ktと同じディレクトリに新しくExampleAppWidgetProvider.ktを作る**

チュートリアルで紹介されているプロバイダをそのまま置いてみる

package com.example.trywidget2.widgets

import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.content.Intent
import android.widget.RemoteViews

class ExampleAppWidgetProvider : AppWidgetProvider() {

    override fun onUpdate(
        context: Context,
        appWidgetManager: AppWidgetManager,
        appWidgetIds: IntArray
    ) {
        // Perform this loop procedure for each App Widget that belongs to this provider
        appWidgetIds.forEach { appWidgetId ->
            // Create an Intent to launch ExampleActivity
            val pendingIntent: PendingIntent = Intent(context, ExampleActivity::class.java)
                .let { intent ->
                    PendingIntent.getActivity(context, 0, intent, 0)
                }

            // Intentから通じてonClickイベントをActivityに設定する
            val views: RemoteViews = RemoteViews(
                context.packageName,
                R.layout.appwidget_provider_layout
            ).apply {
                setOnClickPendingIntent(R.id.button, pendingIntent)
            }

            // AppWidgetManagerに、現在のアプリ・ウィジェットのアップデートを実行するよう指示する。
            appWidgetManager.updateAppWidget(appWidgetId, views)
        }
    }
}

ここで言われているCreate an Intent to launch ExampleActivityIntentがわからないのでbing先生に聞く

Androidアプリ開発における「インテント(Intent)」は、「何かを行う意図」という意味で、特定のアクションをリクエストする時に使用できるメッセージングオブジェクトのことを指します。

例えば、別の画面を開きたい時に、AndroidではActivity単位で画面が作られますが、「開いている画面に対して別の画面を開いてください」とアプリにメッセージとして伝えることで、別の画面を開くことが可能になります

参考

  • Activityを起動
  • Service(バックグラウンドで動く奴)を起動
  • ブロードキャストの配信(全アプリに対して送るメッセージ)

みたいなことができるものみたい。
で、Intentには以下の情報を渡すみたい。

Context:現在コンテキスト(通常は現在のアクティビティ)
Class:遷移先アクティビティ
Action:実行するアクション ex.「電話をかける」、「ウェブページを開く」
Data:アクションに関連するデータ

などなど・・・

ただActivityを用意するのがめんどくさいなぁと思った別の方法も調べてみた。
web開発とかだとalert()とかよく使われるけど、android開発だとLogというものが使われるみたい。

Log.d("MyActivity", "MyClass.getView() — get item number $position")

こうするとLogcatウィンドウでログ出力が確認できるみたい。
ちゃんと読んでみるとちゃんと後でActivityの作成をしているので、今回は言われたとおりにやってみる。

とりあえずインポートできそうなやつだけインポートすると、ExampleActivity,R.layout.appwidget_provider_layout,R.id.buttonだけがエラーの状態になる。

アプリウィジェットを固定する

固定しなくていいので今回はパス

アプリ ウィジェットの設定アクティビティを作成する

新しくアクティビティを宣言する際は、AndroidManifest.xmlに追加する必要がある
.ExampleAppWidgetConfigureがエラーを吐くけど気にしない

<activity android:name=".ExampleAppWidgetConfigure">
    <intent-filter>
        <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
    </intent-filter>
</activity>
sdkのバージョン

sdkのバージョンが34以上が必要といわれたので、build.gradle.ktsandroid.compileSdkandroid.defaultConfig.targetSdk33から34に修正

Widgetないんだけど!?

ただここまで来て、アプリアイコンを長押ししてもwidgetが出てこない
これは仕様ではなくしっかりミス。
自分のやりたいことをと、今回書いたコードを全部bingさんに投げてみたところ、example_appwidget_info.xmlの設定の仕方がミスってたみたい。

正しくはこう

<?xml version="1.0" encoding="utf-8"?>
    <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
        android:minWidth="40dp"
        android:minHeight="40dp"
        android:updatePeriodMillis="86400000"
        android:initialLayout="@layout/example_appwidget"
        android:configure="com.example.trywidget2.ExampleAppWidgetConfigure"
        android:resizeMode="horizontal|vertical"
        android:widgetCategory="home_screen">
    </appwidget-provider>
なんかアプリ開かれるんだけど!?

それはexample_appwidget_info.xmlでandroid:configureにExampleAppWidgetConfigureを設定したから。
今回は何も設定がいらない前提なので、その行を消してみるとこんな感じにwidgetが設置できる。

ここまで3日かかった、長かった…

くそださウィジェット

いまのままだとさすがにくそださウィジェットすぎるので、少しだけデザインをちゃんとしてみる。

私「androidのwidgetのuiを作ってほしいんだ。まず必要な要素としてはテキストとボタンが欲しい、でウィジェットのバックグランドは黒で塗りつぶして、透明度を50%ぐらいにして。ボタンとテキストは横並びにして、テキストの位置としては左寄り、ボタンは右寄り。ボタンのテキストは「更新」、ボタンの横幅はそれに合わせてくれ、でテキストの横幅は余ったスペースを敷き詰めてほしい」

bing「

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#80000000">

    <TextView
        android:id="@+id/text_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentStart="true"
        android:layout_centerVertical="true"
        android:textColor="#FFFFFF"
        android:textSize="16sp" />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentEnd="true"
        android:layout_centerVertical="true"
        android:text="更新" />

</RelativeLayout>

bingの名誉のために行っておくとButtoin.android:textの部分はめんどくさいので私がハードコードするように修正した。

動的な表示

とりえあず静的な画面を表示することに成功したので、次は動的な情報として、時間を表示してみる。

と思って、bingさんに聞いてみたが、どうやらandroidのウィジェットは更新頻度が最短で30分らしい。それ以下の速度で更新したい場合は特殊な方法が必要っぽい。

30分の方法でやりたいところだが、毎回更新されるかどうか試すのに30分松の目現実的ではないので、タップしたら更新されるようにしてみる。

タップした際に更新されるかどうかを確認しやすいように、今回は時間を表示してみる(一般的に時間を表示する場合は後者の方法が望ましいが、今回はあくまで前者の一般的なウィジェットの更新方法の練習のため前者の方法を採用する)

ボタンのクリック
ExampleAppWidgetProvider.kt
package com.example.trywidget2

import android.app.PendingIntent
import android.app.PendingIntent.FLAG_IMMUTABLE
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.content.Intent
import android.util.Log
import android.widget.RemoteViews
import java.util.Calendar
import java.util.Date

class ExampleAppWidgetProvider : AppWidgetProvider() {

    companion object {
        private const val BUTTON_CLICKED = "buttonClicked"
    }

    override fun onUpdate(
        context: Context,
        appWidgetManager: AppWidgetManager,
        appWidgetIds: IntArray
    ) {
        super.onUpdate(context, appWidgetManager, appWidgetIds)
        Log.d("onUpdate","update")
        Log.d("onUpdate",appWidgetIds.toString())
        // ウィジェットのボタンが押されたときの処理を設定します。
        val intent = Intent(context, ExampleAppWidgetProvider::class.java).apply {
            action = BUTTON_CLICKED;
        }
        val pendingIntent = PendingIntent.getBroadcast(context, 0, intent, FLAG_IMMUTABLE )

        // ウィジェットのレイアウトを取得し、ボタンのクリックイベントを設定します。
        val views = RemoteViews(context.packageName, R.layout.example_appwidget).apply {
            setOnClickPendingIntent(R.id.button, pendingIntent)
        }

        // ウィジェットを更新します。
        appWidgetManager.updateAppWidget(appWidgetIds, views)

    }

    override fun onReceive(context: Context, intent: Intent) {
        super.onReceive(context, intent)

        if (BUTTON_CLICKED == intent?.action) {
            // ボタンが押されたときの処理を記述します。
            Log.d("Receive", "Button clicked!")
        }
    }
}

これでwidgetにあるボタンが押されたさいに、LogcatでButton clicked!が表示されるようになった。
ただこれだと後後うまくいかないことが頻出する

ボタンクリックしたwidgetの検知

widgetの更新処理はwidgetProviderのonUpdateに書いているが、外部から明示的にこれを実行するにはwidgetのIdが必要となる。

同じクラスから作れられたwidgetインスタンスのid一覧はonUpdateの第三引数(appWidgetIds)で取得できるが、onReceiveの段階ではどのidのwidgetから贈られてきたものなのかわからないので、Intentに自分がどういうidをもつwidgetなのかを含める必要がある。

その処理を追加したコードが以下のコードである

ExampleWidgetAppProvider.kt
class ExampleAppWidgetProvider : AppWidgetProvider() {

    companion object {
        private const val BUTTON_CLICKED = "buttonClicked"
    }

    override fun onUpdate(
        context: Context,
        appWidgetManager: AppWidgetManager,
        appWidgetIds: IntArray
    ) {
        super.onUpdate(context, appWidgetManager, appWidgetIds)
        Log.d("onUpdate","update")
        Log.d("onUpdate",appWidgetIds.toString())
        // ウィジェットのボタンが押されたときの処理を設定します。
        for (appWidgetId in appWidgetIds) {
            val intent = Intent(context, ExampleAppWidgetProvider::class.java).apply {
                action = BUTTON_CLICKED;
                putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
            }
            val pendingIntent = PendingIntent.getBroadcast(context, appWidgetId, intent, FLAG_IMMUTABLE )

            // ウィジェットのレイアウトを取得し、ボタンのクリックイベントを設定します。
            val views = RemoteViews(context.packageName, R.layout.example_appwidget).apply {
                setOnClickPendingIntent(R.id.button, pendingIntent)
            }

            // ウィジェットを更新します。
            appWidgetManager.updateAppWidget(appWidgetId, views)
        }
    }

    override fun onReceive(context: Context, intent: Intent) {
        super.onReceive(context, intent)

        if (BUTTON_CLICKED == intent?.action) {
            // ボタンが押されたときの処理を記述します。
            Log.d("Receive", "Button clicked!")
            val widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
            Log.d("Receive",widgetId.toString())
        }
    }
}

以上のようにintentをちゃんと設定すると、clickした際にidが出力できると思う。

テキストの更新

今度はwidget上にあるテキストを更新(今の時間を表示する)してみる

以下のようにonReceiveを変え、``

    override fun onReceive(context: Context, intent: Intent) {
        super.onReceive(context, intent)

        if (BUTTON_CLICKED == intent.action) {
            val widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID)
            val appWidgetManager = AppWidgetManager.getInstance(context)
            val views = RemoteViews(context.packageName, R.layout.example_appwidget)

            views.setTextViewText(R.id.text_view, LocalDateTime.now().toString())

            appWidgetManager.updateAppWidget(widgetId, views)
        }
    }

Widgetの設定画面を作る

設定を管理するやつを作る

どうやらWidgetPreferenceというのを使うのが一般的みたい
なのでそれを実装

app/java/com.example.trywidget2/WidgetPreference.kt
package com.example.trywidget2

import android.content.Context
import android.content.SharedPreferences

class WidgetPreferences(context: Context) {
    private val PREFS_NAME = "com.example.app.widgetprefs"
    private val PREF_PREFIX_KEY = "appwidget_"
    private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, 0)

    // ウィジェットの設定を保存する
    fun saveStringPref(appWidgetId: Int, text: String) {
        val editor = prefs.edit()
        editor.putString(PREF_PREFIX_KEY +  "str", text)
        editor.apply()
    }

    // ウィジェットの設定を読み込む
    fun loadStringPref(appWidgetId: Int): String? {
        return prefs.getString(PREF_PREFIX_KEY +"str", null)
    }
}
設定画面の設定

次はwidgetの設定画面を作ってみる

私「widgetの設定画面のつくりかた」
bing「↓こんなかんじやで」

app/java/com.example.trywidget2/SettingActivity.kt
package com.example.trywidget2

import android.os.Bundle
import android.widget.Button
import android.widget.EditText
import androidx.appcompat.app.AppCompatActivity

class InputActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_setting)

        val editText = findViewById<EditText>(R.id.editText)
        val button = findViewById<Button>(R.id.button)

        button.setOnClickListener {
            val input = editText.text.toString()
            // ここでinputを保存または使用します。
        }
    }
}
app/res/layout/activity_setting.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <EditText
        android:id="@+id/editText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="ここにテキストを入力してください" />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="送信" />

</LinearLayout>

ただここでエラーが発生。
アクティビティを設置する瞬間一瞬画面が開いた後にクラッシュするようになってしまった。

今までのコードとエラーメッセージをbingに投げると以下の回答がかえってきた

エラーメッセージを見ると、SettingActivityTheme.AppCompatテーマ(またはその子孫)を使用する必要があるというエラーが発生しているようです¹³。

このエラーは、AppCompatActivityを拡張するアクティビティでTheme.AppCompatテーマ(またはその子孫)を使用しないと発生します¹³。AppCompatActivityは、Androidのバージョンに関係なく、アクションバーとマテリアルデザインのスタイルを使用するためのクラスです¹³。したがって、AppCompatActivityを拡張するアクティビティでは、Theme.AppCompatテーマ(またはその子孫)を使用する必要があります¹³。

あなたのSettingActivityAppCompatActivityを拡張しているため、Theme.AppCompatテーマ(またはその子孫)を使用する必要があります¹³。しかし、AndroidManifest.xmlSettingActivityに対してandroid:theme="@android:style/Theme.Holo.Light.Dialog"を設定しているようです¹³。これはTheme.AppCompatテーマではないため、エラーが発生しています¹³。

この問題を解決するには、SettingActivityのテーマをTheme.AppCompatテーマ(またはその子孫)に変更する必要があります¹³。例えば、AndroidManifest.xmlSettingActivityのテーマをTheme.AppCompat.Light.Dialogに変更すると、エラーが解消されるかもしれません¹³。具体的には、以下のように変更します¹³:

<activity android:name=".SettingActivity" android:exported="true">
    <intent-filter>
        <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
    </intent-filter>
    <meta-data
        android:name="android.appwidget.provider"
        android:resource="@xml/example_appwidget_info" />
    <activity android:name=".SettingActivity" android:theme="@style/Theme.AppCompat.Light.Dialog">
</activity>

これで問題が解決するか試してみてください¹³。それでも問題が解決しない場合は、お知らせください¹³。

ソース: Bing との会話 2023/11/8
(1) このアクティビティでは、Theme.AppCompatテーマ(または子孫 .... https://qastack.jp/programming/21814825/you-need-to-use-a-theme-appcompat-theme-or-descendant-with-this-activity.
(2) Android 12でCaused by java.lang.IllegalStateException You .... https://qiita.com/atsuya046/items/b10ab779978e7cafeb93.
(3) スタイルとテーマ | Android デベロッパー | Android Developers. https://developer.android.google.cn/guide/topics/ui/look-and-feel/themes?hl=ja.

bing先生さすが

以下のように修正

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.TryWidget2"
        tools:targetApi="31">
        <activity
            android:name=".ExampleAppWidgetConfigure"
            android:exported="false"
            android:label="@string/title_activity_example_app_widget_configure"
            android:theme="@style/Theme.TryWidget2">
            <intent-filter>
                <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
            </intent-filter>
        </activity>
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:label="@string/app_name"
            android:theme="@style/Theme.TryWidget2">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name=".SettingActivity" android:theme="@style/Theme.AppCompat.Light.Dialog" android:exported="true">
            <intent-filter>
                <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
            </intent-filter>
        </activity>

        <receiver
            android:name=".ExampleAppWidgetProvider"
            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/example_appwidget_info" />
        </receiver>
    </application>

</manifest>
全画面

ただこれだと、activityが小さすぎてみきれているので、全画面表示にする。
基本的にはmanifestファイルで、全画面であることを指定すればいい感じ。

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
	...
-       <activity android:name=".SettingActivity" android:theme="@style/Theme.AppCompat.Light.Dialog" android:exported="true">
+	<activity android:name=".SettingActivity" android:theme="@android:style/Theme.NoTitleBar.Fullscreen" android:exported="true">
            <intent-filter>
                <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
            </intent-filter>
        </activity>
	...
</manifest>
設定を保存する

どうやらWidgetPreferenceというのを使うのが一般的みたい
なのでそれを実装

app/java/com.example.trywidget2/WidgetPreference.kt
package com.example.trywidget2

import android.content.Context
import android.content.SharedPreferences

class WidgetPreferences(context: Context) {
    private val PREFS_NAME = "com.example.app.widgetprefs"
    private val PREF_PREFIX_KEY = "appwidget_"
    private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, 0)

    // ウィジェットの設定を保存する
    fun saveStringPref(appWidgetId: Int, text: String) {
        val editor = prefs.edit()
        editor.putString(PREF_PREFIX_KEY +  "str", text)
        editor.apply()
    }

    // ウィジェットの設定を読み込む
    fun loadStringPref(appWidgetId: Int): String? {
        return prefs.getString(PREF_PREFIX_KEY +"str", null)
    }
}
ウィジェットが設定を読むようにする

最後にウィジェットが設定を読むようにする

ExampleAppWidgetProvider.kt
class ExampleAppWidgetProvider : AppWidgetProvider() {

    companion object {
        private const val BUTTON_CLICKED = "buttonClicked"
    }

+   private fun setText(myId:Int,context: Context, views: RemoteViews) {
+       val pref = WidgetPreferences(context)
+
+       views.setTextViewText(R.id.text_view, LocalDateTime.now().toString()+"_"+pref.loadStringPref(myId))
+   }

    override fun onUpdate(
        context: Context,
        appWidgetManager: AppWidgetManager,
        appWidgetIds: IntArray
    ) {
        super.onUpdate(context, appWidgetManager, appWidgetIds)
        for (appWidgetId in appWidgetIds) {
            val intent = Intent(context, ExampleAppWidgetProvider::class.java).apply {
                action = BUTTON_CLICKED
                putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
            }

            val pendingIntent =
                PendingIntent.getBroadcast(context, appWidgetId, intent, FLAG_IMMUTABLE)

            val views = RemoteViews(context.packageName, R.layout.example_appwidget).apply {
                setOnClickPendingIntent(R.id.button, pendingIntent)
            }
+           setText(appWidgetId,context,views)

            appWidgetManager.updateAppWidget(appWidgetId, views)
        }
    }

    override fun onReceive(context: Context, intent: Intent) {
        super.onReceive(context, intent)

        if (BUTTON_CLICKED == intent.action) {
            val widgetId = intent.getIntExtra(
                AppWidgetManager.EXTRA_APPWIDGET_ID,
                AppWidgetManager.INVALID_APPWIDGET_ID
            )
            val appWidgetManager = AppWidgetManager.getInstance(context)
            val views = RemoteViews(context.packageName, R.layout.example_appwidget)

+           setText(widgetId,context,views)
            appWidgetManager.updateAppWidget(widgetId, views)
        }
    }
}

ちょっとだけ整理もしている。

初期設定設定項目を反映するようにする

このままでは更新ボタンを押すまで初期設定時の項目がウィジェットに反映されないので、設定用Activityで送信ボタンを押したときに更新処理を走らせるようにする。
AppWidgetManagerを使用すると、特定のウィジェットをIDで指定して更新したり削除したり、情報を取得したりすることができるようになる。
これは、全体を管理するオブジェクトに対するアクセス権を持つことを意味していて、AppWidgetManagerのインスタンスを作成すると、グローバルなウィジェットにアクセスできるようになる。

SettingActivity.kt
package com.example.trywidget

import android.app.Activity
import android.appwidget.AppWidgetManager
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.widget.Button
import android.widget.EditText
import android.widget.RemoteViews
import androidx.appcompat.app.AppCompatActivity
import java.time.LocalDateTime

class SettingActivity : Activity() {
    private var mAppWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val intent = intent
        val extras = intent.extras
        if (extras != null) {
            mAppWidgetId = extras.getInt(
                AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID
            )
        }
        setContentView(R.layout.setting_activity)

        if (mAppWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
            finish()
            return
        }

        val editText = findViewById<EditText>(R.id.editText)
        val button = findViewById<Button>(R.id.send_button)

        button.setOnClickListener {
            val prefs = WidgetPreferences(this)
            prefs.saveStringPref(mAppWidgetId, editText.text.toString())

+           val appWidgetManager = AppWidgetManager.getInstance(this)
+           updateAppWidget(this, appWidgetManager, mAppWidgetId)

            val resultValue = Intent()
            resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId)
            setResult(RESULT_OK, resultValue)
            finish()
        }

        Log.d("SettingActivity","set button onclick")

    }

+   internal fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int) {
+       val views = RemoteViews(context.packageName, R.layout.widget)
+       val pref = WidgetPreferences(context)

+       views.setTextViewText(R.id.text_view, LocalDateTime.now().toString()+"_"+pref.loadStringPref(appWidgetId))
+       appWidgetManager.updateAppWidget(appWidgetId, views)
+   }
}

成果物

結局↓のようなものが出来上がりました
https://github.com/cou723/TryWidget

以下のようなwidgetで、widgetを設置するとその下の画像のような設定画面が表示されます


感想

自分が思ったより記事をちゃんと読まない(知らない単語を間違った推論で読み進める)というのがよくわかる記録になりました。
後日試行錯誤の箇所を除いた記事にする予定です

Discussion