Kotlin多界面UiState与ViewModel设计及数据流转方案咨询
Android Bookshelf 应用架构设计解答
1. 是否需要为三个界面分别设置独立的UiState与ViewModel?
不需要完全独立,可根据界面职责关联度灵活拆分或合并:
- UiState:三个界面的状态结构差异明显(搜索输入/列表数据/详情数据),单独设计是合理的,能避免状态臃肿,让每个UiState只对应自身界面的展示需求。
- ViewModel:QueryScreen和BookListScreen属于同一业务流程(搜索→展示结果),共用一个ViewModel更高效,能避免跨ViewModel通信的复杂度;BookDetailsScreen是独立的详情查看流程,单独用一个ViewModel可保证职责单一,方便维护。
2. 符合Android官方设计模式的最优实现方案
Android官方推荐ViewModel + Repository + 单向数据流的架构,结合手动依赖注入,具体实现如下:
核心架构规则
- 单向数据流:UI事件 → ViewModel → Repository → 数据返回 → ViewModel更新UiState → UI渲染
- 职责划分:
- Repository:封装数据来源(网络/本地),统一处理API调用与异常,返回标准化结果
- ViewModel:持有UI状态,处理业务逻辑,调用Repository并将结果转换为UiState
- UI:仅负责展示UiState,发送用户交互事件给ViewModel
具体代码实现
1. 优化UiState设计
合并Query与BookList的状态(同属一个业务流),保留独立的详情状态:
// 合并搜索输入与列表展示的状态 data class QueryListUiState( val query: String = "", val books: List<Book> = emptyList(), val isLoading: Boolean = false, val errorMessage: String? = null ) // 详情界面独立状态 data class BookDetailsUiState( val book: Book? = null, val isLoading: Boolean = false, val errorMessage: String? = null )
2. ViewModel实现
设计两个ViewModel,分别对应「搜索-列表」和「详情」流程:
import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch // 处理搜索与列表逻辑的ViewModel class BookListViewModel( private val repository: BookshelfRepository ) : ViewModel() { private val _uiState = MutableStateFlow(QueryListUiState()) val uiState = _uiState.asStateFlow() // 更新搜索输入文本 fun onQueryChanged(newQuery: String) { _uiState.update { it.copy(query = newQuery) } } // 触发搜索请求 fun searchBooks() { val currentQuery = _uiState.value.query.trim() if (currentQuery.isEmpty()) return viewModelScope.launch { _uiState.update { it.copy(isLoading = true, errorMessage = null) } when (val result = repository.getBooks(currentQuery)) { is BookshelfResult.Success -> { _uiState.update { it.copy( books = result.data, isLoading = false ) } } is BookshelfResult.Error -> { _uiState.update { it.copy( errorMessage = result.exception.localizedMessage ?: "搜索失败", isLoading = false ) } } } } } } // 处理书籍详情逻辑的ViewModel class BookDetailsViewModel( private val repository: BookshelfRepository ) : ViewModel() { private val _uiState = MutableStateFlow(BookDetailsUiState()) val uiState = _uiState.asStateFlow() // 加载书籍详情 fun loadBookDetails(bookId: String) { viewModelScope.launch { _uiState.update { it.copy(isLoading = true, errorMessage = null) } when (val result = repository.getBook(bookId)) { is BookshelfResult.Success -> { _uiState.update { it.copy( book = result.data, isLoading = false ) } } is BookshelfResult.Error -> { _uiState.update { it.copy( errorMessage = result.exception.localizedMessage ?: "加载详情失败", isLoading = false ) } } } } } }
3. 手动依赖注入
在Activity/Application中手动创建依赖实例,传递给ViewModel:
import androidx.lifecycle.ViewModelProvider import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory // 创建ApiService实例 val bookshelfApiService = Retrofit.Builder() .baseUrl("https://www.googleapis.com/books/v1/") .addConverterFactory(GsonConverterFactory.create()) .build() .create(BookshelfApiService::class.java) // 创建Repository实例 val bookshelfRepository = DefaultBookshelfRepository(bookshelfApiService) // 在Activity中创建ViewModel val bookListViewModel = ViewModelProvider(this, object : ViewModelProvider.Factory { override fun <T : ViewModel> create(modelClass: Class<T>): T { return BookListViewModel(bookshelfRepository) as T } })[BookListViewModel::class.java] val bookDetailsViewModel = ViewModelProvider(this, object : ViewModelProvider.Factory { override fun <T : ViewModel> create(modelClass: Class<T>): T { return BookDetailsViewModel(bookshelfRepository) as T } })[BookDetailsViewModel::class.java]
4. UI层实现
Compose界面仅依赖ViewModel的状态,发送事件触发逻辑:
import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.* import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsStateWithLifecycle import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable // 搜索输入界面 @Composable fun QueryScreen( onSearchClicked: () -> Unit, query: String, onQueryChanged: (String) -> Unit, isLoading: Boolean ) { Column( modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.Center ) { TextField( value = query, onValueChange = onQueryChanged, label = { Text("搜索书籍") }, modifier = Modifier.fillMaxWidth() ) Spacer(modifier = Modifier.height(16.dp)) Button( onClick = onSearchClicked, enabled = !isLoading, modifier = Modifier.align(Alignment.CenterHorizontally) ) { if (isLoading) { CircularProgressIndicator(modifier = Modifier.size(20.dp)) } else { Text("搜索") } } } } // 书籍列表界面 @Composable fun BookListScreen( uiState: QueryListUiState, onBookClicked: (String) -> Unit ) { when { uiState.isLoading -> { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { CircularProgressIndicator() } } uiState.errorMessage != null -> { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text(text = uiState.errorMessage ?: "未知错误", color = Color.Red) } } uiState.books.isEmpty() -> { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text(text = "暂无书籍") } } else -> { LazyColumn(modifier = Modifier.padding(16.dp)) { items(uiState.books) { book -> BookItem(book = book, onClick = { onBookClicked(book.id) }) Spacer(modifier = Modifier.height(8.dp)) } } } } } // 书籍详情界面 @Composable fun BookDetailsScreen(uiState: BookDetailsUiState) { when { uiState.isLoading -> { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { CircularProgressIndicator() } } uiState.errorMessage != null -> { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text(text = uiState.errorMessage ?: "未知错误", color = Color.Red) } } uiState.book != null -> { Column(modifier = Modifier.padding(16.dp)) { Text(text = uiState.book.title, style = MaterialTheme.typography.h5) Spacer(modifier = Modifier.height(8.dp)) Text(text = "作者: ${uiState.book.authors?.joinToString(", ")}") // 其他详情字段... } } } } // 主界面导航与组合 @Composable fun BookshelfApp( navController: NavHostController, bookListViewModel: BookListViewModel = viewModel() ) { val uiState by bookListViewModel.uiState.collectAsStateWithLifecycle() NavHost(navController = navController, startDestination = "query_list") { composable("query_list") { Column(modifier = Modifier.fillMaxSize()) { QueryScreen( onSearchClicked = bookListViewModel::searchBooks, query = uiState.query, onQueryChanged = bookListViewModel::onQueryChanged, isLoading = uiState.isLoading ) BookListScreen( uiState = uiState, onBookClicked = { bookId -> navController.navigate("book_details/$bookId") } ) } } composable("book_details/{bookId}") { backStackEntry -> val bookId = backStackEntry.arguments?.getString("bookId") ?: return@composable val detailsViewModel: BookDetailsViewModel = viewModel() val detailsUiState by detailsViewModel.uiState.collectAsStateWithLifecycle() LaunchedEffect(bookId) { detailsViewModel.loadBookDetails(bookId) } BookDetailsScreen(uiState = detailsUiState) } } }
方案优势
- 数据流清晰:严格遵循单向数据流,状态变化可追踪,避免乱序更新
- 职责分离:各层职责明确,便于独立测试与维护
- 状态集中管理:同一业务流的状态集中在一个ViewModel,减少跨组件通信成本
- 符合官方规范:完全对齐Android Jetpack架构指南,适配Compose的生命周期特性
内容的提问来源于stack exchange,提问作者Andrei Rusu
相关产品推荐
相关产品推荐

