Mastering API Integration in Jetpack Compose: A Comprehensive Guide Using MoviesDB API


Jetpack Compose has revolutionized how we build UIs in Android apps, but beautiful UIs mean little if your app can’t communicate with the outside world. That’s where API integration comes in.
In this hands-on guide, we’ll go beyond static previews and learn how to fetch and display real-time movie data using the MoviesDB API. You’ll learn how to set up Retrofit, Moshi, and Hilt the right way, build a clean architecture around them, and present data through sleek, modern Compose UIs.
Whether you’re building a client project, a startup MVP, or your next side hustle, this article is your practical blueprint to integrating APIs in Jetpack Compose like a pro.
Basics of API Integration in Jetpack Compose
APIs are the backbone of modern mobile applications, enabling them to fetch data from remote servers. When it comes to Jetpack Compose, integrating APIs involves setting up network calls, managing state, and displaying the fetched data in a composable UI. Let’s dive into the steps required to achieve this.
Setting Up Your Project
Before diving into API integration, ensure that your development environment is ready.
Before we start coding, let’s ensure that your development environment is ready. Make sure you have the latest version of Android Studio and Kotlin installed.
Step 1: Create a New Jetpack Compose Project (Official guide)
Open Android Studio and select “New Project”.
Choose “Empty Compose Activity” from the project templates.
Name your project and set the minimum SDK to 21 (or higher).
Click “Finish” to create your project.
Step 2: Add Required Dependencies
To make network calls, we’ll use the following libraries:
Retrofit: For making HTTP requests.
Moshi: For JSON parsing.
Hilt: For dependency injection.
Add the following dependencies to your build.gradle
file:
// In your project-level build.gradle
plugins {
...
id("com.google.dagger.hilt.android") version "2.48" apply false
}
// In your app-level build.gradle
plugins {
...
id 'kotlin-kapt'
id 'com.google.dagger.hilt.android'
}
android {
...
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
}
dependencies {
...
...
implementation("androidx.compose.ui:ui:1.6.8")
implementation("androidx.compose.material:material:1.6.8")
implementation("androidx.compose.ui:ui-tooling-preview:1.6.8")
implementation("androidx.activity:activity-compose:1.7.2")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2")
// Networking
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-moshi:2.9.0")
implementation("com.squareup.moshi:moshi-kotlin:1.13.0")
implementation("com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.2")
// Dependency Injection
implementation("com.google.dagger:hilt-android:2.48")
kapt("com.google.dagger:hilt-compiler:2.48")
implementation("androidx.hilt:hilt-navigation-compose:1.2.0")
// Navigation
implementation("androidx.navigation:navigation-compose:2.7.7")
// Image loading
implementation("io.coil-kt:coil-compose:2.7.0")
}
kapt {
correctErrorTypes = true
}
Make sure to sync your project after adding these dependencies.
That’s it. Now we have enabled Compose in our Android project. Now we are ready to move forward.
Structuring Your Jetpack Compose Project
Before diving into API integration, it’s essential to establish a well-organized folder structure. Here’s a recommended structure for your project:
data/api
: Contains classes for API service interfaces.data/model
: Contains data models representing API responses.data/repository
: Contains repository classes to handle data operations.di
: Contains dependency injection-related classes.ui/theme
: Contains theme-related classes like colors, typography, etc.ui/components
: Contains reusable UI components.ui/screens
: Contains composable functions representing different screens.viewmodel
: Contains ViewModel classes for managing UI-related data.
Setting Up the MoviesDB API Integration
Step 1: Define the API Service
First, create an interface that defines the endpoints for the MoviesDB API. In the data/api
folder, create a file named MoviesApiService.kt
:
interface MoviesApiService {
@GET("movie/popular")
suspend fun getPopularMovies(
@Header("Authorization") token: String,
@Query("page") page: Int
): Response
@GET("movie/{movie_id}")
suspend fun getMovieDetails(
@Header("Authorization") token: String,
@Path("movie_id") movieId: Int
): Response
}
Step 2: Create Data Models
Next, create data models to represent the JSON structure of the API responses. In the data/model
folder, create files Movie.kt
and MoviesResponse.kt
and add the below classes in it respectively.
data class Movie(
val id: Int,
val title: String,
val overview: String,
val poster_path: String
)
data class MoviesResponse(
val results: List
)
Next, add a data model for the movie details response. In the data/model
folder, create a file named MovieDetailsResponse.kt
:
data class MovieDetailsResponse(
val id: Int,
val title: String,
val overview: String,
val poster_path: String,
val release_date: String,
val runtime: Int,
val vote_average: Float
)
Step 3: Set Up Retrofit and Hilt
Now, let’s set up Retrofit to handle the API calls. In the di
folder, create a file named NetworkModule.kt
:
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl("https://api.themoviedb.org/3/")
.addConverterFactory(MoshiConverterFactory.create())
.build()
}
@Provides
@Singleton
fun provideMoviesApiService(retrofit: Retrofit): MoviesApiService {
return retrofit.create(MoviesApiService::class.java)
}
}
Implementing the Repository Pattern
Repositories serve as an intermediary between the API and the ViewModel. In the data/repository
folder, create a file named MoviesRepository.kt
:
@Singleton
class MoviesRepository @Inject constructor(
private val apiService: MoviesApiService
) {
suspend fun getPopularMovies(token: String, page: Int): List {
val response = apiService.getPopularMovies(token, page)
return if (response.isSuccessful) {
response.body()?.results ?: emptyList()
} else {
emptyList()
}
}
suspend fun getMovieDetails(token: String, movieId: Int): MovieDetailsResponse? {
val response = apiService.getMovieDetails(token, movieId)
return if (response.isSuccessful) {
response.body()
} else {
null
}
}
}
Creating the ViewModel
The ViewModel will manage the UI state and interact with the repository. In the viewmodel
folder, create a file named MoviesViewModel.kt
:
@HiltViewModel
class MoviesViewModel @Inject constructor(
private val repository: MoviesRepository
) : ViewModel() {
private val _movies = mutableStateOf<List>(emptyList())
val movies: State<List> = _movies
private val _movieDetails = mutableStateOf<MovieDetailsResponse?>(null)
val movieDetails: State<MovieDetailsResponse?> = _movieDetails
init {
getMovies()
}
private fun getMovies() {
viewModelScope.launch {
try {
val moviesList = repository.getPopularMovies("Bearer YOUR_ACCESS_TOKEN", 1)
_movies.value = moviesList
} catch (e: Exception) {
// Handle exceptions
}
}
}
fun getMovieDetails(movieId: Int) {
viewModelScope.launch {
try {
val details = repository.getMovieDetails("Bearer YOUR_ACCESS_TOKEN", movieId)
_movieDetails.value = details
} catch (e: Exception) {
// Handle exceptions
}
}
}
}
Building the UI with Jetpack Compose
Now, let’s create the UI to display the list of movies. In the ui/screens
folder, create a file named MoviesScreen.kt
:
@Composable
fun MoviesScreen(navController: NavController, viewModel: MoviesViewModel = hiltViewModel()) {
val movies by viewModel.movies
LazyColumn {
items(movies) { movie ->
MovieItem(movie = movie, onClick = {
navController.navigate("movie_details/${movie.id}")
})
}
}
}
@Composable
fun MovieItem(movie: Movie, onClick: () -> Unit) {
Row(
modifier = Modifier
.padding(8.dp)
.clickable { onClick() }
) {
AsyncImage(
model = "https://image.tmdb.org/t/p/w500${movie.poster_path}",
contentDescription = null,
modifier = Modifier.size(120.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Column {
Text(text = movie.title, style = MaterialTheme.typography.h6)
Text(text = movie.overview, maxLines = 3, overflow = TextOverflow.Ellipsis)
}
}
}
Now, create the UI to display the movie details. In the ui/screens
folder, create a new file named MovieDetailsScreen.kt
:
@Composable
fun MovieDetailsScreen(movieId: Int, viewModel: MoviesViewModel = hiltViewModel()) {
val movieDetails by remember { viewModel.movieDetails }
LaunchedEffect(movieId) {
viewModel.getMovieDetails(movieId)
}
movieDetails?.let { details ->
Column(
modifier = Modifier.padding(16.dp)
) {
Text(text = details.title, style = MaterialTheme.typography.h4)
Text(text = "Released: ${details.release_date}")
Text(text = "Runtime: ${details.runtime} minutes")
Text(text = "Rating: ${details.vote_average}/10")
Spacer(modifier = Modifier.height(8.dp))
AsyncImage(
model = "https://image.tmdb.org/t/p/w500${details.poster_path}",
contentDescription = null,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
Text(text = details.overview)
}
}
}
Setting Up the Navigation Graph
Finally, create a navigation graph in your MainActivity.kt
:
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApp {
MoviesNavGraph()
}
}
}
}
@Composable
fun MoviesNavGraph(startDestination: String = "movies_list") {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = startDestination) {
composable("movies_list") {
MoviesScreen(navController = navController)
}
composable(
"movie_details/{movieId}",
arguments = listOf(navArgument("movieId") { type = NavType.IntType })
) { backStackEntry ->
val movieId = backStackEntry.arguments?.getInt("movieId") ?: return@composable
MovieDetailsScreen(movieId = movieId)
}
}
}
Setting Up Hilt for Dependency Injection
Now, create an Application
class and annotate it with @HiltAndroidApp
: in the root of the package.
@HiltAndroidApp
class MyApp : Application()
This step sets up Hilt in your project. Hilt will now be able to inject dependencies into your Android components.
Now, in your AndroidManifest.xml
file, add network permission and MyApp
name in the Application Tag.
<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
android:name=".MyApp"
...
>
...
</application>
</manifest>
Now our application is finally ready😃. You can run it on a real device or emulator to bring your app to life and experience the smooth, intuitive UI firsthand. This marks the beginning of your journey into building more complex and feature-rich applications with Jetpack Compose.
Wrapping Up
With both popular movie listings and detailed pages wired up through MoviesDB API, you’ve now built a full-fledged Jetpack Compose app that actually feels alive. The clean architecture, modern tools like Hilt, Retrofit, and Coil, and the declarative UI all come together to showcase how powerful Compose can be when used right.
As you continue to explore and experiment, remember that the world of Jetpack Compose is vast and constantly evolving. Whether you’re refining this app, building something new, or integrating more advanced features, the possibilities are endless.
But we’re not done yet! In the upcoming parts of this Compose series, we’ll dive into real-world enhancements like local caching, paginated loading, and animating transitions that delight users.
🚀 If you found this guide helpful, consider bookmarking it or dropping a reaction to support more in-depth tutorials.
🔔 Follow @themodularmindset to stay updated with the next articles in the Jetpack Compose Mastery series.
Subscribe to my newsletter
Read articles from Dhaval Asodariya directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Dhaval Asodariya
Dhaval Asodariya
👨💻 Software Engineer | 💡 Kotlin Expert | 📱 Android Enthusiast Previously SDE-III at DhiWise, I’m a Kotlin-focused developer passionate about building scalable, modern software-primarily on Android, but also exploring AI 🤖 and backend technologies. I use this space to share practical insights, clean code practices, and thoughts on the future of tech 🚀