Wednesday, 17 February 2021

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)



from Need to bind Adapter to RecyclerView twice for data to appear

No comments:

Post a Comment