Skip to content

Instantly share code, notes, and snippets.

@sergejsha
Last active October 11, 2024 04:39
Show Gist options
  • Save sergejsha/ad70fdb3afcaa7a38fe29effb49d0bf6 to your computer and use it in GitHub Desktop.
Save sergejsha/ad70fdb3afcaa7a38fe29effb49d0bf6 to your computer and use it in GitHub Desktop.
Pure-Compose ViewModel (like Molecule, but without Molecule)
package de.halfbit.seventysix
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
import org.jetbrains.compose.ui.tooling.preview.Preview
import kotlin.time.Duration.Companion.seconds
@Composable
fun App(viewModel: AppViewModel) {
MaterialTheme {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
val state = viewModel.state()
Button(onClick = state.onLoadRss) { Text("Load RSS") }
if (state.loading) Text("Loading...")
if (state.rssResult.isNotEmpty()) Text(state.rssResult)
}
}
}
@Preview
@Composable
fun PreviewApp() {
App(
viewModel = object : AppViewModel {
@Composable
override fun state(): State = State(
loading = true,
rssResult = "",
onLoadRss = {},
)
}
)
}
data class State(
val loading: Boolean,
val rssResult: String,
val onLoadRss: () -> Unit,
)
sealed interface Event {
data object LoadRss : Event
}
interface AppViewModel {
@Composable
fun state(): State
}
class DefaultAppViewModel(
private val fetchRss: suspend () -> String =
{ delay(3.seconds); "RSS data is here" },
) : AppViewModel {
@Composable
override fun state(): State {
var loading by remember { mutableStateOf(false) }
var rssResult by remember { mutableStateOf("") }
val onLoadRes = { post(Event.LoadRss) }
onEvent { event ->
when (event) {
Event.LoadRss -> {
if (loading) return@onEvent
loading = true
rssResult = ""
launch {
rssResult = fetchRss()
loading = false
}
}
}
}
return State(
loading = loading,
rssResult = rssResult,
onLoadRss = onLoadRes,
)
}
private val events =
MutableSharedFlow<Event>(extraBufferCapacity = 20)
@Composable
private inline fun onEvent(crossinline block: CoroutineScope.(event: Event) -> Unit) {
LaunchedEffect(Unit) {
events.collect { event ->
block(event)
}
}
}
private fun post(event: Event) {
if (!events.tryEmit(event)) {
error("Buffer overflow")
}
}
}
// to be put under src/commonTest/kotlin
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.runComposeUiTest
import de.halfbit.seventysix.DefaultAppViewModel
import de.halfbit.seventysix.State
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlin.test.Test
import kotlin.test.assertEquals
@OptIn(ExperimentalCoroutinesApi::class)
class AppViewModelTest {
@OptIn(ExperimentalTestApi::class)
@Test
fun test_Fancy_VM() = runComposeUiTest {
mainClock.autoAdvance = false
val model = DefaultAppViewModel()
lateinit var state: State
setContent {
state = model.state()
}
// initial state
assertEquals(false, state.loading)
assertEquals("", state.rssResult)
// post event
state.onLoadRss()
waitForIdle()
// before data is loaded
assertEquals(true, state.loading)
assertEquals("", state.rssResult)
// let rss data to arrive
mainClock.advanceTimeBy(3_000)
// after data is loaded
assertEquals(false, state.loading)
assertEquals("RSS data is here", state.rssResult)
}
}
package de.halfbit.seventysix
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
App(DefaultAppViewModel())
}
}
}
@Preview
@Composable
fun AppAndroidPreview() {
App(DefaultAppViewModel())
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment