💡

Glance入門 はじめの一歩 - 2 (Activity/Service/Workerの起動など)

2024/01/08に公開

前回は一覧を表示するウィジェットを作りました。

この記事では、クリックの処理やバックグラウンド処理の方法を追記していきます。

アクティビティを起動し、データを渡す

clickable()メソッドの引数のActionインターフェイスにその実装クラス StartActivityClassActionクラスを返すactionStartActivity()メソッドを呼び出すだけです。
その引数として、ActionParametersクラスを渡す、つまり、actionParametersOf()メソッドで値を設定することで、対象のActivityにデータを渡せます。

                items(samples.size) { id ->
                    Text(
                        text = "${LocalContext.current.getString(R.string.widget_title)} ${samples[id]}",
                        modifier = GlanceModifier
                            .fillMaxWidth()
                            .padding(8.dp)
+                           .clickable(actionStartActivity<MainActivity>(
+                               actionParametersOf(
+                                   ActionParameters.Key<Int>(MainActivity.widgetItemKey) to id
+                               ))
+                           ),
                        style = TextStyle(
                            fontWeight = FontWeight.Bold,
                            fontSize = 18.sp,
                        ),
                    )
                }

受ける側は、intent.extrasで取得できます。

class MainActivity : ComponentActivity() {

    companion object {
        const val widgetItemKey = "widgetItemKey"
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val valueFromParam = intent?.extras?.getInt(widgetItemKey, -1)

サービスを起動する

これも同じように用意されているactionStartService()メソッドを呼び出すだけです。
最近ではサービス利用のほとんどは、フォアグランドサービスだと思うので覚えることはこれぐらいかと思います。

.clickable(actionStartService<MyService>(
    isForegroundService = true,
)),

止めるときのメソッドは無さそうなので、普通に以下で止められます。

val intent = Intent(context, MyForegroundService::class.java)
context.stopService(intent)

ちなみにサンプルとしてサービス自体を記述しときます。上記でactionStartService()が呼ばれると通知を表示します。stopService()が呼ばれるとonDestory()が呼ばれます(通知も削除されます)。

class MyForegroundService : Service(), CoroutineScope {

    private val job = Job()
    override val coroutineContext: CoroutineContext
        get() = Dispatchers.IO + job

    override fun onBind(intent: Intent): IBinder? {
        return null
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        val channelId = "service"

        val builder = NotificationCompat.Builder(applicationContext, channelId).also {
            it.setContentTitle("通知のタイトルです")
            it.setContentText("通知のテキストです")
            it.setSmallIcon(R.drawable.ic_launcher_foreground)
        }.build()

        val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            if (manager.getNotificationChannel(channelId) == null) {
                val mChannel = NotificationChannel(channelId, "チャンネルのタイトル", NotificationManager.IMPORTANCE_HIGH)
                mChannel.apply {
                    description = "概要"
                }
                manager.createNotificationChannel(mChannel)
            }
        }

        startForeground(1, builder)

        return START_STICKY
    }

    override fun onDestroy() {
        super.onDestroy()
        stopSelf()
    }
}

権限をAndroidManifest.xmlファイルに追加する必要があります。

    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

特にAndroid 13以降の場合は、通知許可の実装も必要です。

通知許可に限らずウィジェットを動かす際に必ず必要になる設定は、<appwidget-provider>android:configureでActivityを作ってそこで設定をするのがいいでしょう。これは別途記事にします。

BroadcastReceiverを起動する

公式マニュアルでは、actionSendBroadcast<MyReceiver>()が記述されていますが、ほとんどの場合は、action自体を指定する気がするので、そのサンプルを載せておきます。

Button(
    text = "Broadcast送信",
    onClick = actionSendBroadcast(
        MyBroadcastReceiver.ACTION_RESTART_APP,
        ComponentName(context, MyBroadcastReceiver::class.java)
    ),
)

サンプルとして、ここでのBroadcastReceiverのクラスでは、アプリ自体を再起動するものです。

class MyBroadcastReceiver : BroadcastReceiver() {

    companion object {

        const val ACTION_RESTART_APP = "ACTION_RESTART_APP"
    }

    override fun onReceive(context: Context, intent: Intent) {
        if (intent.action != null && intent.action.equals(ACTION_RESTART_APP)) {
            restartApplication(context)
        }
    }

    private fun restartApplication(context: Context) {
        val restartIntent = context.packageManager
            .getLaunchIntentForPackage(context.packageName)
        restartIntent!!.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
        context.startActivity(restartIntent)
    }
}

Workerを起動する

これはウィジェットから呼び出す場合は、対象のWorkerをインスタンス化して、WorkManagerクラスにリクエストをenqueueするだけです。以下は、MyWorkerという独自のWorkerを呼び出しています。
OneTimeWorkRequestBuilderを使うことで一度だけ起動するWorkerを作っています。

Button(
        text = "Worker起動",
        onClick = {
            val appWidgetId = GlanceAppWidgetManager(context).getAppWidgetId(id)
            val request = OneTimeWorkRequestBuilder<MyWorker>()
                .setInputData(
                    workDataOf(
                        KEY_APP_WIDGET_IDS to appWidgetIds,
                    )
                )
                .build()
            WorkManager.getInstance(context).enqueue(request)
       })

で、Glanceのウィジェットの状態管理は、Workerを使うときは、DataStoreを使うのが定石で、currentState()メソッドを呼び出すことによって、DataStoreに保存されている値を取得できます。
ここでは、LazyColumnに指定するリストで利用してます。

+   companion object {
+       val PREF_KEY_LIST = stringSetPreferencesKey("pref_list")
+       val INITIAL_STATE = setOf("0", "1", "2", "3", "4", "5")
+   }
・・・・    
    override suspend fun provideGlance(context: Context, id: GlanceId) {
        provideContent {
+           val pref: Preferences = currentState()
+           val state = pref[PREF_KEY_LIST] ?: INITIAL_STATE
            Content(id, state)
        }
    }
・・・
                LazyColumn(
                    modifier = GlanceModifier
                        .fillMaxSize()
                        .padding(16.dp)
                ) {
                    items(state.toList()) { id ->
    

ここでは、たとえば、以下のようにMyWorkerクラスを作成しています。このクラスは、上記のLazyColumnのアイテムのタイトルを変更するだけです。

Workerクラスの実装ポイントはCoroutineWorkerクラスを継承し、doWork()メソッドを実装します。
また、getForegroundInfo()を実装することで、Android 12以前でも通知に表示することができます。
で、Glanceの状態は、DataStoreにあるので、updateAppWidgetState()メソッドを使って、DataStoreの値を更新しておき、ウィジェットのupdate()を呼び出して、更新しています。

class MyWorker constructor(
    private val context: Context,
    private val params: WorkerParameters,
) : CoroutineWorker(context, params) {

    companion object {

        const val KEY_APP_WIDGET_IDS = "key_app_widget_ids"
        fun start(context: Context, appWidgetIds: IntArray) {
            val request = OneTimeWorkRequestBuilder<MyWorker>()
                .setInputData(
                    workDataOf(
                        KEY_APP_WIDGET_IDS to appWidgetIds,
                    )
                )
                .build()
            WorkManager.getInstance(context).enqueue(request)
        }
    }

    override suspend fun doWork() = withContext(Dispatchers.IO) {
        val glanceAppWidgetManager = GlanceAppWidgetManager(context)
        val appWidgetIds = params.inputData.getIntArray(KEY_APP_WIDGET_IDS) ?: throw IllegalArgumentException()
        val widget = MyGlanceAppWidget()
        appWidgetIds
            .map { appWidgetId -> glanceAppWidgetManager.getGlanceIdBy(appWidgetId) }
            .map { glanceId ->
                launch {
                    updateAppWidgetState(context, glanceId) { preferences ->
                        val current = preferences[MyGlanceAppWidget.PREF_KEY_LIST] ?: MyGlanceAppWidget.INITIAL_STATE
                        preferences[MyGlanceAppWidget.PREF_KEY_LIST] =
                            current.map { (it.toInt() * it.toInt()).toString() }.toSet()
                    }
                    widget.update(context, glanceId)
                }
            }
            .joinAll()

        Result.success()
    }
}
NewsPicks の Zenn

Discussion