Android Kotlin Paging Library with Retrofit
20-05-2020PostsDataSource
class PostsDataSource(private val scope: CoroutineScope) : PageKeyedDataSource<Int, Question>() { private val repository = QuestionRepository() override fun loadInitial( params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, Question> ) { scope.launch { try { val response = repository.getQuestionsAsync(1) when { response.success -> { val listing = response.data val nextPage = 2 callback.onResult(listing?.data ?: listOf(), null, nextPage) } } } catch (exception: Exception) { Log.e("PostsDataSource", "Failed to fetch data!") } } } override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, Question>) { scope.launch { try { val response = repository.getQuestionsAsync(params.key) when { response.success -> { val listing = response.data val items = listing?.data callback.onResult(items ?: listOf(), params.key + 1) } } } catch (exception: Exception) { Log.e("PostsDataSource", "Failed to fetch data!") } } } override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, Question>) { } override fun invalidate() { super.invalidate() scope.cancel() } }
QuestionAdapter Class
import android.view.LayoutInflater import android.view.ViewGroup import androidx.databinding.DataBindingUtil import androidx.paging.PagedListAdapter import androidx.recyclerview.widget.RecyclerView import com.codesenior.period.tracker.R import com.codesenior.period.tracker.databinding.RecyclerviewQuestionsBinding import com.codesenior.period.tracker.models.Question import com.codesenior.period.tracker.ui.fragments.QuestionFragment class QuestionAdapter(private val listener: QuestionFragment.OnQuestionFragmentListener?) : PagedListAdapter<Question, QuestionAdapter.QuestionViewHolder>(DiffUtilCallBack()) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QuestionViewHolder { return QuestionViewHolder( DataBindingUtil.inflate( LayoutInflater.from(parent.context), R.layout.recyclerview_questions, parent, false ) ) } override fun onBindViewHolder(holder: QuestionAdapter.QuestionViewHolder, position: Int) { holder.recyclerViewItem.question = getItem(position) holder.recyclerViewItem.textViewTitle.setOnClickListener {view-> getItem(position)?.let { listener?.onQuestionItemClicked(it) } } } inner class QuestionViewHolder( val recyclerViewItem: RecyclerviewQuestionsBinding) : RecyclerView.ViewHolder(recyclerViewItem.root) }
DiffUtilCallBack
import androidx.recyclerview.widget.DiffUtil import com.codesenior.period.tracker.models.Question class DiffUtilCallBack : DiffUtil.ItemCallback<Question>() { override fun areItemsTheSame(oldItem: Question, newItem: Question): Boolean { return oldItem.id == newItem.id } override fun areContentsTheSame(oldItem: Question, newItem: Question): Boolean { return oldItem.title == newItem.title && oldItem.content == newItem.content } }
recyclerview_questions.xml File
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:card_view="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> <data> <variable name="question" type="com.codesenior.period.tracker.models.Question" /> </data> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:layout_marginBottom="@dimen/dp_20" android:padding="@dimen/dp_20" android:background="@drawable/shape"> <TextView android:id="@+id/text_view_title" android:layout_width="match_parent" android:layout_height="wrap_content" android:textColor="@color/colorAccent" android:text="@{question.title}" android:textAppearance="@style/TextAppearance.AppCompat.Large" tools:text="Adet Gecikmesi" /> </LinearLayout> </layout>
QuestionListViewModel
class QuestionListViewModel : ViewModel() { var postsLiveData: LiveData<PagedList<Question>> init { val config = PagedList.Config.Builder() .setPageSize(10) .setEnablePlaceholders(false) .build() postsLiveData = initializedPagedListBuilder(config).build() } fun getPosts(): LiveData<PagedList<Question>> = postsLiveData private fun initializedPagedListBuilder(config: PagedList.Config): LivePagedListBuilder<Int, Question> { val dataSourceFactory = object : DataSource.Factory<Int, Question>() { override fun create(): DataSource<Int, Question> { return PostsDataSource(viewModelScope) } } return LivePagedListBuilder<Int, Question>(dataSourceFactory, config) } }
QuestionFragment
class QuestionFragment : Fragment() { private lateinit var viewModel: QuestionListViewModel var listener: OnQuestionFragmentListener? = null private val adapter = QuestionAdapter(listener) companion object { fun newInstance() = QuestionFragment() } interface OnQuestionFragmentListener { fun onQuestionItemClicked(question: Question) } override fun onAttach(context: Context) { super.onAttach(context) listener = context as? OnQuestionFragmentListener if (listener == null) { throw ClassCastException("$context must implement OnArticleSelectedListener") } } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { return inflater.inflate(R.layout.question_fragment, container, false) } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) viewModel = ViewModelProvider(this).get(QuestionListViewModel::class.java) viewModel.getPosts().observe(viewLifecycleOwner, Observer { adapter.submitList(it) recycler_view_questions.layoutManager = LinearLayoutManager(requireContext()) recycler_view_questions.adapter = adapter }) fab_create_question.setOnClickListener { view -> Snackbar.make(view, "Here's a Snackbar", Snackbar.LENGTH_LONG) .setAction("Action", null) .show() } } }
Repository, Provider, Retrofit
class QuestionRepository { private var questionProvider = QuestionProvider() suspend fun getQuestionsAsync(page: Int) = questionProvider.getQuestions(page) } class QuestionProvider { suspend fun getQuestions(page: Int): PaginationResponse<Question> { val apiClient = ApiClient.getClient(Config.REST_API) val service = apiClient.create(QuestionService::class.java) return service.getQuestions(page) } } interface QuestionService { @GET("questions") suspend fun getQuestions(@Query("page") page: Int): PaginationResponse<Question> } object ApiClient { fun getClient(baseUrl: String?): Retrofit { return getClient(baseUrl, null) } fun getClient(baseUrl: String?, accessToken: String?): Retrofit { val interceptor = HttpLoggingInterceptor() interceptor.level = HttpLoggingInterceptor.Level.BODY val client = OkHttpClient.Builder() .connectTimeout(60, TimeUnit.SECONDS) .addInterceptor(interceptor) .addInterceptor { chain -> val ongoing = chain.request().newBuilder() ongoing.addHeader("Accept", "application/json;versions=1") if (accessToken != null) { ongoing.addHeader("Authorization", "Bearer $accessToken") } chain.proceed(ongoing.build()) }.build() val gson = GsonBuilder() .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) .setDateFormat("yyyy-MM-dd HH:mm:ss") .create() return Retrofit.Builder() .baseUrl(baseUrl) .addConverterFactory(GsonConverterFactory.create(gson)) .client(client) .build() } }