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" + />