Search code examples
androidkotlinadapterviewmodelandroid-cardview

How do I maintain expandable cardview visible state using viewmodel


I am trying to maintain my expandable cardview to remain open when the apps configuration changes (e.g. screen orientation) using the viewmodel architecture. I have done zero progress regarding this.

The following code is the logic for opening and closing the cardview

binding.expandMoreButton.setOnClickListener {
    if (binding.collapsableItemSection.visibility == View.VISIBLE) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            TransitionManager.beginDelayedTransition(binding.cardView, AutoTransition())
        }
        binding.collapsableItemSection.visibility = View.GONE
        binding.expandMoreButton.setImageResource(R.drawable.ic_expand_more)
        println(binding.collapsableItemSection.visibility == View.GONE)
    } else {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            TransitionManager.beginDelayedTransition(binding.cardView, AutoTransition())
        }
        binding.collapsableItemSection.visibility = View.VISIBLE
        binding.expandMoreButton.setImageResource(R.drawable.ic_expand_less)
        }
    }

How should I implement the above logic into the viewmodel so that the expanded cardview is not closed when the screen orientation is changed to landscape or vice versa?

I tried to implement the logic into the viewmodel class using the backing properties and mutablelive data but couldn't come up with a solution.

private val _isExpanded = MutableLiveData(false)
    val isExpanded : LiveData<Boolean>
        get() = _isExpanded

    fun toggleCard(value: Boolean) {
        _isExpanded.value = value
        Log.d("HomeFragment", "Is Card Expanded : ${_isExpanded.value}")
    }

Solution

  • You're almost there. Now your View needs to use the ViewModel as the source of truth.

    So, instead of checking the current state of the view, it just depends on the state of the ViewModel:

    viewModel.isExpanded.observe(...) { isExpanded ->
        if (!isExpanded) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                TransitionManager.beginDelayedTransition(binding.cardView, AutoTransition())
            }
            binding.collapsableItemSection.visibility = View.GONE
            binding.expandMoreButton.setImageResource(R.drawable.ic_expand_more)
            println(binding.collapsableItemSection.visibility == View.GONE)
        } else {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                TransitionManager.beginDelayedTransition(binding.cardView, AutoTransition())
            }
            binding.collapsableItemSection.visibility = View.VISIBLE
            binding.expandMoreButton.setImageResource(R.drawable.ic_expand_less)
        }
    }
    

    And instead of toggling the visibility of the view, it toggles the state in the ViewModel:

    // No need to pass a boolean to toggleCard - the ViewModel should toggle this internally
    // This in turn triggers the callbackback above to update the view state
    binding.expandMoreButton.setOnClickListener { viewModel.toggleCard() }
    

    UPDATE

    the card view is inside a recyclerview, all the items of the recyclerview gets expanded when I only click on one item. Is there any kind of fix for that behavior?

    Well, that wasn't clear from your original question. It does complicate things a bit :) But the concepts are the same.

    ViewModel stores the view state. This time instead of a single bool, maybe a list of them:

    // ViewModel
    
    // ListItemState is some data class with "isExpanded" flag and other data for each item in the recyclerview
    val listItemStates = MutableLiveDate<List<ListItemState>>()
    
    fun toggleItemExpanded(position: Int) {
        // toggle expanded flag on item at position and update listItemStates
    }
    

    Your fragment / activity observes this state and then passes the changes to the recyclerview adapter:

    // Fragment / Activity
    
    viewModel.listItemStates.observe(...) { states ->
        adapter.submitList(states) // Assumes you're using ListAdapter
    }
    
    // Also listen for click events to toggle an item (you define this method)
    adapter.setItemClickListener { position ->
        viewModel.toggleItemExpanded(position)
    }
    

    Now the adapter will have the data it needs to render the correct states:

    // Adapter
    override fun onBindViewHolder(holder : YourViewHolder position : Int) {
        val listItemState = getItem(position)
        // Now set item view state with listItemState.isExpanded
    
        // Set button click listener to notify fragment / activity that this item should toggle
    

    }