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.

Revisions

  1. sergejsha revised this gist Oct 10, 2024. 1 changed file with 44 additions and 0 deletions.
    44 changes: 44 additions & 0 deletions AppViewModelTest.kt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,44 @@
    // 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)
    }
    }
  2. sergejsha created this gist Oct 9, 2024.
    112 changes: 112 additions & 0 deletions App.kt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,112 @@
    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")
    }
    }
    }
    23 changes: 23 additions & 0 deletions MainActivity.kt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,23 @@
    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())
    }