Paging 3 in Android not display any data

98 views Asked by At

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
0

There are 0 answers