javakotlinarraylistandroid-jetpack-composeviewmodel

How to observe changes in a List when the ViewModel is in Java and the UI is in Jetpack Compose (Kotlin)


I am migrating an application from View to Compose. The application was written in Java so I need to migrate the UI to Kotlin. I prefer to do it as gradually as possible so I'm leaving the ViewModel in Java.

The UI is a lazy column of Cards. A long click on a card toggles its selected state. It sends an event to the ViewModel - adding/deleting its ID to/from a List that contains the IDs of the selected cards.

I would like the UI to observe the changes in the List of IDs. Currently, I use a workaround: An observable Boolean that toggles with every change in the List of IDs. I wonder if there is a better approach.

Some code: The UI part - The function that displays the card:

    @OptIn(ExperimentalFoundationApi::class)
    @Composable
    private fun DisplayAlarmItem(alarmItem: AlarmItem) {

        // Force this function to be called when list of selected changes
        val selectToggle by parent!!.alarmViewModel.selectToggleObserve.observeAsState( )
        val toggled = if (selectToggle!=null && selectToggle as Boolean) "A" else "B"

        // Get list of selected alarms and mark this item as selected(yes/no)
        val selectedAlarmList by parent!!.alarmViewModel.selectedItems.observeAsState()
        val filterList = selectedAlarmList?.filter {  it.equals(alarmItem.createTime.toInt()) }
        var selected= (filterList != null && filterList.isNotEmpty())
        val backgroundColor = if (selected) MaterialTheme.colorScheme.surfaceVariant else MaterialTheme.colorScheme.surface


        val currentAlpha = if (alarmItem.isActive) 1.0f else 0.3f
        Card(
            modifier = Modifier
                .padding(5.dp)
                .fillMaxWidth()
                .combinedClickable(
                    onLongClick = { AlarmItemLongClicked(alarmItem.getCreateTime()) },
                    onClickLabel = "Edit Alarm"
                )
                { AlarmItemEdit(alarmItem, true) }
                .background(MaterialTheme.colorScheme.surface)
                .wrapContentHeight(),
            shape = MaterialTheme.shapes.small,
            elevation = CardDefaults.elevatedCardElevation(5.dp),
            colors = CardDefaults.cardColors(containerColor = backgroundColor),
            
            ) {
                ConstraintLayout(modifier = Modifier.fillMaxSize()) {
                    val refLabel = createRef()
                    val refBell = createRef()
                    val refAlarmTime = createRef()
                    val refAlarmActive = createRef()
                    val refAmPm24h = createRef()
                    val refWeekdays = createRef()

               }
            }
    }

Now, The Long Click callback:

    private fun AlarmItemLongClicked(id: Long) {
    // get the list of alarms
    //List<AlarmItem> alarmItems = parent.alarmViewModel.getAlarmList().getValue();
    //if (alarmItems==null) return;

    // Update the list of the selected items - simply toggle
    parent!!.alarmViewModel.toggleSelection(id)

    // Modify toolbar according to number of selected items
    parent!!.UpdateOptionMenu()

    // Inform the ViewModel that the selection List was changed
    parent?.alarmViewModel?.selectToggleObserve()
}

On the ViewModel, the code that adds/removes ID from the List of selected IDs:

   public void toggleSelection(long id){
  int index = selectedItems.indexOf((int)id);
  if (index >=0)
     selectedItems.remove(index);
  else
     selectedItems.add((int)id);

  LiveSelectedItems.setValue(selectedItems);
  selectToggleObserve();

}

The definitions of the above properties:

private final MutableLiveData<ArrayList<Integer>> LiveSelectedItems;
private final ArrayList<Integer> selectedItems;

Solution

  • State needs to know when its value has been updated, but it can not track changes in a List. To trigger State update you need to create a new collection instance instead of updating the LiveData with the same instance of ArrayList.

    View model:

    private final MutableLiveData<ArrayList<Integer>> liveSelectedItems = new MutableLiveData<>(new ArrayList<>());
    public final LiveData<ArrayList<Integer>> selectedItems = liveSelectedItems;
    
    public void toggleSelection(int id) {
        var currentList = Objects.requireNonNull(liveSelectedItems.getValue());
        var newList = new ArrayList<>(currentList);
        int index = currentList.indexOf(id);
        if (index >= 0) {
            newList.remove(index);
        } else {
            newList.add(id);
        }
        liveSelectedItems.setValue(newList);
    }
    

    Sample composable:

    val selectedItems by viewModel.selectedItems.observeAsState(ArrayList())
    Column {
        repeat(10) { index ->
            Card(
                modifier = Modifier
                    .padding(5.dp)
                    .fillMaxWidth()
                    .clickable { viewModel.toggleSelection(index) },
            ) {
                Text("Item $index selected: ${selectedItems.contains(index)}")
            }
        }
    }