News API Android Kotlin App Programming Tutorial & Google Play
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.
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.