How to Build a News App in Android Using Jetpack Compose

How to Build a News App in Android Using Jetpack Compose, Retrofit, and Room – A Complete Step-by-Step Guide
In the age of rapid digital transformation, staying updated with real-time information has become as natural as checking the weather in the morning. Whether it's global headlines, tech news, sports highlights, or entertainment buzz — we all consume news on the go. But have you ever wondered how these news apps are actually built?

Imagine building your own personalized news app, fetching live data from a public API, showing trending stories, caching them for offline access, and presenting them in a modern and interactive UI — all using Jetpack Compose, Android’s most exciting UI toolkit.

In this in-depth tutorial, we’ll go beyond the basics and walk you through the process of building a complete, real-world News App in Android from scratch using Jetpack Compose, Retrofit, Room Database, and Hilt Dependency Injection. This is not just another simple app — we’ll structure it using the MVVM pattern, handle real-time API responses from NewsAPI.org, cache the results locally for offline support, and display it beautifully using Compose.

This project is perfect if you're:

  • An Android developer learning Jetpack Compose
  • Preparing for interviews or freelance gigs
  • Trying to impress recruiters with a practical Android app
  • Building your own blog or content app
  • Curious about how Room, Retrofit, and Compose work together
Throughout this guide, you’ll not only learn how to write clean, maintainable Kotlin code, but also how modern Android apps are structured in production.

So grab a cup of chai(tea) or coffee, open Android Studio, and follow along — by the end of this article, you’ll have a working News App that pulls live data, works offline, and looks great on any screen.

Let’s get started! 🚀

How to Build a News App in Android Using Jetpack Compose, Retrofit, and Room – A Complete Step-by-Step Guide

🏗️ Project Setup

🔌 Step 1. Create New Android Project

  • Name: NewsPlus
  • Language: Kotlin
  • Minimum SDK: 21+
Note:   Internet Permission Required to fetch news from API
<uses-permission android:name="android.permission.INTERNET"/>

🔌 Step 2. Dependencies in build.gradle(:app)

plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
// 2️⃣ Then apply KSP
id("com.google.devtools.ksp")
// 1️⃣ Apply Hilt plugin first
id("com.google.dagger.hilt.android")

}
 // Compose
implementation("androidx.navigation:navigation-compose:2.9.0")

// Hilt
implementation("com.google.dagger:hilt-android:2.56.2")
implementation("androidx.room:room-common:2.7.1")
ksp("com.google.dagger:hilt-compiler:2.56.2")

// Retrofit
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")

implementation("androidx.room:room-runtime:2.7.1")
implementation("androidx.room:room-ktx:2.7.1")
ksp("androidx.room:room-compiler:2.7.1")

// Coil (Image loading)
implementation("io.coil-kt:coil-compose:2.6.0")
Android News APP using Jetpack Compose File Structure
SN File Name Purpose
1ArticleDao.ktDAO interface for Room to fetch, insert, and delete articles.
2NewsDatabase.ktRoom database setup class, provides access to ArticleDao.
3ArticleEntity.ktEntity class representing each news article in Room DB.
4NewsApi.ktRetrofit interface to fetch top headlines from the API.
5NewsResponse.ktData class representing the entire API response structure.
6ArticleDto.ktRepresents a single news article from API; maps to ArticleEntity.
7NewsRepository.ktHandles data logic between API, Room, and ViewModel.
8NewsApp.kt → Rename to NewsApplication.ktHilt-enabled Application class for initializing Dependency Injection.
9AppModule.ktProvides Retrofit, Room DB, DAO, and Repository using Hilt.
10NewsScreen.ktJetpack Compose screen showing a list of news articles.
11NewsViewModel.ktManages UI state and data fetching using the repository.
12MainActivity.ktApp’s entry point. Loads Compose UI and launches NewsScreen().

🔌 Step 3: Retrofit API Setup

NewsApi.kt

package com.example.newshub

import retrofit2.http.GET
import retrofit2.http.Query

interface NewsApi {
@GET("v2/top-headlines")
suspend fun getTopHeadlines(
@Query("country") country: String = "us",
@Query("apiKey") apiKey: String
): NewsResponse
}

NewsResponse.kt

package com.example.newshub

data class NewsResponse(
val status: String,
val totalResults: Int,
val articles: List<ArticleDto>
)

ArticleDto.kt

package com.example.newshub

data class ArticleDto(
val title: String?,
val description: String?,
val urlToImage: String?,
val url: String?
) {
fun toEntity(): ArticleEntity {
return ArticleEntity(
title = title ?: "No Title",
description = description ?: "No description available",
imageUrl = urlToImage ?: "",
url = url ?: ""
)
}
}

🗄️ Step 4: Room Setup

ArticleEntity.kt

package com.example.newshub

import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "articles")
data class ArticleEntity(
@PrimaryKey val title: String,
val description: String,
val imageUrl: String,
val url: String
)

ArticleDao.kt

package com.example.newshub

import androidx.room.Dao
import androidx.room.Query
import kotlinx.coroutines.flow.Flow
import androidx.room.Insert
import androidx.room.OnConflictStrategy

@Dao
interface ArticleDao {
@Query("SELECT * FROM articles")
fun getAllArticles(): Flow<List<ArticleEntity>>

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertArticles(articles: List<ArticleEntity>)

@Query("DELETE FROM articles")
suspend fun clearArticles()
}

NewsDatabase.kt

package com.example.newshub

import androidx.room.Database
import androidx.room.RoomDatabase

@Database(entities = [ArticleEntity::class], version = 1)
abstract class NewsDatabase : RoomDatabase() {
abstract fun articleDao(): ArticleDao
}

🧠 Step 5: Repository

NewsRepository.kt

package com.example.newshub

import kotlinx.coroutines.flow.Flow
import javax.inject.Inject

class NewsRepository @Inject constructor(
private val api: NewsApi,
private val dao: ArticleDao
) {
val articles: Flow<List<ArticleEntity>> = dao.getAllArticles()

suspend fun fetchAndSaveNews(apiKey: String) {
val response = api.getTopHeadlines(apiKey = apiKey)
val articles = response.articles.map { it.toEntity() }
dao.clearArticles()
dao.insertArticles(articles)
}
}

🧩 Step 6: Dependency Injection

AppModule.kt

package com.example.newshub

import android.content.Context
import androidx.room.Room
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object AppModule {

@Provides
@Singleton
fun provideNewsApi(): NewsApi = Retrofit.Builder()
.baseUrl("https://newsapi.org/")
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(NewsApi::class.java)

@Provides
@Singleton
fun provideDatabase(@ApplicationContext context: Context): NewsDatabase =
Room.databaseBuilder(context, NewsDatabase::class.java, "news_db").build()

@Provides
fun provideDao(db: NewsDatabase) = db.articleDao()

@Provides
@Singleton
fun provideRepository(api: NewsApi, dao: ArticleDao): NewsRepository =
NewsRepository(api, dao)
}

🎯 Step 7: ViewModel

NewsViewModel.kt

package com.example.newshub

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
class NewsViewModel @Inject constructor(
private val repository: NewsRepository
) : ViewModel() {

val articles = repository.articles.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)

init {
viewModelScope.launch {
try {
repository.fetchAndSaveNews("YOUR_API_KEY")
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}

🎨 Step 8: UI with Jetpack Compose

NewsScreen.kt

package com.example.newshub

import android.content.Intent
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import coil.compose.AsyncImage
import androidx.core.net.toUri

@Composable
fun NewsScreen(viewModel: NewsViewModel = hiltViewModel()) {
val articles by viewModel.articles.collectAsState()
val context = LocalContext.current
Column(
Modifier.fillMaxSize().systemBarsPadding()
) {
Box(Modifier.fillMaxWidth().background(Color.Black),
contentAlignment = Alignment.Center){
Text(stringResource(R.string.app_name),
Modifier.padding(10.dp),
color = Color.Red,
style = MaterialTheme.typography.displayMedium)
}
Row (Modifier.fillMaxWidth().padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween){
Text("Made with Jetpack Compose", style = MaterialTheme.typography.titleMedium)
Text("CodingBihar", style = MaterialTheme.typography.titleMedium)
}
LazyColumn {
items(articles) { article ->
Card(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
elevation = CardDefaults.cardElevation(4.dp)
) {
Column(modifier = Modifier.padding(8.dp)) {

article.imageUrl.takeIf { it.isNotEmpty() }?.let { imageUrl ->
AsyncImage(
model = imageUrl,
contentDescription = null,
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
)
}

Text(
text = article.title,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(top = 4.dp)
)

Text(
text = article.description,
modifier = Modifier.padding(vertical = 4.dp)
)

Text(
text = "Read More",
color = Color.Blue,
modifier = Modifier
.clickable {
val uri = article.url.toUri()
val intent = Intent(Intent.ACTION_VIEW, uri)
context.startActivity(intent)
}
.padding(top = 4.dp)
)
}
}
}
}
}
}

🚀 Step 9: MainActivity Setup

MainActivity.kt

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
NewsHubTheme {
/* Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Greeting(
name = "Android",
modifier = Modifier.padding(innerPadding)
)
}*/
NewsScreen()
}
}
}
}

🚀 Step 10: 🔑 2. Create the Application Class

NewsAPP

package com.example.newshub

import android.app.Application
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
class NewsApp : Application()

➡️ Register it in AndroidManifest.xml:

android:name=".NewsApp"

Final Output:

News App in Android Using Jetpack Compose Sreenshot

Previous Post Next Post