【Android/Kotlin】同じWidgetを複数作成した際に，別々のものとして動作させる方法

#はじめに

この記事は[【Android/Kotlin】Widgetを作ってクリック時の動作を設定する
](https://qiita.com/hac-chi/items/3c4a72818b9bc69d10aa)の続きとなっていますすが，ウィジェットの基本が分かっている方はこの記事だけ見れば大丈夫です。

また，執筆者は趣味レベルのAndroidアプリ開発者である為，説明間違いが含まれている可能性があります。お気づきの点がありましたら，是非コメントお願いいたします。また，アップデートにより，機能やコードが記事執筆時点と異なる場合があります


#本記事の内容
本記事では，[【Android/Kotlin】Widgetを作ってクリック時の動作を設定する
](https://qiita.com/hac-chi/items/3c4a72818b9bc69d10aa)で作成した，ボタンを押すとカウンターが1ずつ増えていくウィジェットについて，複数が同じものとして動作してしまう問題を解消します。
![ウィジェットボタン問題.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260207/b8883f80-f2c9-ab3d-e943-c92ab498283a.gif)


# 修正毎のソースコード
特に複雑なことはしていませんが，ソースに関する詳しい説明は[【Android/Kotlin】Widgetを作ってクリック時の動作を設定する](https://qiita.com/hac-chi/items/3c4a72818b9bc69d10aa)を御覧ください。


ウィジェットのレイアウトファイル

```sample_app_widget.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/white"
    android:gravity="center"
    android:orientation="vertical"
    android:padding="@dimen/widget_margin">

    <TextView
        android:id="@+id/counter"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="0"
        android:textAppearance="@style/TextAppearance.AppCompat.Display1" />

    <Button
        android:id="@+id/add_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="add" />

</LinearLayout>
```
ウィジェットの動作を実装したファイル

```SampleAppWidget.kt
//com.example.android.stackwidgetの部分は自分のアプリのIDに修正する
private const val COUNT_UP = "com.example.android.buttonwidgettest.COUNT_UP"

class SampleAppWidget : AppWidgetProvider() {
    override fun onUpdate(
        context: Context,
        appWidgetManager: AppWidgetManager,
        appWidgetIds: IntArray
    ) {
        // There may be multiple widgets active, so update all of them
        for (appWidgetId in appWidgetIds) {
            updateAppWidget(context, appWidgetManager, appWidgetId)
        }
    }

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

        if (context == null || intent == null) return

        when (intent.action) {
            COUNT_UP -> {
                //　カウント値を読み込み
                val dataStore = context.getSharedPreferences("widget", Context.MODE_PRIVATE)
                var clickCount = dataStore.getInt("clickCount", -1)

                clickCount++

                //　カウントアップ後のカウント値を書き込み
                dataStore.edit().putInt("clickCount", clickCount).commit()

                //TextViewにカウント値を適用
                val views = RemoteViews(context.packageName, R.layout.sample_app_widget)
                views.setTextViewText(R.id.counter, clickCount.toString())

                // ウィジェットを更新
                val myWidget = ComponentName(context, SampleAppWidget::class.java)
                val manager = AppWidgetManager.getInstance(context)
                manager.updateAppWidget(myWidget, views)
            }
        }
    }
}

//onUpdateのappWidgetId毎の処理はこちらで実装する
internal fun updateAppWidget(
    context: Context,
    appWidgetManager: AppWidgetManager,
    appWidgetId: Int
) {

    // SharedPreferencesからデータを読み出し（登録されていない場合は0）
    val dataStore = context.getSharedPreferences("widget", Context.MODE_PRIVATE)
    var clickCount = dataStore.getInt("clickCount", -1)
    if (clickCount == -1) {
        dataStore.edit().putInt("clickCount", 0).apply()
        clickCount = 0
    }

    // RemoteViews オブジェクトを作成
    val views = RemoteViews(context.packageName, R.layout.sample_app_widget)

    //TextViewにカウント値を適用
    views.setTextViewText(R.id.counter, clickCount.toString())

    //Button押下通知用のPendingIntentを作成しに登録
    val countIntent = Intent(context, SampleAppWidget::class.java).apply { action = COUNT_UP }
    val countPendingIntent =
        PendingIntent.getBroadcast(context, 0, countIntent, PendingIntent.FLAG_UPDATE_CURRENT)
    views.setOnClickPendingIntent(R.id.add_button, countPendingIntent)

    // ウィジェットを更新
    appWidgetManager.updateAppWidget(appWidgetId, views)
}
```

# 修正毎のソースコード

ウィジェットのレイアウトファイルの変更はありません。

AppWidgetProviderを継承したクラスのファイルを以下のように修正しています。

```SampleAppWidget.kt
//com.example.android.stackwidgetの部分は自分のアプリのIDに修正する
private const val COUNT_UP = "com.example.android.buttonwidgettest.COUNT_UP"

class SampleAppWidget : AppWidgetProvider() {
    override fun onUpdate(
        context: Context,
        appWidgetManager: AppWidgetManager,
        appWidgetIds: IntArray
    ) {
        // There may be multiple widgets active, so update all of them
        for (appWidgetId in appWidgetIds) {
            updateAppWidget(context, appWidgetManager, appWidgetId)
        }
    }

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

        if (context == null || intent == null) return

        when (intent.action) {
            COUNT_UP -> {

                // 修正：IntentからappWidgetIdを取得し，appWidgetIdを用いてにカウント値を読み書き
                val appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, 0)

                //　カウント値を読み込み
                val dataStore =
                    context.getSharedPreferences("widgetClickCount", Context.MODE_PRIVATE)
                var clickCount = dataStore.getInt(appWidgetId.toString(), -1)

                clickCount++

                //　カウントアップ後のカウント値を書き込み
                dataStore.edit().putInt(appWidgetId.toString(), clickCount).commit()

                //TextViewにカウント値を適用
                val views = RemoteViews(context.packageName, R.layout.sample_app_widget)
                views.setTextViewText(R.id.counter, clickCount.toString())

                // 修正：ウィジェット更新を通知する際に,appWidgetIdを指定
                // ウィジェットを更新
                val manager = AppWidgetManager.getInstance(context)
                manager.updateAppWidget(appWidgetId, views)
            }
        }
    }
}

//appWidgetId毎のonUpdateの処理はこちらで実装する
internal fun updateAppWidget(
    context: Context,
    appWidgetManager: AppWidgetManager,
    appWidgetId: Int
) {

    // 修正：SharedPreferencesにカウント値を保存する際，appWidgetId毎に保存するように修正G

    // SharedPreferencesからデータを読み出し（登録されていない場合は0）
    val dataStore = context.getSharedPreferences("widgetClickCount", Context.MODE_PRIVATE)
    var clickCount = dataStore.getInt(appWidgetId.toString(), -1)
    if (clickCount == -1) {
        dataStore.edit().putInt(appWidgetId.toString(), 0).apply()
        clickCount = 0
    }

    // RemoteViews オブジェクトを作成
    val views = RemoteViews(context.packageName, R.layout.sample_app_widget)

    //TextViewにカウント値を適用
    views.setTextViewText(R.id.counter, clickCount.toString())

    //Button押下通知用のPendingIntentを作成しに登録
    val countIntent = Intent(context, SampleAppWidget::class.java).apply {
        action = COUNT_UP
        // 修正：IntentにappWidgetIdを付加
        putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
    }

    // 修正：PendingIntentのrequestCodeにappWidgetIdを指定
    val countPendingIntent =
        PendingIntent.getBroadcast(
            context,
            appWidgetId,
            countIntent,
            PendingIntent.FLAG_UPDATE_CURRENT
        )
    
    views.setOnClickPendingIntent(R.id.add_button, countPendingIntent)

    // ウィジェットを更新
    appWidgetManager.updateAppWidget(appWidgetId, views)
}
```


修正点は以下のとおりです。
修正1・4は，このアプリの特有の仕様によるものですのであまり重要ではありませんが，
修正2・3・5は，ウィジェット毎に各自の動作を行いたい場合には必須の実装かと思います。

####修正1
SharedPreferencesにカウント値を保存する際，keyをappWidgetId，valueをカウント値として，appWidgetId毎にカウント値を保存するように修正しました。
####修正2
Intentに，putExtraを用いてappWidgetIdを付加しました。
これにより，Intentを受信した際，onReceiveでappWidgetIdを参照することが可能になります。
####修正3
PendingIntentのrequestCodeにappWidgetIdを指定しました。

※因みに余談ですが，他の修正点を実装したうえでappWidgetIdを設定しない（=0を設定）場合はどうなるかというと，どのボタンを押しても最新のウィジェットの値だけが更新されるという現象が起こります。これは，FLAG_UPDATE_CURRENTフラグの挙動に関係あります。このフラグを設定した際，同じrequestCodeで新規にPendingIntentを作成する度，いままで作成したPendingIntentに設定したIntentが全て，新しく設定したIntentに置き換わってしまいます。（つまりCURRENTのものにUPDATEされている）
他のFLAGに関しては[こちらの記事](http://mashi.exciton.jp/archives/tag/pendingintent)で分かりやすく解説されています。（他のFLAGを設定した場合にもそれぞれでおかしな動作が発生しますので興味ある方は試してみてください。）とりあえずどのFLAGを設定する場合でも，別々のPendingIntentを複数作成したい場合には，requestCodeにはユニークな値を設定したほうが良いです。

####修正4
onReceive内で，受け取ったIntentからappWidgetIdを取得し，appWidgetIdを用いてSharedPreferencesにカウント値を読み書きするようにしました
####修正5
onReceive内で，ウィジェット更新を通知する際に，appWidgetIdを指定するようにしました

#おわりに

もし説明や実装におかしな点がありましたら是非コメントください。
修正させて頂きます。