Reputation: 2187
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
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
Reputation: 735
Instead of adding the adapter again try calling adapter.notifyDataSetChanged()
after adapter.submitList(services)
Upvotes: 1