Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Androidx Paging Library with GraphQL

Avatar for Leon Leon
September 04, 2019

Androidx Paging Library with GraphQL

lightning talk about the new paging library

Avatar for Leon

Leon

September 04, 2019

Other Decks in Programming

Transcript

  1. What we already had: - RecyclerView with ListAdapter - one

    initial request when starting the fragment - MVVM Architecture with databinding enabled What we needed: - Paging functionality
  2. Components ‣ PagedList ‣ DataSource + DataSource.Factory ‣ PagedListAdapter ‣

    LiveListPageBuilder ‣ ApolloClient for GraphQL (not required)
  3. GraphQL Schema query GetAllGroups($cursor: String) { getAll(cursor: $cursor) { page

    { total nextCursor hasNextPage } groups { ...GroupField } } } fragment GroupField on Group { id title description category { id title } location { title } owner { id } }
  4. DataSource abstract class GroupDataSource(val repository: GroupsRepository) : PageKeyedDataSource<String, Group>() {

    override fun loadInitial(params: LoadInitialParams<String>, callback: LoadInitialCallback<String, Group>) { } override fun loadAfter(params: LoadParams<String>, callback: LoadCallback<String, Group>) { } override fun loadBefore(params: LoadParams<String>, callback: LoadCallback<String, Group>) { } companion object { const val PAGE_SIZE = 10 } }
  5. DataSource abstract class GroupDataSource(val repository: GroupsRepository) : PageKeyedDataSource<String, Group>() {

    override fun loadInitial(params: LoadInitialParams<String>, callback: LoadInitialCallback<String, Group>) { GlobalScope.launch(Dispatchers.IO) { try { val groups = fetchNetwork() callback.onResult(groups.first, null, groups.second) } catch(e: Exception) { Timber.e(e) } } } override fun loadAfter(params: LoadParams<String>, callback: LoadCallback<String, Group>) { GlobalScope.launch(Dispatchers.IO) { try { val groups = fetchNetwork(params.key) callback.onResult(groups.first, groups.second) } catch (e: Exception) { Timber.e(e) } } } override fun loadBefore(params: LoadParams<String>, callback: LoadCallback<String, Group>) { } abstract suspend fun fetchNetwork(cursor: String? = null): Pair<List<Group>, String?> companion object { const val PAGE_SIZE = 10 } }
  6. DataSource class AllGroupsDataSource(repository: GroupsRepository) : GroupDataSource(repository) { override suspend fun

    fetchNetwork(cursor: String?): Pair<List<Group>, String?> = repository.getAllGroups(cursor) } class GroupsRepository(private val apolloClient: ApolloClient) { suspend fun getAllGroups(cursor: String? = null): Pair<List<Group>, String?> { val query: GetAllGroupsQuery = GetAllGroupsQuery.builder().cursor(cursor).build() val response = apolloClient.query(query).toDeferred().await() return Pair(response.data()?.all?.groups()?.map { it.toGroup() }.orEmpty(), response.data()?.all?.page()?.nextCursor()) } } query GetAllGroups($cursor: String) { getAll(cursor: $cursor) { page { total nextCursor hasNextPage } groups { ...GroupField } } }
  7. DataSource abstract class GroupDataSource(val repository: GroupsRepository) : PageKeyedDataSource<String, Group>() {

    override fun loadInitial(params: LoadInitialParams<String>, callback: LoadInitialCallback<String, Group>) { GlobalScope.launch(Dispatchers.IO) { try { val groups = fetchNetwork() callback.onResult(groups.first, null, groups.second) } catch(e: Exception) { Timber.e(e) } } } override fun loadAfter(params: LoadParams<String>, callback: LoadCallback<String, Group>) { GlobalScope.launch(Dispatchers.IO) { try { val groups = fetchNetwork(params.key) callback.onResult(groups.first, groups.second) } catch (e: Exception) { Timber.e(e) } } } override fun loadBefore(params: LoadParams<String>, callback: LoadCallback<String, Group>) { } abstract suspend fun fetchNetwork(cursor: String? = null): Pair<List<Group>, String?> companion object { const val PAGE_SIZE = 10 } } List of Groups Cursor
  8. Groups ViewModel class GroupsViewModel(val repository: GroupsRepository) : ViewModel() { val

    allGroupsDataSourceFactory = AllGroupDataSourceFactory(repository) private val pagedListConfig: PagedList.Config = PagedList.Config.Builder() .setEnablePlaceholders(false) .setPageSize(GroupDataSource.PAGE_SIZE).build() val groups: LiveData<PagedList<Group>> = LivePagedListBuilder(allGroupDataSourceFactory, pagedListConfig).build() }
  9. <?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:tools="http://schemas.android.com/tools"> <data> <variable name="viewModel"

    type="com.conceptworks.spontacts.groups.overview.GroupsViewModel" /> <import type="com.conceptworks.spontacts.groups.Status" /> <import type="android.view.View" /> </data> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> <androidx.swiperefreshlayout.widget.SwipeRefreshLayout android:id="@+id/swipe_container" android:layout_width="match_parent" android:layout_height="match_parent"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/list" android:layout_width="0dp" android:layout_height="0dp" app:groupItems="@{viewModel.groups}" app:spanCount="2" app:layoutManager="androidx.recyclerview.widget.GridLayoutManager" tools:listitem="@layout/group_list_item" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"/> </androidx.swiperefreshlayout.widget.SwipeRefreshLayout> </androidx.constraintlayout.widget.ConstraintLayout>
  10. Adapter: class GroupsAdapter : PagedListAdapter<Group, GroupsAdapter.GroupViewholder>(DIFF_UTIL) { override fun onCreateViewHolder(parent:

    ViewGroup, viewType: Int): GroupViewholder = GroupViewholder( GroupListItemBinding.inflate( LayoutInflater.from(parent.context), parent, false ) ) override fun onBindViewHolder(holder: GroupViewholder, position: Int) { getItem(position)?.let { holder.bind(it) } } class GroupViewholder(private val binding: GroupListItemBinding) : RecyclerView.ViewHolder(binding.root) { fun bind(item: Group) { binding.group = item binding.executePendingBindings() } } companion object { val DIFF_UTIL = object : DiffUtil.ItemCallback<Group>() { override fun areItemsTheSame(oldItem: Group, newItem: Group) = oldItem == newItem override fun areContentsTheSame(oldItem: Group, newItem: Group) = oldItem == newItem } @JvmStatic @BindingAdapter("groupItems") fun RecyclerView.bindItems(items: PagedList<Group>?) { (adapter as GroupsAdapter).submitList(items) } }
  11. Fragment: private val viewModel by sharedViewModel<GroupsViewModel>() private val adapter =

    GroupsAdapter() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) = OverviewGroupsFragmentBinding .inflate(inflater) .apply { lifecycleOwner = this@OverviewFragment viewModel = [email protected] { groups.observe(viewLifecycleOwner, Observer { adapter.submitList(it) }) } setHasOptionsMenu(true) list.adapter = adapter } .root MVP
  12. DataSource.Factory class AllGroupDataSourceFactory(private val repository: GroupsRepository) : DataSource.Factory<String, Group>() {

    val allGroupsSource = MutableLiveData<AllGroupsDataSource>() override fun create(): DataSource<String, Group> = AllGroupsDataSource(repository).apply { allGroupsSource.postValue(this) } } abstract class GroupDataSource(val repository: GroupsRepository) : PageKeyedDataSource<String, Group>() { val loadingStatus = MutableLiveData<Status>() override fun loadInitial(params: LoadInitialParams<String>, callback: LoadInitialCallback<String, Group>) { GlobalScope.launch(Dispatchers.IO) { try { loadingStatus.postValue(Status.Loading) val groups = fetchNetwork() callback.onResult(groups.first, null, groups.second) loadingStatus.postValue(Status.Success) } catch(e: Exception) { Timber.e(e) loadingStatus.postValue(Status.Error) } } } DataSource
  13. <ProgressBar android:layout_width="0dp" android:layout_height="wrap_content" android:indeterminate="true" android:visibility="@{viewModel.status instanceof Status.Loading ? View.VISIBLE :

    View.GONE}" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> XML val status: LiveData<Status> = Transformations.switchMap(allGroupDataSourceFactory.allGroupsSource) { it.loadingStatus } ViewModel
  14. What if the data is being changed? fun refresh() {

    allGroupDataSourceFactory.allGroupsSource.value?.invalidate() }