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.