Consuming JSON APIs with Jetpack Compose

Photo by Logan Voss on Unsplash

Photo by Logan Voss on Unsplash

Updated 10. December 2025

Consuming web-based JSON APIs is quite different on Android than in web development.

In this article, I will guide you through setting up an MVVM (Model-View-ViewModel) architecture using Jetpack Compose and Kotlin in Android Studio.

Permissions

To make the app able to connect to the internet, you will have to add the user-permission to the Android manifest

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-permission android:name="android.permission.INTERNET" />

    <application>
        ...
    </application>
</manifest>

Gradle Script

To make it easier for us to perform HTTP requests, use JSON data in our app, and render images, add the following dependencies to your build.gradle.kts (Module :app)

build.gradle.kts (:app)
...
dependencies {
    // ViewModel
    implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0")

    // Network calls
    implementation("com.squareup.retrofit2:retrofit:2.9.0")

    // JSON to Kotlin object mapping
    implementation("com.squareup.retrofit2:converter-gson:2.9.0")

    // Image loader
    implementation("io.coil-kt:coil-compose:2.6.0")

    ....
}

Sync the Gradle build by clicking on "Sync now". This will download the dependencies.

Data Class

A class to store the data from the API

When fetching data from the web API, the data must be placed in objects.

Add a Data Class Kotlin file appropriately named after the data you will get from the API.

ArticleThumbnail.kt
package no.haxor.jsonapi

data class ArticleThumbnail(
    val title: String,
    val image: String
)

The attributes of the ArticleThumbnail class match 1:1 with the names of the values in the JSON received from the API. In this example, I only use two of the attributes coming from the JSON API.

Model / API Service

To fetch the data and store it in memory, you have to create a Model acting as an API service. This Model will be used to get the data from the API.

ThumbnailApiService.kt
package no.haxor.jsonapi

import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET

private val retrofit = Retrofit.Builder()
    .baseUrl("https://haxor.no/actions/haxor/")
    .addConverterFactory(GsonConverterFactory.create())
    .build()

val thumbnailApiService= retrofit.create(ThumbnailApiService::class.java)

interface ThumbnailApiService {
    @GET("articles/all")
    suspend fun getThumbnails(): List<ArticleThumbnail>
}

The getThumbnails() function in the ThumbnailApiInterface will automagically transform the list of articles in the JSON response from the API, to a list of ArticlesThumbnail objects.

By creating this abstraction in this Model, we can focus on storing the data in memory in the ViewModel, and presenting the data in the View.

ViewModel

Connecting the view to the model

Add a Kotlin class contaning the ViewModel.

ThumbnailViewModel.kt
package no.haxor.myrecipeapp

import android.util.Log
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.compose.runtime.State // Manually import this!
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch

class ThumbnailViewModel: ViewModel() {
    private val _thumbnailState = mutableStateOf(ThumbnailState())
    val thumbnailState : State<ThumbnailState> = _thumbnailState

    init {
        fetchThumbnails()
    }

    private fun fetchThumbnails(){
        viewModelScope.launch { // Async scope.
            try {
                val response = thumbnailApiService.getThumbnails()
                _thumbnailState .value = _thumbnailState .value.copy(
                    loading = false,
                    list = response,
                    error = null
                )
            } catch (e: Exception){
                _thumbnailState .value = _thumbnailState .value.copy(
                    loading = false,
                    error = "Error fetching thumbnails. ${e.message}"
                )
                Log.i("info", e.message.toString())
            }
        }
    }

    data class ThumbnailState(
        val loading: Boolean = true,
        val list: List<ArticleThumbnail> = emptyList(),
        val error: String? = null
    )
}

On lines 10 and 11, we create a private value to store the ViewModel's state and a public value accessible outside this class.

When the class is instantiated, it fetches data from the API. It stores it in a private variable, along with other related states, to indicate whether the data is being loaded and to track any errors.

By using the ViewModel, data from the API will remain in memory even if you suspend the app or rotate your device.

View

Displaying the data

Finally, you can use the ViewModel in a View using Jetpack Compose.

Create the Kotlin file containing the View.

ThumbnailView.kt
package no.haxor.jsonapi

import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.rememberAsyncImagePainter

@Composable
fun ThumbnailView(padding: PaddingValues){
    val thumbnailViewModel: ThumbnailViewModel = viewModel()
    val viewState by thumbnailViewModel.thumbnailState

    Box(modifier = Modifier.fillMaxSize().padding(padding)){
        when{
            viewState.loading -> {
                CircularProgressIndicator(Modifier.align(Alignment.Center))
            }
            viewState.error != null -> {
                Text(viewState.error.toString())
            }
            else -> {
                ThumbnailListing(viewState.list)
            }
        }
    }
}

@Composable
fun ThumbnailListing(thumbnails: List<ArticleThumbnail>){
    LazyVerticalGrid(
        columns = GridCells.Fixed(2)
    ) {
        items(thumbnails){
                thumbnail -> ThumbnailItem(thumbnail = thumbnail)
        }
    }
}

@Composable
fun ThumbnailItem(thumbnail: ArticleThumbnail){
    Column(
        modifier = Modifier.padding(8.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        Image(
            painter = rememberAsyncImagePainter(thumbnail.image),
            contentDescription = null,
            modifier = Modifier.fillMaxWidth()
                .aspectRatio(3f / 2f)
                .clip(RoundedCornerShape(12.dp)),
            contentScale = ContentScale.Crop
        )

        Text(
            text = thumbnail.title,
            color = Color.Black,
            style = TextStyle(fontWeight = FontWeight.Medium),
            modifier = Modifier.padding(2.dp).wrapContentSize(),
            textAlign = TextAlign.Center,
        )
    }
}

Finally, we can implement the View in the Main Activity.

MainActivity.kt
package no.haxor.jsonapi

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Scaffold
import androidx.compose.ui.Modifier
import no.haxor.jsonapi.ui.theme.JsonApiTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            JsonApiTheme{
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    ThumbnailView(
                        padding = innerPadding
                    )
                }
            }
        }
    }
}
Bilde / Image

The PoC in all its glory

Congratulations, you have now fetched some data from a JSON API and used it in an Android app using the MVVM architecture, and Jetpack Compose. 🎉