I am currently trying to write an Android Application that makes API calls to retrieve the estimated arrival timings of the incoming buses. The ViewModel files shows me using my Bus Repository to make the API Call, where listResult contains the data I want.
How can I pass the API Call I made to one of my Android Application's screens? I'm supposed to use the UiState Variable to do so right? Thank you.
The Pastebin contains my code as well if it's easier to see there!!
AppViewModel.kt https://pastebin.com/qPVrDF9i
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 Service Number
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
}
Example: DefaultScreen.kt https://pastebin.com/UiZPwZHG
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 (Contains the function to make the API Call) https://pastebin.com/miAt8x8H
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
}
CodePudding user response:
You can use SharedViewModel to share the data between the Fragments hosted by a common parent activity or between parent activity and its child fragments as per your need.
For eg, You can declare a MutableStateFlow variable in the SharedViewModel, set its value when you receive response from API call (which you trigger from one Fragment or parent activity) and then collect the value of that flow via the same SharedViewModel inside child Fragment and update the UI accordingly.
Refer https://developer.android.com/codelabs/basic-android-kotlin-training-shared-viewmodel#0