News App using Kotlin, MVVM, Navigation Component, Room, Retrofit, and Coroutines

Golap Gunjan Barman
10 min readDec 26, 2021

News App using Kotlin, MVVM, Navigation Component, Room, Retrofit, and Coroutines

Dependencies (build.gradle of App)

plugins {

id ‘com.android.application’

id ‘kotlin-android’

id ‘kotlin-android-extensions’

id ‘kotlin-kapt’

id “androidx.navigation.safeargs.kotlin”

}

// Architectural Components
implementation “androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0”
// Room
implementation “androidx.room:room-runtime:2.2.5”
kapt “androidx.room:room-compiler:2.2.5”
// Kotlin Extensions and Coroutines support for Room
implementation “androidx.room:room-ktx:2.2.5”
// Coroutines
implementation ‘org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.5’
implementation ‘org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.5’
// Coroutine Lifecycle Scopes
implementation “androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0”
implementation “androidx.lifecycle:lifecycle-runtime-ktx:2.2.0”
// Retrofit
implementation ‘com.squareup.retrofit2:retrofit:2.6.0’
implementation ‘com.squareup.retrofit2:converter-gson:2.6.0’
implementation “com.squareup.okhttp3:logging-interceptor:4.5.0”
// Navigation Components
implementation “androidx.navigation:navigation-fragment-ktx:2.2.1”
implementation “androidx.navigation:navigation-ui-ktx:2.2.1”
// Glide
implementation ‘com.github.bumptech.glide:glide:4.11.0’
kapt ‘com.github.bumptech.glide:compiler:4.11.0’
//shimmer effect or loading effect
implementation ‘com.facebook.shimmer:shimmer:0.1.0@aar’
def paging_version = “2.1.2”

implementation “androidx.paging:paging-runtime:$paging_version” //

//swipe refresh layout
implementation ‘androidx.swiperefreshlayout:swiperefreshlayout:1.1.0’

dependencies (build.gradle of Project)

dependencies {
classpath “com.android.tools.build:gradle:4.1.1”
classpath “org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version”
classpath “androidx.navigation:navigation-safe-args-gradle-plugin:2.3.0-alpha04”
}

Drawables

Click Here

Model (Article)

import androidx.room.Entity
import androidx.room.PrimaryKey
import com.google.gson.annotations.SerializedName
import java.io.Serializable
@Entity(
tableName = “articles”
)
data class Article(
@PrimaryKey(autoGenerate = true)
var id : Int? = null,
@SerializedName(“author”)
var author : String?,
@SerializedName(“content”)
var content : String?,
@SerializedName(“description”)
var description : String?,
@SerializedName(“publishedAt”)
var publishedAt : String?,
@SerializedName(“source”)
var source : Source?,
@SerializedName(“title”)
var title : String?,
@SerializedName(“url”)
var url : String?,
@SerializedName(“urlToImage”)
var urlToImage : String?
) : Serializable

NewsResponse

import com.google.gson.annotations.SerializedName
data class NewsResponse(
@SerializedName(“articles”)
var articles : MutableList<Article>,
@SerializedName(“status”)
var status: String,
@SerializedName(“totalResults”)
var totalResults: Int?
)

Source

import com.google.gson.annotations.SerializedName
data class Source(
@SerializedName(“id”)
var id : String?,
@SerializedName(“name”)
var name: String
)

Utils (Constants)

class Constants {
companion object {
const val API_KEY = “API_KEY”
const val BASE_URL = “https://newsapi.org/"
const val SEARCH_TIME_DELAY = 500L
}
}

Resource

sealed class Resource<T>(
val data : T? = null,
val message : String? = null
) {
class Success<T>(data: T) : Resource<T>(data)
class Error<T>(message: String, data: T? = null) : Resource<T>(data, message)
class Loading<T> : Resource<T>()
}

Util

import android.content.Context
import android.content.Intent
import android.widget.ImageView
import androidx.databinding.BindingAdapter
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.codewithgolap.newssplash.R
import com.codewithgolap.newssplash.model.Article
/// share news
fun shareNews(context: Context?, article: Article){
val intent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, article.urlToImage)
putExtra(Intent.EXTRA_STREAM, article.urlToImage)
putExtra(Intent.EXTRA_TITLE, article.title)
type =”image/*”
}
context?.startActivity(Intent.createChooser(intent, “Share News On”))
}
// load image in image view
fun getCircularDrawable(context: Context): CircularProgressDrawable {
return CircularProgressDrawable(context).apply {
strokeWidth = 8f
centerRadius = 48f
setTint(context.resources.getColor(R.color.bgLineColor))
start()
}
}
fun ImageView.loadImage(url : String, progressDrawable : CircularProgressDrawable){
val options = RequestOptions()
.placeholder(progressDrawable)
.error(R.drawable.ic_launcher)
Glide.with(context)
.setDefaultRequestOptions(options)
.load(url)
.into(this)
}
@BindingAdapter(“loadImage”)
fun loadImage(imageView : ImageView, url : String?){
if (url != null){
imageView.loadImage(url!!, getCircularDrawable(imageView.context))
}
}

Repository > Service (RetrofitClient)

import com.codewithgolap.newssplash.utils.Constants
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
class RetrofitClient {
companion object{
private val retrofitClient by lazy {
val loggingInterceptor = HttpLoggingInterceptor()
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY)
val client = OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.build()
Retrofit.Builder()
.baseUrl(Constants.BASE_URL)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
val api by lazy {
retrofitClient.create(NewsApi::class.java)
}
}
}

NewsApi

import com.codewithgolap.newssplash.model.NewsResponse
import com.codewithgolap.newssplash.utils.Constants
import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Query
interface NewsApi {
@GET(“v2/top-headlines”)
suspend fun getBreakingNews(
@Query(“country”) country: String = “in”,
@Query(“page”) pageNumber: Int,
@Query(“apiKey”) apiKey: String = Constants.API_KEY
): Response<NewsResponse>
@GET(“v2/everything”)
suspend fun getSearchNews(
@Query(“q”) searchQuery: String,
@Query(“page”) pageNumber: Int,
@Query(“apiKey”) apiKey: String = Constants.API_KEY
): Response<NewsResponse>
}

Repository > datasource (ArticleDataSource)

import android.util.Log
import androidx.lifecycle.MutableLiveData
import androidx.paging.PageKeyedDataSource
import com.codewithgolap.newssplash.model.Article
import com.codewithgolap.newssplash.model.NewsResponse
import com.codewithgolap.newssplash.repository.service.RetrofitClient
import com.codewithgolap.newssplash.utils.Constants
import com.codewithgolap.newssplash.utils.Resource
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
class ArticleDataSource(val scope: CoroutineScope) : PageKeyedDataSource<Int, Article>(){
val breakingNews : MutableLiveData<MutableList<Article>> = MutableLiveData()
var brekingPageNumebr = 1
var breakingNewsResponse : NewsResponse? = null
val searchNews : MutableLiveData<Resource<NewsResponse>> = MutableLiveData()
var searchPageNumber = 1
var searchNewsResponse : NewsResponse? = null
//var articles : MutableLiveData<List<Article>> = MutableLiveData()
override fun loadInitial(
params: LoadInitialParams<Int>,
callback: LoadInitialCallback<Int, Article>
) {
scope.launch {
try {
val response = RetrofitClient.api.getBreakingNews(“in”, 1, Constants.API_KEY)
when {
response.isSuccessful -> {
response.body()?.articles?.let {
breakingNews.postValue(it)
callback.onResult(it, null, 2)
}
}
}
}catch (exception : Exception) {
Log.e(“DataSource :: “, exception.message.toString())
}
}
}
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, Article>) {
try {
scope.launch {
val response = RetrofitClient.api.getBreakingNews(“in”, params.requestedLoadSize, Constants.API_KEY)
when{
response.isSuccessful -> {
response.body()?.articles?.let {
callback.onResult(it, params.key+1)
}
}
}
}
} catch (exception : Exception) {
Log.e(“DataSource :: “, exception.message.toString())
}
}
override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, Article>) {
TODO(“Not yet implemented”)
}
}

ArticleDataSourceFActory

import androidx.lifecycle.MutableLiveData
import androidx.paging.DataSource
import com.codewithgolap.newssplash.model.Article
import kotlinx.coroutines.CoroutineScope
class ArticleDataSourceFactory(private val scope: CoroutineScope) : DataSource.Factory<Int, Article>() {
val articleDataSourceLiveData = MutableLiveData<ArticleDataSource>()
override fun create(): DataSource<Int, Article> {
val newArticleDataSource =ArticleDataSource(scope)
articleDataSourceLiveData.postValue(newArticleDataSource)
return newArticleDataSource
}
}

Repository > db(ArticleDAO)

import androidx.lifecycle.LiveData

import androidx.room.*

import com.codewithgolap.newssplash.model.Article

@Dao

interface ArticleDAO {

@Insert(onConflict = OnConflictStrategy.REPLACE)

suspend fun insert(article: Article) : Long

@Query(“SELECT * FROM articles”)

fun getArticles() : LiveData<List<Article>>

@Delete

suspend fun deleteArticle(article: Article)

}

ArticleDAtabase

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import com.codewithgolap.newssplash.model.Article
@Database(
entities = [Article::class],
version = 3
)
@TypeConverters(Converters::class)
abstract class ArticleDatabase : RoomDatabase(){
abstract fun getArticleDoa() : ArticleDAO
companion object {
@Volatile
private var articleDbInstance : ArticleDatabase? = null
private val LOCK = Any()
operator fun invoke(context: Context) = articleDbInstance ?: synchronized(LOCK){
articleDbInstance ?: createDatabaseInstance(context).also {
articleDbInstance = it
}
}
private fun createDatabaseInstance(context: Context) =
Room.databaseBuilder(
context, ArticleDatabase::class.java,
“articles_db.db”
).fallbackToDestructiveMigration().build()
}
}

Converters

import androidx.room.TypeConverter
import com.codewithgolap.newssplash.model.Source
class Converters {
@TypeConverter
fun fromSource(source: Source) :String? {
return source.name
}
@TypeConverter
fun toSource(name: String) : Source {
return Source(name, name)
}
}

Respository > NewsRepository

import com.codewithgolap.newssplash.model.Article
import com.codewithgolap.newssplash.repository.db.ArticleDatabase
import com.codewithgolap.newssplash.repository.service.RetrofitClient
class NewsRepository(
val db: ArticleDatabase
) {
suspend fun getBreakingNews(countryCode : String, pageNumber : Int) =
RetrofitClient.api.getBreakingNews(countryCode, pageNumber)
suspend fun getSearchNews(q : String, pageNumber : Int) =
RetrofitClient.api.getSearchNews(q, pageNumber)
suspend fun upsert(article: Article) = db.getArticleDoa().insert(article)
suspend fun delete(article: Article) = db.getArticleDoa().deleteArticle(article)
fun getAllArticles() = db.getArticleDoa().getArticles()
}

ViewModel > NewsViewModel

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.PagedList
import com.codewithgolap.newssplash.model.Article
import com.codewithgolap.newssplash.model.NewsResponse
import com.codewithgolap.newssplash.repository.NewsRepository
import com.codewithgolap.newssplash.utils.Resource
import kotlinx.coroutines.launch
import retrofit2.Response
class NewsViewModel(
val newsrepository: NewsRepository
) : ViewModel() {
val breakingNews: MutableLiveData<Resource<NewsResponse>> = MutableLiveData()
var brekingPageNumebr = 1
var breakingNewsResponse: NewsResponse? = null
val searchNews: MutableLiveData<Resource<NewsResponse>> = MutableLiveData()
var searchPageNumber = 1
var searchNewsResponse: NewsResponse? = null
lateinit var articles: LiveData<PagedList<Article>>
init {
getBreakingNews(“in”)
}
private fun getBreakingNews(countryCode: String) = viewModelScope.launch {
breakingNews.postValue(Resource.Loading())
val response = newsrepository.getBreakingNews(countryCode, brekingPageNumebr)
breakingNews.postValue(handleBreakingNewsResponse(response))
}
private fun handleBreakingNewsResponse(response: Response<NewsResponse>): Resource<NewsResponse>? {
if (response.isSuccessful) {
response.body()?.let { resultResponse ->
brekingPageNumebr++
if (breakingNewsResponse == null) {
breakingNewsResponse = resultResponse
} else {
val oldArticles = breakingNewsResponse?.articles
val newArticles = resultResponse.articles
oldArticles?.addAll(newArticles)
}
return Resource.Success(breakingNewsResponse ?: resultResponse)
}
}
return Resource.Error(response.message())
}
fun getSearchedNews(queryString: String) = viewModelScope.launch {
searchNews.postValue(Resource.Loading())
val searchNewsResponse = newsrepository.getSearchNews(queryString, searchPageNumber)
searchNews.postValue(handleSearchNewsResponse(searchNewsResponse))
}
private fun handleSearchNewsResponse(respons: Response<NewsResponse>): Resource<NewsResponse>? {
if (respons.isSuccessful) {
respons.body()?.let { resultResponse ->
searchPageNumber++
if (searchNewsResponse == null){
searchNewsResponse = resultResponse
}else{
val oldArticles = searchNewsResponse?.articles
val newArticles = resultResponse.articles
oldArticles?.addAll(newArticles)
}
return Resource.Success(searchNewsResponse ?: resultResponse)
}
}
return Resource.Error(respons.message())
}
fun insertArticle(article: Article) = viewModelScope.launch {
newsrepository.upsert(article)
}
fun deleteArticle(article: Article) = viewModelScope.launch {
newsrepository.delete(article)
}
fun getSavedArticles() = newsrepository.getAllArticles()
fun getBreakingNews() : LiveData<PagedList<Article>>{
return articles
}
}

NewsViewModelFactory

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.codewithgolap.newssplash.repository.NewsRepository
class NewsViewModelFactory(val newsRepository: NewsRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return NewsViewModel(newsRepository) as T
}
}

MainActivity

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.setupWithNavController
import com.codewithgolap.newssplash.repository.NewsRepository
import com.codewithgolap.newssplash.repository.db.ArticleDatabase
import com.codewithgolap.newssplash.viewModel.NewsViewModel
import com.codewithgolap.newssplash.viewModel.NewsViewModelFactory
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
lateinit var viewModel : NewsViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val newsRepository = NewsRepository(ArticleDatabase(this))
val viewModelProvider = NewsViewModelFactory(newsRepository)
viewModel = ViewModelProvider(this, viewModelProvider).get(NewsViewModel::class.java)
bottomNavigationView.setupWithNavController(newsFragment.findNavController())
}
}

Adapters > ArticleAdapter

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.codewithgolap.newssplash.R
import com.codewithgolap.newssplash.databinding.ItemArticleBinding
import com.codewithgolap.newssplash.model.Article
class ArticleAdapter : RecyclerView.Adapter<ArticleAdapter.ArticleViewHolder>() {
companion object {
private val diffUtilCallback = object : DiffUtil.ItemCallback<Article>() {
override fun areItemsTheSame(oldItem: Article, newItem: Article): Boolean {
return oldItem.url == newItem.url
}
override fun areContentsTheSame(oldItem: Article, newItem: Article) : Boolean {
return newItem.title == oldItem.title
}
}
}
class ArticleViewHolder(var view: ItemArticleBinding) : RecyclerView.ViewHolder(view.root)
val differ = AsyncListDiffer(this, diffUtilCallback)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) : ArticleViewHolder {
val inflater = LayoutInflater.from(parent.context)
val view = DataBindingUtil.inflate<ItemArticleBinding>(inflater, R.layout.item_article, parent, false)
return ArticleViewHolder(view)
}
override fun getItemCount() = differ.currentList.size
override fun onBindViewHolder(holder: ArticleViewHolder, position: Int) {
val article = differ.currentList[position]
holder.view.article = article
// Item CLick Listener
//Bind these click listeners later
holder.itemView.setOnClickListener {
onItemClickListener?.let {
article.let { article ->
it(article)
}
}
}
holder.view.ivShare.setOnClickListener {
onShareNewsClick?.let {
article?.let { it1 -> it(it1) }
}
}
holder.view.ivSave.setOnClickListener {
if (holder.view.ivSave.tag.toString().toInt() == 0) {
holder.view.ivSave.tag = 1
holder.view.ivSave.setImageDrawable(it.resources.getDrawable(R.drawable.ic_saved))
onArticleSaveClick?.let {
if (article != null) {
it(article)
}
}
}
else {
holder.view.ivSave.tag = 0
holder.view.ivSave.setImageDrawable(it.resources.getDrawable(R.drawable.ic_save))
onArticleDeleteClick?.let {
if (article != null) {
it(article)
}
}
}
onArticleSaveClick?.let {
article?.let { it1 -> it(it1) }
}
}
}
var isSave = false
override fun getItemId(position: Int) = position.toLong()
private var onItemClickListener: ((Article) -> Unit)? = null
private var onArticleSaveClick: ((Article) -> Unit)? = null
private var onArticleDeleteClick: ((Article) -> Unit)? = null
private var onShareNewsClick: ((Article) -> Unit)? = null
fun setOnItemCLickListener(listener: ((Article) -> Unit)) {
onItemClickListener = listener
}
fun onSaveClickListener(listener: (Article) -> Unit) {
onArticleSaveClick = listener
}
fun onDeleteClickListener(listener: (Article) -> Unit) {
onArticleDeleteClick = listener
}
fun onShareNewsClick(listener: (Article) -> Unit) {
onShareNewsClick = listener
}
}

BreakingNewsFragment

import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import com.codewithgolap.newssplash.MainActivity
import com.codewithgolap.newssplash.R
import com.codewithgolap.newssplash.adapters.ArticleAdapter
import com.codewithgolap.newssplash.utils.Resource
import com.codewithgolap.newssplash.utils.shareNews
import com.codewithgolap.newssplash.viewModel.NewsViewModel
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.fragment_breaking_news.*
import kotlin.random.Random
class BreakingNewsFragment : Fragment(R.layout.fragment_breaking_news) {
lateinit var viewModel : NewsViewModel
lateinit var newsAdapter: ArticleAdapter
val TAG = “BreakingNewsfragment”
private fun setupRecyclerView() {
newsAdapter = ArticleAdapter()
rvbreakingNews.apply {
adapter = newsAdapter
layoutManager = LinearLayoutManager(activity)
}
newsAdapter.setOnItemCLickListener {
val bundle = Bundle().apply {
putSerializable(“article”, it)
}
findNavController().navigate(
R.id.action_breakingNewsFragment_to_articleFragment,
bundle
)
}
newsAdapter.onSaveClickListener {
if (it.id == null){
it.id = Random.nextInt(0, 1000);
}
viewModel.insertArticle(it)
Snackbar.make(requireView(),”Saved”, Snackbar.LENGTH_SHORT).show()
}
newsAdapter.onDeleteClickListener {
viewModel.deleteArticle(it)
Snackbar.make(requireView(),”Removed”, Snackbar.LENGTH_SHORT).show()
}
newsAdapter.onShareNewsClick {
shareNews(context, it)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = (activity as MainActivity).viewModel
setupRecyclerView()
setViewModelObserver()
}
private fun setViewModelObserver() {
viewModel.breakingNews.observe(viewLifecycleOwner, Observer { newsResponse ->
when (newsResponse) {
is Resource.Success -> {
shimmerFrameLayout.stopShimmerAnimation()
shimmerFrameLayout.visibility = View.GONE
newsResponse.data?.let { news ->
rvbreakingNews.visibility = View.VISIBLE
newsAdapter.differ.submitList(news.articles)
}
}
is Resource.Error -> {
shimmerFrameLayout.visibility = View.GONE
newsResponse.message?.let { message ->
Log.e(TAG,”Error :: $message”)
}
}
is Resource.Loading -> {
shimmerFrameLayout.startShimmerAnimation()
}
}
})
}

}

ArticleFragment

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.navigation.fragment.navArgs
import com.codewithgolap.newssplash.MainActivity
import com.codewithgolap.newssplash.R
import com.codewithgolap.newssplash.viewModel.NewsViewModel
import kotlinx.android.synthetic.main.fragment_article.*
class ArticleFragment : Fragment(R.layout.fragment_article) {
lateinit var viewModel: NewsViewModel
val args : ArticleFragmentArgs by navArgs()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = (activity as MainActivity).viewModel
val article = args.article
webView.apply {
webViewClient = object : WebViewClient() {
override fun onPageCommitVisible(view: WebView?, url: String?) {
super.onPageCommitVisible(view, url)
progressBar.visibility = View.GONE
}
}
loadUrl(article.url.toString())
}
}
}

SavedNewsAdapter

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.codewithgolap.newssplash.R
import com.codewithgolap.newssplash.databinding.ItemArticleBinding
import com.codewithgolap.newssplash.databinding.ItemSavedNewsBinding
import com.codewithgolap.newssplash.model.Article
class SavedNewsAdapter : RecyclerView.Adapter<SavedNewsAdapter.SavedNewsViewHolder>() {

private val diffUtilCallback = object : DiffUtil.ItemCallback<Article>() {
override fun areItemsTheSame(oldItem: Article, newItem: Article): Boolean {
return oldItem.url == newItem.url
}
override fun areContentsTheSame(oldItem: Article, newItem: Article): Boolean {
return oldItem.id == newItem.id
}
}
inner class SavedNewsViewHolder(var view: ItemSavedNewsBinding) :
RecyclerView.ViewHolder(view.root)
val differ = AsyncListDiffer(this, diffUtilCallback)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SavedNewsViewHolder {
val inflater = LayoutInflater.from(parent.context)
val view = DataBindingUtil.inflate<ItemSavedNewsBinding>(
inflater,
R.layout.item_saved_news,
parent,
false
)
return SavedNewsViewHolder(view)
}
override fun getItemCount() = differ.currentList.size
override fun onBindViewHolder(holder: SavedNewsViewHolder, position: Int) {
val article = differ.currentList[position]
holder.view.article = article
// Item CLick Listener
//Bind these click listeners later
holder.itemView.setOnClickListener {
onItemClickListener?.let {
article.let { article ->
it(article)
}
}
}
holder.view.ivShare.setOnClickListener {
onShareNewsClick?.let {
article?.let { it1 -> it(it1) }
}
}
}
private var onItemClickListener: ((Article) -> Unit)? = null
private var onShareNewsClick: ((Article) -> Unit)? = null
fun setOnItemCLickListener(listener: ((Article) -> Unit)) {
onItemClickListener = listener
}
fun onShareNewsClick(listener: (Article) -> Unit) {
onShareNewsClick = listener
}
}

SavedNewsFragment

import android.os.Bundle
import android.util.Log
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.Observer
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.codewithgolap.newssplash.MainActivity
import com.codewithgolap.newssplash.R
import com.codewithgolap.newssplash.adapters.ArticleAdapter
import com.codewithgolap.newssplash.adapters.SavedNewsAdapter
import com.codewithgolap.newssplash.utils.Resource
import com.codewithgolap.newssplash.utils.shareNews
import com.codewithgolap.newssplash.viewModel.NewsViewModel
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.fragment_breaking_news.*
import kotlinx.android.synthetic.main.fragment_saved_news.*
import kotlin.random.Random

class SavedNewsFragment : Fragment(R.layout.fragment_saved_news) {
lateinit var viewModel: NewsViewModel
lateinit var newsAdapter: SavedNewsAdapter
val TAG = “SavedNewsFragment”
private fun setupRecyclerView() {
newsAdapter = SavedNewsAdapter()
rvSavedNews.apply {
adapter = newsAdapter
layoutManager = LinearLayoutManager(activity)
}
newsAdapter.setOnItemCLickListener {
val bundle = Bundle().apply {
putSerializable(“article”, it)
}
findNavController().navigate(
R.id.action_savedNewsFragment_to_articleFragment,
bundle
)
}
newsAdapter.onShareNewsClick {
shareNews(context, it)
}
// swipe to delete
val onItemTouchHelperCallback = object : ItemTouchHelper.SimpleCallback(
ItemTouchHelper.UP or ItemTouchHelper.DOWN,
ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
)
{
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position = viewHolder.adapterPosition
val article = newsAdapter.differ.currentList[position]
viewModel.deleteArticle(article)
Snackbar.make(requireView(), “Deleted Successfully”, Snackbar.LENGTH_LONG).apply {
setAction(“Undo”){
viewModel.insertArticle(article)
}
show()
}
}
}
ItemTouchHelper(onItemTouchHelperCallback).apply {
attachToRecyclerView(rvSavedNews)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = (activity as MainActivity).viewModel
setupRecyclerView()
setViewModelObserver()
}
private fun setViewModelObserver() {
viewModel = (activity as MainActivity).viewModel
viewModel.getSavedArticles().observe(viewLifecycleOwner, Observer {
Log.i(TAG,””+it.size)
newsAdapter.differ.submitList(it)
rvSavedNews.visibility = View.VISIBLE
shimmerFrameLayout2.stopShimmerAnimation()
shimmerFrameLayout2.visibility = View.GONE
})
}

}

SearchNewsFragment

import android.os.Bundle
import android.util.Log
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.widget.addTextChangedListener
import androidx.lifecycle.Observer
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import com.codewithgolap.newssplash.MainActivity
import com.codewithgolap.newssplash.R
import com.codewithgolap.newssplash.adapters.ArticleAdapter
import com.codewithgolap.newssplash.adapters.SavedNewsAdapter
import com.codewithgolap.newssplash.utils.Constants
import com.codewithgolap.newssplash.utils.Resource
import com.codewithgolap.newssplash.utils.shareNews
import com.codewithgolap.newssplash.viewModel.NewsViewModel
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.fragment_search_news.*
import kotlinx.coroutines.Job
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.random.Random
class SearchNewsFragment : Fragment(R.layout.fragment_search_news) {
lateinit var viewModel: NewsViewModel
lateinit var newsAdapter: ArticleAdapter
val TAG = “SearchNewsFragment”
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = (activity as MainActivity).viewModel
setupRecyclerView()
newsAdapter.setOnItemCLickListener {
val bundle = Bundle().apply {
putSerializable(“article”, it)
}
findNavController().navigate(
R.id.action_breakingNewsFragment_to_articleFragment,
bundle
)
}
newsAdapter.onSaveClickListener {
viewModel.insertArticle(it)
Snackbar.make(requireView(),”Saved”, Snackbar.LENGTH_SHORT).show()
}
newsAdapter.onDeleteClickListener {
viewModel.deleteArticle(it)
Snackbar.make(requireView(),”Removed”, Snackbar.LENGTH_SHORT).show()
}
newsAdapter.onShareNewsClick {
shareNews(context, it)
}
var searchJob : Job? = null
etSearch.addTextChangedListener{ editable ->
searchJob?.cancel()
searchJob = MainScope().launch {
delay(Constants.SEARCH_TIME_DELAY)
editable?.let{
if (!editable.toString().trim().isEmpty()){
viewModel.getSearchedNews(editable.toString())
}
}
}
}
viewModel.searchNews.observe(viewLifecycleOwner, Observer { newsResponse ->
when(newsResponse){
is Resource.Success -> {
shimmerFrameLayout3.stopShimmerAnimation()
shimmerFrameLayout3.visibility = View.GONE
newsResponse.data?.let { news->
newsAdapter.differ.submitList(news.articles)
}
}
is Resource.Error -> {
shimmerFrameLayout3.stopShimmerAnimation()
shimmerFrameLayout3.visibility = View.GONE
newsResponse.message?.let { message ->
Log.e(TAG,”Error :: $message”)
}
}
is Resource.Loading -> {
shimmerFrameLayout3.startShimmerAnimation()
shimmerFrameLayout3.visibility = View.VISIBLE
}
}
})
}
private fun setupRecyclerView() {
newsAdapter = ArticleAdapter()
rvSearchNews.apply {
adapter = newsAdapter
layoutManager = LinearLayoutManager(activity)
}
}
}

Watch the full video on YouTube:

--

--

Golap Gunjan Barman

Hi everyone, myself Golap an Android app developer with UI/UX designer.