Getting Started

Table of contents

  1. Installation
  2. Basic Usage with LoadState
    1. ViewModel
    2. Composable
  3. Basic Usage with MutationState
    1. ViewModel
    2. Composable
  4. Choosing the Right State Class

Installation

Add the Maven Central repository to your settings.gradle.kts (if not already present):

dependencyResolutionManagement {
    repositories {
        mavenCentral()
    }
}

Then add the dependency to your build.gradle.kts:

implementation("com.felipearpa:viewing-state:1.0.0")

Basic Usage with LoadState

LoadState is the simplest state class, managing four states: Idle, Loading, Loaded, and Failure.

ViewModel

data class MyData(val id: Int, val name: String)

class MyViewModel : ViewModel() {
    private val _state = MutableStateFlow<LoadState<MyData>>(LoadState.Idle)
    val state: StateFlow<LoadState<MyData>> = _state.asStateFlow()

    fun fetchData() {
        viewModelScope.launch {
            _state.value = LoadState.Loading

            try {
                delay(2000)
                val result = MyData(1, "Hello, Android!")
                _state.value = LoadState.Loaded(result)
            } catch (exception: Exception) {
                _state.value = LoadState.Failure(exception)
            }
        }
    }

    fun resetState() {
        _state.value = LoadState.Idle
    }
}

Composable

@Composable
fun MyScreen(viewModel: MyViewModel = viewModel()) {
    val viewModelState by viewModel.state.collectAsState()

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    )
    when (val state = viewModelState) {
        is LoadState.Idle -> {
            Text("Click the button to load data.")
            Button(onClick = { viewModel.fetchData() }) {
                Text("Load Data")
            }
        }
        is LoadState.Loading -> {
            CircularProgressIndicator()
            Text("Loading...")
        }
        is LoadState.Loaded -> {
            Text("Data Loaded Successfully!")
            Text("ID: ${state().id}, Name: ${state().name}")
            Button(onClick = { viewModel.resetState() }) {
                Text("Load Again")
            }
        }
        is LoadState.Failure -> {
            Text("Error: ${state().message}")
            Button(onClick = { viewModel.fetchData() }) {
                Text("Retry")
            }
        }
    }
}

Basic Usage with MutationState

MutationState provides granular control over data mutations, tracking both original and updated values.

ViewModel

data class UserProfile(val displayName: String)

class ProfileViewModel : ViewModel() {
    private val _state = MutableStateFlow<MutationState<UserProfile>>(
        MutationState.Idle(UserProfile("Initial Name"))
    )
    val state = _state.asStateFlow()

    fun updateDisplayName(newName: String) {
        viewModelScope.launch {
            val currentState = _state.value
            val currentUserProfile = currentState.activeValue()

            val targetProfile = UserProfile(newName)
            _state.value =
                MutationState.Mutating(original = currentUserProfile, updated = targetProfile)

            try {
                delay(1500)
                if (newName.length < 3) {
                    throw IllegalArgumentException("Name must be at least 3 characters long.")
                }
                _state.value =
                    MutationState.Mutated(original = currentUserProfile, updated = targetProfile)
            } catch (e: Exception) {
                _state.value = MutationState.Failure(
                    original = currentUserProfile,
                    updated = targetProfile,
                    exception = e
                )
            }
        }
    }

    fun resetToIdle(initialProfile: UserProfile? = null) {
        val currentData = initialProfile ?: _state.value.activeValue()
        _state.value = MutationState.Idle(currentData)
    }
}

Composable

@Composable
fun ProfileEditScreen(viewModel: ProfileViewModel = viewModel()) {
    val viewModelState by viewModel.state.collectAsState()
    var editingName by remember { mutableStateOf("") }

    LaunchedEffect(viewModelState) {
        when (val state = viewModelState) {
            is MutationState.Idle -> editingName = state.value.displayName
            is MutationState.Mutated -> editingName = state.updated.displayName
            else -> {}
        }
    }

    Column(
        modifier = Modifier.fillMaxSize().padding(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.spacedBy(12.dp)
    ) {
        OutlinedTextField(
            value = editingName,
            onValueChange = { editingName = it },
            label = { Text("New Display Name") },
            isError = viewModelState.isFailure()
        )

        Button(
            onClick = { viewModel.updateDisplayName(editingName) },
            enabled = !viewModelState.isMutating() && editingName.isNotBlank()
        ) {
            Text("Save Name")
        }

        viewModelState.onFailure { _, _, exception ->
            Text("Error: ${exception.message}", color = MaterialTheme.colorScheme.error)
        }
    }
}

Choosing the Right State Class

Scenario Recommended State
Fetching data from an API LoadState
Submitting a form SaveState
Editing existing data with undo capability MutationState
Screen that loads data and allows edits ResourceState

This site uses Just the Docs, a documentation theme for Jekyll.