Here is a better argument for using the Factory model. Consider Listing 4.6 where we make currentIndex
public so we can access and assign its value via the Bundle.
// Listing 4.6 - QuizViewModel.kt
class QuizViewModel : ViewModel() {
var currentIndex = 0
...
}
This is now used in Listing 4.11
// Listing 4.11 - MainActivity.kt
override fun onSaveInstanceState(savedInstanceState: Bundle) {
super.onSaveInstanceState(savedInstanceState)
Log.i(TAG, "onSaveInstanceState")
savedInstanceState.putInt(KEY_INDEX, quizViewModel.currentIndex)
}
and in Listing 4.12
// Listing 4.12 - MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
...
val currentIndex = savedInstanceState?.getInt(KEY_INDEX, 0) ?: 0
quizViewModel.currentIndex = currentIndex
}
While this suffices, it publicly exposes the currentIndex
to the Activity. It could be possible the Activity sets the currentIndex
on our ViewModel to -1 or 100, a value out of range for our questionBank
. If you have been working through the challenges and are keeping score, then this is the same issue that the Activity could set the score to any value.
We would much rather have the currentIndex
value be kept private and only expose its value via a getter and not allow the Activity to set its value. Likewise with the score.
The ViewModelProvider.Factory
allows us to set the currentIndex
upon ViewModel creation. We would structure our classes as follows:
// QuizViewModel.kt
class QuizViewModel(private var currentQuestionIndex: Int = 0) : ViewModel() {
...
val currentIndex: Int
get() = currentQuestionIndex
...
fun moveToNext() {
currentQuestionIndex = (currentQuestionIndex + 1) % questionBank.size
}
fun moveToPrevious() {
currentQuestionIndex = (currentQuestionIndex - 1 + questionBank.size) % questionBank.size
}
}
// QuizViewModelFactory.kt
class QuizViewModelFactory(private val initialQuestionIndex: Int) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return modelClass.getConstructor(Int::class.java).newInstance(initialQuestionIndex)
}
}
// MainActivity.kt
class QuizActivity : AppCompatActivity() {
private lateinit var quizViewModel: QuizViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val currentIndex = savedInstanceState?.getInt(KEY_INDEX, 0) ?: 0
val factory = QuizViewModelFactory(currentIndex)
quizViewModel = ViewModelProvider(this@MainActivity, factory).get(QuizViewModel::class.java)
...
}
override fun onSaveInstanceState(savedInstanceState: Bundle) {
super.onSaveInstanceState(savedInstanceState)
savedInstanceState.putInt(KEY_INDEX, quizViewModel.currentIndex)
}
}
Now we have the proper protection in place that only QuizViewModel can edit the current question index AND we can store information on the bundle to persist across process death. This is also easily extendable to track current score, number of cheats remaining, and other information from the challenges in Chapters 1-7.