Build.gradle
plugins {
alias(libs.plugins.androidApplication)
alias(libs.plugins.jetbrainsKotlinAndroid)
}
android {
namespace = "com.akashpal.gcrg.mvvm"
compileSdk = 34
defaultConfig {
applicationId = "com.akashpal.gcrg.mvvm"
minSdk = 24
targetSdk = 34
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
buildFeatures {
dataBinding=true
viewBinding=true
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
implementation(libs.androidx.activity)
implementation(libs.androidx.constraintlayout)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("com.google.code.gson:gson:2.10.1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.4")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.8.4")
implementation ("androidx.paging:paging-runtime:3.1.1")
https://github.com/Akashpal12/AndroidPagging3.git
https://drive.google.com/file/d/10SQXXa8MhRvGxsIs4-FfHImwhI1D9yjF/view?usp=sharing
}
api/ApiService
package com.akashpal.gcrg.mvvm.api
import com.akashpal.gcrg.mvvm.models.PostModelItem
import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Query
interface ApiService {
@GET("/posts")
suspend fun getPosts(): Response<List<PostModelItem>>
@GET("posts")
suspend fun getPagePosts(
@Query("_page") page: Int,
@Query("_limit") limit: Int
): List<PostModelItem>
}
api/RetrofitClient
package com.akashpal.gcrg.mvvm.api
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
object RetrofitClient {
private const val BASE_URL = "https://jsonplaceholder.typicode.com"
fun getClient(): ApiService {
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
return retrofit.create(ApiService::class.java)
}
}
respository/PostRepository
package com.akashpal.gcrg.mvvm.repository
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.map
import com.akashpal.gcrg.mvvm.api.ApiService
import com.akashpal.gcrg.mvvm.models.PostModelItem
class PostRepository(private val apiService: ApiService) {
private val postsLiveData = MutableLiveData<List<PostModelItem>>()
val posts: LiveData<List<PostModelItem>> get() = postsLiveData
suspend fun getPosts() {
val result = apiService.getPosts()
if (result.isSuccessful && result.body() != null) {
postsLiveData.postValue(result.body())
}
}
}
respository/PostPagingRepository
package com.akashpal.gcrg.mvvm.repository
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.liveData
import com.akashpal.gcrg.mvvm.api.ApiService
import com.akashpal.gcrg.mvvm.models.PostModelItem
import com.akashpal.gcrg.mvvm.paging.PostPagingSource
class PostPagingRepository(private val apiService: ApiService) {
fun getPagePosts() = Pager<Int, PostModelItem>(
config = PagingConfig(
pageSize = 10,
),
pagingSourceFactory = { PostPagingSource(apiService) }
).liveData
}
viewModel/PostViewModel
package com.akashpal.gcrg.mvvm.viewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.map
import androidx.lifecycle.viewModelScope
import com.akashpal.gcrg.mvvm.models.PostModelItem
import com.akashpal.gcrg.mvvm.repository.PostRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class PostViewModel(private val repository: PostRepository) : ViewModel() {
val posts: LiveData<List<PostModelItem>> get() = repository.posts
init {
viewModelScope.launch(Dispatchers.IO) {
repository.getPosts()
}
}
// Function to get a specific post by ID
fun getPostById(id: Int): LiveData<PostModelItem?> {
return posts.map { list ->
list.find { it.id == id }
}
}
// Function to get a list of post titles for the Spinner
fun getPostTitles(): LiveData<List<String>> {
return posts.map { list ->
list.map { "${it.userId} - ${it.id}" }
}
}
}
respository/PostViewModelFactory
package com.akashpal.gcrg.mvvm.viewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.akashpal.gcrg.mvvm.repository.PostRepository
class PostViewModelFactory(private val repository: PostRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return PostViewModel(repository) as T
}
}
respository/PostPagingViewModel
package com.akashpal.gcrg.mvvm.viewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.PagingData
import androidx.paging.cachedIn
import com.akashpal.gcrg.mvvm.models.PostModelItem
import com.akashpal.gcrg.mvvm.repository.PostPagingRepository
class PostPagingViewModel(private val repository: PostPagingRepository) : ViewModel() {
val posts: LiveData<PagingData<PostModelItem>> = repository.getPagePosts().cachedIn(viewModelScope)
}
respository/PostPagingViewModelFactory
package com.akashpal.gcrg.mvvm.viewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.akashpal.gcrg.mvvm.repository.PostPagingRepository
import com.akashpal.gcrg.mvvm.repository.PostRepository
class PostPagingViewModelFactory(private val repository: PostPagingRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return PostPagingViewModel(repository) as T
}
}
adpter/PostAdapter(Only showing for recyclerlist)
package com.akashpal.gcrg.mvvm.adapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.akashpal.gcrg.mvvm.R
import com.akashpal.gcrg.mvvm.models.PostModelItem
class PostAdapter : ListAdapter<PostModelItem, PostAdapter.PostViewHolder>(DiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PostViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_post, parent, false)
return PostViewHolder(view)
}
override fun onBindViewHolder(holder: PostViewHolder, position: Int) {
val post = getItem(position)
if (post != null) {
holder.titleTextView.text = post.title
holder.bodyTextView.text = post.body
}
}
class PostViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val titleTextView: TextView = itemView.findViewById(R.id.tvTitle)
val bodyTextView: TextView = itemView.findViewById(R.id.tvBody)
}
class DiffCallback : DiffUtil.ItemCallback<PostModelItem>() {
override fun areItemsTheSame(oldItem: PostModelItem, newItem: PostModelItem): Boolean =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: PostModelItem, newItem: PostModelItem): Boolean =
oldItem == newItem
}
}
paging/PostPagingAdapter(for pagination adapter)
package com.akashpal.gcrg.mvvm.paging
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.akashpal.gcrg.mvvm.R
import com.akashpal.gcrg.mvvm.models.PostModelItem
class PostPagingAdapter : PagingDataAdapter<PostModelItem, PostPagingAdapter.PostViewHolder>(POST_COMPARATOR) {
class PostViewHolder(view: View) : RecyclerView.ViewHolder(view) {
private val titleTextView: TextView = view.findViewById(R.id.tvTitle)
private val bodyTextView: TextView = view.findViewById(R.id.tvBody)
fun bind(post: PostModelItem) {
titleTextView.text = post.title
bodyTextView.text = post.body
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PostViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_post, parent, false)
return PostViewHolder(view)
}
override fun onBindViewHolder(holder: PostViewHolder, position: Int) {
val post = getItem(position)
if (post != null) {
holder.bind(post)
}
}
companion object {
private val POST_COMPARATOR = object : DiffUtil.ItemCallback<PostModelItem>() {
override fun areItemsTheSame(oldItem: PostModelItem, newItem: PostModelItem): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: PostModelItem, newItem: PostModelItem): Boolean {
return oldItem == newItem
}
}
}
}
paging/PostPagingSource(for pagination)
package com.akashpal.gcrg.mvvm.paging
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.akashpal.gcrg.mvvm.api.ApiService
import com.akashpal.gcrg.mvvm.models.PostModelItem
class PostPagingSource(private val apiService: ApiService) : PagingSource<Int, PostModelItem>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, PostModelItem> {
return try {
val currentPage = params.key ?: 1
val response = apiService.getPagePosts(page = currentPage, limit = params.loadSize)
LoadResult.Page(
data = response,
prevKey = if (currentPage == 1) null else currentPage - 1,
nextKey = if (response.isEmpty()) null else currentPage + 1
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
override fun getRefreshKey(state: PagingState<Int, PostModelItem>): Int? {
return state.anchorPosition?.let { position ->
state.closestPageToPosition(position)?.prevKey?.plus(1)
?: state.closestPageToPosition(position)?.nextKey?.minus(1)
}
}
}
paging/PostLoadStateAdapter(for loading pages)
package com.akashpal.gcrg.mvvm.adapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ProgressBar
import android.widget.TextView
import androidx.core.view.isVisible
import androidx.paging.LoadState
import androidx.paging.LoadStateAdapter
import androidx.recyclerview.widget.RecyclerView
import com.akashpal.gcrg.mvvm.R
class PostLoadStateAdapter(private val retry: () -> Unit) : LoadStateAdapter<PostLoadStateAdapter.LoadStateViewHolder>() {
// ViewHolder class to hold and bind the views
class LoadStateViewHolder(itemView: View, retry: () -> Unit) : RecyclerView.ViewHolder(itemView) {
private val progressBar: ProgressBar = itemView.findViewById(R.id.progressBar)
private val btnRetry: Button = itemView.findViewById(R.id.btnRetry)
private val errorMsg: TextView = itemView.findViewById(R.id.errorMsg)
init {
btnRetry.setOnClickListener { retry.invoke() }
}
fun bind(loadState: LoadState) {
progressBar.isVisible = loadState is LoadState.Loading
btnRetry.isVisible = loadState is LoadState.Error
errorMsg.isVisible = loadState is LoadState.Error
}
}
// Create ViewHolder from the layout
override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): LoadStateViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_load_state, parent, false)
return LoadStateViewHolder(view, retry)
}
// Bind the data to the ViewHolder
override fun onBindViewHolder(holder: LoadStateViewHolder, loadState: LoadState) {
holder.bind(loadState)
}
}
res/activity_main.xml(for loading pages)
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:orientation="vertical"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:background="#FBDFDF">
<Spinner
android:id="@+id/spinner_posts"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="8dp"
android:padding="6dp" />
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18dp"
android:textStyle="bold"
android:text="Recycler List"
android:gravity="center"
android:layout_marginBottom="4dp"
android:textColor="@color/black"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
<Button
android:id="@+id/btn"
android:layout_width="match_parent"
android:layout_alignParentBottom="true"
android:layout_marginBottom="10dp"
android:layout_height="wrap_content"
android:text="Pagination Activity"/>
</RelativeLayout>
res/activity_second.xml(for loading pages)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".SecondActivity">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18dp"
android:textStyle="bold"
android:text="Recycler Pagination"
android:gravity="center"
android:layout_marginBottom="4dp"
android:textColor="@color/black"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</LinearLayout>
res/item_load_state.xml(for loading pages)
<!-- res/layout/item_load_state.xml -->
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp"
android:gravity="center">
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/errorMsg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Error occurred"
android:visibility="gone"
android:paddingTop="8dp" />
<Button
android:id="@+id/btnRetry"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Retry"
android:visibility="gone"
android:paddingTop="8dp" />
</LinearLayout>
</layout>
res/item_post.xml(for loading pages)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textStyle="bold"
android:textSize="16sp"
android:text="Post Title" />
<TextView
android:id="@+id/tvBody"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textSize="14sp"
android:text="Post Body" />
</LinearLayout>
MainActivity.kt
<package com.akashpal.gcrg.mvvm
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Spinner
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.akashpal.gcrg.mvvm.adapter.PostAdapter
import com.akashpal.gcrg.mvvm.api.RetrofitClient
import com.akashpal.gcrg.mvvm.databinding.ActivityMainBinding
import com.akashpal.gcrg.mvvm.repository.PostRepository
import com.akashpal.gcrg.mvvm.viewModel.PostViewModel
import com.akashpal.gcrg.mvvm.viewModel.PostViewModelFactory
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
lateinit var mainViewModel: PostViewModel
private lateinit var postAdapter: PostAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
val postService=RetrofitClient.getClient()
val repository=PostRepository(postService)
setupRecyclerView()
mainViewModel= ViewModelProvider(this,PostViewModelFactory(repository)).get(PostViewModel::class.java)
mainViewModel.posts.observe(this, Observer {
if (it != null) {
Log.d("PostData", it.toString())
postAdapter.submitList(it)
}
})
mainViewModel.getPostById(2).observe(this, Observer {
if (it != null) {
Log.d("SpecificPost", "Post with ID 2: ${it.title}")
} else {
Log.d("SpecificPost", "No post found with ID $2")
}
})
val spinner: Spinner = findViewById(R.id.spinner_posts)
mainViewModel.getPostTitles().observe(this) { titles ->
val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, titles)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
spinner.adapter = adapter
}
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) {
// Get the selected item
val selectedItem = parent.getItemAtPosition(position) as String
// Show a Toast with the selected index and value
Toast.makeText(
this@MainActivity, "Selected index: $position, Value: $selectedItem",
Toast.LENGTH_SHORT
).show()
}
override fun onNothingSelected(parent: AdapterView<*>) {
// Handle the case when nothing is selected if needed
}
}
binding.btn.setOnClickListener {
val intent = Intent(this, SecondActivity::class.java)
startActivity(intent)
}
}
private fun setupRecyclerView() {
val recyclerView = findViewById(R.id.recyclerView)
recyclerView.layoutManager = LinearLayoutManager(this)
postAdapter = PostAdapter()
recyclerView.adapter = postAdapter
}
}
SecondActivity.kt
<package com.akashpal.gcrg.mvvm
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import com.akashpal.gcrg.mvvm.adapter.PostLoadStateAdapter
import com.akashpal.gcrg.mvvm.api.RetrofitClient
import com.akashpal.gcrg.mvvm.databinding.ActivitySecondBinding
import com.akashpal.gcrg.mvvm.paging.PostPagingAdapter
import com.akashpal.gcrg.mvvm.repository.PostPagingRepository
import com.akashpal.gcrg.mvvm.viewModel.PostPagingViewModel
import com.akashpal.gcrg.mvvm.viewModel.PostPagingViewModelFactory
class SecondActivity : AppCompatActivity() {
private lateinit var binding: ActivitySecondBinding
private lateinit var mainViewModel: PostPagingViewModel
private lateinit var postAdapter: PostPagingAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
binding = ActivitySecondBinding.inflate(layoutInflater)
setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
// Initialize Retrofit and Repository
val postService = RetrofitClient.getClient()
val repository = PostPagingRepository(postService)
// Set up RecyclerView with PagingDataAdapter and LoadStateAdapter
setupRecyclerView()
// Initialize ViewModel
mainViewModel = ViewModelProvider(
this,
PostPagingViewModelFactory(repository)
).get(PostPagingViewModel::class.java)
mainViewModel.posts.observe(this, Observer { pagingData ->
postAdapter.submitData(lifecycle, pagingData)
})
}
private fun setupRecyclerView() {
binding.recyclerView.apply {
layoutManager = LinearLayoutManager(this@SecondActivity)
postAdapter = PostPagingAdapter()
adapter = postAdapter.withLoadStateHeaderAndFooter(
header = PostLoadStateAdapter { postAdapter.retry() },
footer = PostLoadStateAdapter { postAdapter.retry() }
)
}
}
}