如何使用UiState变量在Android界面间传递API调用结果?
问题描述
我正在开发一款Android应用,通过API调用获取公交预计到站时间。ViewModel中已通过Bus Repository完成API调用,listResult包含所需数据。请问如何将API调用结果传递到应用界面?是否应该使用UiState变量来实现?
AppViewModel.kt
package com.example.busexpress.ui.screens import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory import com.example.busexpress.BusExpressApplication import com.example.busexpress.data.SingaporeBusRepository import com.example.busexpress.network.SingaporeBus import kotlinx.coroutines.launch import retrofit2.HttpException import java.io.IOException /** * [AppViewModel] holds information about a cupcake order in terms of quantity, flavor, and * pickup date. It also knows how to calculate the total price based on these order details. */ class AppViewModel(private val singaporeBusRepository: SingaporeBusRepository): ViewModel() { /** The mutable State that stores the status of the most recent request */ var busUiState: BusUiState by mutableStateOf(BusUiState.Loading) // Loading as Default Value // Setter is private to protect writes to the busUiState private set /** * Call init so we can display status immediately. */ init { getBusTimings(null) } fun getBusTimings(userInput: String?) { // Determine if UserInput is a BusStopCode var busStopCode: String? var busServiceNumber: String? val userInputLength = userInput?.length ?: 0 if (userInputLength == 5) { // Bus Stop Code busStopCode = userInput busServiceNumber = null } else { // Bus Stop Code busStopCode = null busServiceNumber = userInput } // Launch the Coroutine using a ViewModelScope viewModelScope.launch { busUiState = BusUiState.Loading // Might have Connectivity Issues busUiState = try { // Within this Scope, use the Repository, not the Object to access the Data, abstracting the data within the Data Layer val listResult: SingaporeBus = singaporeBusRepository.getBusTimings( busServiceNumber = busServiceNumber, busStopCode = busStopCode ) // Assign results from backend server to busUiState {A mutable state object that represents the status of the most recent web request} BusUiState.Success(timings = listResult) } catch (e: IOException) { BusUiState.Error } catch (e: HttpException) { BusUiState.Error } } } // Factory Object to retrieve the singaporeBusRepository and pass it to the ViewModel companion object { val Factory: ViewModelProvider.Factory = viewModelFactory { initializer { val application = (this[APPLICATION_KEY] as BusExpressApplication) val singaporeBusRepository = application.container.singaporeBusRepository AppViewModel(singaporeBusRepository = singaporeBusRepository) } } } // private val _uiState = MutableStateFlow(AppUiState(65199)) // val uiState: StateFlow<AppUiState> = _uiState.asStateFlow() } // Simply saving the UiState as a Mutable State prevents us from saving the different status // like Loading, Error, and Success sealed interface BusUiState { data class Success(val timings: SingaporeBus) : BusUiState // The 2 States below need not set new data and create new objects, which is why an object is sufficient for the web response object Error: BusUiState object Loading: BusUiState // Sealed Interface used instead of Interface to remove Else Branch }
DefaultScreen.kt
package com.example.busexpress.ui.screens import androidx.annotation.StringRes import androidx.compose.foundation.Image import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Search import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import com.example.busexpress.BusExpressApp import com.example.busexpress.BusExpressScreen import com.example.busexpress.R import com.example.busexpress.network.SingaporeBus import com.example.busexpress.ui.component.BusStopComposable @Composable fun DefaultScreen( busUiState: BusUiState, modifier: Modifier = Modifier, appViewModel: AppViewModel = viewModel(), // navController: NavController ) { // Mutable State for User Input var userInput = remember { mutableStateOf(TextFieldValue("")) } Column( modifier = modifier .fillMaxWidth() .padding(16.dp), ) { // Search Field for Bus Stop or Bus Numbers SearchView( label = R.string.search_field_instructions, state = userInput, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Number, imeAction = ImeAction.Search ), onKeyboardSearch = { appViewModel.getBusTimings(userInput.value.text) // navController.navigate(BusExpressScreen.Search.name) } ) val busArrivalsJson = appViewModel.getBusTimings(userInput.value.text) when(busUiState) { is BusUiState.Success -> ResultScreen(busUiState = busUiState, busArrivalsJSON = busArrivalsJson) is BusUiState.Loading -> LoadingScreen() is BusUiState.Error -> ErrorScreen() else -> ErrorScreen() } } } @Composable fun LoadingScreen(modifier: Modifier = Modifier) { Box( contentAlignment = Alignment.Center, modifier = modifier .fillMaxSize() ) { Image( modifier = Modifier.size(200.dp), painter = painterResource(id = R.drawable.loading_img), contentDescription = stringResource(R.string.loading_flavor_text) ) } } @Composable fun ErrorScreen(modifier: Modifier = Modifier) { Box( contentAlignment = Alignment.Center, modifier = modifier.fillMaxSize() ) { Text(text = stringResource(R.string.loading_failed_flavor_text)) } } /** * The home screen displaying result of fetching photos. */ @Composable fun ResultScreen( busUiState: BusUiState, busArrivalsJSON: SingaporeBus, modifier: Modifier = Modifier, ) { // Results of Search BusStopComposable( busArrivalsJSON = busArrivalsJSON, modifier = modifier ) // Box( // contentAlignment = Alignment.Center, // modifier = modifier.fillMaxSize() // ) { // Text(busUiState.toString()) // } } @Composable fun SearchView( @StringRes label: Int, state: MutableState<TextFieldValue>, modifier: Modifier = Modifier, keyboardOptions: KeyboardOptions, onKeyboardSearch: () -> Unit, ) { Column() { TextField( value = state.value, onValueChange = {value -> state.value = value }, label = { if (state.value == TextFieldValue("")) { Text( stringResource(id = label), modifier = Modifier .fillMaxWidth(), style = MaterialTheme.typography.h6 ) } }, modifier = modifier .fillMaxWidth(), singleLine = true, // Search Icon at the Start for Aesthetics leadingIcon = { Icon( imageVector = Icons.Filled.Search, contentDescription = null, modifier = Modifier.padding(10.dp) ) }, // Cancel Button to delete all Input trailingIcon = { // Icon appears iif the Search Field is not Empty if (state.value != TextFieldValue("")) { IconButton(onClick = { // Clear the Search Field state.value = TextFieldValue("") }) { Icon( imageVector = Icons.Filled.Close, contentDescription = "Delete all User Input", modifier = Modifier.padding(10.dp) ) } } }, keyboardActions = KeyboardActions( onSearch = { onKeyboardSearch() } ), keyboardOptions = keyboardOptions, shape = RoundedCornerShape(25) ) Row() { Spacer(modifier = modifier.weight(3f)) // Button for User to Click to begin Search Button( onClick = { // TODO Pass the User Query to the Search Function onKeyboardSearch() }, modifier = modifier .align(Alignment.CenterVertically) .padding(2.dp) .weight(1f), ) { Text(text = stringResource(R.string.search_button_flavor_text)) } } } }
BusApi.kt
package com.example.busexpress.network import com.example.busexpress.LTA_API_SECRET_KEY import retrofit2.http.GET import retrofit2.http.Headers import retrofit2.http.Query interface BusApiService { /** * Function to get JSON Objects from URI by specifying Type of Request and Endpoint like "/photos" a URL of sorts */ // 1. Returns Bus Timings for Bus Stop and/or Service Number @Headers( "accept: application/json", "AccountKey: $LTA_API_SECRET_KEY" ) @GET("BusArrivalv2") suspend fun getTimingsOfBusStop( @Query("BusStopCode") BusStopCode: String? = null, @Query("ServiceNo") ServiceNo: String? = null ): SingaporeBus // 2. Returns the details for all the Bus Stops in Singapore @Headers( "accept: application/json", "AccountKey: $LTA_API_SECRET_KEY" ) @GET("BusStops") suspend fun getDetailsOfBusStop(): BusStop }
解决方案
关于UiState的使用
你当前实现的BusUiState是完全正确的方案。用密封接口封装Loading/Success/Error三种状态,能清晰区分API请求的不同阶段,同时确保界面能响应状态变化自动更新UI。
当前代码的问题及修复
你的DefaultScreen存在几个关键错误,导致API结果无法正确传递到界面:
- 错误地将
busUiState作为参数传入,没有直接读取ViewModel的实时状态,导致界面无法感知状态变化 - 重复调用
getBusTimings,一次在搜索回调、一次在Column中,造成不必要的API请求 - 试图直接从
getBusTimings获取返回值,但该函数是无返回值的,结果已经存储在busUiState的Success状态中
修改后的核心代码
@Composable fun DefaultScreen( modifier: Modifier = Modifier, appViewModel: AppViewModel = viewModel(factory = AppViewModel.Factory) ) { // 直接读取ViewModel中的实时状态 val busUiState = appViewModel.busUiState val userInput = remember { mutableStateOf(TextFieldValue("")) } Column( modifier = modifier .fillMaxWidth() .padding(16.dp), ) { SearchView( label = R.string.search_field_instructions, state = userInput, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Number, imeAction = ImeAction.Search ), onKeyboardSearch = { appViewModel.getBusTimings(userInput.value.text) } ) // 根据状态渲染对应UI when(busUiState) { is BusUiState.Success -> ResultScreen(timings = busUiState.timings) is BusUiState.Loading -> LoadingScreen() is BusUiState.Error -> ErrorScreen() } } } @Composable fun ResultScreen( timings: SingaporeBus, modifier: Modifier = Modifier, ) { BusStopComposable( busArrivalsJSON = timings, modifier = modifier ) }
关键要点说明
- 直接绑定ViewModel状态:通过
appViewModel.busUiState获取实时状态,由于busUiState用mutableStateOf修饰,Compose会自动感知变化并重组界面 - 移除重复API调用:仅在搜索回调中触发
getBusTimings,避免冗余请求 - 从Success状态提取数据:在
when分支中直接从BusUiState.Success取出timings,传递给结果界面 - 正确使用ViewModel工厂:获取ViewModel时指定
factory = AppViewModel.Factory,确保Repository能正确注入
修改后,API请求的状态变化会实时反映到界面:加载时显示加载动画,成功时展示公交到站数据,失败时显示错误提示。
内容的提问来源于stack exchange,提问作者Michelin_Boi
相关产品推荐
相关产品推荐

