From e03570c5c5218dc4c0d4625c242b45807b4a365c Mon Sep 17 00:00:00 2001 From: Vitalii Kukoba Date: Fri, 27 Sep 2024 10:02:43 +0300 Subject: [PATCH] Implemented ViewModels and LiveData --- app/build.gradle | 55 ++++---- app/src/main/AndroidManifest.xml | 7 +- .../guesstheword/screens/game/GameFragment.kt | 110 ++++----------- .../screens/game/GameViewModel.kt | 129 +++++++++++++++++- .../screens/score/ScoreFragment.kt | 23 +++- .../screens/score/ScoreViewModel.kt | 27 ++++ .../screens/score/ScoreViewModelFactory.kt | 13 ++ app/src/main/res/layout/game_fragment.xml | 11 ++ app/src/main/res/layout/score_fragment.xml | 7 + .../main/res/navigation/main_navigation.xml | 11 +- build.gradle | 12 +- gradle.properties | 3 + gradle/wrapper/gradle-wrapper.properties | 12 +- 13 files changed, 280 insertions(+), 140 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/build.gradle b/app/build.gradle index dddf682c3..1b428467e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,51 +13,60 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply plugin: 'kotlin-kapt' -apply plugin: 'kotlin-android-extensions' -apply plugin: "androidx.navigation.safeargs.kotlin" +plugins{ + id 'com.android.application' + id 'kotlin-android' + id 'kotlin-kapt' + id "androidx.navigation.safeargs" +} android { - compileSdkVersion 28 + compileSdkVersion 34 defaultConfig { applicationId "com.example.android.guesstheword" - minSdkVersion 19 - targetSdkVersion 28 + minSdkVersion 21 + targetSdkVersion 34 versionCode 1 versionName "1.0" - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' } buildTypes { release { minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } - dataBinding { - enabled = true + buildFeatures { + dataBinding true + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17 } + namespace 'com.example.android.guesstheword' } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'androidx.appcompat:appcompat:1.0.2' - implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'androidx.appcompat:appcompat:1.7.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.legacy:legacy-support-v4:1.0.0' - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test:runner:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test:runner:1.6.2' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' // KTX - implementation 'androidx.core:core-ktx:1.0.1' + implementation 'androidx.core:core-ktx:1.13.1' // Navigation - implementation "android.arch.navigation:navigation-fragment-ktx:1.0.0-rc02" - implementation "android.arch.navigation:navigation-ui-ktx:1.0.0-rc02" + implementation "androidx.navigation:navigation-fragment-ktx:2.8.1" + implementation "androidx.navigation:navigation-ui-ktx:2.8.1" - // TODO (01) Add lifecycle-extensions gradle + // Lifecycles + implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ebac42795..06c5d9f41 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -15,8 +15,7 @@ --> + xmlns:tools="http://schemas.android.com/tools"> - - + 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 afde263a1..7eacb0d85 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 @@ -17,12 +17,16 @@ package com.example.android.guesstheword.screens.game import android.os.Bundle +import android.text.format.DateUtils import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.databinding.DataBindingUtil +import androidx.databinding.Observable import androidx.fragment.app.Fragment -import androidx.navigation.fragment.NavHostFragment.findNavController +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.NavHostFragment.Companion.findNavController import com.example.android.guesstheword.R import com.example.android.guesstheword.databinding.GameFragmentBinding @@ -31,114 +35,48 @@ 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 lateinit var gameViewModel: GameViewModel private lateinit var binding: GameFragmentBinding override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - // Inflate view and obtain an instance of the binding class binding = DataBindingUtil.inflate( - inflater, - R.layout.game_fragment, - container, - false + inflater, + R.layout.game_fragment, + container, + false ) - // TODO (04) Create and initialize a GameViewModel, using ViewModelProviders; Add a log - // statement + gameViewModel = ViewModelProvider(this)[GameViewModel::class.java] - resetList() - nextWord() - - binding.correctButton.setOnClickListener { onCorrect() } - binding.skipButton.setOnClickListener { onSkip() } - updateScoreText() - updateWordText() - return binding.root + binding.gameViewModel = gameViewModel + binding.setLifecycleOwner(this) - } + gameViewModel.eventGameFinished.observe(viewLifecycleOwner) { hasFinished -> + if (hasFinished) { + gameFinished() + gameViewModel.onGameFinished() + } + } - /** - * 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() + action.setScore(gameViewModel.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 + binding.wordText.text = gameViewModel.word.value } private fun updateScoreText() { - binding.scoreText.text = score.toString() + binding.scoreText.text = gameViewModel.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 index 44cd5165c..6d57fa826 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,5 +16,130 @@ package com.example.android.guesstheword.screens.game -// TODO (02) Create the GameViewModel class, extending ViewModel -// TODO (03) Add init and override onCleared; Add log statements to both \ No newline at end of file +import android.os.CountDownTimer +import android.text.format.DateUtils +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.map + +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 = 5000L + } + + private var timer: CountDownTimer + + private var _currentTime = MutableLiveData() + val currentTime: LiveData + get() = _currentTime + + val currentTimeString = currentTime.map { time -> + DateUtils.formatElapsedTime(time) + } + + // The current word + private var _word = MutableLiveData() + val word: LiveData + get() = _word + + // The current score with backing property + private var _score = MutableLiveData() + val score: LiveData + get() = _score + + private var _eventGameFinished = MutableLiveData() + val eventGameFinished: LiveData + get() = _eventGameFinished + + init { + _eventGameFinished.value = false + Log.i("GameViewModel", "GameViewModel was created.") + resetList() + _word.value = "" + nextWord() + _score.value = 0 + + timer = object : CountDownTimer(COUNTDOWN_TIME, ONE_SECOND) { + override fun onTick(millisUntilFinished: Long) { + _currentTime.value = (millisUntilFinished/ ONE_SECOND) + } + + override fun onFinish() { + _currentTime.value = DONE + _eventGameFinished.value = true + } + + } + + timer.start() + } + + // The list of words - the front of the list is the next word to guess + private lateinit var wordList: MutableList + + 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() + } + + private fun nextWord() { + //Select and remove a word from the list + if (wordList.isEmpty()) { + resetList() + } + _word.value = wordList.removeAt(0) + } + + override fun onCleared() { + super.onCleared() + timer.cancel() + Log.i("GameViewModel", "GameViewModel was cleared.") + } + + fun onSkip() { + _score.value = score.value?.minus(1) + nextWord() + } + + fun onCorrect() { + _score.value = score.value?.plus(1) + nextWord() + } + + fun onGameFinished() { + _eventGameFinished.value = false + } +} \ No newline at end of file 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..18d9fd8f0 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,7 @@ import android.view.View import android.view.ViewGroup import androidx.databinding.DataBindingUtil import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import com.example.android.guesstheword.R @@ -32,6 +33,9 @@ import com.example.android.guesstheword.databinding.ScoreFragmentBinding */ class ScoreFragment : Fragment() { + private lateinit var scoreViewModel: ScoreViewModel + private lateinit var scoreViewModelFactory: ScoreViewModelFactory + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -46,15 +50,24 @@ class ScoreFragment : Fragment() { false ) - // Get args using by navArgs property delegate - val scoreFragmentArgs by navArgs() - binding.scoreText.text = scoreFragmentArgs.score.toString() - binding.playAgainButton.setOnClickListener { onPlayAgain() } + scoreViewModelFactory = ScoreViewModelFactory(ScoreFragmentArgs.fromBundle(requireArguments()).score) + scoreViewModel = ViewModelProvider(this, scoreViewModelFactory)[ScoreViewModel::class.java] + + binding.scoreViewModel = scoreViewModel + + binding.playAgainButton.setOnClickListener { scoreViewModel.onGameRestart() } + + scoreViewModel.eventGameRestart.observe(viewLifecycleOwner) { isRestarting -> + if (isRestarting) { + onPlayAgain() + scoreViewModel.onGameRestartFinished() + } + } return binding.root } private fun onPlayAgain() { - findNavController().navigate(ScoreFragmentDirections.actionRestart()) + findNavController().navigate(ScoreFragmentDirections.actionScoreDestinationToTitleDestination()) } } 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..c7097a628 --- /dev/null +++ b/app/src/main/java/com/example/android/guesstheword/screens/score/ScoreViewModel.kt @@ -0,0 +1,27 @@ +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 var _score = MutableLiveData() + val score : LiveData + get() = _score + + private var _eventGameRestart = MutableLiveData() + val eventGameRestart : LiveData + get() = _eventGameRestart + + init { + _score.value = finalScore + } + + fun onGameRestart() { + _eventGameRestart.value = true + } + + fun onGameRestartFinished() { + _eventGameRestart.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..07f9163ac --- /dev/null +++ b/app/src/main/java/com/example/android/guesstheword/screens/score/ScoreViewModelFactory.kt @@ -0,0 +1,13 @@ +package com.example.android.guesstheword.screens.score + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider + +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.") + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/game_fragment.xml b/app/src/main/res/layout/game_fragment.xml index 8fbd82f0c..95bb29c02 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"> + + + + + + + + - + \ No newline at end of file diff --git a/build.gradle b/build.gradle index d2f6ae152..9002d34e6 100644 --- a/build.gradle +++ b/build.gradle @@ -17,15 +17,15 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.3.21' + ext.kotlin_version = '1.9.20' repositories { google() - jcenter() + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:3.3.2' + classpath 'com.android.tools.build:gradle:8.4.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath "android.arch.navigation:navigation-safe-args-gradle-plugin:1.0.0-rc02" + classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.8.1" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files @@ -35,10 +35,10 @@ buildscript { allprojects { repositories { google() - jcenter() + mavenCentral() } } -task clean(type: Delete) { +tasks.register('clean', Delete) { delete rootProject.buildDir } \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 897244309..a0dd720e3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -29,3 +29,6 @@ org.gradle.jvmargs=-Xmx1536m android.databinding.enableV2=true android.useAndroidX=true android.enableJetifier=true +android.defaults.buildfeatures.buildconfig=true +android.nonTransitiveRClass=false +android.nonFinalResIds=false diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 9b75a9185..e7c213dd0 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Oct 01 14:14:39 PDT 2018 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip +#Thu Sep 26 10:33:45 EEST 2024 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists