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")
SN | File Name | Purpose |
---|---|---|
1 | ArticleDao.kt | DAO interface for Room to fetch, insert, and delete articles. |
2 | NewsDatabase.kt | Room database setup class, provides access to ArticleDao . |
3 | ArticleEntity.kt | Entity class representing each news article in Room DB. |
4 | NewsApi.kt | Retrofit interface to fetch top headlines from the API. |
5 | NewsResponse.kt | Data class representing the entire API response structure. |
6 | ArticleDto.kt | Represents a single news article from API; maps to ArticleEntity . |
7 | NewsRepository.kt | Handles data logic between API, Room, and ViewModel. |
8 | NewsApp.kt → Rename to NewsApplication.kt | Hilt-enabled Application class for initializing Dependency Injection. |
9 | AppModule.kt | Provides Retrofit, Room DB, DAO, and Repository using Hilt. |
10 | NewsScreen.kt | Jetpack Compose screen showing a list of news articles. |
11 | NewsViewModel.kt | Manages UI state and data fetching using the repository. |
12 | MainActivity.kt | App’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"