Mervin Hemaraju
Mervin Hemaraju

Reputation: 2187

Need to bind Adapter to RecyclerView twice for data to appear

I have an Android app where I bind a list of service to a RecyclerView as such:

fragment.kt

 override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        mBinding = FragmentAllServicesBinding.inflate(inflater, container, false)
        mViewModel = ViewModelProvider(this).get(AllServicesViewModel::class.java)
        binding.viewModel = viewModel
        binding.lifecycleOwner = this
        return binding.root
    }

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

    subscribeServices()
}


// Private Functions
private fun subscribeServices(){
    val adapter = ServiceAdapter()

    binding.RecyclerViewServices.apply {
        /*
        * State that layout size will not change for better performance
        */
        setHasFixedSize(true)

        /* Bind the layout manager */
        layoutManager = LinearLayoutManager(requireContext())

        this.adapter = adapter
    }

        viewModel.services.observe(viewLifecycleOwner, { services ->
            if(services != null){
                lifecycleScope.launch {
                    adapter.submitList(services)
                }
            }
        })

}

viewmodel.kt

package com.th3pl4gu3.mes.ui.main.all_services

import android.app.Application
import androidx.lifecycle.*
import com.th3pl4gu3.mes.api.ApiRepository
import com.th3pl4gu3.mes.api.Service
import com.th3pl4gu3.mes.ui.utils.extensions.lowercase
import kotlinx.coroutines.launch
import kotlin.collections.ArrayList

class AllServicesViewModel(application: Application) : AndroidViewModel(application) {

    // Private Variables
    private val mServices = MutableLiveData<List<Service>>()
    private val mMessage = MutableLiveData<String>()
    private val mLoading = MutableLiveData(true)
    private var mSearchQuery = MutableLiveData<String>()
    private var mRawServices = ArrayList<Service>()

    // Properties
    val message: LiveData<String>
        get() = mMessage

    val loading: LiveData<Boolean>
        get() = mLoading

    val services: LiveData<List<Service>> = Transformations.switchMap(mSearchQuery) { query ->
        if (query.isEmpty()) {
            mServices.value = mRawServices
        } else {
            mServices.value = mRawServices.filter {
                it.name.lowercase().contains(query.lowercase()) ||
                        it.identifier.lowercase().contains(query.lowercase()) ||
                        it.type.lowercase().contains(query.lowercase())
            }
        }

        mServices
    }

    init {
        loadServices()
    }

    // Functions
    internal fun loadServices() {

        // Set loading to true to
        // notify the fragment that loading
        // has started and to show loading animation
        mLoading.value = true

        viewModelScope.launch {
            //TODO("Ensure connected to internet first")

            val response = ApiRepository.getInstance().getServices()

            if (response.success) {
                // Bind raw services
                mRawServices = ArrayList(response.services)

                // Set the default search string
                mSearchQuery.value = ""
            } else {
                mMessage.value = response.message
            }
        }.invokeOnCompletion {
            // Set loading to false to
            // notify the fragment that loading
            // has completed and to hide loading animation
            mLoading.value = false
        }
    }

    internal fun search(query: String) {
        mSearchQuery.value = query
    }
}

ServiceAdapter.kt

    class ServiceAdapter : ListAdapter<Service, ServiceViewHolder>(
    diffCallback
) {
    companion object {
        private val diffCallback = object : DiffUtil.ItemCallback<Service>() {
            override fun areItemsTheSame(oldItem: Service, newItem: Service): Boolean {
                return oldItem.identifier == newItem.identifier
            }

            override fun areContentsTheSame(oldItem: Service, newItem: Service): Boolean {
                return oldItem == newItem
            }
        }
    }

    override fun onBindViewHolder(holder: ServiceViewHolder, position: Int) {
        holder.bind(
            getItem(position)
        )
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ServiceViewHolder {
        return ServiceViewHolder.from(
            parent
        )
    }
}

ServiceViewHolder.kt

    class ServiceViewHolder private constructor(val binding: CustomRecyclerviewServiceBinding) :
    RecyclerView.ViewHolder(binding.root) {
    fun bind(
        service: Service?
    ) {
        binding.service = service
        binding.executePendingBindings()
    }

    companion object {
        fun from(parent: ViewGroup): ServiceViewHolder {
            val layoutInflater = LayoutInflater.from(parent.context)
            val binding =
                CustomRecyclerviewServiceBinding.inflate(layoutInflater, parent, false)
            return ServiceViewHolder(
                binding
            )
        }
    }
}

The problem here is that, the data won't show on the screen.

For some reasons, if i change my fragment's code to this:

viewModel.services.observe(viewLifecycleOwner, { services ->
            if(services != null){
                lifecycleScope.launch {
                    adapter.submitList(services)

                    // Add this code
                    binding.RecyclerViewServices.adapter = adapter
                }
            }
        })

Then the data shows up on the screen.

Does anyone have any idea why I need to set the adapter twice for this to work ?

I have another app where I didn't have to set it twice, and it worked. For some reason, this app is not working. (The only difference between the other app and this one is that this one fetches the data from an API whereas the other one fetches data from Room (SQLite) database)

Upvotes: 2

Views: 697

Answers (2)

Rinat Diushenov
Rinat Diushenov

Reputation: 1245

Inside

binding.RecyclerViewServices.apply {
 ...
} 

Change this.adapter = adapter to this.adapter = [email protected]

The reason is, you named your Adapter variable "adapter" which conflicts the property name of RecyclerView.adapter. You are actually not setting the adapter for the first time. It's very sneaky, because lint doesn't give any warning and code compiles with no errors...


Or you could rename your "adapter" variable in your fragment to something like "servicesAdapter" an shortly use

 binding.RecyclerViewServices.apply {
     adapter = servicesAdapter
    } 

Upvotes: 1

Stoyan Milev
Stoyan Milev

Reputation: 735

Instead of adding the adapter again try calling adapter.notifyDataSetChanged() after adapter.submitList(services)

Upvotes: 1

Related Questions