Part 5 : Implementing Search Screen in app

Mohit Damke
4 min readJul 4, 2024

--

Here we will implement the search screen where we can search any keyword and we will get result from the api

Implementation

Here we have to add the suspend function for search news

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
}
  • Create new file as a Search News Paging Source
package com.example.newsapp.data.remote

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

class SearchNewsPagingSource(
private val searchQuery: String,
private val newsApi: NewsApi,
private val sources: String
): PagingSource<Int, Article>() {

private var totalNewsCount = 0


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)
}
}

override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Article> {
val page = params.key ?: 1
return try {
val newsResponse = newsApi.searchNews(searchQuery = searchQuery,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
)
}
}
}
  • We have to add the function in the News Repository interface
interface NewsRepository {
fun getNews(sources : List<String>): Flow<PagingData<Article>>


fun searchNews(searchQuery : String, sources : List<String>): Flow<PagingData<Article>>
  • And also have to describe it in the interface
override fun searchNews(searchQuery: String, sources: List<String>): Flow<PagingData<Article>> {
return Pager(
config = PagingConfig(pageSize = 10),
pagingSourceFactory = {
SearchNewsPagingSource(
searchQuery = searchQuery,
newsApi = newsApi,
sources = sources.joinToString(",")
)
}
).flow
}
  • Create new package in the presentation layer as Search
  • Create new file name as Search State
package com.example.newsapp.presentation.search

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

data class SearchState(
val searchQuery: String = "",
val articles : Flow<PagingData<Article>>? = null
) {
}
sealed class SearchEvent {

data class UpdateSearchQuery(val searchQuery: String) : SearchEvent()

data object SearchNews : SearchEvent()
}
  • Search event Sealed class us use to update and search for query
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 SearchNews(
private val newsRepository: NewsRepository
) {

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

}
}
  • You have to add the Use Cases in the Search News.
@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),
)
}
  • Add Search News in the Di Layer
  • We have to implement it now in the view model
package com.example.newsapp.presentation.search

import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
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 SearchViewModel @Inject constructor(
private val newsUseCases: NewsUseCases
) : ViewModel() {

private val _state = mutableStateOf(SearchState())
val state: State<SearchState> = _state

fun onEvent(event: SearchEvent) {
when (event) {
is SearchEvent.UpdateSearchQuery -> {
_state.value = state.value.copy(searchQuery = event.searchQuery)
}

is SearchEvent.SearchNews -> {
searchNews()
}
}
}

private fun searchNews() {
val articles = newsUseCases.searchNews(
searchQuery = _state.value.searchQuery,
sources = listOf("bbc-news", "abc-news", "al-jazeera-english")
).cachedIn(viewModelScope)
_state.value = state.value.copy(articles = articles)
}
}
  • Finally we have to make the UI of the Search Screen
package com.example.newsapp.presentation.search

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.paging.compose.collectAsLazyPagingItems
import com.example.newsapp.domain.model.Article
import com.example.newsapp.presentation.Dimens.MediumPadding1
import com.example.newsapp.presentation.common.ArticlesList
import com.example.newsapp.presentation.common.SearchBar

@Composable
fun SearchScreen(
modifier: Modifier = Modifier, state: SearchState,
event: (SearchEvent) -> Unit, navigateToDetails: (Article) -> Unit
) {

Column(
modifier = modifier
.padding(top = MediumPadding1, start = MediumPadding1, end = MediumPadding1)
.statusBarsPadding()
.fillMaxSize()
) {
SearchBar(
text = state.searchQuery,
readOnly = false,
onValueChange = { event(SearchEvent.UpdateSearchQuery(it)) },
onSearch = {
event(SearchEvent.SearchNews)
}
)
Spacer(modifier = modifier.height(MediumPadding1))
state.articles?.let {
val articles = it.collectAsLazyPagingItems()
ArticlesList(
articles = articles,
onClick = {
navigateToDetails(it)
}
)
}
}
}

That’s how it will work

If you have not read Part 4 of the series the here is the link below
https://medium.com/@mohitrdamke/part-4-implementing-home-screen-where-all-the-news-will-be-visible-4fd3052c8072

You can visit the introduction article for the news app with the link below
https://medium.com/@mohitrdamke/news-app-clean-code-architecture-paging-room-db-in-android-studio-24919ba7d16a

You can add 50 clap by just pressing on clap icon
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