Home > other >  Attempt to invoke virtual method 'void androidx.lifecycle.MutableLiveData.setValue(java.lang.Ob
Attempt to invoke virtual method 'void androidx.lifecycle.MutableLiveData.setValue(java.lang.Ob

Time:07-06

Sorry for the long title, however, I am unsure where this error is at in my code, however, I do suspect the error lies in the implementation of the liveData and Observation.

The app which I am working on is an Unscrambler word app where users have to unscramble the letters displayed on the fragment. My code so far consists of 2 Kotlin classes listed below, a fragment and a ViewModel class.

I have currently assigned the variable _currentScrambledWord as a MutableLiveData<String>() and utilised the backing property in ViewModel.kt

private val _currentScrambledWord = MutableLiveData<String>()
val currentScrambledWord: LiveData<String>
    get() = _currentScrambledWord

I then tried to attach an observer to the GameFragment using the code below.

viewModel.currentScrambledWord.observe(viewLifecycleOwner,
        { newWord -> binding.textViewUnscrambledWord.text = newWord
        })

From what I understand the LiveData simplifies the process of retrieving data from the ViewModel reducing the amount of code needed.

Here is the error I found in Logcat

Attempt to invoke virtual method 'void androidx.lifecycle.MutableLiveData.setValue(java.lang.Object)' on a null object reference 

The error lines in the logcat is as follows

at com.example.android.unscramble.ui.game.GameViewModel.getNextWord(GameViewModel.kt:57)
at com.example.android.unscramble.ui.game.GameViewModel.<init>(GameViewModel.kt:18)
at com.example.android.unscramble.ui.game.GameFragment.getViewModel(GameFragment.kt:39)
at com.example.android.unscramble.ui.game.GameFragment.onViewCreated(GameFragment.kt:76)

Viewmodel.kt

class GameViewModel:ViewModel() {

private var wordsList: MutableList<String> = mutableListOf()
private lateinit var currentWord: String

init {
    Log.d("GameFragment", "GameViewModel created!")
    getNextWord()
}

private var _score = 0
val score: Int
    get() = _score

private var _currentWordCount = 0
val currentWordCount: Int
    get() = _currentWordCount

private var _currentScrambledWord = MutableLiveData<String>()
val currentScrambledWord: LiveData<String>
    get() = _currentScrambledWord

private fun increaseScore() {
    _score  = SCORE_INCREASE
}

fun isUserWordCorrect(playerWord: String): Boolean {
    if (playerWord.equals(currentWord, true)) {
        increaseScore()
        return true
    }
    return false
}


private fun getNextWord() {
    currentWord = allWordsList.random()
    val tempWord = currentWord.toCharArray()
    tempWord.shuffle()

    while (String(tempWord).equals(currentWord, false)) {
        tempWord.shuffle()
    }
    if (wordsList.contains(currentWord)) {
        getNextWord()
    } else {
        _currentScrambledWord.value = String(tempWord)
          _currentWordCount
        wordsList.add(currentWord)
    }
}
fun nextWord(): Boolean {
    return if (currentWordCount < MAX_NO_OF_WORDS) {
        getNextWord()
        true
    } else false
}



override fun onCleared() {
    super.onCleared()
    Log.d("GameFragment", "GameViewModel destroyed!")
}

fun reinitializeData() {
    _score = 0
    _currentWordCount = 0
    wordsList.clear()
    getNextWord()
}





}

GameFragment.kt

class GameFragment : Fragment() {




private val viewModel: GameViewModel by viewModels()


// Binding object instance with access to the views in the game_fragment.xml layout
private lateinit var binding: GameFragmentBinding

// Create a ViewModel the first time the fragment is created.
// If the fragment is re-created, it receives the same GameViewModel instance created by the
// first fragment



override fun onCreateView(
    inflater: LayoutInflater, container: ViewGroup?,
    savedInstanceState: Bundle?
): View {
    binding = GameFragmentBinding.inflate(inflater, container, false)
    Log.d("GameFragment1", "GameFragment created/re-created!")



    return binding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    Log.d("OnViewCreated", "OnViewCreated working")
    // Setup a click listener for the Submit and Skip buttons.
    binding.submit.setOnClickListener { onSubmitWord() }
    binding.skip.setOnClickListener { onSkipWord() }
    // Update the UI

    binding.score.text = getString(R.string.score, 0)
    binding.wordCount.text = getString(
            R.string.word_count, 0, MAX_NO_OF_WORDS)

    viewModel.currentScrambledWord.observe(viewLifecycleOwner,
        { newWord -> binding.textViewUnscrambledWord.text = newWord
        })
}

/*
* Checks the user's word, and updates the score accordingly.
* Displays the next scrambled word.
*/


private fun onSubmitWord() {
    val playerWord = binding.textInputEditText.text.toString()

    if (viewModel.isUserWordCorrect(playerWord)) {
        setErrorTextField(false)
        if (!viewModel.nextWord()) {
            showFinalScoreDialog()
        }
    } else {
        setErrorTextField(true)
    }
}



/*
 * Skips the current word without changing the score.
 * Increases the word count.
 */
private fun onSkipWord() {
    if (viewModel.nextWord()) {
        setErrorTextField(false)

    } else {
        showFinalScoreDialog()
    }
}

/*
 * Gets a random word for the list of words and shuffles the letters in it.
 */
private fun getNextScrambledWord(): String {
    val tempWord = allWordsList.random().toCharArray()
    tempWord.shuffle()
    return String(tempWord)
}

/*
 * Re-initializes the data in the ViewModel and updates the views with the new data, to
 * restart the game.
 */

private fun showFinalScoreDialog() {
    MaterialAlertDialogBuilder(requireContext())
        .setTitle(getString(R.string.congratulations))
        .setMessage(getString(R.string.you_scored, viewModel.score))
        .setCancelable(false)
        .setNegativeButton(getString(R.string.exit)) { _, _ ->
            exitGame()
        }
        .setPositiveButton(getString(R.string.play_again)) { _, _ ->
            restartGame()
        }
        .show()

}

private fun restartGame() {
    viewModel.reinitializeData()
    setErrorTextField(false)

}

/*
 * Exits the game.
 */
private fun exitGame() {
    activity?.finish()
}

/*
* Sets and resets the text field error status.
*/
private fun setErrorTextField(error: Boolean) {
    if (error) {
        binding.textField.isErrorEnabled = true
        binding.textField.error = getString(R.string.try_again)
    } else {
        binding.textField.isErrorEnabled = false
        binding.textInputEditText.text = null
    }
}

/*
 * Displays the next scrambled word on screen.
 */



override fun onDetach() {
    super.onDetach()
    Log.d("GameFragment", "GameFragment destroyed!")
}
}

CodePudding user response:

Initialization in Kotlin happens in the order written (top to bottom). Because your init block is listed before you initialize _currentScrambledWord, it is null when you try to use it in init. You should move the init block to the end of the class definition, so it is at least after the LiveData, something like this:

private var wordsList: MutableList<String> = mutableListOf()
private var currentWord: String = "" // doesn't have to be lateinit if you always set it in "init" - better yet, just put a default

private var _score = 0
val score: Int
    get() = _score

private var _currentWordCount = 0
val currentWordCount: Int
    get() = _currentWordCount

private var _currentScrambledWord = MutableLiveData<String>()
val currentScrambledWord: LiveData<String>
    get() = _currentScrambledWord

// other stuff

// init at the very bottom
init {
    Log.d("GameFragment", "GameViewModel created!")
    getNextWord()
}

Have a look here for some additional context.

  • Related