I was developing a Jetpack Compose app, where I try to recover some data from the network and keep it into a local database.
I'm using the Marvel API to recover the data, and the call which bring me all the heros works fine, but I've not been able to launch the call which filter by id, duee to I try to do inside a LaunchEffect scope.
The code works fine, but I don't see in the logs, that the api call is made, and the value where I keep the hero information is empty, and then the app crash whithout show me any error.
The code of my screen and ViewModel are the following:
DetailScreen.kt
@Composable
fun HeroDetailScreen(
activity: Activity,
context: Context,
dominantColor: Color,
heroNumber: Long,
navController: NavController,
topPadding: Dp = 20.dp,
heroImageSize: Dp = 200.dp,
viewModel: HeroDetailViewModel = hiltViewModel()
) {
LaunchedEffect(key1 = heroNumber) {
viewModel.getHeroInfo(heroNumber)// The API call which should launch this method, it doesn't
}
val heroInfo = viewModel.hero.value
//heroInfo.data?.dataResponse.results = 14.70.toLong()
Box(
modifier = Modifier
.fillMaxSize()
.background(dominantColor)
.padding(bottom = 16.dp)
) {
HeroDetailTopSection(
activity = activity,
navController = navController,
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(0.2f)
.align(Alignment.TopCenter),
context = context,
viewModel = viewModel,
heroInfo = heroInfo[0],
heroNumber = heroNumber.toInt()
)
HeroDetailStateWrapper(
heroInfo = heroInfo[0],
modifier = Modifier
.fillMaxSize()
.padding(
top = topPadding heroImageSize / 2f,
start = 16.dp,
end = 16.dp,
bottom = 16.dp
)
.shadow(10.dp, RoundedCornerShape(10.dp))
.clip(RoundedCornerShape(10.dp))
.background(MaterialTheme.colors.surface)
.padding(16.dp)
.align(Alignment.BottomCenter),
loadingModifier = Modifier
.size(100.dp)
.align(Alignment.Center)
.padding(
top = topPadding heroImageSize / 2f,
start = 16.dp,
end = 16.dp,
bottom = 16.dp
)
)
Box(
contentAlignment = Alignment.TopCenter,
modifier = Modifier
.fillMaxSize()
) {
heroInfo[0].thumbnail.let {
CoilImage(
imageRequest = ImageRequest.Builder(LocalContext.current)
.data("$it")
.crossfade(true)
.build(),
imageLoader = {
ImageLoader.Builder(LocalContext.current)
.memoryCache(MemoryCache.Builder(LocalContext.current).maxSizePercent(0.25).build())
.crossfade(true)
.build()
},
contentDescription = heroInfo[0].name,
modifier = Modifier
.size(heroImageSize)
.offset(y = topPadding)
)
}
}
}
}
@Composable
fun HeroDetailTopSection(
activity: Activity,
navController: NavController,
modifier: Modifier = Modifier,
context:Context,
heroNumber: Int,
viewModel: HeroDetailViewModel,
heroInfo: MarvelListModel?
)
{
val isReadyToPay = (activity as MainActivity).possiblyShowGooglePayButton()
var imageVisibility = 0f
if(isReadyToPay){
imageVisibility = 1f
}
val width = Resources.getSystem().displayMetrics.widthPixels
val widthDp = convertPixelsToDp(px = width/2, context = context)
Box(
contentAlignment = Alignment.TopStart,
modifier = modifier
.background(
Brush.verticalGradient(
listOf(
Color.Black,
Color.Transparent
)
)
)
) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = null,
tint = Color.White,
modifier = Modifier
.size(36.dp)
.offset(16.dp, 16.dp)
.clickable {
navController.popBackStack()
}
)
Image(
painter = painterResource(id = R.drawable.buy_with_googlepay_button_content),
contentDescription = null,
alignment = Alignment.TopCenter,
modifier = Modifier
.alpha(imageVisibility)
.offset((widthDp / 2).dp, 16.dp)
.fillMaxWidth(0.5f)
.fillMaxHeight(0.6f)
.background(Color.Transparent)
.clickable {
if (!isReadyToPay) {
Toast
.makeText(
context,
activity.getString(R.string.googlepay_status_unavailable),
Toast.LENGTH_LONG
)
.show()
} else {
(activity as MainActivity).payWithGooglePay(
"$price", MarvelRoom(
id = heroInfo?.id,
numberId = heroInfo!!.id,
name = heroInfo?.name!!,
image = "${heroInfo?.thumbnail}",
bought = 0
)
)
}
}
)
}
}
@Composable
fun HeroDetailStateWrapper(
heroInfo: MarvelListModel,
modifier: Modifier = Modifier,
loadingModifier: Modifier = Modifier
) {
HeroDetailSection(heroInfo = heroInfo)
}
@Composable
fun HeroDetailSection(
heroInfo: MarvelListModel,
modifier: Modifier = Modifier
) {
val scrollState = rememberScrollState()
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier
.fillMaxSize()
.offset(y = 100.dp)
.verticalScroll(scrollState)
) {
Text(
text = "#${heroInfo.id} ${heroInfo.name.capitalize(Locale.ROOT)}",
fontWeight = FontWeight.Bold,
fontSize = 30.sp,
textAlign = TextAlign.Center,
color = MaterialTheme.colors.onSurface
)
Spacer(
modifier = Modifier
.size(1.dp, 20.dp)
.background(Color.LightGray)
)
}
}
HeroDetailViewModel.kt
@HiltViewModel
class HeroDetailViewModel @Inject constructor(
private val repository: MarvelRepository
): ViewModel() {
var loadError = mutableStateOf("")
var isLoading = mutableStateOf(false)
var hero = mutableStateOf<List<MarvelListModel>>(listOf())
suspend fun getHeroInfo(id : Long) {
viewModelScope.launch {
isLoading.value = true
val resp = repository.getHeroInfo(id)
when(resp){
is WrapperResponse.Sucess -> {
hero.value = MarvelMapper().fromResponse(resp.data!!.dataResponse)
loadError.value = ""
isLoading.value = false
}
is WrapperResponse.Error -> {
loadError.value = resp.message!!
isLoading.value = false
}
}
}
}
}
And the other layer of my network implementation, I swear there fine, because in the case which bring me all the hero's works fine, but they are this:
MarvelRepository.kt
@Singleton
class MarvelRepository @Inject constructor(
private val api: MarvelApi
) {
suspend fun getHeroList(limit:Int, offset:Int):WrapperResponse<CharacterDataContainerResponse>{
val response = try{
api.getCharacters(limit,offset)
}catch(e:Exception){
return WrapperResponse.Error("An Unknown error occured.")
}
return WrapperResponse.Sucess(response.dataResponse)
}
suspend fun getHeroInfo(heroId:Long):WrapperResponse<CharacterResultResponse>{
val response = try{
api.getCharacterById(heroId)
}catch(e:Exception){
return WrapperResponse.Error("An Unknown error occured.")
}
return WrapperResponse.Sucess(response)
}
}
MarvelAPI.kt
interface MarvelApi {
@GET("v1/public/characters")
suspend fun getCharacters(
@Query("limit") limit:Int,
@Query("offset") offset:Int,
): CharacterResultResponse
@GET("v1/public/characters/{characterId}")
suspend fun getCharacterById(
@Path("characterId") characterId: Long
): CharacterResultResponse
@GET("v1/public/comics")
suspend fun fetchComics(
@Query("characters") characterId: Long
): ComicDataWrapperResponse
}
I'm pretty sure that the problem is related which the LaunchEffect scope, due to the other API call it's working, and I don't using that scope.
So if you have some expirence related which this, take thanks in advance !
CodePudding user response:
First of all, you can check if LaunchedEffect
is being called or not by adding simple Log. If it is not called, try LaunchedEffect(Unit)
.
Another problem is that all network calls are async. That is why you need to wait for the result. So you can do:
val isLoading = viewModel.isLoading.value
if (isLoading) {
<LoadingComponent />
} else {
Box(
modifier = Modifier
.fillMaxSize()
.background(dominantColor)
.padding(bottom = 16.dp)
) {..............}
}
In addition, you can remove suspend
from getHeroInfo
of ViewModel