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.