From 2f4680d7ce15fbe9993daf846b5aa655e5fa87c7 Mon Sep 17 00:00:00 2001 From: Filipe Bezerra Date: Fri, 18 Sep 2020 21:50:29 -0300 Subject: [PATCH 1/8] Start implementation using MVVM architecture with ViewModel and LiveData --- app/build.gradle | 13 +++ .../guesstheword/screens/game/GameFragment.kt | 100 +++--------------- .../screens/game/GameViewModel.kt | 79 ++++++++++++++ 3 files changed, 109 insertions(+), 83 deletions(-) create mode 100644 app/src/main/java/com/example/android/guesstheword/screens/game/GameViewModel.kt diff --git a/app/build.gradle b/app/build.gradle index 6166c369e..f3e57d22d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -39,6 +39,18 @@ android { buildFeatures { dataBinding true } + + // Configure only for each module that uses Java 8 + // language features (either in its source code or + // through dependencies). + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + // For Kotlin projects + kotlinOptions { + jvmTarget = "1.8" + } } dependencies { @@ -53,6 +65,7 @@ dependencies { // KTX implementation 'androidx.core:core-ktx:1.3.1' + implementation "androidx.fragment:fragment-ktx:1.2.5" // Navigation implementation "android.arch.navigation:navigation-fragment-ktx:1.0.0-rc02" diff --git a/app/src/main/java/com/example/android/guesstheword/screens/game/GameFragment.kt b/app/src/main/java/com/example/android/guesstheword/screens/game/GameFragment.kt index 0dbe12118..f359bd04a 100644 --- a/app/src/main/java/com/example/android/guesstheword/screens/game/GameFragment.kt +++ b/app/src/main/java/com/example/android/guesstheword/screens/game/GameFragment.kt @@ -22,6 +22,8 @@ import android.view.View import android.view.ViewGroup import androidx.databinding.DataBindingUtil import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.Observer import androidx.navigation.fragment.NavHostFragment.findNavController import com.example.android.guesstheword.R import com.example.android.guesstheword.databinding.GameFragmentBinding @@ -31,14 +33,7 @@ import com.example.android.guesstheword.databinding.GameFragmentBinding */ class GameFragment : Fragment() { - // The current word - private var word = "" - - // The current score - private var score = 0 - - // The list of words - the front of the list is the next word to guess - private lateinit var wordList: MutableList + private val viewModel by viewModels() private lateinit var binding: GameFragmentBinding @@ -53,89 +48,28 @@ class GameFragment : Fragment() { false ) - resetList() - nextWord() - - binding.correctButton.setOnClickListener { onCorrect() } - binding.skipButton.setOnClickListener { onSkip() } - updateScoreText() - updateWordText() - return binding.root + binding.correctButton.setOnClickListener { + viewModel.onCorrect() + } + binding.skipButton.setOnClickListener { + viewModel.onSkip() + } - } + viewModel.score.observe(viewLifecycleOwner, Observer { newScore -> + binding.scoreText.text = newScore.toString() + }) + viewModel.word.observe(viewLifecycleOwner, Observer { newWord -> + binding.wordText.text = newWord + }) - /** - * Resets the list of words and randomizes the order - */ - private fun resetList() { - wordList = mutableListOf( - "queen", - "hospital", - "basketball", - "cat", - "change", - "snail", - "soup", - "calendar", - "sad", - "desk", - "guitar", - "home", - "railway", - "zebra", - "jelly", - "car", - "crow", - "trade", - "bag", - "roll", - "bubble" - ) - wordList.shuffle() + return binding.root } /** * Called when the game is finished */ private fun gameFinished() { - val action = GameFragmentDirections.actionGameToScore(score) + val action = GameFragmentDirections.actionGameToScore(viewModel.score.value ?: 0) findNavController(this).navigate(action) } - - /** - * Moves to the next word in the list - */ - private fun nextWord() { - //Select and remove a word from the list - if (wordList.isEmpty()) { - gameFinished() - } else { - word = wordList.removeAt(0) - } - updateWordText() - updateScoreText() - } - - /** Methods for buttons presses **/ - - private fun onSkip() { - score-- - nextWord() - } - - private fun onCorrect() { - score++ - nextWord() - } - - /** Methods for updating the UI **/ - - private fun updateWordText() { - binding.wordText.text = word - - } - - private fun updateScoreText() { - binding.scoreText.text = score.toString() - } } diff --git a/app/src/main/java/com/example/android/guesstheword/screens/game/GameViewModel.kt b/app/src/main/java/com/example/android/guesstheword/screens/game/GameViewModel.kt new file mode 100644 index 000000000..7bdd13be6 --- /dev/null +++ b/app/src/main/java/com/example/android/guesstheword/screens/game/GameViewModel.kt @@ -0,0 +1,79 @@ +package com.example.android.guesstheword.screens.game + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel + +class GameViewModel : ViewModel() { + + // The current word + private val _word = MutableLiveData("") + val word: LiveData + get() = _word + + // The current score + private val _score = MutableLiveData(0) + val score: LiveData + get() = _score + + // The list of words - the front of the list is the next word to guess + private lateinit var wordList: MutableList + + init { + resetList() + nextWord() + } + + /** + * Resets the list of words and randomizes the order + */ + private fun resetList() { + wordList = mutableListOf( + "queen", + "hospital", + "basketball", + "cat", + "change", + "snail", + "soup", + "calendar", + "sad", + "desk", + "guitar", + "home", + "railway", + "zebra", + "jelly", + "car", + "crow", + "trade", + "bag", + "roll", + "bubble" + ) + wordList.shuffle() + } + + /** + * Moves to the next word in the list + */ + private fun nextWord() { + //Select and remove a word from the list + if (wordList.isEmpty()) { +// gameFinished() + } else { + _word.value = wordList.removeAt(0) + } + } + + /** Methods for buttons presses **/ + fun onSkip() { + _score.value = score.value?.minus(1) + nextWord() + } + + fun onCorrect() { + _score.value = score.value?.plus(1) + nextWord() + } +} \ No newline at end of file From 72de48ae026738b1f980cc229cf55da702fb87a3 Mon Sep 17 00:00:00 2001 From: Filipe Bezerra Date: Fri, 18 Sep 2020 22:21:13 -0300 Subject: [PATCH 2/8] Using LiveData state to signal game finish event --- .../android/guesstheword/screens/game/GameFragment.kt | 6 ++++++ .../android/guesstheword/screens/game/GameViewModel.kt | 10 +++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/example/android/guesstheword/screens/game/GameFragment.kt b/app/src/main/java/com/example/android/guesstheword/screens/game/GameFragment.kt index f359bd04a..ac66b94e1 100644 --- a/app/src/main/java/com/example/android/guesstheword/screens/game/GameFragment.kt +++ b/app/src/main/java/com/example/android/guesstheword/screens/game/GameFragment.kt @@ -61,6 +61,12 @@ class GameFragment : Fragment() { viewModel.word.observe(viewLifecycleOwner, Observer { newWord -> binding.wordText.text = newWord }) + viewModel.eventGameFinish.observe(viewLifecycleOwner, Observer { hasFinished -> + if (hasFinished) { + gameFinished() + viewModel.onGameFinishComplete() + } + }) return binding.root } diff --git a/app/src/main/java/com/example/android/guesstheword/screens/game/GameViewModel.kt b/app/src/main/java/com/example/android/guesstheword/screens/game/GameViewModel.kt index 7bdd13be6..133956d81 100644 --- a/app/src/main/java/com/example/android/guesstheword/screens/game/GameViewModel.kt +++ b/app/src/main/java/com/example/android/guesstheword/screens/game/GameViewModel.kt @@ -16,6 +16,10 @@ class GameViewModel : ViewModel() { val score: LiveData get() = _score + private val _eventGameFinish = MutableLiveData(false) + val eventGameFinish: LiveData + get() = _eventGameFinish + // The list of words - the front of the list is the next word to guess private lateinit var wordList: MutableList @@ -60,7 +64,7 @@ class GameViewModel : ViewModel() { private fun nextWord() { //Select and remove a word from the list if (wordList.isEmpty()) { -// gameFinished() + _eventGameFinish.value = true } else { _word.value = wordList.removeAt(0) } @@ -76,4 +80,8 @@ class GameViewModel : ViewModel() { _score.value = score.value?.plus(1) nextWord() } + + fun onGameFinishComplete() { + _eventGameFinish.value = false + } } \ No newline at end of file From e54d87761d17093b02d71f861af4bebcd6b0a89f Mon Sep 17 00:00:00 2001 From: Filipe Bezerra Date: Mon, 21 Sep 2020 19:04:16 -0300 Subject: [PATCH 3/8] Display timer as elapsed time --- .../guesstheword/screens/game/GameFragment.kt | 3 ++ .../screens/game/GameViewModel.kt | 48 +++++++++++++++++-- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/example/android/guesstheword/screens/game/GameFragment.kt b/app/src/main/java/com/example/android/guesstheword/screens/game/GameFragment.kt index ac66b94e1..138f00532 100644 --- a/app/src/main/java/com/example/android/guesstheword/screens/game/GameFragment.kt +++ b/app/src/main/java/com/example/android/guesstheword/screens/game/GameFragment.kt @@ -67,6 +67,9 @@ class GameFragment : Fragment() { viewModel.onGameFinishComplete() } }) + viewModel.timerText.observe(viewLifecycleOwner, Observer { timer -> + binding.timerText.text = timer + }) return binding.root } diff --git a/app/src/main/java/com/example/android/guesstheword/screens/game/GameViewModel.kt b/app/src/main/java/com/example/android/guesstheword/screens/game/GameViewModel.kt index 133956d81..31acd04d6 100644 --- a/app/src/main/java/com/example/android/guesstheword/screens/game/GameViewModel.kt +++ b/app/src/main/java/com/example/android/guesstheword/screens/game/GameViewModel.kt @@ -1,11 +1,24 @@ package com.example.android.guesstheword.screens.game +import android.os.CountDownTimer +import android.text.format.DateUtils import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Transformations import androidx.lifecycle.ViewModel class GameViewModel : ViewModel() { + companion object { + // These represent different important times + // This is when the game is over + const val DONE = 0L + // This is the number of milliseconds in a second + const val ONE_SECOND = 1000L + // This is the total time of the game + const val COUNTDOWN_TIME = 60000L + } + // The current word private val _word = MutableLiveData("") val word: LiveData @@ -23,11 +36,34 @@ class GameViewModel : ViewModel() { // The list of words - the front of the list is the next word to guess private lateinit var wordList: MutableList + private val timer: CountDownTimer + + private val _currentTime = MutableLiveData(0) + val currentTime: LiveData + get() = _currentTime + + val timerText: LiveData + get() = Transformations.map(currentTime, ::getCurrentTimeAsElapsedTime) + init { resetList() nextWord() + timer = object : CountDownTimer(COUNTDOWN_TIME, ONE_SECOND) { + override fun onTick(millisUntilFinished: Long) { + _currentTime.value = millisUntilFinished / ONE_SECOND + } + + override fun onFinish() { + _eventGameFinish.value = true + _currentTime.value = DONE + } + } + timer.start() } + private fun getCurrentTimeAsElapsedTime(time: Long) = DateUtils.formatElapsedTime(time) + + /** * Resets the list of words and randomizes the order */ @@ -62,12 +98,11 @@ class GameViewModel : ViewModel() { * Moves to the next word in the list */ private fun nextWord() { - //Select and remove a word from the list if (wordList.isEmpty()) { - _eventGameFinish.value = true - } else { - _word.value = wordList.removeAt(0) + resetList() } + //Select and remove a word from the list + _word.value = wordList.removeAt(0) } /** Methods for buttons presses **/ @@ -84,4 +119,9 @@ class GameViewModel : ViewModel() { fun onGameFinishComplete() { _eventGameFinish.value = false } + + override fun onCleared() { + super.onCleared() + timer.cancel() + } } \ No newline at end of file From c8eb7362937fd3bb4b920bbf8ffb9f7dd5861df5 Mon Sep 17 00:00:00 2001 From: Filipe Bezerra Date: Mon, 21 Sep 2020 19:36:59 -0300 Subject: [PATCH 4/8] Create ViewModel for ScoreFragment --- .../screens/score/ScoreFragment.kt | 19 +++++++++++++-- .../screens/score/ScoreViewModel.kt | 24 +++++++++++++++++++ .../screens/score/ScoreViewModelFactory.kt | 14 +++++++++++ 3 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/example/android/guesstheword/screens/score/ScoreViewModel.kt create mode 100644 app/src/main/java/com/example/android/guesstheword/screens/score/ScoreViewModelFactory.kt diff --git a/app/src/main/java/com/example/android/guesstheword/screens/score/ScoreFragment.kt b/app/src/main/java/com/example/android/guesstheword/screens/score/ScoreFragment.kt index 63bcb6191..576370285 100644 --- a/app/src/main/java/com/example/android/guesstheword/screens/score/ScoreFragment.kt +++ b/app/src/main/java/com/example/android/guesstheword/screens/score/ScoreFragment.kt @@ -22,6 +22,8 @@ import android.view.View import android.view.ViewGroup import androidx.databinding.DataBindingUtil import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import com.example.android.guesstheword.R @@ -48,8 +50,21 @@ class ScoreFragment : Fragment() { // Get args using by navArgs property delegate val scoreFragmentArgs by navArgs() - binding.scoreText.text = scoreFragmentArgs.score.toString() - binding.playAgainButton.setOnClickListener { onPlayAgain() } + val viewModelFactory = ScoreViewModelFactory(scoreFragmentArgs.score) + val viewModel = ViewModelProvider(this, viewModelFactory) + .get(ScoreViewModel::class.java) + + viewModel.score.observe(viewLifecycleOwner, Observer { score -> + binding.scoreText.text = score.toString() + }) + viewModel.eventPlayAgain.observe(viewLifecycleOwner, Observer { playAgain -> + if (playAgain) { + onPlayAgain() + viewModel.onPlayAgainComplete() + } + }) + + binding.playAgainButton.setOnClickListener { viewModel.onPlayAgain() } return binding.root } diff --git a/app/src/main/java/com/example/android/guesstheword/screens/score/ScoreViewModel.kt b/app/src/main/java/com/example/android/guesstheword/screens/score/ScoreViewModel.kt new file mode 100644 index 000000000..4b3346da0 --- /dev/null +++ b/app/src/main/java/com/example/android/guesstheword/screens/score/ScoreViewModel.kt @@ -0,0 +1,24 @@ +package com.example.android.guesstheword.screens.score + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel + +class ScoreViewModel(finalScore: Int) : ViewModel() { + + private val _score = MutableLiveData(finalScore) + val score: LiveData + get() = _score + + private val _eventPlayAgain = MutableLiveData(false) + val eventPlayAgain: LiveData + get() = _eventPlayAgain + + fun onPlayAgain() { + _eventPlayAgain.value = true + } + + fun onPlayAgainComplete() { + _eventPlayAgain.value = false + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/android/guesstheword/screens/score/ScoreViewModelFactory.kt b/app/src/main/java/com/example/android/guesstheword/screens/score/ScoreViewModelFactory.kt new file mode 100644 index 000000000..b50510e86 --- /dev/null +++ b/app/src/main/java/com/example/android/guesstheword/screens/score/ScoreViewModelFactory.kt @@ -0,0 +1,14 @@ +package com.example.android.guesstheword.screens.score + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import java.lang.IllegalArgumentException + +class ScoreViewModelFactory(private val finalScore: Int) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(ScoreViewModel::class.java)) { + return ScoreViewModel(finalScore) as T + } + throw IllegalArgumentException("Unknown viewModel class: $modelClass") + } +} \ No newline at end of file From 329d8ade2dd8224ee751a1954d9328bde9a64b1c Mon Sep 17 00:00:00 2001 From: Filipe Bezerra Date: Mon, 21 Sep 2020 20:06:21 -0300 Subject: [PATCH 5/8] Data binding lifecycle aware --- .../guesstheword/screens/game/GameFragment.kt | 18 ++---------------- .../guesstheword/screens/game/GameViewModel.kt | 2 +- .../screens/score/ScoreFragment.kt | 7 ++----- app/src/main/res/layout/game_fragment.xml | 13 ++++++++++++- app/src/main/res/layout/score_fragment.xml | 8 ++++++++ 5 files changed, 25 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/example/android/guesstheword/screens/game/GameFragment.kt b/app/src/main/java/com/example/android/guesstheword/screens/game/GameFragment.kt index 138f00532..11476e496 100644 --- a/app/src/main/java/com/example/android/guesstheword/screens/game/GameFragment.kt +++ b/app/src/main/java/com/example/android/guesstheword/screens/game/GameFragment.kt @@ -48,29 +48,15 @@ class GameFragment : Fragment() { false ) - binding.correctButton.setOnClickListener { - viewModel.onCorrect() - } - binding.skipButton.setOnClickListener { - viewModel.onSkip() - } + binding.viewmodel = viewModel + binding.lifecycleOwner = viewLifecycleOwner - viewModel.score.observe(viewLifecycleOwner, Observer { newScore -> - binding.scoreText.text = newScore.toString() - }) - viewModel.word.observe(viewLifecycleOwner, Observer { newWord -> - binding.wordText.text = newWord - }) viewModel.eventGameFinish.observe(viewLifecycleOwner, Observer { hasFinished -> if (hasFinished) { gameFinished() viewModel.onGameFinishComplete() } }) - viewModel.timerText.observe(viewLifecycleOwner, Observer { timer -> - binding.timerText.text = timer - }) - return binding.root } diff --git a/app/src/main/java/com/example/android/guesstheword/screens/game/GameViewModel.kt b/app/src/main/java/com/example/android/guesstheword/screens/game/GameViewModel.kt index 31acd04d6..08fb0fda2 100644 --- a/app/src/main/java/com/example/android/guesstheword/screens/game/GameViewModel.kt +++ b/app/src/main/java/com/example/android/guesstheword/screens/game/GameViewModel.kt @@ -16,7 +16,7 @@ class GameViewModel : ViewModel() { // This is the number of milliseconds in a second const val ONE_SECOND = 1000L // This is the total time of the game - const val COUNTDOWN_TIME = 60000L + const val COUNTDOWN_TIME = 10000L } // The current word diff --git a/app/src/main/java/com/example/android/guesstheword/screens/score/ScoreFragment.kt b/app/src/main/java/com/example/android/guesstheword/screens/score/ScoreFragment.kt index 576370285..867b89f10 100644 --- a/app/src/main/java/com/example/android/guesstheword/screens/score/ScoreFragment.kt +++ b/app/src/main/java/com/example/android/guesstheword/screens/score/ScoreFragment.kt @@ -53,10 +53,9 @@ class ScoreFragment : Fragment() { val viewModelFactory = ScoreViewModelFactory(scoreFragmentArgs.score) val viewModel = ViewModelProvider(this, viewModelFactory) .get(ScoreViewModel::class.java) + binding.viewmodel = viewModel + binding.lifecycleOwner = viewLifecycleOwner - viewModel.score.observe(viewLifecycleOwner, Observer { score -> - binding.scoreText.text = score.toString() - }) viewModel.eventPlayAgain.observe(viewLifecycleOwner, Observer { playAgain -> if (playAgain) { onPlayAgain() @@ -64,8 +63,6 @@ class ScoreFragment : Fragment() { } }) - binding.playAgainButton.setOnClickListener { viewModel.onPlayAgain() } - return binding.root } diff --git a/app/src/main/res/layout/game_fragment.xml b/app/src/main/res/layout/game_fragment.xml index 8fbd82f0c..bc102ba79 100644 --- a/app/src/main/res/layout/game_fragment.xml +++ b/app/src/main/res/layout/game_fragment.xml @@ -18,6 +18,12 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> + + + + + tools:text="Current Score: 2" />