Part 4 : Implementing News Api with Retrofit in App

Mohit Damke
12 min readJul 4, 2024

--

Here we will implement the home screen where we can see the Bottom Nav Bar and the news field where all news will be visible

You can have a look at the article on retrofit and api calling

News Api

  • Here is the news api website from where we have to generate our api key and then after we have to put it in the
  • News Api is the Api which help to get the news response from the internet on your device.

News Api Generation

  • First we have to create our Account and then after generate your api key.
  • Visit THIS for the api response
  • Check for the response references and then copy it and paste in the postman
  • Click on new request
  • Paste this link in the Input Box.
  • Make sure to remove GET Text from the api link.
  • Copy the response
  • And open android studio make one package “remote” in the “data” package.
  • And another “dto” package inside “remote”.
  • Add Plugin in your android studio
  • Then after click on the dto package and add new and click on Kotlin data class File from JSON.
  • And paste the response there.
  • And Click on Generate.
  • 3 files will be created depending on your response.
  • Make another package “model” inside “domain”.
  • And then after cut and paste 2 file except main file.

News Api Interface

  • We will create new file name NewsApi
package com.example.newsapp.data.remote

import com.example.newsapp.data.remote.dto.NewsResponse
import com.example.newsapp.util.Constants.API_KEY
import retrofit2.http.GET
import retrofit2.http.Query

interface NewsApi {

@GET("everything")
suspend fun getNews(
@Query("page") page: Int,
@Query("sources") sources: String,
@Query("apiKey") apiKey: String = API_KEY
): NewsResponse

@GET("everything")
suspend fun searchNews(
@Query("q") searchQuery: String,
@Query("page") page: Int,
@Query("sources") sources: String,
@Query("apiKey") apiKey: String = API_KEY
): NewsResponse
}
  • Search News will be created later on
  • In get News We have implemented lots of annotations which have @GET and @Query methods
  • And its return type will be NewsResponse

Make News Repository

  • Here we will implement the paging and first lets see what is paging
interface NewsRepository {
fun getNews(sources : List<String>): Flow<PagingData<Article>>

Here is the article

  • Paging basically means that the we will get small, chunks amount of response from the api.

Paging Implementation

  • Here we will implement the paging library in the app.
  • Create on file in the remote package
package com.example.newsapp.data.remote

import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.example.newsapp.domain.model.Article

class NewsPagingSource(
private val newsApi: NewsApi,
private val sources: String
) : PagingSource<Int, Article>() {
private var totalNewsCount = 0

override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Article> {
val page = params.key ?: 1
return try {
val newsResponse = newsApi.getNews(sources = sources, page = page)
totalNewsCount += newsResponse.articles.size
val articles = newsResponse.articles.distinctBy {
it.title
}
LoadResult.Page(
data = articles,
prevKey = if (page == 1) null else page - 1,
nextKey = if (totalNewsCount >= newsResponse.totalResults) null else page + 1
)
} catch (e: Exception) {
e.printStackTrace()
LoadResult.Error(
throwable = e
)
}


}

override fun getRefreshKey(state: PagingState<Int, Article>): Int? {

return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}




}
}
  • So here we have implemented the paging library
  • The basic function of the paging is to load the data from the api and show it to user
  • And refresh key shows the

Article

https://proandroiddev.com/pagination-in-jetpack-compose-with-and-without-paging-3-e45473a352f4

  • Int is the type of paging key, for our case it’s index numbers for pages.
  • Article is the type of data loaded.
  • getRefreshKey, provides a key used for the initial load for the next PagingSource due to invalidation of this PagingSource.
  • load, function will be called by the Paging library to asynchronously fetch more data to be displayed as the user scrolls around.
  • That’s it for PagingSource, we can create repository & view model.

Implementing News Repository Implementation

package com.example.newsapp.data.repository

import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import com.example.newsapp.data.local.NewsDao
import com.example.newsapp.data.remote.NewsApi
import com.example.newsapp.data.remote.NewsPagingSource
import com.example.newsapp.data.remote.SearchNewsPagingSource
import com.example.newsapp.domain.model.Article
import com.example.newsapp.domain.repository.NewsRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onEach

class NewsRepositoryImpl(
private val newsApi: NewsApi,
private val newsDao: NewsDao
) : NewsRepository {
override fun getNews(sources: List<String>): Flow<PagingData<Article>> {
return Pager(
config = PagingConfig(pageSize = 10),
pagingSourceFactory = {
NewsPagingSource(
newsApi = newsApi,
sources = sources.joinToString(",")
)
}
).flow
}

}
  • Implementing news repository implementation such as
  • Function which returns PagingData

Make Use Case

  • Create new package name “usecase” and inside it create “news” package.
  • Make File Get News
package com.example.newsapp.domain.usecases.news

import androidx.paging.PagingData
import com.example.newsapp.domain.model.Article
import com.example.newsapp.domain.repository.NewsRepository
import kotlinx.coroutines.flow.Flow

class GetNews(
private val newsRepository: NewsRepository
) {

operator fun invoke(sources: List<String>): Flow<PagingData<Article>> {
return newsRepository.getNews(sources = sources)

}
}

Now we have to implement usecases , di, viewmodel

  • Create ViewModel for the HomeViewModel
  • First implement data class for the usecases
data class NewsUseCases(
val getNews: GetNews,)
  • Later on we will add some more usecases.
  • And provide some functions in di.
package com.example.newsapp.di

import android.app.Application
import android.health.connect.datatypes.AppInfo
import androidx.room.Room
import com.example.newsapp.data.local.NewsDao
import com.example.newsapp.data.local.NewsDatabase
import com.example.newsapp.data.local.NewsTypeConverter
import com.example.newsapp.data.manager.LocalUserManagerImpl
import com.example.newsapp.data.remote.NewsApi
import com.example.newsapp.data.repository.NewsRepositoryImpl
import com.example.newsapp.domain.manager.LocalUserManager
import com.example.newsapp.domain.repository.NewsRepository
import com.example.newsapp.domain.usecases.app_entry.AppEntryUseCases
import com.example.newsapp.domain.usecases.app_entry.ReadAppEntry
import com.example.newsapp.domain.usecases.app_entry.SaveAppEntry
import com.example.newsapp.domain.usecases.news.DeleteArticle
import com.example.newsapp.domain.usecases.news.GetNews
import com.example.newsapp.domain.usecases.news.NewsUseCases
import com.example.newsapp.domain.usecases.news.SearchNews
import com.example.newsapp.domain.usecases.news.SelectArticle
import com.example.newsapp.domain.usecases.news.SelectArticles
import com.example.newsapp.domain.usecases.news.UpsertArticle
import com.example.newsapp.util.Constants.BASE_URL
import com.example.newsapp.util.Constants.NEW_DATABASE
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
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 provideLocalUserManager(
application: Application
): LocalUserManager = LocalUserManagerImpl(application)


@Provides
@Singleton
fun provideAppEntryUseCases(
localUserManger: LocalUserManager
): AppEntryUseCases = AppEntryUseCases(
readAppEntry = ReadAppEntry(localUserManger),
saveAppEntry = SaveAppEntry(localUserManger)
)


@Provides
@Singleton
fun provideNewsApi(): NewsApi {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(NewsApi::class.java)
}

@Provides
@Singleton
fun provideNewsRepository(newsApi: NewsApi, newsDao: NewsDao): NewsRepository {
return NewsRepositoryImpl(newsApi, newsDao)
}

@Provides
@Singleton
fun provideNewsUseCases(
newsRepository: NewsRepository,
): NewsUseCases {
return NewsUseCases(
getNews = GetNews(newsRepository),
searchNews = SearchNews(newsRepository),
upsertArticle = UpsertArticle(newsRepository),
deleteArticle = DeleteArticle(newsRepository),
selectArticles = SelectArticles(newsRepository),
selectArticle = SelectArticle(newsRepository),
)
}


@Provides
@Singleton
fun provideNewsDatabase(
application: Application
): NewsDatabase {
return Room.databaseBuilder(
context = application,
klass = NewsDatabase::class.java,
name = NEW_DATABASE
).addTypeConverter(NewsTypeConverter())
.fallbackToDestructiveMigration()
.build()

}

@Provides
@Singleton
fun provideNewsDao(
newsDatabase: NewsDatabase
): NewsDao {
return newsDatabase.newsDao
}


}
  • HomeViewModel consist of
package com.example.newsapp.presentation.home

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.cachedIn
import com.example.newsapp.domain.usecases.news.NewsUseCases
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject

@HiltViewModel
class HomeViewModel @Inject constructor(
private val newsUseCases: NewsUseCases
): ViewModel() {

val news = newsUseCases.getNews(
sources = listOf("bbc-news", "abc-news", "al-jazeera-english")
).cachedIn(viewModelScope)
}
  • sources are provided in the documentation.

Now Time For UI of the News Page

  • Make Article card
package com.example.newsapp.presentation.common

import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import coil.compose.AsyncImage
import coil.request.ImageRequest
import com.example.newsapp.R
import com.example.newsapp.domain.model.Article
import com.example.newsapp.domain.model.Source
import com.example.newsapp.presentation.Dimens.ArticleCardSize
import com.example.newsapp.presentation.Dimens.ExtraSmallPadding
import com.example.newsapp.presentation.Dimens.ExtraSmallPadding2
import com.example.newsapp.presentation.Dimens.SmallIconSize
import com.example.newsapp.ui.theme.NewsAppTheme

@Composable
fun ArticleCard(
modifier: Modifier = Modifier,
article: Article,
onClick: (() -> Unit)? = null
) {

val context = LocalContext.current
Row(
modifier = modifier.clickable { onClick?.invoke() },

) {
AsyncImage(
modifier = Modifier
.size(ArticleCardSize)
.clip(MaterialTheme.shapes.medium),
model = ImageRequest.Builder(context).data(article.urlToImage).build(),
contentDescription = null,
contentScale = ContentScale.Crop
)
Column(
verticalArrangement = Arrangement.SpaceAround,
modifier = Modifier
.padding(horizontal = ExtraSmallPadding)
.height(ArticleCardSize)
) {
Text(
text = article.title,
style = MaterialTheme.typography.bodyMedium.copy(),
color = colorResource(id = R.color.text_title),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Row(
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = article.source.name,
style = MaterialTheme.typography.labelSmall.copy(fontWeight = FontWeight.Bold),
color = colorResource(id = R.color.body)
)
Spacer(modifier = Modifier.width(ExtraSmallPadding2))
Icon(
painter = painterResource(id = R.drawable.baseline_access_time_24),
contentDescription = null,
modifier = Modifier.size(SmallIconSize),
tint = colorResource(id = R.color.body)
)
Spacer(modifier = Modifier.width(ExtraSmallPadding))
Text(
text = article.publishedAt,
style = MaterialTheme.typography.labelSmall,
color = colorResource(id = R.color.body)
)
}
}
}
}

@Preview(showBackground = true)
@Composable
fun ArticleCardPreview() {
NewsAppTheme(dynamicColor = false) {
ArticleCard(
article = Article(
author = "",
content = "",
description = "",
publishedAt = "2 hours",
source = Source(id = "", name = "BBC"),
title = "Her train broke down. Her phone died. And then she met her Saver in a",
url = "",
urlToImage = "https://ichef.bbci.co.uk/live-experience/cps/624/cpsprodpb/11787/production/_124395517_bbcbreakingnewsgraphic.jpg"
)
)
}
}

Shimmer Effect

package com.example.newsapp.presentation.common

import android.annotation.SuppressLint
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
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.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.newsapp.R
import com.example.newsapp.presentation.Dimens
import com.example.newsapp.presentation.Dimens.MediumPadding1
import com.example.newsapp.ui.theme.NewsAppTheme


@SuppressLint("ModifierFactoryUnreferencedReceiver")
fun Modifier.shimmerEffect() = composed {
val transition = rememberInfiniteTransition(label = "")
val alpha = transition.animateFloat(
initialValue = 0.2f, targetValue = 0.9f, animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 1000),
repeatMode = RepeatMode.Reverse
), label = ""
).value
background(color = colorResource(id = R.color.shimmer).copy(alpha = alpha))
}

@Composable
fun ArticleCardShimmerEffect(modifier: Modifier = Modifier) {
Row(
modifier = modifier
) {
Box(
modifier = Modifier
.size(Dimens.ArticleCardSize)
.clip(MaterialTheme.shapes.medium)
.shimmerEffect()
)
Column(
verticalArrangement = Arrangement.SpaceAround,
modifier = Modifier
.padding(horizontal = Dimens.ExtraSmallPadding)
.height(Dimens.ArticleCardSize)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(30.dp)
.padding(horizontal = MediumPadding1)
.shimmerEffect()
)
Row(
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.fillMaxWidth(0.5f)
.padding(horizontal = MediumPadding1)
.height(15.dp)
.shimmerEffect()
)

}
}
}
}

@Preview(showBackground = true)
@Composable
private fun ArticlePreview() {
NewsAppTheme {
ArticleCardShimmerEffect()
}
}

Article List

package com.example.newsapp.presentation.common

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.paging.LoadState
import androidx.paging.compose.LazyPagingItems
import com.example.newsapp.domain.model.Article
import com.example.newsapp.presentation.Dimens.ExtraSmallPadding2
import com.example.newsapp.presentation.Dimens.MediumPadding1
import com.example.newsapp.presentation.Dimens.MediumPadding2


@Composable
fun ArticlesList(
modifier: Modifier = Modifier,
articles: List<Article>,
onClick: (Article) -> Unit
) {
if (articles.isEmpty()){
EmptyScreen()
}
LazyColumn(
modifier = modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(MediumPadding1),
contentPadding = PaddingValues(all = ExtraSmallPadding2)
) {
items(
count = articles.size,
) {
articles[it].let { article ->
ArticleCard(article = article, onClick = { onClick(article) })
}
}
}

}



@Composable
fun ArticlesList(
modifier: Modifier = Modifier,
articles: LazyPagingItems<Article>,
onClick: (Article) -> Unit
) {

val handlePagingResult = handlePagingResult(articles = articles)


if (handlePagingResult) {
LazyColumn(
modifier = modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(MediumPadding1),
contentPadding = PaddingValues(all = ExtraSmallPadding2)
) {
items(
count = articles.itemCount,
) {
articles[it]?.let { article ->
ArticleCard(article = article, onClick = { onClick(article) })
}
}
}
}
}

@Composable
fun handlePagingResult(
modifier: Modifier = Modifier,
articles: LazyPagingItems<Article>
): Boolean {

val loadState = articles.loadState
val error = when {
loadState.refresh is LoadState.Error -> loadState.refresh as LoadState.Error
loadState.prepend is LoadState.Error -> loadState.prepend as LoadState.Error
loadState.append is LoadState.Error -> loadState.append as LoadState.Error
else -> null
}

return when {
loadState.refresh is LoadState.Loading -> {
ShimmerEffect()
false
}

else -> {
true
}
}


}

@Composable
private fun ShimmerEffect(modifier: Modifier = Modifier) {
Column(verticalArrangement = Arrangement.spacedBy(MediumPadding2)) {
repeat(10) {
ArticleCardShimmerEffect(
modifier = Modifier.padding(horizontal = MediumPadding2)
)
}
}
}
  • Here we have implemented the Article list that how one article look like and how the article will be requested.
  • Also added Empty Screen if screen does not contains any news or any kind of error takes place.

Here is the Empty Screen UI

package com.example.newsapp.presentation.common

import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color.Companion.DarkGray
import androidx.compose.ui.graphics.Color.Companion.LightGray
import androidx.compose.ui.graphics.DefaultAlpha
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.paging.LoadState
import com.example.newsapp.R
import java.net.ConnectException
import java.net.SocketTimeoutException

@Composable
fun EmptyScreen(error: LoadState.Error? = null) {

var message by remember {
mutableStateOf(parseErrorMessage(error = error))
}

var icon by remember {
mutableIntStateOf(R.drawable.baseline_signal_cellular_connected_no_internet_0_bar_24)
}

if (error == null) {
message = "You have not saved news so far !"
icon = R.drawable.baseline_document_scanner_24
}

var startAnimation by remember {
mutableStateOf(false)
}

val alphaAnimation by animateFloatAsState(
targetValue = if (startAnimation) 0.3f else 0f,
animationSpec = tween(durationMillis = 1000), label = ""
)

LaunchedEffect(key1 = true) {
startAnimation = true
}

EmptyContent(alphaAnim = alphaAnimation, message = message, iconId = icon)

}

@Composable
fun EmptyContent(alphaAnim: Float, message: String, iconId: Int) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
painter = painterResource(id = iconId),
contentDescription = null,
tint = if (isSystemInDarkTheme()) LightGray else DarkGray,
modifier = Modifier
.size(120.dp)
.alpha(alphaAnim)
)
Text(
modifier = Modifier
.padding(10.dp)
.alpha(alphaAnim),
text = message,
style = MaterialTheme.typography.bodyMedium,
color = if (isSystemInDarkTheme()) LightGray else DarkGray,
)
}
}


fun parseErrorMessage(error: LoadState.Error?): String {
return when (error?.error) {
is SocketTimeoutException -> {
"Server Unavailable."
}

is ConnectException -> {
"Internet Unavailable."
}

else -> {
"Unknown Error."
}
}
}

@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_YES)
@Composable
fun EmptyScreenPreview() {
EmptyContent(
alphaAnim = 0.3f,
message = "Internet Unavailable.",
R.drawable.baseline_signal_cellular_connected_no_internet_0_bar_24
)
}
  • Change Drawable According to you.

Finally the Home Screen UI

package com.example.newsapp.presentation.home


import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
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.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.paging.compose.LazyPagingItems
import com.example.newsapp.R
import com.example.newsapp.domain.model.Article
import com.example.newsapp.presentation.Dimens.MediumPadding1
import com.example.newsapp.presentation.Dimens.MediumPadding2
import com.example.newsapp.presentation.Dimens.MediumPadding3
import com.example.newsapp.presentation.common.ArticlesList
import com.example.newsapp.presentation.common.SearchBar
import com.example.newsapp.presentation.navgraph.Route


@OptIn(ExperimentalFoundationApi::class)
@Composable
fun HomeScreen(
articles: LazyPagingItems<Article>,
navigateToSearch: () -> Unit,
navigateToDetails: (Article) -> Unit
) {

val titles by remember {
derivedStateOf {
if (articles.itemCount > 10) {
articles.itemSnapshotList.items
.slice(IntRange(start = 0, endInclusive = 9))
.joinToString(separator = " 🔷 ") { it.title }
} else {
""
}
}
}

Column(
modifier = Modifier
.fillMaxSize()
.padding(top = 10.dp)
.statusBarsPadding()
) {
Image(
painter = painterResource(id = R.drawable.banner),
contentDescription = null,
modifier = Modifier
.size(width = 900.dp, height = 30.dp)
.align(Alignment.CenterHorizontally)
, contentScale = ContentScale.Crop
)

Spacer(modifier = Modifier.padding(10.dp))

SearchBar(
modifier = Modifier
.padding(horizontal = MediumPadding2)
.fillMaxWidth(),
text = "",
readOnly = true,
onValueChange = {},
onSearch = {},
onClick = {
navigateToSearch()
}
)

Spacer(modifier = Modifier.padding(bottom = 20.dp))

Text(
text = titles, modifier = Modifier
.fillMaxWidth()
.padding(start = MediumPadding1)
.basicMarquee(), fontSize = 14.sp,
color = colorResource(id = R.color.placeholder)
)

Spacer(modifier = Modifier.height(MediumPadding1))

ArticlesList(
modifier = Modifier.padding(horizontal = MediumPadding1),
articles = articles,
onClick = {
navigateToDetails(it)
}
)
}
}

Add Search Bar

package com.example.newsapp.presentation.common


import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.border
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.newsapp.R
import com.example.newsapp.presentation.Dimens.IconSize
import com.example.newsapp.ui.theme.NewsAppTheme

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SearchBar(
modifier: Modifier = Modifier,
text: String,
readOnly: Boolean,
onClick: (() -> Unit)? = null,
onValueChange: (String) -> Unit,
onSearch: () -> Unit
) {

val interactionSource = remember {
MutableInteractionSource()
}
val isClicked = interactionSource.collectIsPressedAsState().value
LaunchedEffect(key1 = isClicked) {
if (isClicked) {
onClick?.invoke()
}
}

Box(modifier = modifier) {
TextField(
modifier = Modifier
.fillMaxWidth()
.searchBar(),
value = text,
onValueChange = onValueChange,
readOnly = readOnly,
leadingIcon = {
Icon(
painter = painterResource(id = R.drawable.baseline_search_24),
contentDescription = null,
modifier = Modifier.size(IconSize),
tint = colorResource(id = R.color.body)
)
},
placeholder = {
Text(
text = "Search",
style = MaterialTheme.typography.bodySmall,
color = colorResource(id = R.color.placeholder)
)
},
shape = MaterialTheme.shapes.medium,
colors = TextFieldDefaults.textFieldColors(
containerColor = colorResource(id = R.color.input_background),
focusedTextColor = if (isSystemInDarkTheme()) Color.White else Color.Black,
cursorColor = if (isSystemInDarkTheme()) Color.White else Color.Black,
disabledIndicatorColor = Color.Transparent,
errorIndicatorColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent
),
singleLine = true,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
keyboardActions = KeyboardActions(
onSearch = {
onSearch()
}
),
textStyle = MaterialTheme.typography.bodySmall,
interactionSource = interactionSource
)
}
}

fun Modifier.searchBar(): Modifier = composed {
if (!isSystemInDarkTheme()) {
border(
width = 1.dp,
color = Color.Black,
shape = MaterialTheme.shapes.medium
)
} else {
this
}
}

@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_YES)
@Composable
fun SearchBarPreview() {
NewsAppTheme {
SearchBar(text = "", onValueChange = {}, readOnly = false) {

}
}
}

Now Check for the Nav Graph

  • Add the Home Screen to the NavGraph and implement it properly
  • You will be able to make the Home Screen.

If you have not read Part 3 of the series the here is the link below

You can visit the introduction article for the news app with the link below

Visit my GitHub Repository : https://github.com/mohitdamke/NewsApp

Make sure to follow me on
Link-tree :
https://linktr.ee/MohitDamke01
LinkedIn :
https://www.linkedin.com/in/mohitdamke01

--

--

Mohit Damke

Junior Android Developer | Kotlin | Jetpack | Firebase | Android Studio | MVVM & Clean Code Architecture