Asad Nawaz
Asad Nawaz

Reputation: 395

onClick not working in MVVM with DataBinding

For some reason, onClick isn't being registered with my adapter. I'm using the MVVM pattern and I've made sure that all the pieces are tied together but for the life of me I can't figure out why this won't work.

StoreFragment

package com.example.brandroidtest.main


import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.navigation.fragment.findNavController
import com.example.brandroidtest.databinding.FragmentStoreBinding


class StoreFragment : Fragment() {



    //Will Create a ViewModelProivders object of class DetailViewModel the first time viewModel is used
    //Allows us to move this code from on create to the declaration
    private val viewModel: StoreViewModel by lazy {
        val factory = StoreViewModelFactory(requireNotNull(activity).application)
        ViewModelProviders.of(this, factory).get(StoreViewModel::class.java)
    }


    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {


        Log.i("onCreateView", "StoreFragment created")


        val binding = FragmentStoreBinding.inflate(inflater)



        binding.setLifecycleOwner(this)
        binding.viewModel = viewModel

        binding.storeList.adapter = StoreAdapter(StoreAdapter.OnClickListener {
            viewModel.displayStoreDetails(it)
            Log.i("inside OnClickListener", "after displayDetails")
        })

        Log.i("between adapter.onclick", "and viewModel observe")

        viewModel.selectedStore.observe(this, Observer {
            Log.i("observe", "inside the selectedStore observe method")
            if (null != it) {
                this.findNavController().navigate(
                    StoreFragmentDirections.actionMainListFragmentToDetailFragment(
                        it
                    )
                )
                viewModel.displayStoreDetailsComplete()
            }

        })



        return binding.root

    }

}

StoreViewModel

package com.example.brandroidtest.main

import android.app.Application
import android.content.Context
import android.net.ConnectivityManager
import android.util.Log
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.example.brandroidtest.model.Store
import com.example.brandroidtest.network.StoreAPI
import kotlinx.coroutines.*


enum class StoreAPIStatus {LOADING, DONE, NO_CONNECTION}

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

    // Response from server: Either Store Data or Failure Message
    private val _status = MutableLiveData<StoreAPIStatus>()

    // for status of get request
    //displayed when there is no internet connection or if the connection is unstable and the data is being loaded
    val status: LiveData<StoreAPIStatus>
        get() = _status


    //internal variable accessed within this file
    private val listOfStores = MutableLiveData<List<Store>>()


    //external variable for anywhere else
    val stores: LiveData<List<Store>>
        get() = listOfStores


    private val _selectedStore = MutableLiveData<Store>()

    val selectedStore: LiveData<Store>
        get() = _selectedStore


    private var viewModelJob = Job()
    private val coroutineScope = CoroutineScope(viewModelJob + Dispatchers.Main)


    /**
     * Call getStoreData() in init so we can display the result immediately.
     */
    init {


        Log.i("viewModel init", "inside StoreViewModel init block")
        if (isNetworkConnected(application.applicationContext))
            getStoreData()
        else
//            Log.i("Bypassed network call", "")
            listOfStores.value = emptyList()
            _status.value = StoreAPIStatus.NO_CONNECTION

    }
    /**
     * Sets the value of the status LiveData to the Store API data.
     */

    private fun getStoreData() {

        Log.i("getStoreData()", " inside getStoreData")


        coroutineScope.launch {
            try {
                Log.i("getStoreData()", "Inside the coroutine before getData")

               _status.value =  StoreAPIStatus.LOADING

                var storeData = async { StoreAPI.retrofitService.getData().stores }.await()

                Log.i("getStoreData()", "Inside the coroutine after getData")

                _status.value = StoreAPIStatus.DONE

                listOfStores.value = storeData

            } catch (e: Exception) {
                _status.value = StoreAPIStatus.NO_CONNECTION
                listOfStores.value = ArrayList()
                e.printStackTrace()
            }
        }

    }

    override fun onCleared() {
        super.onCleared()
        viewModelJob.cancel()
    }

    private fun isNetworkConnected(context: Context): Boolean {
        val cm =
            context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager?
        return cm!!.activeNetworkInfo != null && cm.activeNetworkInfo.isConnected
    }

    //will be called to set the store as the one that was clicked
    fun displayStoreDetails(store : Store){
        Log.i("displayStoreDetails", "inside this method")
        _selectedStore.value = store
    }

    //sets the selected store's value to null so that live data can be updated when we select a new store and not show us the detail apge of the same store
    fun displayStoreDetailsComplete() {
        Log.i("displayStoreDetailsComplete", "inside this method")
        _selectedStore.value = null
    }

}

StoreAdapter

package com.example.brandroidtest.main

import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.example.brandroidtest.model.Store
import com.example.brandroidtest.databinding.ListItemBinding


class StoreAdapter(val onClickListener: OnClickListener) :
    ListAdapter<Store, StoreAdapter.StoreViewHolder>(DiffCallback) {
    class StoreViewHolder(private var binding: ListItemBinding) :
        RecyclerView.ViewHolder(binding.root) {

        fun bind(store: Store) {
            binding.store = store
            Log.i("Adapter bind", store.storeLogoURL)
            binding.executePendingBindings()

        }
    }

    companion object DiffCallback : DiffUtil.ItemCallback<Store>() {
        override fun areItemsTheSame(oldItem: Store, newItem: Store): Boolean {
            return oldItem === newItem
        }

        override fun areContentsTheSame(oldItem: Store, newItem: Store): Boolean {
            return oldItem.storeID == newItem.storeID
        }
    }

    override fun onCreateViewHolder(
        parent: ViewGroup,
        viewType: Int
    ): StoreViewHolder {
        return StoreViewHolder(ListItemBinding.inflate(LayoutInflater.from(parent.context)))
    }

    override fun onBindViewHolder(holder: StoreViewHolder, position: Int) {

        val store = getItem(position)

        Log.i("inside onBindViewHolder", "")

        holder.itemView.setOnClickListener {
            Log.i("inside onBindViewHolder", "setOnClickListener")
            onClickListener.onClick(store)
        }

        holder.bind(store)
    }

    class OnClickListener(val clickListener: (store: Store) -> Unit) {

        fun onClick(store: Store) {
            Log.i("inside onClick", "click is being registered ${store.city}")
            return clickListener(store)
        }
    }
}

StoreDetailFragment

package com.example.brandroidtest.detailed


import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProviders
import com.example.brandroidtest.R
import com.example.brandroidtest.databinding.FragmentStoreDetailBinding


/**
 * A simple [Fragment] subclass.
 */
class StoreDetailFragment : Fragment() {


    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {

        val application = requireNotNull(activity).application

        val binding = FragmentStoreDetailBinding.inflate(inflater)

        binding.setLifecycleOwner(this)

        val store = StoreDetailFragmentArgs.fromBundle(arguments!!).selectedStore

        val viewModelFactory = StoreDetailViewModelFactory(store, application)

        binding.viewModel = ViewModelProviders.of(this, viewModelFactory).get(StoreDetailViewModel::class.java)

        return binding.root

    }
}


StoreDetailViewModel

package com.example.brandroidtest.detailed

import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.example.brandroidtest.model.Store

class StoreDetailViewModel(store: Store, application: Application) : AndroidViewModel(application) {



    private val _selectedStore = MutableLiveData<Store>()

    val selectedStore : LiveData<Store>
        get() = _selectedStore


    init {
        _selectedStore.value = store
    }

}

I have no idea why onClick won't work and the Detail Fragment won't show because of it

Here is the project link: https://drive.google.com/open?id=1m8R8HvCt4m0KIp_IwdeO1YdB5yY8A8LK

Upvotes: 0

Views: 873

Answers (1)

Duy Khanh Nguyen
Duy Khanh Nguyen

Reputation: 509

The issue come from your adapter item layout. The height of every item show be wrap_content. But you are using a ScrollView as the root view of your item view. Remove the useless ScrollView and also the next LinearLayout. You layout should become like this:

<LinearLayout
    ...
    android:padding="16dp"/>

    <ImageView
        android:id="@+id/store_logo"
        .../>

    <LinearLayout
        android:id="@+id/store_detail"
        ...>
</LinearLayout>

Upvotes: 1

Related Questions