Android: LoadStateAdapter not centered inside recyclerview gridlayout
Asked Answered
H

4

11

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"/>
Hoedown answered 14/12, 2020 at 15:2 Comment(4)
I think it's related to setSpanSizeLookup you can check #63510161Tendon
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.Hoedown
It should be the paging library issue. It works fine with every layoutmanager besides gridlayoutmanager. I have created an issue on issuetracker. you could star the issue to get quick attention.Armelda
@Armelda I did.Hoedown
P
11

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
            }
        }
    }
Piefer answered 20/2, 2021 at 21:45 Comment(1)
This is the right solution, provided by Yigit Bojar himself. Thanks for finding this!Deitz
F
5

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

Flagellum answered 18/12, 2020 at 22:3 Comment(3)
In which class does the glidlayoutmanager logic belong? In the fragment? Thanks for the approach, I will test this and answer laterHoedown
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.Flagellum
What if you reach the end of the results and the last item is not the loadstate footer but an actual item?Deitz
I
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
    }
}
Intertidal answered 31/1, 2021 at 23:19 Comment(1)
I will try both solutions later. Currently, I have no time to do so, but I appreciate your answer. Thank youHoedown
T
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
            }
        }
    }
Thoracoplasty answered 23/8, 2021 at 8:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.