【Kotlin】可変の間隔で値が更新される Flow

現在時刻の表示など、一定間隔で値を更新したい場合があります。
さらに状況によっては更新間隔を変更したり、更新を止めたりしたい場合もあるでしょう。

そのような場合の実装例です。

まず汎用の、一定間隔（間隔は変更可能）で `Unit` を emit する `Flow` を実装します。

```Kotlin
/**
 * 可変の間隔で [Unit] を emit する [Flow]。
 *
 * 間隔はコンストラクター引数で指定するが、
 * [setDuration] 関数で変更できる。
 *
 * @param duration emit 間隔。
 *  負の値は不可。
 *  null の場合、[setDuration] 関数で null でない値に変更されるまで emit を行わない。
 *  null でない場合、最初に一度 emit し、その後指定された間隔ごとに emit する。
 */
class VariableTicker(
    duration: Duration? = null,
) : Flow<Unit> {
    init {
        requireDuration(duration)
    }

    /** emit 間隔。 */
    private val duration: MutableStateFlow<Duration?> = MutableStateFlow(duration)

    /**
     * 指定された間隔ごとに [Unit] を emit する [Flow]。
     *
     * this が実装する [Flow] はこのインスタンスに処理を委譲する。
     */
    private val _variableTicker: Flow<Unit> =
        @OptIn(ExperimentalCoroutinesApi::class)
        this.duration
            .flatMapLatest { duration ->
                duration
                    ?.let { createTicker(it) }
                    ?: emptyFlow()
            }

    /**
     * emit 間隔を変更する。
     *
     * @param duration emit 間隔。
     *  負の値は不可。
     *  null の場合、emit を行わない。
     *  null でない場合、変更直後に一度 emit し、その後指定された間隔ごとに emit する。
     */
    fun setDuration(duration: Duration?) {
        requireDuration(duration)

        this.duration.value = duration
    }

    override suspend fun collect(collector: FlowCollector<Unit>) {
        // 委譲
        _variableTicker.collect(collector)
    }

    companion object {
        /**
         * 指定された間隔が妥当でなければ [IllegalArgumentException] をスローする。
         */
        private fun requireDuration(duration: Duration?) {
            require(duration == null || duration.isNegative().not()) {
                "`duration` に負の値を指定することはできません。duration: $duration"
            }
        }

        /**
         * 最初と指定された間隔ごとに [Unit] を emit する [Flow] を生成して返す。
         */
        private fun createTicker(duration: Duration): Flow<Unit> = flow {
            while (true) {
                emit(Unit)
                delay(duration)
            }
        }
    }
}
```

次にそれを使って、一定間隔（間隔は変更可能）で値が更新される `StateFlow` を実装します。
（この例では `StateFlow` を実装したクラスを作っていますが、そうしなくてもかまいません。）

```Kotlin
/**
 * [LocalTime] を保持する [StateFlow]。
 *
 * 保持している [LocalTime] は指定された間隔ごとにその時点での [LocalTime] に更新される。
 * 更新間隔は [setUpdatingDuration] 関数から変更できる。
 *
 * @param updatingDuration 更新間隔。
 *  負の値は不可。
 *  null の場合、インスタンス生成時に更新した後、
 *  [setUpdatingDuration] 関数で null でない値が設定されるまで、更新を行わない。
 *  null でない場合、最初に一度更新し、その後指定された間隔ごとに更新する。
 */
class LocalTimeStateFlow(
    updatingDuration: Duration?,
    coroutineScope: CoroutineScope,
) : StateFlow<LocalTime> {
    private val variableTicker = VariableTicker(updatingDuration)

    /**
     * [LocalTime] を保持する [StateFlow]。
     *
     * this が実装する [StateFlow] はこのインスタンスに処理を委譲する。
     */
    private val localTime: StateFlow<LocalTime> = variableTicker
        .map { LocalTime.now() }
        .stateIn(
            coroutineScope,
            SharingStarted.WhileSubscribed(),
            LocalTime.now(),
        )

    /**
     * 更新間隔を変更する。
     *
     * @param updatingDuration 更新間隔。
     *  負の値は不可。
     *  null の場合、更新を行わない。
     *  null でない場合、変更直後に一度更新し、その後指定された間隔ごとに更新する。
     */
    fun setUpdatingDuration(updatingDuration: Duration?) {
        variableTicker.setDuration(updatingDuration)
    }

    override val replayCache: List<LocalTime>
        get() = localTime.replayCache

    override val value: LocalTime
        get() = localTime.value

    override suspend fun collect(collector: FlowCollector<LocalTime>): Nothing {
        localTime.collect(collector)
    }
}
```

最後に、それを UI に組み込むなどします。

```Kotlin
val coroutineScope = rememberCoroutineScope()

val localTimeStateFlow by remember {
    mutableStateOf(
        LocalTimeStateFlow(
            updatingDuration = null,
            coroutineScope,
        )
    )
}
val localTime by localTimeStateFlow.collectAsState()

val updatingDurationOptions = listOf(
    "1秒" to 1.seconds,
    "2秒" to 2.seconds,
    "3秒" to 3.seconds,
    "更新しない" to null,
)
var selectedOption by remember { mutableStateOf(updatingDurationOptions.last()) }

Column(Modifier.padding(8.dp)) {
    // 現在時刻
    Row {
        Text(
            "現在時刻: ",
            Modifier.align(Alignment.CenterVertically),
        )
        OutlinedTextField(
            value = localTime.toString(),
            onValueChange = {},
            Modifier.align(Alignment.CenterVertically),
            readOnly = true,
        )
    }

    Spacer(Modifier.height(8.dp))

    // 更新間隔
    Text("更新間隔: ")
    updatingDurationOptions.forEach { option ->
        val (text, duration) = option

        Row {
            RadioButton(
                selected = option == selectedOption,
                onClick = {
                    selectedOption = option

                    localTimeStateFlow.setUpdatingDuration(duration)
                },
                Modifier.align(Alignment.CenterVertically),
            )
            Text(
                text,
                Modifier.align(Alignment.CenterVertically),
            )
        }
    }
}
```

![実行例](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61280/fd78b6ca-6c16-d6b9-1ab2-0144907f51bf.gif)


／以上