現在時刻の表示など、一定間隔で値を更新したい場合があります。
さらに状況によっては更新間隔を変更したり、更新を止めたりしたい場合もあるでしょう。
そのような場合の実装例です。
まず汎用の、一定間隔(間隔は変更可能)で 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),
)
}
}
}
/以上