You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用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结果无法正确传递到界面:

  1. 错误地将busUiState作为参数传入,没有直接读取ViewModel的实时状态,导致界面无法感知状态变化
  2. 重复调用getBusTimings,一次在搜索回调、一次在Column中,造成不必要的API请求
  3. 试图直接从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
    )
}

关键要点说明

  1. 直接绑定ViewModel状态:通过appViewModel.busUiState获取实时状态,由于busUiState用mutableStateOf修饰,Compose会自动感知变化并重组界面
  2. 移除重复API调用:仅在搜索回调中触发getBusTimings,避免冗余请求
  3. 从Success状态提取数据:在when分支中直接从BusUiState.Success取出timings,传递给结果界面
  4. 正确使用ViewModel工厂:获取ViewModel时指定factory = AppViewModel.Factory,确保Repository能正确注入

修改后,API请求的状态变化会实时反映到界面:加载时显示加载动画,成功时展示公交到站数据,失败时显示错误提示。


内容的提问来源于stack exchange,提问作者Michelin_Boi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.07 10:05:22