0

EDIT - I tried to set android:focusable and android:clickable attributes to true in the XML, but it didn't changed anything. Still looking!

I can't understand why my onClick handler isn't working on this particular case.

I want to make a CardView clickable, using databinding. I've seen a lot of code for onClickListener, but i decided to use this pattern, as described in the Android doc.

I have a class, named Recipe(), which contains 3 elements : an ID, a Title and an URL. The ultimate goal of this click handler is to navigate to a new fragment, passing the ID as a parameter to display new elements. I'll later get the ID in the Fragment using observer.

In this example, I already use data provided by the ViewModel in the XML (recipe.image and recipe.title), and it works just fine: data is correctly binded and displayed. However, clicking on the CardView don't result in anything: as the Log isn't display, I suppose clicking don't trigger the onClick event.

You'll find element from the ViewModel and XML below. Thanks in advance!

class DefaultRecipeListViewModel : ViewModel() {

    private val _recipeList = MutableLiveData<List<Recipe>>()

    val recipeList: LiveData<List<Recipe>>
    get() = _recipeList

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

    //Livedata observed in the Fragment
    private var _navigateToRecipe = MutableLiveData<Int>()
    val navigateToRecipe: LiveData<Int>
        get() = _navigateToRecipe

    private var _showSnackbarEvent = MutableLiveData<Boolean>()
    val showSnackBarEvent: LiveData<Boolean>
        get() = _showSnackbarEvent

    init {
        getRecipesForNewbie()
    }

  fun getRecipesForNewbie() {
        coroutineScope.launch {
            var getRecipes = service.getRecipe().await()
            try {
                _recipeList.value = getRecipes.results
            } catch (e: Exception) {
                Log.i("ViewModel","Error: $e")
            }
        }
    }

    fun onRecipeClicked(id: Int) {
        _showSnackbarEvent.value = true
        _navigateToRecipe.value = id
        Log.i("ViewModel", "Item clicked, id: $id")
    }

    fun doneNavigating(){
        Log.i("ViewModel", "done navigating, navigateToRecipe set to -1")
        _navigateToRecipe.value = -1
    }

    fun doneShowingSnackbar() {
        _showSnackbarEvent.value = false
    }

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

Here's the XML

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable
            name="recipe"
            type="com.example.recipesfornewbies.recipes.Recipe" />
        <variable
            name="viewModel"
            type="com.example.recipesfornewbies.defaultrecipelist.DefaultRecipeListViewModel" />
    </data>

    <LinearLayout
        android:layout_height="wrap_content"
        android:layout_width="match_parent">

        <androidx.cardview.widget.CardView
            android:id="@+id/card_view"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_marginBottom="6dp"
            android:layout_marginTop="6dp"
            app:cardCornerRadius="10dp"
            app:cardElevation="6dp"
            android:onClick="@{()-> viewModel.onRecipeClicked(recipe.id)}">
            <androidx.constraintlayout.widget.ConstraintLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent">

                <ImageView
                    android:id="@+id/recipe_image"
                    android:layout_width="match_parent"
                    android:layout_height="100dp"
                    android:contentDescription="@{recipe.title}"
                    app:imageFromUrl="@{recipe.image}"
                    app:layout_constraintEnd_toEndOf="parent"
                    app:layout_constraintStart_toStartOf="parent"
                    app:layout_constraintTop_toTopOf="parent"
                    tools:layout_editor_absoluteX="0dp" />

                <TextView
                    android:id="@+id/recipe_name"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:paddingStart="16dp"
                    android:paddingEnd="16dp"
                    android:text="@{recipe.title}"
                    android:textSize="24sp"
                    app:layout_constraintTop_toBottomOf="@id/recipe_image"/>
            </androidx.constraintlayout.widget.ConstraintLayout>
        </androidx.cardview.widget.CardView>
    </LinearLayout>
</layout>

Fragment code:

class DefaultRecipeListFragment: Fragment(){

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

    val binding = FragmentDefaultRecipeListBinding.inflate(inflater)

    val viewModel: DefaultRecipeListViewModel by lazy {
        ViewModelProviders.of(this).get(DefaultRecipeListViewModel::class.java)
    }
    binding.viewModel = viewModel

    // Allows Data Binding to Observe LiveData with the lifecycle of this Fragment
    binding.setLifecycleOwner(this)

    //Creating the RecyclerView
    val manager = LinearLayoutManager(activity)
    binding.recyclerRecipeList.layoutManager = manager

    binding.recyclerRecipeList.adapter = RecipeListAdapter()

    viewModel.navigateToRecipe.observe(this, Observer{id ->

        if (id != -1){
            Log.i("Fragment","Navigate to ${id}")
        }
        viewModel.doneNavigating()
    })

    viewModel.showSnackBarEvent.observe(this, Observer {
        if (it == true) {
            Snackbar.make(
                activity!!.findViewById(android.R.id.content),
                "Clicked!",
                Snackbar.LENGTH_SHORT
            ).show()
            viewModel.doneShowingSnackbar()
        }
    })

   return binding.root
}

}

ViewHolder class, used in the RecyclerView Adapter

class RecipeViewHolder(private var binding: RecipeViewBinding):
    RecyclerView.ViewHolder(binding.root) {
    fun bind(Recipe: Recipe) {
        val imageURI = "https://spoonacular.com/recipeImages/"
        Recipe.image = imageURI + Recipe.image
        binding.recipe = Recipe
        // Forces the data binding to execute immediately,to correctly size RecyclerVieW
        binding.executePendingBindings()

    }
LRobert
  • 23
  • 1
  • 6
  • Welcome to StackOverflow! Can you please tell me where is your ViewModel binding code? You need to bind the variables using a ViewModelProvider. – MrMikimn Sep 20 '19 at 08:09
  • Hi, thank's for the quick response! I just updated the post to show the binding in Fragment, and I also provided the class I use to display the CardViews in a RecyclerView – LRobert Sep 20 '19 at 08:33
  • Could you try and replace the `FragmentDefaultRecipeListBinding.inflate()` expression with using the `DataBindingUtil`? Something like `binding = DataBindingUtil.inflate(layoutInflater, viewGroup, false)` – MrMikimn Sep 20 '19 at 09:42
  • I edited this expression to: val binding : FragmentDefaultRecipeListBinding = DataBindingUtil.inflate( inflater, R.layout.fragment_default_recipe_list, container, false) , but I still can't click the CardView generated. – LRobert Sep 20 '19 at 10:33

1 Answers1

0

I finally decided to use this solution on my code, which works pretty well.

I stopped trying to access the function in my ViewModel directly from the XML: I actually listen for event on the Fragment, even if I find the solution to be less pretty that the one I wanted.

In the Fragment, this is how I handle the click: binding.recyclerRecipeList.addOnItemTouchListener(RecyclerItemClickListener(this.context!!, binding.recyclerRecipeList, object : RecyclerItemClickListener.OnItemClickListener {

        override fun onItemClick(view: View, position: Int) {
            viewModel.recipeList.value?.let {
            val ident = it[position].id
            findNavController().navigate(
                DefaultRecipeListFragmentDirections.actionDefaultRecipeListFragmentToDetailedRecipeFragment(ident))
                Log.i("Fragment", "id: $ident")
            }
        }
        override fun onItemLongClick(view: View?, position: Int) {
            TODO("do nothing")
        }
    }))

Still haven't understood why my first solution didn't work.

LRobert
  • 23
  • 1
  • 6