9

My current problem is, that my LoadStateAdapter which shows the loading and error state, is not centered inside my recyclerview, which has a gridlayout as a layoutmanager. I didn't find anything about this at the official android developer website, so I am asking here: How can I center my LoadStateAdapter inside my Recyclerview?

Current

enter image description here

Fragment

@AndroidEntryPoint
class ShopFragment : Fragment(R.layout.fragment_shop), ShopAdapter.OnItemClickListener {
    private val shopViewModel: ShopViewModel by viewModels()
    private val shopBinding: FragmentShopBinding by viewBinding()
    @Inject lateinit var shopListAdapter: ShopAdapter

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        bindObjects()
    }

    private fun bindObjects() {
        shopBinding.adapter = shopListAdapter.withLoadStateFooter(ShopLoadAdapter(shopListAdapter::retry))
        shopListAdapter.clickHandler(this)
    }

    override fun onDestroyView() {
        requireView().findViewById<RecyclerView>(R.id.rv_shop).adapter = null
        super.onDestroyView()
    }
}

Adapter

@FragmentScoped
class ShopLoadAdapter(private val retry: () -> Unit): LoadStateAdapter<ShopLoadAdapter.ShopLoadStateViewHolder>() {

    inner class ShopLoadStateViewHolder(private val binding: ShopLoadStateFooterBinding) : RecyclerView.ViewHolder(binding.root) {
        fun bind(loadState: LoadState) {
            with(binding) {
                shopLoadPb.isVisible = loadState is LoadState.Loading
                shopLoadMbtnRetry.isVisible = loadState is LoadState.Error
                shopLoadTvError.isVisible = loadState is LoadState.Error
            }
        }
    }

    override fun onBindViewHolder(holder: ShopLoadStateViewHolder, loadState: LoadState) = holder.bind(loadState)

    override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): ShopLoadStateViewHolder {
        val layoutInflater = LayoutInflater.from(parent.context)
        val binding = ShopLoadStateFooterBinding.inflate(layoutInflater, parent, false)
        return ShopLoadStateViewHolder(binding).also {
            binding.shopLoadMbtnRetry.setOnClickListener { retry.invoke() }
        }
    }
}

Layout.xml

<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/rv_shop"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_marginStart="8dp"
    android:layout_marginEnd="8dp"
    app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
    app:spanCount="2"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/headline"
    app:recyclerview_adapter="@{adapter}"
    tools:listitem="@layout/shop_list_item"/>
Andrew
  • 4,264
  • 1
  • 21
  • 65
  • I think it's related to `setSpanSizeLookup` you can check https://stackoverflow.com/questions/63509661/how-to-prepare-setspansizelookup-in-gridlayoutmanager/63511928#63511928 – Mohammed Alaa Dec 14 '20 at 15:39
  • And how would this help me? I know that `LoadStateAdapter` is always the last item in my recylerview, so I have to set the span size of my last item different to the others right? But the problem here would be that sometimes the LoadStateAdapter is not showing, because there is no error or loading state etc. – Andrew Dec 15 '20 at 15:03
  • 1
    It should be the paging library issue. It works fine with every layoutmanager besides gridlayoutmanager. I have created an [issue](https://issuetracker.google.com/u/1/issues/178460672) on issuetracker. you could star the issue to get quick attention. – gowtham6672 Jan 26 '21 at 17:33
  • @gowtham6672 I did. – Andrew Jan 26 '21 at 20:50

4 Answers4

9

If you use "withLoadStateFooter" try this code. source

val footerAdapter = MainLoadStateAdapter(adapter)
recyclerView.adapter = adapter.withLoadStateFooter(footer = footerAdapter)
gridLayoutManager.spanSizeLookup =  object : GridLayoutManager.SpanSizeLookup() {
        override fun getSpanSize(position: Int): Int {
            return if (position == adapter.itemCount  && footerAdapter.itemCount > 0) {
                2
            } else {
                1
            }
        }
    }
4

First you have to create multiple view types in your PagingDataAdapter After that override getItemViewType method as shown below

// Define Loading ViewType
public static final int LOADING_ITEM = 0;
// Define Movie ViewType
public static final int MOVIE_ITEM = 1;

@Override
public int getItemViewType(int position) {
    // set ViewType
    return position == getItemCount() ? MOVIE_ITEM : LOADING_ITEM;
}

Then set span size dynamically

// set Grid span
    gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
        @Override
        public int getSpanSize(int position) {
            // If progress will be shown then span size will be 1 otherwise it will be 2
            return moviesAdapter.getItemViewType(position) == MoviesAdapter.LOADING_ITEM ? 1 : 2;
        }
    });

You can checkout this paging 3 example which includes displaying loading state view in center

Amar Yadav
  • 170
  • 11
  • In which class does the glidlayoutmanager logic belong? In the fragment? Thanks for the approach, I will test this and answer later – Andrew Dec 23 '20 at 11:14
  • Your recyclerview shown in the image contain 2 items per row which means you must be using *GridLayoutManager* with 2 as span count. You have to setup setSpanSizeLookup callback on that layout manager. – Amar Yadav Dec 24 '20 at 09:00
  • What if you reach the end of the results and the last item is not the loadstate footer but an actual item? – Florian Walther Apr 05 '21 at 18:46
1

After I google your question, I found this anwser about spanSizeLookup. First we create an instance of the GridLayoutManager and add it to the RecyclerView

    // Create a grid layout with two columns
    val adapter = ShopLoadAdapter(yourItems)
    val layoutManager = GridLayoutManager(context, 2)

    // Create a custom SpanSizeLookup where the first item spans both columns
    layoutManager.spanSizeLookup = object : SpanSizeLookup() {
        override fun getSpanSize(position: Int): Int {
            return if (adapter.getItemViewType(position) == ShopLoadAdapter.ERROR_RETRY) 2 else 1
        }
    }

    val recyclerView = findViewById<RecyclerView>(R.id.recyclerview)
    recyclerView.layoutManager = layoutManager
    recyclerView.adapter = adapter

We have to create two different ViewHolders because we want to display two different layouts.

class ViewHolderA(view: View) : RecyclerView.ViewHolder(view) {
    var image: ImageView = view.findViewById(R.id.image)
    //initialize your views for your items
}

class ViewHolderB(view: View) : RecyclerView.ViewHolder(view) {
    val button: Button = view.findViewById(R.id.retry_button)
    //initalize your views for the retry item
}

The ShopLoadAdapter should look like this:

class ShopLoadAdapter(private val items: ArrayList<Item>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

    val NORMAL_ITEM = 0
    val ERROR_RETRY = 1

    override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        val view: View

        if(viewType == ShopLoadAdapter.NORMAL_ITEM){
            view = LayoutInflater.from(viewGroup.context)
                    .inflate(R.layout.normal_item, viewGroup, false)
            return ViewHolderA(view)
        }

        view = LayoutInflater.from(viewGroup.context)
                .inflate(R.layout.error_retry_item, viewGroup, false)
        return ViewHolderB(view)
    }

    override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, i: Int)     {
        // Bind your items using i as position
    }

    override fun getItemViewType(position: Int): Int {
        return when {
            items[position].hasError -> 1
            else -> 0
        }
    }

    override fun getItemCount(): Int {
        return items.size
    }
}
JexSrs
  • 155
  • 6
  • 18
  • I will try both solutions later. Currently, I have no time to do so, but I appreciate your answer. Thank you – Andrew Feb 01 '21 at 11:37
1

Use this if you also have loadStateAdapter for refresh state.

gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
        override fun getSpanSize(position: Int): Int {
            return if ((position == adapter.itemCount) && footerAdapter.itemCount > 0) {
                spanCount
            } else if (adapter.itemCount == 0 && headerAdapter.itemCount > 0) {
                spanCount
            } else {
                1
            }
        }
    }
Daniyal Javaid
  • 1,426
  • 2
  • 18
  • 32