xvlaze
xvlaze

Reputation: 869

Update LazyColumn after API response in Jetpack Compose

I am completely new to Jetpack Compose AND Kotlin, but not to Android development in Java. Wanting to make first contact with both technologies, I wanted to make a really simple app which populates a LazyColumn with images from Dog API.

All the Retrofit connection part works OK, as I've managed to populate one card with a random puppy, but when the time comes to populate the list, it's just impossible. This is what happens:

  1. The interface is created and a white screen is shown.
  2. The API is called.
  3. Wait about 20 seconds (there's about 400 images!).
  4. dogImages gets updated automatically.
  5. The LazyColumn never gets recomposed again so the white screen stays like that.

Do you have any ideas? I can't find any tutorial on this matter, just vague explanations about state for scroll listening.

Here's my code:

class MainActivity : ComponentActivity() {
    private val dogImages = mutableStateListOf<String>()

    @ExperimentalCoilApi
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            PuppyWallpapersTheme {
                // A surface container using the 'background' color from the theme
                Surface(color = MaterialTheme.colors.background) {
                    DogList(dogImages)
                    searchByName("poodle")
                }
            }
        }
    }

    private fun getRetrofit():Retrofit {
        return Retrofit.Builder()
            .baseUrl("https://dog.ceo/api/breed/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }

    private fun searchByName(query: String) {
        CoroutineScope(Dispatchers.IO).launch {
            val call = getRetrofit().create(APIService::class.java).getDogsByBreed("$query/images")
            val puppies = call.body()
            runOnUiThread {
                if (call.isSuccessful) {
                    val images = puppies?.images ?: emptyList()
                    dogImages.clear()
                    dogImages.addAll(images)
                }
            }
        }
    }

    @ExperimentalCoilApi
    @Composable
    fun DogList(dogs: SnapshotStateList<String>) {
        LazyColumn() {
            items(dogs) { dog ->
                DogCard(dog)
            }
        }
    }

    @ExperimentalCoilApi
    @Composable
    fun DogCard(dog: String) {
        Card(
            modifier = Modifier
                .fillMaxWidth()
                .padding(15.dp),
            elevation = 10.dp
        ) {
            Image(
                painter = rememberImagePainter(dog),
                contentDescription = null
            )
        }
    }
}

Thank you in advance! :)

Upvotes: 4

Views: 7336

Answers (1)

Phil Dukhov
Phil Dukhov

Reputation: 88082

Your view of the image cannot determine the aspect ratio before it loads, and it does not start loading because the calculated height is zero. See this reply for more information.

Also a couple of tips about your code.

  1. Storing state inside MainActivity is bad practice, you can use view models. Inside a view model you can use viewModelScope, which will be bound to your screen: all tasks will be cancelled, and the object will be destroyed when the screen is closed.
  2. You should not make state-modifying calls directly from the view constructor, as you do with searchByName. This code can be called many times during recomposition, so your call will be repetitive. You should do this with side effects. In this case you can use LaunchedEffect, but you can also do it in the init view model, because it will be created when your screen appears.
  3. It's very convenient to pass Modifier as the last argument, in this case you don't need to add a comma at the end and you can easily add/remove modifiers.
  4. You may have many composables, storing them all inside MainActivity is not very convenient. A good practice is to store them simply in a file, and separate them logically by files.

Your code can be updated to the following:

class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            PuppyWallpapersTheme {
                DogsListScreen()
            }
        }
    }
}

@Composable
fun DogsListScreen(
    // pass the view model in this form for convenient testing
    viewModel: DogsModel = viewModel()
) {
    // A surface container using the 'background' color from the theme
    Surface(color = MaterialTheme.colors.background) {
        DogList(viewModel.dogImages)
    }
}

@Composable
fun DogList(dogs: SnapshotStateList<String>) {
    LazyColumn {
        items(dogs) { dog ->
            DogCard(dog)
        }
    }
}

@Composable
fun DogCard(dog: String) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(15.dp),
        elevation = 10.dp
    ) {
        Image(
            painter = rememberImagePainter(
                data = dog,
                builder = {
                    // don't use it blindly, it can be tricky.
                    // check out https://stackoverflow.com/a/68908392/3585796
                    size(OriginalSize)
                },
            ),
            contentDescription = null,
        )
    }
}

class DogsModel : ViewModel() {
    val dogImages = mutableStateListOf<String>()

    init {
        searchByName("poodle")
    }

    private fun getRetrofit(): Retrofit {
        return Retrofit.Builder()
            .baseUrl("https://dog.ceo/api/breed/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }

    private fun searchByName(query: String) {
        viewModelScope
            .launch {
                val call = getRetrofit()
                    .create(APIService::class.java)
                    .getDogsByBreed("$query/images")
                val puppies = call.body()
                if (call.isSuccessful) {
                    val images = puppies?.images ?: emptyList()
                    dogImages.clear()
                    dogImages.addAll(images)
                }
            }
    }
}

Upvotes: 10

Related Questions