Android
02 / 03

Architecture: ViewModel, StateFlow & Room

Android: Architecture ViewModel, StateFlow & Room

ViewModel & StateFlow

// ViewModel — survives configuration changes (rotation)
// implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")

class ArticlesViewModel(
    private val repository: ArticleRepository
) : ViewModel() {

    // UI state
    private val _uiState = MutableStateFlow(ArticlesUiState())
    val uiState: StateFlow<ArticlesUiState> = _uiState.asStateFlow()

    init {
        loadArticles()
    }

    fun loadArticles() {
        viewModelScope.launch {
            _uiState.update { it.copy(isLoading = true) }
            try {
                val articles = repository.getArticles()
                _uiState.update { it.copy(articles = articles, isLoading = false) }
            } catch (e: Exception) {
                _uiState.update { it.copy(error = e.message, isLoading = false) }
            }
        }
    }

    fun deleteArticle(id: Int) {
        viewModelScope.launch {
            repository.deleteArticle(id)
            loadArticles()
        }
    }
}

data class ArticlesUiState(
    val articles: List<Article> = emptyList(),
    val isLoading: Boolean = false,
    val error: String? = null,
)

// In Composable
@Composable
fun ArticlesScreen(viewModel: ArticlesViewModel = hiltViewModel()) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

    when {
        uiState.isLoading -> CircularProgressIndicator()
        uiState.error != null -> Text("Error: ${uiState.error}")
        else -> ArticlesList(articles = uiState.articles)
    }
}

Room Database

// implementation("androidx.room:room-runtime:2.6.1")
// implementation("androidx.room:room-ktx:2.6.1")
// kapt("androidx.room:room-compiler:2.6.1")

@Entity(tableName = "articles")
data class ArticleEntity(
    @PrimaryKey(autoGenerate = true) val id: Int = 0,
    val title: String,
    val content: String,
    val createdAt: Long = System.currentTimeMillis(),
)

@Dao
interface ArticleDao {
    @Query("SELECT * FROM articles ORDER BY createdAt DESC")
    fun getAllArticles(): Flow<List<ArticleEntity>>   // Flow = reactive stream

    @Query("SELECT * FROM articles WHERE id = :id")
    suspend fun getArticleById(id: Int): ArticleEntity?

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insert(article: ArticleEntity)

    @Update
    suspend fun update(article: ArticleEntity)

    @Delete
    suspend fun delete(article: ArticleEntity)

    @Query("DELETE FROM articles WHERE id = :id")
    suspend fun deleteById(id: Int)
}

@Database(entities = [ArticleEntity::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
    abstract fun articleDao(): ArticleDao

    companion object {
        @Volatile private var INSTANCE: AppDatabase? = null

        fun getDatabase(context: Context): AppDatabase {
            return INSTANCE ?: synchronized(this) {
                Room.databaseBuilder(
                    context.applicationContext,
                    AppDatabase::class.java,
                    "app_database"
                ).build().also { INSTANCE = it }
            }
        }
    }
}

Repository Pattern

class ArticleRepository(private val dao: ArticleDao, private val api: ArticleApi) {

    // Combine local DB (offline-first) with network
    val articles: Flow<List<Article>> = dao.getAllArticles()
        .map { entities -> entities.map { it.toArticle() } }

    suspend fun refreshArticles() {
        val remoteArticles = api.getArticles()
        dao.insertAll(remoteArticles.map { it.toEntity() })
    }

    suspend fun deleteArticle(id: Int) {
        dao.deleteById(id)
    }
}

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free