diff --git a/app/build.gradle b/app/build.gradle
index 6166c369e..d63d8e5c3 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -17,7 +17,6 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
-apply plugin: 'kotlin-android-extensions'
apply plugin: "androidx.navigation.safeargs.kotlin"
android {
@@ -39,25 +38,35 @@ android {
buildFeatures {
dataBinding true
}
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+
+ kotlinOptions {
+ jvmTarget = JavaVersion.VERSION_1_8
+ }
}
dependencies {
- implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.2.0'
- implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+ implementation 'androidx.constraintlayout:constraintlayout:2.0.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.1'
+ androidTestImplementation 'androidx.test:runner:1.3.0'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
- // KTX
- implementation 'androidx.core:core-ktx:1.3.1'
+ // https://developer.android.com/jetpack/androidx/releases/core
+ implementation "androidx.core:core-ktx:$core_version"
- // Navigation
- implementation "android.arch.navigation:navigation-fragment-ktx:1.0.0-rc02"
- implementation "android.arch.navigation:navigation-ui-ktx:1.0.0-rc02"
+ // https://developer.android.com/jetpack/androidx/releases/navigation
+ implementation "androidx.navigation:navigation-fragment-ktx:$rootProject.nav_version"
+ implementation "androidx.navigation:navigation-ui-ktx:$rootProject.nav_version"
- // Lifecycles
- implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
+ // https://developer.android.com/jetpack/androidx/releases/lifecycle
+ implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
+ implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
+ implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index ebac42795..805f6df1f 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -18,6 +18,8 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.example.android.guesstheword">
+
+
-
- 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
- )
-
- resetList()
- nextWord()
-
- binding.correctButton.setOnClickListener { onCorrect() }
- binding.skipButton.setOnClickListener { onSkip() }
- updateScoreText()
- updateWordText()
- return binding.root
-
- }
-
- /**
- * 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()
+ private val viewModel: GameViewModel by viewModels()
+
+ private val navController: NavController by lazy { findNavController() }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View = GameFragmentBinding.inflate(inflater, container, false)
+ .apply {
+ viewModel = this@GameFragment.viewModel
+ lifecycleOwner = viewLifecycleOwner
+ }
+ .root
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ with(viewModel) {
+ gameFinishedEvent.observe(viewLifecycleOwner) { hasFinished ->
+ if (hasFinished) navigateToScore()
+ }
+ buzzGameEvent.observe(viewLifecycleOwner) { buzzType -> vibrate(buzzType.pattern) }
+ }
}
/**
* Called when the game is finished
*/
- private fun gameFinished() {
- val action = GameFragmentDirections.actionGameToScore(score)
- 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()
- }
+ private fun navigateToScore() =
+ GameFragmentDirections.actionGameToScore(viewModel.score.value ?: INITIAL_SCORE)
+ .run { navController.navigate(this) }
+ .also { viewModel.onGameFinishedNavigated() }
+
+ private fun vibrate(pattern: LongArray) =
+ activity?.getSystemService()?.run {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ vibrate(VibrationEffect.createWaveform(pattern, -1))
+ } else {
+ //deprecated in API 26
+ @Suppress("DEPRECATION")
+ vibrate(pattern, -1)
+ }
+ }
}
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..b8819dbd0
--- /dev/null
+++ b/app/src/main/java/com/example/android/guesstheword/screens/game/GameViewModel.kt
@@ -0,0 +1,163 @@
+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() {
+
+ // The current word
+ private val _word = MutableLiveData("")
+ val word: LiveData
+ get() = _word
+
+ // The current score
+ private val _score = MutableLiveData(INITIAL_SCORE)
+ 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
+
+ private val _gameFinishedEvent = MutableLiveData(false)
+ val gameFinishedEvent: LiveData
+ get() = _gameFinishedEvent
+
+ private lateinit var timer: CountDownTimer
+
+ private val _currentTime = MutableLiveData(DONE)
+
+ val currentTimeText: LiveData = Transformations.map(_currentTime) {
+ DateUtils.formatElapsedTime(it)
+ }
+
+ private val _buzzGameEvent = MutableLiveData()
+ val buzzGameEvent: LiveData
+ get() = _buzzGameEvent
+
+ init {
+ resetList()
+ nextWord()
+ configureTimer()
+ }
+
+ /**
+ * 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()) {
+ finishGame()
+ } else {
+ _word.value = wordList.removeAt(0)
+ }
+ }
+
+ /**
+ * Setup and start the game timer
+ */
+ private fun configureTimer() {
+ timer = object : CountDownTimer(COUNTDOWN_TIME, ONE_SECOND) {
+ override fun onTick(millisUntilFinished: Long) = gameTicking(millisUntilFinished)
+ override fun onFinish() = finishGame()
+ }.run { start() }
+ }
+
+ /**
+ * Game event fired when [timer] completed one cycle of [ONE_SECOND]
+ */
+ private fun gameTicking(millisUntilFinished: Long) {
+ (millisUntilFinished / ONE_SECOND).let { secondsUntilFinished ->
+ _currentTime.value = secondsUntilFinished
+ secondsUntilFinished.takeIf { it <= SECONDS_TO_FINISH }?.apply {
+ _buzzGameEvent.value = BuzzType.COUNTDOWN_PANIC
+ }
+ }
+ }
+
+ /**
+ * Game event fired when [timer] is [DONE] counting
+ */
+ private fun finishGame() {
+ _currentTime.value = DONE
+ _gameFinishedEvent.value = true
+ _buzzGameEvent.value = BuzzType.GAME_FINISHED
+ }
+
+ /**
+ * User interaction event fired when user touched SKIP button
+ */
+ fun onSkip() {
+ _score.value = score.value?.minus(1)
+ nextWord()
+ }
+
+ /**
+ * User interaction event fired when user touched GOT IT button
+ */
+ fun onCorrect() {
+ _buzzGameEvent.value = BuzzType.CORRECT
+ _score.value = score.value?.plus(1)
+ nextWord()
+ }
+
+ /**
+ * Navigation event fired when user completed navigating to Score screen
+ */
+ fun onGameFinishedNavigated() {
+ _gameFinishedEvent.value = false
+ }
+
+ override fun onCleared() {
+ super.onCleared()
+ timer.cancel()
+ }
+
+ companion object {
+ // 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
+
+ // This is the initial score of the game
+ const val INITIAL_SCORE = 0
+
+ // This is the number os seconds to finish the game
+ const val SECONDS_TO_FINISH = 10
+ }
+}
\ 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..74e024b7e 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
@@ -20,11 +20,10 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
-import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
+import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
-import com.example.android.guesstheword.R
import com.example.android.guesstheword.databinding.ScoreFragmentBinding
/**
@@ -32,29 +31,34 @@ import com.example.android.guesstheword.databinding.ScoreFragmentBinding
*/
class ScoreFragment : Fragment() {
+ private val arguments: ScoreFragmentArgs by navArgs()
+
+ private val viewModel: ScoreViewModel by viewModels { ScoreViewModelFactory(arguments.score) }
+
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
- ): View? {
-
- // Inflate view and obtain an instance of the binding class.
- val binding: ScoreFragmentBinding = DataBindingUtil.inflate(
- inflater,
- R.layout.score_fragment,
- container,
- false
- )
-
- // Get args using by navArgs property delegate
- val scoreFragmentArgs by navArgs()
- binding.scoreText.text = scoreFragmentArgs.score.toString()
- binding.playAgainButton.setOnClickListener { onPlayAgain() }
-
- return binding.root
+ ): View = ScoreFragmentBinding.inflate(inflater, container, false)
+ .apply {
+ viewModel = this@ScoreFragment.viewModel
+ lifecycleOwner = viewLifecycleOwner
+ }
+ .root
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ with(viewModel) {
+ playAgainGameEvent.observe(viewLifecycleOwner) { hasClicked ->
+ if (hasClicked) onPlayAgain()
+ }
+ }
}
private fun onPlayAgain() {
findNavController().navigate(ScoreFragmentDirections.actionRestart())
+ .also {
+ viewModel.navigatedToPlayAgain()
+ }
}
}
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..b8eaba46c
--- /dev/null
+++ b/app/src/main/java/com/example/android/guesstheword/screens/score/ScoreViewModel.kt
@@ -0,0 +1,20 @@
+package com.example.android.guesstheword.screens.score
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+
+class ScoreViewModel(score: Int) : ViewModel() {
+
+ private val _scoreText = MutableLiveData(score.toString())
+ val scoreText: LiveData
+ get() = _scoreText
+
+ private val _playAgainGameEvent = MutableLiveData(false)
+ val playAgainGameEvent: LiveData
+ get() = _playAgainGameEvent
+
+ fun playAgain() = _playAgainGameEvent.postValue(true)
+
+ fun navigatedToPlayAgain() = _playAgainGameEvent.postValue(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..bc20f2298
--- /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
+
+class ScoreViewModelFactory(private val score: Int) : ViewModelProvider.Factory {
+ @Suppress("UNCHECKED_CAST")
+ override fun create(modelClass: Class): T {
+ if (modelClass.isAssignableFrom(ScoreViewModel::class.java)) {
+ return ScoreViewModel(score) as T
+ }
+ throw IllegalArgumentException("Unknown ViewModel class ${modelClass::class.java}")
+ }
+}
\ 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..2f51e341d 100644
--- a/app/src/main/res/layout/game_fragment.xml
+++ b/app/src/main/res/layout/game_fragment.xml
@@ -16,13 +16,22 @@
+ xmlns:tools="http://schemas.android.com/tools"
+ >
+
+
+
+
+ tools:context=".screens.game.GameFragment"
+ >
+ app:layout_constraintVertical_chainStyle="packed"
+ />
+ tools:text=""Tuna""
+ />
+ tools:text="0:00"
+ />
+ tools:text="Current Score: 2"
+ />
+ app:layout_constraintTop_toTopOf="@+id/guideline"
+ />
+ app:layout_constraintGuide_end="96dp"
+ />
+ app:layout_constraintTop_toTopOf="@+id/guideline"
+ />
\ No newline at end of file
diff --git a/app/src/main/res/layout/score_fragment.xml b/app/src/main/res/layout/score_fragment.xml
index 7a1999319..15e574ffb 100644
--- a/app/src/main/res/layout/score_fragment.xml
+++ b/app/src/main/res/layout/score_fragment.xml
@@ -16,13 +16,22 @@
+ xmlns:tools="http://schemas.android.com/tools"
+ >
+
+
+
+
+ tools:context=".screens.score.ScoreFragment"
+ >
+ app:layout_constraintVertical_chainStyle="packed"
+ />
+ tools:text="40"
+ />
+ app:layout_constraintStart_toStartOf="parent"
+ />
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 23285629f..3cae14440 100644
--- a/build.gradle
+++ b/build.gradle
@@ -17,18 +17,16 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
- ext.kotlin_version = '1.3.72'
+ ext.kotlin_version = "1.4.21"
+ ext.nav_version = "2.3.2"
repositories {
google()
jcenter()
}
dependencies {
- classpath 'com.android.tools.build:gradle:4.0.1'
+ classpath 'com.android.tools.build:gradle:4.1.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
- classpath "android.arch.navigation:navigation-safe-args-gradle-plugin:1.0.0-rc02"
-
- // NOTE: Do not place your application dependencies here; they belong
- // in the individual module build.gradle files
+ classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
}
}
@@ -41,4 +39,9 @@ allprojects {
task clean(type: Delete) {
delete rootProject.buildDir
+}
+
+ext {
+ lifecycle_version = "2.2.0"
+ core_version = "1.3.2"
}
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 5e277db02..959ff6be2 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Thurs Aug 11 17:10:25 PDT 2020
+#Mon Dec 21 14:43:50 BRT 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip
diff --git a/settings.gradle b/settings.gradle
index 99e067489..83859e207 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -15,3 +15,4 @@
*/
include ':app'
+rootProject.name = "Guess the Word"
\ No newline at end of file