LoginSignup
0
0

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

Posted at

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

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

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

/**
 * 可変の間隔で [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 を実装したクラスを作っていますが、そうしなくてもかまいません。)

/**
 * [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 に組み込むなどします。

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),
            )
        }
    }
}

実行例

/以上

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0