Android Livedata updates his states with copy of objects

Viewed 295

Hi i think i don't really now how live data works.

I am having a 2D Array with prefilled Values.

val randomboard =
    arrayOf(arrayOf(0, 0, 0, 0), arrayOf(0, 0, 0, 0), arrayOf(0, 0, 0, 0), arrayOf(0, 0, 0, 0))

And a Live Data Object that posts just once at init Function of my ViewModel the current State of my Array.

init {
    _preFillButtons.postValue(randomboard.copyOf())
}

So i want that only the current State of my Array and no future states are pushed to my LiveData Objects so i gave it a copy of my Array.

When i am changing any value of my Array randomboard and then change like my orientation my livedata object has just this new Value in his state without that i pushed any new State to that LiveData object.

Maybe Livedata object is not that what i need there but i don't know why even the copy object is updated.

UPDATE:

The complete Code

class GameActivity : AppCompatActivity(), View.OnClickListener {

val viewModel: GameViewModel by viewModels()

val buttonIDs = arrayOf(
    intArrayOf(R.id.button1, R.id.button2, R.id.button3, R.id.button4),
    intArrayOf(R.id.button5, R.id.button6, R.id.button7, R.id.button8),
    intArrayOf(R.id.button9, R.id.button10, R.id.button11, R.id.button12),
    intArrayOf(R.id.button13, R.id.button14, R.id.button15, R.id.button16)
)

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    viewModel.preFillButtons.observe(this) {
        prefillButtons(it)
    }
}

private fun prefillButtons(board: Array<Array<Int>>) {
    for (row in 0..3) {
        for (col in 0..3) {
            val button = findViewById<Button>(buttonIDs[row][col])
            button.setTag(R.id.row, row)
            button.setTag(R.id.column, col)
            button.setOnClickListener(this)
            if (board[row][col] != 0) {
                button.text = board[row][col].toString()
                button.isEnabled = false
            } else {
                button.text = getString(R.string.defaultbuttontext)
                button.isEnabled = true
            }
        }
    }
}

override fun onClick(view: View?) {
    val button = view as? Button
    if (button != null) {
        val row = button.getTag(R.id.row) as Int
        val col = button.getTag(R.id.column) as Int
        showAlertDialog(col, row)
    }
}

private fun showAlertDialog(col: Int, row: Int) {
    val builder = AlertDialog.Builder(this)
    builder.setItems(R.array.choices) { dialogInterface: DialogInterface, i: Int ->
        if (i != 4) {
            viewModel.trySetValue(row, col, i + 1)
        }
    }
    builder.setTitle(getString(R.string.dialogtitle, row + 1, col + 1))
    builder.show()
}
}

ViewModel:

class GameViewModel : ViewModel() {

val preFillButtons: LiveData<Array<Array<Int>>>
    get() = _preFillButtons
private val _preFillButtons = MutableLiveData<Array<Array<Int>>>()

val randomboard =
    arrayOf(arrayOf(0, 0, 0, 0), arrayOf(0, 0, 0, 0), arrayOf(0, 0, 0, 0), arrayOf(0, 0, 0, 0))

init {
    _preFillButtons.postValue(randomboard.copyOf())
}

fun trySetValue(row: Int, col: Int, num: Int) {
    randomboard[0][0] = 1
}
}
1 Answers

Seeing the new state after a screen rotation (after a configuration change) is not related to how LiveData works. You are observing your LiveData correctly.

So i want that only the current State of my Array and no future states are pushed to my LiveData Objects so i gave it a copy of my Array.

A slight correction here, you gave it a shallow copy of your array. A shallow copy is a copy of the original array, but if the items are instances (references to objects), they will be copied by reference. Which means that changes on items in the copy of the array will affect the items in the original array as well, since it is the same references (= same instances).

And since you have an array of arrays, items in your array are instances of 4 other arrays. And when you change the values inside randomboard in your trySetValue function, you access the same 4 array instances every time.

The arrayOf calls create a new array instance and fill it with the values provided as parameters. So your initial assignment to randomboard creates 5 array instances:

  • 4 are of type Integer[] and contain values 0, 0, 0, 0
  • 1 is of type Integer[][] and contains references to the 4 arrays above. The reference to this array is stored into the field randomboards

This snippet of code compares the values by reference and shows that the main array is a new instance (because of the copyOf call), but the arrays inside are the same instances.

init {
    val copy = randomboard.copyOf()
    Log.d("Test", "copy === randomboard: ${copy === randomboard}") // false
    Log.d("Test", "copy[0] === randomboard[0]: ${copy[0] === randomboard[0]}") // true
    Log.d("Test", "copy[1] === randomboard[1]: ${copy[1] === randomboard[1]}") // true
    Log.d("Test", "copy[2] === randomboard[2]: ${copy[2] === randomboard[2]}") // true
    Log.d("Test", "copy[3] === randomboard[3]: ${copy[3] === randomboard[3]}") // true
    //...
}

If you don't want the changes to affect the copy of the array, you have to make a deep copy. Which you can do by calling .map on the source array or using the Array<T>(size: Int, init: (Int) -> T) constructor that takes the initializer function as a 2nd parameter.

By calling map on the source array

init {
    val deepCopy = randomboard.map { it.copyOf() }.toTypedArray()
    _preFillButtons.postValue(deepCopy)
}

By using the Array<T>(size: Int, init: (Int) -> T) constructor

init {
    // Same as: val deepCopy = Array(randomboard.size) { idx -> randomboard[idx].copyOf() } }
    val deepCopy = randomboard.let { Array(it.size) { idx -> it[idx].copyOf() } }
    _preFillButtons.postValue(deepCopy)
}

Then if you compare all values by reference, you will see that now also the inner arrays are a new instance.

init {
    val deepCopy = randomboard.map { it.copyOf() }.toTypedArray()
    Log.d("Test", "deepCopy === randomboard: ${deepCopy === randomboard}") // false
    Log.d("Test", "deepCopy[0] === randomboard[0]: ${deepCopy[0] === randomboard[0]}") // false
    Log.d("Test", "deepCopy[1] === randomboard[1]: ${deepCopy[1] === randomboard[1]}") // false
    Log.d("Test", "deepCopy[2] === randomboard[2]: ${deepCopy[2] === randomboard[2]}") // false
    Log.d("Test", "deepCopy[3] === randomboard[3]: ${deepCopy[3] === randomboard[3]}") // false
    // ...
}
Related