News API Android Kotlin App Programming Tutorial & Google Play

ibrahimcanerdogan
6 min readJan 18, 2024

--

In this section I want to teach News API App in Android Kotlin. With this application structure, you can write your own application using the API you want.

GOOGLE PLAY DOWNLOAD!

GITHUB PROJECT

YOUTUBE TUTORIAL PLAYLIST!

App Preview

Clean Architecture

Data Layer

The data layer is responsible for managing the application’s data and interacting with external data sources. It consists of the following components:

Entities

Entities represent the core data structures of the application. They encapsulate the business rules and state, ensuring that the business logic is not coupled to any specific data storage or presentation details.

data class News(
val articles: List<Article>?,
val status: String,
val totalResults: Int
)
data class Article(
val articleUUID : String = UUID.randomUUID().toString(),
@SerializedName("author")
val articleAuthor: String?,
@SerializedName("content")
val articleContent: String?,
@SerializedName("description")
val articleDescription: String?,
@SerializedName("publishedAt")
val articlePublishedAt: String?,
@SerializedName("source")
val articleSource: Source?,
@SerializedName("title")
val articleTitle: String?,
@SerializedName("url")
val articleUrl: String?,
@SerializedName("urlToImage")
val articleUrlToImage: String?
)
data class Source(
@SerializedName("id")
val sourceID: String?,
@SerializedName("name")
val sourceName: String?
)

API Service

interface APIService {

@GET("v2/top-headlines")
suspend fun getTopHeadlines(
@Query("country")
country: String = "us",
@Query("category")
category: String = NewsCategory.BUSINESS.param,
@Query("page")
page: Int,
@Query("apiKey")
apiKey: String = "YOUR_API_KEY"
): Response<News>

}

DataSource

Data sources implement the interfaces defined by the repositories in the domain layer. They handle data retrieval and storage, whether it be from a local database, a remote server, or any other source.

interface NewsRemoteDataSource {
suspend fun getTopHeadlines(country : String, category: String, page : Int): Response<News>

}
class NewsRemoteDataSourceImpl(
private val apiService: APIService
) : NewsRemoteDataSource {

override suspend fun getTopHeadlines(
country: String,
category: String,
page: Int
): Response<News> {
return apiService.getTopHeadlines(country, category, page)
}
}

Repositories in the data layer implement the interfaces defined in the domain layer. They coordinate with data sources to fetch and store data, ensuring that the domain layer remains agnostic to the specific data storage mechanisms.

class NewsRepositoryImpl(
private val newsRemoteDataSource: NewsRemoteDataSource
) : NewsRepository {

override suspend fun getNewsHeadlines(country: String, category: String, page: Int): Resource<News> {
val apiResult = newsRemoteDataSource.getTopHeadlines(country, category, page)
if (apiResult.isSuccessful) {
apiResult.body()?.let {
return Resource.Success(it)
}
}
return Resource.Error(apiResult.message())
}
}

Domain Layer

The domain layer contains the core business logic of the application. It is independent of any specific framework or technology and consists of the following components:

Repository

Repositories define interfaces for data access, providing an abstraction for data sources such as databases, network services, or external APIs. They allow the use cases to interact with data without being aware of the underlying implementation.

interface NewsRepository {

suspend fun getNewsHeadlines(country: String, category: String, page: Int) : Resource<News>
}

UseCase

Use cases encapsulate the business rules and logic of the application. They act as intermediaries between the presentation and data layers, orchestrating the flow of data and performing business operations.

class GetNewsHeadlinesUseCase(private val newsRepository: NewsRepository) {

suspend fun execute(country : String, category: String, page : Int): Resource<News> {
return newsRepository.getNewsHeadlines(country, category, page)
}
}

Presentation Layer

The presentation layer is the outermost layer of the Clean Architecture, responsible for handling user interactions and presenting information to the user. In Android, this layer typically consists of the following components:

Adapter

Adapters are responsible for binding data from the ViewModels to the UI components, such as RecyclerViews and ListViews. They play a crucial role in maintaining separation between the presentation and domain layers.

class NewsAdapter : RecyclerView.Adapter<NewsViewHolder>() {

var onNewsItemClick: ((Article) -> Unit)? = null

private val callback = object : DiffUtil.ItemCallback<Article>(){
override fun areItemsTheSame(oldItem: Article, newItem: Article): Boolean {
return oldItem.articleUUID == newItem.articleUUID
}
override fun areContentsTheSame(oldItem: Article, newItem: Article): Boolean {
return oldItem == newItem
}
}

val differ = AsyncListDiffer(this,callback)

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NewsViewHolder {
val binding = ItemNewsBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return NewsViewHolder(binding, onNewsItemClick)
}

override fun getItemCount(): Int {
return differ.currentList.size
}

override fun onBindViewHolder(holder: NewsViewHolder, position: Int) {
val article = differ.currentList[position]
holder.bind(article)
}
}
class NewsViewHolder(
private val binding : ItemNewsBinding,
private val onNewsItemClick : ((Article) -> Unit)?
) : RecyclerView.ViewHolder(binding.root) {

fun bind(article: Article) {
binding.apply {
card.setOnClickListener { onNewsItemClick?.invoke(article) }
textTitle.text = article.articleTitle
textDate.text = article.articlePublishedAt!!.split("T")[0]
textDescription.text = article.articleDescription ?: ""

setImage(article, imageViewBackground)
setImage(article, imageViewThumbnail)

}
}

private fun setImage(article: Article, imageView: ImageView) {
Glide.with(binding.root.context)
.load(if(!article.articleUrlToImage.isNullOrEmpty()) article.articleUrlToImage else R.drawable.news_background)
.into(imageView)
}
}

ViewModel

ViewModels are part of the Android Architecture Components and are responsible for managing UI-related data. They interact with the use cases from the domain layer and expose observable data to the UI components.

@HiltViewModel
class NewsViewModel @Inject constructor(
private val getNewsHeadlinesUseCase: GetNewsHeadlinesUseCase
) : ViewModel() {

private val headlines: MutableLiveData<Resource<News>> = MutableLiveData()
val headlinesData : LiveData<Resource<News>>
get() = headlines

fun getNewsHeadLines(
context: Context? = null,
country: String,
category: String,
page: Int
) = viewModelScope.launch(Dispatchers.IO) {

headlines.postValue(Resource.Loading())
try {
if (NetworkController.isNetworkAvailable(context)) {
val apiResult = getNewsHeadlinesUseCase.execute(country, category, page)
headlines.postValue(apiResult)
} else {
headlines.postValue(Resource.Error("Internet is not available!"))
}
} catch (e: Exception) {
headlines.postValue(Resource.Error(e.message.toString()))
}
}

}
class NewsViewModelFactory(
private val getNewsHeadlinesUseCase: GetNewsHeadlinesUseCase
) : ViewModelProvider.Factory {

override fun <T : ViewModel> create(modelClass: Class<T>): T {
return NewsViewModel(getNewsHeadlinesUseCase) as T
}
}

Activity

Activities and Fragments are responsible for displaying the user interface and handling user input. They delegate business logic to the next layer, preventing the presentation layer from becoming cluttered with application-specific rules.

@AndroidEntryPoint
class HomeActivity : AppCompatActivity() {

private lateinit var binding: ActivityHomeBinding

private val viewModel by lazy {
ViewModelProvider(this, factory).get(NewsViewModel::class.java)
}
@Inject
lateinit var factory: NewsViewModelFactory
@Inject
lateinit var newsAdapter: NewsAdapter

private var page = 1
private var totalResults = 0
private var newsCategory: String = NewsCategory.BUSINESS.param

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
binding = ActivityHomeBinding.inflate(layoutInflater)
setContentView(binding.root)

// Categories configuration.
binding.setCategoryCard()
binding.layoutCategory.cardBusiness.setCardBackgroundColor(getColor(R.color.MainLigth))

viewModel.getNewsHeadLines(context = this, country = "us", category = newsCategory, page = page)
viewModel.headlinesData.observe(this, ::setData)

binding.viewPager.apply {
adapter = newsAdapter
registerOnPageChangeCallback(scrollListener)
}
}

private fun setData(resource: Resource<News>?) {
when(resource) {
is Resource.Success -> {
resource.data?.let {
newsAdapter.differ.submitList(it.articles?.toList())
totalResults = it.totalResults
showLoadingAnimation()
Log.i(TAG, it.status)
}
}
is Resource.Error -> {
resource.message?.let { error ->
showLoadingAnimation()
Log.e(TAG, error)
}
}
is Resource.Loading -> {
showLoadingAnimation(true)
}
else -> Log.d(TAG, "Resource not found!")
}
}

private val scrollListener = object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
if (position == newsAdapter.itemCount - 1) {
page += 1
if (page == (totalResults / 20) + 2 ) page = 1 // If max page ((totalResults / 20) + 1) + 1 make return first
updateNewsHeadlines()
}
}
}

private fun updateNewsHeadlines() {
showLoadingAnimation(true)
Handler(Looper.getMainLooper()).postDelayed({
viewModel.getNewsHeadLines(
context = this@HomeActivity,
country = "us",
category = newsCategory,
page = page
)
}, 1000)

binding.viewPager.currentItem = 0
}

private fun ActivityHomeBinding.setCategoryCard() {
layoutCategory.apply {
setCategoryInfo(cardBusiness, NewsCategory.BUSINESS.param)
setCategoryInfo(cardEntertainment, NewsCategory.ENTERTAINMENT.param)
setCategoryInfo(cardGeneral, NewsCategory.GENERAL.param)
setCategoryInfo(cardHealth, NewsCategory.HEALTH.param)
setCategoryInfo(cardScience, NewsCategory.SCIENCE.param)
setCategoryInfo(cardSports, NewsCategory.SPORTS.param)
setCategoryInfo(cardTechnology, NewsCategory.TECHNOLOGY.param)
}
}

private fun setCategoryInfo(card: MaterialCardView, categoryParam: String) {
card.setOnClickListener {
if (newsCategory != categoryParam) {
setUnSelectCategoryCard()
page = 1
newsCategory = categoryParam
updateNewsHeadlines()
card.setCardBackgroundColor(getColor(R.color.MainLigth))
}
}
}

private fun setUnSelectCategoryCard() {
with(binding.layoutCategory) {
cardBusiness.setCardBackgroundColor(getColor(R.color.white))
cardEntertainment.setCardBackgroundColor(getColor(R.color.white))
cardGeneral.setCardBackgroundColor(getColor(R.color.white))
cardHealth.setCardBackgroundColor(getColor(R.color.white))
cardScience.setCardBackgroundColor(getColor(R.color.white))
cardSports.setCardBackgroundColor(getColor(R.color.white))
cardTechnology.setCardBackgroundColor(getColor(R.color.white))
}
}
private fun showLoadingAnimation(isShown: Boolean = false) {
with(binding){
lottieAnimation.visibility = if (isShown) View.VISIBLE else View.INVISIBLE
viewPager.visibility= if (isShown) View.INVISIBLE else View.VISIBLE
}

}

companion object {
private val TAG = HomeActivity::class.simpleName.toString()
}
}

If you want detail for this application visit my Github profile. Thanks for everything. See you later next article.

İbrahim Can Erdoğan

LINKEDIN

YOUTUBE

GITHUB

UDEMY

--

--

ibrahimcanerdogan
ibrahimcanerdogan

Written by ibrahimcanerdogan

Hi, My name is Ibrahim, I am developing ebebek android app within Ebebek. I publish various articles in the field of programming and self-improvement.

Responses (1)