I'm currently working on an Android app, and I've encountered an issue while using Paging 3 to display data obtained from an API. Unfortunately, the data is not showing up in my app at all, and I've been trying to find a solution for hours without success. The app runs smoothly in other aspects, but this particular problem is causing me some trouble.
To provide more details about the issue, I've set up my app to fetch data from an API using Paging 3 for efficient data loading and display. The API call seems to work fine, and I can confirm that the data is being received correctly. However, when I try to display this data in my app, it's not showing up as expected.
What's more, I've integrated a local database and a DAO (Data Access Object) to save the data obtained from Paging 3. The data is successfully stored in the database, but it's not appearing in the app's user interface when I try to retrieve it from the database.
I've checked my implementation, reviewed my code, and ensured that the RecyclerView, adapter, and database components are correctly set up, but still no luck in displaying the data.
I would greatly appreciate any insights or suggestions from you guys to help me resolve this issue. If anyone has encountered a similar problem or has experience with Paging 3 and local database integration, please feel free to share your thoughts and advice on how to make the data appear in my app as intended.
Thank you in advance for your assistance!
Here is my code:
///////////////////////////////////////////
StoryActivity.kt
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.provider.Settings
import android.view.MenuItem
import android.widget.Toast
import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.widget.Toolbar
import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.dicodingsocialmedia.R
import com.example.dicodingsocialmedia.databinding.ActivityMainBinding
import com.example.sosmeddicoding.data.di.Injection
import com.example.sosmeddicoding.data.model.ErrorResponse
import com.example.sosmeddicoding.ui.WelcomeActivity
import com.example.sosmeddicoding.ui.camera.UploadActivity
import com.example.sosmeddicoding.ui.detailStory.DetailStory
import com.example.sosmeddicoding.ui.map.MapsActivity
import com.example.sosmeddicoding.ui.story.StoryAdapter.Companion.diffCallback
import com.example.sosmeddicoding.ui.story.adapter.LoadingStateAdapter
import com.example.sosmeddicoding.utils.AuthPreferences
import com.example.sosmeddicoding.utils.dataStore
import com.google.android.material.navigation.NavigationView
import com.google.gson.Gson
import kotlinx.coroutines.launch
import retrofit2.HttpException
class StoryActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener {
private lateinit var binding: ActivityMainBinding
private lateinit var drawerLayout: DrawerLayout
private lateinit var viewModel: StoryViewModel
private lateinit var authPreferences: AuthPreferences
private val adapter by lazy {
StoryAdapter(diffCallback)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// NavBar
drawerLayout = findViewById<DrawerLayout>(R.id.drawerLayout)
val toolbar = findViewById<Toolbar>(R.id.toolbar)
setSupportActionBar(toolbar)
val navigationView = findViewById<NavigationView>(R.id.nav_view)
navigationView.setNavigationItemSelectedListener(this)
val toggle = ActionBarDrawerToggle(this, drawerLayout, toolbar, R.string.open_nav, R.string.close_nav)
drawerLayout.addDrawerListener(toggle)
toggle.syncState()
// Check Token
authPreferences = AuthPreferences.getInstance(application.dataStore)
lifecycleScope.launch {
authPreferences.getAuthToken.collect { savedToken ->
if (savedToken == "") {
startActivity(Intent(applicationContext, WelcomeActivity::class.java))
authPreferences.clearToken()
} else {
authPreferences.getAuthToken.collect {
this@StoryActivity
}
}
}
}
// Observe
val storyRepo = Injection.provideRepository(this)
binding.recyclerView.layoutManager = LinearLayoutManager(this)
binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.adapter = adapter.withLoadStateFooter(
footer = LoadingStateAdapter {
adapter.retry()
}
)
// viewModel.setImageUrls(imageUrls)
viewModel = ViewModelProvider(
this,
StoryViewModelFactory(storyRepo)
).get(StoryViewModel::class.java)
viewModel.allStories.observe(this) { response ->
if (response != null) {
adapter.submitData(lifecycle, response)
} else {
val error = response.toString()
showToast(error)
}
}
binding.upload.setOnClickListener {
startActivity(Intent(applicationContext, UploadActivity::class.java))
}
setContentView(binding.root)
}
override fun onNavigationItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.nav_logout -> {
lifecycleScope.launch {
try {
authPreferences.saveToken("")
startActivity(Intent(applicationContext, WelcomeActivity::class.java))
} catch (e: HttpException) {
val jsonInString = e.response()?.errorBody()?.string()
val errorBody = Gson().fromJson(jsonInString, ErrorResponse::class.java)
val errorMessage = errorBody.message
if (errorMessage != null) {
showToast(errorMessage)
Toast.makeText(
this@StoryActivity,
"Register Failed, please register again",
Toast.LENGTH_LONG
).show()
}
}
}
}
R.id.nav_languange -> {
startActivity(Intent(Settings.ACTION_LOCALE_SETTINGS))
}
R.id.nav_map -> {
startActivity(Intent(applicationContext, MapsActivity::class.java))
}
}
drawerLayout.closeDrawer(GravityCompat.START)
return true
}
override fun onResume() {
super.onResume()
viewModel.allStories.observe(this) { response ->
if (response != null) {
adapter.submitData(lifecycle, response)
} else {
val error = response.toString()
showToast(error)
}
}
}
override fun onBackPressed() {
val intent = Intent(Intent.ACTION_MAIN)
intent.addCategory(Intent.CATEGORY_HOME)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
startActivity(intent)
}
private fun showToast(message: String) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
}
}
StoryViewModel
import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.liveData
import androidx.lifecycle.viewModelScope
import androidx.paging.PagingData
import androidx.paging.cachedIn
import com.example.sosmeddicoding.data.database.entity.StoryResponseItem
import com.example.sosmeddicoding.data.di.Injection
import com.example.sosmeddicoding.data.model.ErrorResponse
import com.example.sosmeddicoding.data.model.ListStoryItem
import com.example.sosmeddicoding.data.model.ResponseGetAllStory
import com.example.sosmeddicoding.data.repo.AuthRepo
import com.example.sosmeddicoding.ui.WelcomeViewModel
import com.example.sosmeddicoding.utils.AuthPreferences
import com.google.gson.Gson
import kotlinx.coroutines.launch
import retrofit2.HttpException
class StoryViewModel(private val storyRepo: StoryRepo) : ViewModel() {
val allStories: LiveData<PagingData<StoryResponseItem>> =
storyRepo.getAllStories().cachedIn(viewModelScope)
val allStoriesWithLocation: LiveData<ResponseGetAllStory> = liveData {
try{
val location = 1
val result = storyRepo.getStoriesWithLocation(location)
emit(result)
} catch (e:Exception) {
emit(ResponseGetAllStory(error = true, message = "Terjadi kesalahan ${e.message}", listStory = emptyList()))
}
}
}
class StoryViewModelFactory(private val storyRepo: StoryRepo) :
ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(StoryViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return StoryViewModel(storyRepo) as T
}
throw IllegalAccessException("Unkwon ViewModel :" + modelClass.name)
}
}
StoryRepo
class StoryRepo(private val apiService: ApiService, authPreferences: AuthPreferences, private val storyDatabase: StoryDatabase) {
@OptIn(ExperimentalPagingApi::class)
fun getAllStories() : LiveData<PagingData<StoryResponseItem>> {
return Pager(
config = PagingConfig(
pageSize = 5
),
remoteMediator = StoryRemoteMediator(storyDatabase, apiService),
pagingSourceFactory = {
// StoryPagingSource(apiService)
storyDatabase.storyDao().getAllStory()
}
).liveData
}
suspend fun getStoriesWithLocation(location: Int) : ResponseGetAllStory {
return apiService.getStoriesWithLocation()
}
fun uploadImage(imageFile: File, description: String) = liveData {
emit(ResultViewModel.Loading)
val requestBody = description.toRequestBody("text/plain".toMediaType())
val requestImageFile = imageFile.asRequestBody("image/jpeg".toMediaType())
val multipartBody = MultipartBody.Part.createFormData(
"photo",
imageFile.name,
requestImageFile
)
try {
val successResponse = apiService.postStory(multipartBody, requestBody)
emit(ResultViewModel.Success(successResponse))
} catch (e: HttpException) {
val errorBody = e.response()?.errorBody()?.string()
val errorResponse = Gson().fromJson(errorBody, ResponseAddNewStory::class.java)
emit(errorResponse.message?.let { ResultViewModel.Error(it) })
}
}
companion object {
private var instance: StoryRepo? = null
fun getInstance(apiService: ApiService, authPreferences: AuthPreferences, storyDatabase: StoryDatabase): StoryRepo {
return instance ?: synchronized(this) {
instance ?: StoryRepo(apiService, authPreferences, storyDatabase).also { instance = it }
}
}
}
}
ApiService
@GET("stories")
suspend fun getAllStories( @Query("page") page: Int = 1,
@Query("size") size: Int = 20): ResponseGetAllStory
StoryPagingSource
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.example.sosmeddicoding.data.database.entity.StoryResponseItem
import com.example.sosmeddicoding.data.model.ListStoryItem
import com.example.sosmeddicoding.data.model.ResponseGetAllStory
import com.example.sosmeddicoding.data.service.ApiService
class StoryPagingSource(private val apiService: ApiService): PagingSource<Int, StoryResponseItem>() {
private companion object {
const val INITIAL_PAGE_INDEX = 1
}
override fun getRefreshKey(state: PagingState<Int, StoryResponseItem>): Int? {
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, StoryResponseItem> {
return try {
val position = params.key ?: INITIAL_PAGE_INDEX
val responseData = apiService.getAllStories(position, params.loadSize)
val stories = responseData.listStory ?: emptyList()
// Melakukan pemetaan dari ListStoryItem ke StoryResponseItem
val mappedStories = stories.map { listStoryItem ->
StoryResponseItem(
id = listStoryItem?.id!!,
name = listStoryItem?.name!!,
description = listStoryItem?.description!!,
createdAt = listStoryItem?.createdAt!!,
photoUrl = listStoryItem?.photoUrl!!,
lon = listStoryItem?.lon,
lat = listStoryItem?.lat
)
}
LoadResult.Page(
data = mappedStories,
prevKey = if (position == INITIAL_PAGE_INDEX) null else position - 1,
nextKey = if (stories.isEmpty()) null else position + 1
)
} catch (exception: Exception) {
LoadResult.Error(exception)
}
}
}
StoryDatabase
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import com.example.sosmeddicoding.data.database.entity.StoryResponseItem
import com.example.sosmeddicoding.data.model.ListStoryItem
@Database(
entities = [StoryResponseItem::class, RemoteKeys::class],
version = 2,
exportSchema = false
)
abstract class StoryDatabase: RoomDatabase() {
abstract fun storyDao(): StoryDao
abstract fun remoteKeysDao(): RemoteKeysDao
companion object {
@Volatile
private var INSTANCE: StoryDatabase? = null
@JvmStatic
fun getDatabase(context: Context): StoryDatabase {
return INSTANCE ?: synchronized(this) {
INSTANCE ?: Room.databaseBuilder(
context.applicationContext,
StoryDatabase::class.java, "quote_database"
)
.fallbackToDestructiveMigration()
.build()
.also { INSTANCE = it }
}
}
}
}
ResponseGetAllStory
import kotlinx.parcelize.Parcelize
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.google.gson.annotations.SerializedName
data class ResponseGetAllStory(
@field:SerializedName("listStory")
val listStory: List<ListStoryItem?>? = emptyList(),
@field:SerializedName("error")
var error: Boolean? = null,
@field:SerializedName("message")
val message: String? = null
)
@Parcelize
data class ListStoryItem(
@field:SerializedName("id")
var id: String? = null,
@field:SerializedName("photoUrl")
var photoUrl: String? = null,
@field:SerializedName("createdAt")
var createdAt: String? = null,
@field:SerializedName("name")
var name: String? = null,
@field:SerializedName("description")
var description: String? = null,
@field:SerializedName("lon")
var lon: Double? = null,
@field:SerializedName("lat")
var lat: Double? = null
) : Parcelable
StoryResponseItem
import android.os.Parcelable
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
@Entity(tableName = "story")
@Parcelize
data class StoryResponseItem(
@PrimaryKey
val id: String,
val name: String,
val description: String,
@ColumnInfo(name = "photo_url")
val photoUrl: String,
@ColumnInfo(name = "created_at")
val createdAt: String,
val lat: Double?,
val lon: Double?
): Parcelable