diff --git a/GuessTheWord-Starter/app/build.gradle b/GuessTheWord-Starter/app/build.gradle index b1848a1c4..710cec948 100755 --- a/GuessTheWord-Starter/app/build.gradle +++ b/GuessTheWord-Starter/app/build.gradle @@ -56,4 +56,7 @@ dependencies { // Navigation implementation "androidx.navigation:navigation-fragment-ktx:2.3.0" implementation "androidx.navigation:navigation-ui-ktx:2.3.0" + + //ViewModel + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0' } diff --git a/GuessTheWord-Starter/app/src/main/java/com/example/android/guesstheword/screens/game/GameFragment.kt b/GuessTheWord-Starter/app/src/main/java/com/example/android/guesstheword/screens/game/GameFragment.kt index 57d8b72ac..ad3f78b70 100755 --- a/GuessTheWord-Starter/app/src/main/java/com/example/android/guesstheword/screens/game/GameFragment.kt +++ b/GuessTheWord-Starter/app/src/main/java/com/example/android/guesstheword/screens/game/GameFragment.kt @@ -17,11 +17,15 @@ package com.example.android.guesstheword.screens.game import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.Toast import androidx.databinding.DataBindingUtil import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.NavHostFragment import com.example.android.guesstheword.R import com.example.android.guesstheword.databinding.GameFragmentBinding @@ -30,17 +34,10 @@ 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 binding: GameFragmentBinding + private lateinit var viewModel: GameViewModel + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { @@ -52,79 +49,49 @@ class GameFragment : Fragment() { false ) - resetList() - nextWord() + Log.i("GameFragment", "Called ViewModelProvider.get") + viewModel = ViewModelProvider(this).get(GameViewModel::class.java) binding.correctButton.setOnClickListener { onCorrect() } binding.skipButton.setOnClickListener { onSkip() } + binding.endGameButton.setOnClickListener { onEndGame() } 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() - } - - /** Methods for buttons presses **/ + /** Methods for button click handlers **/ private fun onSkip() { - score-- - nextWord() + viewModel.onSkip() + updateWordText() + updateScoreText() } private fun onCorrect() { - score++ - nextWord() - } - - /** - * Moves to the next word in the list - */ - private fun nextWord() { - if (!wordList.isEmpty()) { - //Select and remove a word from the list - word = wordList.removeAt(0) - } + viewModel.onCorrect() updateWordText() updateScoreText() } - /** Methods for updating the UI **/ - private fun updateWordText() { - binding.wordText.text = word + binding.wordText.text = viewModel.word } private fun updateScoreText() { - binding.scoreText.text = score.toString() + binding.scoreText.text = viewModel.score.toString() + } + + private fun onEndGame() { + gameFinished() + } + + private fun gameFinished() { + Toast.makeText(activity, "Game has just finished", Toast.LENGTH_SHORT).show() + val action = GameFragmentDirections.actionGameToScore() + action.score = viewModel.score + NavHostFragment.findNavController(this).navigate(action) } } diff --git a/GuessTheWord-Starter/app/src/main/java/com/example/android/guesstheword/screens/game/GameViewModel.kt b/GuessTheWord-Starter/app/src/main/java/com/example/android/guesstheword/screens/game/GameViewModel.kt new file mode 100644 index 000000000..1bca08627 --- /dev/null +++ b/GuessTheWord-Starter/app/src/main/java/com/example/android/guesstheword/screens/game/GameViewModel.kt @@ -0,0 +1,80 @@ +package com.example.android.guesstheword.screens.game + +import android.util.Log +import androidx.lifecycle.ViewModel + +class GameViewModel : ViewModel() { + + // The current word + var word = "" + + // The current score + var score = 0 + + // The list of words - the front of the list is the next word to guess + private lateinit var wordList: MutableList + + /** + * Resets the list of words and randomizes the order + */ + 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() + } + + + init { + resetList() + nextWord() + Log.i("GameViewModel", "GameViewModel created!") + } + + /** + * Moves to the next word in the list + */ + private fun nextWord() { + if (!wordList.isEmpty()) { + //Select and remove a word from the list + word = wordList.removeAt(0) + } + + } + + /** Methods for buttons presses **/ + fun onSkip() { + score-- + nextWord() + } + + fun onCorrect() { + score++ + nextWord() + } + + override fun onCleared() { + super.onCleared() + Log.i("GameViewModel", "GameViewModel destroyed!!") + } +} \ No newline at end of file diff --git a/GuessTheWord-Starter/app/src/main/java/com/example/android/guesstheword/screens/score/ScoreFragment.kt b/GuessTheWord-Starter/app/src/main/java/com/example/android/guesstheword/screens/score/ScoreFragment.kt index 4962b73ea..5dd914abe 100755 --- a/GuessTheWord-Starter/app/src/main/java/com/example/android/guesstheword/screens/score/ScoreFragment.kt +++ b/GuessTheWord-Starter/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.navArgs import com.example.android.guesstheword.R import com.example.android.guesstheword.databinding.ScoreFragmentBinding @@ -31,6 +32,9 @@ import com.example.android.guesstheword.databinding.ScoreFragmentBinding */ class ScoreFragment : Fragment() { + private lateinit var viewModel: ScoreViewModel + private lateinit var viewModelFactory: ScoreViewModelFactory + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -45,6 +49,10 @@ class ScoreFragment : Fragment() { false ) + viewModelFactory = ScoreViewModelFactory(ScoreFragmentArgs.fromBundle(requireArguments()).score) + viewModel = ViewModelProvider(this, viewModelFactory).get(ScoreViewModel::class.java) + + binding.scoreText.text = viewModel.score.toString() return binding.root } } diff --git a/GuessTheWord-Starter/app/src/main/java/com/example/android/guesstheword/screens/score/ScoreViewModel.kt b/GuessTheWord-Starter/app/src/main/java/com/example/android/guesstheword/screens/score/ScoreViewModel.kt new file mode 100644 index 000000000..0bed049b9 --- /dev/null +++ b/GuessTheWord-Starter/app/src/main/java/com/example/android/guesstheword/screens/score/ScoreViewModel.kt @@ -0,0 +1,13 @@ +package com.example.android.guesstheword.screens.score + +import android.util.Log +import androidx.lifecycle.ViewModel + +class ScoreViewModel(finalScore: Int) : ViewModel() { + // The final score + var score = finalScore + + init { + Log.i("ScoreViewModel", "Final score is $finalScore") + } +} \ No newline at end of file diff --git a/GuessTheWord-Starter/app/src/main/java/com/example/android/guesstheword/screens/score/ScoreViewModelFactory.kt b/GuessTheWord-Starter/app/src/main/java/com/example/android/guesstheword/screens/score/ScoreViewModelFactory.kt new file mode 100644 index 000000000..73d0c2d56 --- /dev/null +++ b/GuessTheWord-Starter/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 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/MarsRealEstate-Starter/app/build.gradle b/MarsRealEstate-Starter/app/build.gradle index 62948142f..2f05cef37 100644 --- a/MarsRealEstate-Starter/app/build.gradle +++ b/MarsRealEstate-Starter/app/build.gradle @@ -39,6 +39,15 @@ android { buildFeatures { dataBinding true } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } } dependencies { @@ -59,4 +68,15 @@ dependencies { // Core with Ktx implementation "androidx.core:core-ktx:$version_core" + + // Retrofit + implementation "com.squareup.retrofit2:retrofit:$version_retrofit" + implementation "com.squareup.retrofit2:converter-scalars:$version_retrofit" + + implementation "com.squareup.moshi:moshi-kotlin:$version_moshi" + implementation "com.squareup.retrofit2:retrofit:$version_retrofit" + implementation "com.squareup.retrofit2:converter-scalars:$version_retrofit" + implementation "com.squareup.retrofit2:converter-moshi:$version_retrofit" + + implementation "com.github.bumptech.glide:glide:$version_glide" } diff --git a/MarsRealEstate-Starter/app/src/main/AndroidManifest.xml b/MarsRealEstate-Starter/app/src/main/AndroidManifest.xml index 7f383b8ab..1fb8cfcfc 100644 --- a/MarsRealEstate-Starter/app/src/main/AndroidManifest.xml +++ b/MarsRealEstate-Starter/app/src/main/AndroidManifest.xml @@ -19,6 +19,8 @@ + + ?) { + val adapter = recyclerView.adapter as PhotoGridAdapter + adapter.submitList(data) +} + +@BindingAdapter("marsApiStatus") +fun bindStatus(statusImageView: ImageView, status: MarsApiStatus?) { + when (status) { + MarsApiStatus.LOADING -> { + statusImageView.visibility = View.VISIBLE + statusImageView.setImageResource(R.drawable.loading_animation) + } + MarsApiStatus.ERROR -> { + statusImageView.visibility = View.VISIBLE + statusImageView.setImageResource(R.drawable.ic_connection_error) + } + MarsApiStatus.DONE -> { + statusImageView.visibility = View.GONE + } + } +} \ No newline at end of file diff --git a/MarsRealEstate-Starter/app/src/main/java/com/example/android/marsrealestate/detail/DetailFragment.kt b/MarsRealEstate-Starter/app/src/main/java/com/example/android/marsrealestate/detail/DetailFragment.kt index fd1a972e2..00c518c65 100644 --- a/MarsRealEstate-Starter/app/src/main/java/com/example/android/marsrealestate/detail/DetailFragment.kt +++ b/MarsRealEstate-Starter/app/src/main/java/com/example/android/marsrealestate/detail/DetailFragment.kt @@ -21,19 +21,30 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider import com.example.android.marsrealestate.databinding.FragmentDetailBinding /** * This [Fragment] will show the detailed information about a selected piece of Mars real estate. */ class DetailFragment : Fragment() { - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle?): View? { + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { @Suppress("UNUSED_VARIABLE") val application = requireNotNull(activity).application val binding = FragmentDetailBinding.inflate(inflater) binding.lifecycleOwner = this + + val marsProperty = DetailFragmentArgs.fromBundle(arguments!!).selectedProperty + val viewModelFactory = DetailViewModelFactory(marsProperty, application) + binding.viewModel = ViewModelProvider( + this, viewModelFactory + ).get(DetailViewModel::class.java) + return binding.root } } \ No newline at end of file diff --git a/MarsRealEstate-Starter/app/src/main/java/com/example/android/marsrealestate/detail/DetailViewModel.kt b/MarsRealEstate-Starter/app/src/main/java/com/example/android/marsrealestate/detail/DetailViewModel.kt index 0f6020bab..cab6274f7 100644 --- a/MarsRealEstate-Starter/app/src/main/java/com/example/android/marsrealestate/detail/DetailViewModel.kt +++ b/MarsRealEstate-Starter/app/src/main/java/com/example/android/marsrealestate/detail/DetailViewModel.kt @@ -17,12 +17,38 @@ package com.example.android.marsrealestate.detail import android.app.Application -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.ViewModel +import androidx.lifecycle.* import com.example.android.marsrealestate.network.MarsProperty +import com.example.android.marsrealestate.R /** * The [ViewModel] that is associated with the [DetailFragment]. */ -class DetailViewModel(@Suppress("UNUSED_PARAMETER")marsProperty: MarsProperty, app: Application) : AndroidViewModel(app) { +class DetailViewModel(@Suppress("UNUSED_PARAMETER") marsProperty: MarsProperty, app: Application) : + AndroidViewModel(app) { + + private val _selectedProperty = MutableLiveData() + val selectedProperty: LiveData + get() = _selectedProperty + + val displayPropertyPrice = Transformations.map(selectedProperty) { + app.applicationContext.getString( + when (it.isRental) { + true -> R.string.display_price_monthly_rental + false -> R.string.display_price + }, it.price) + } + + val displayPropertyType = Transformations.map(selectedProperty) { + app.applicationContext.getString(R.string.display_type, + app.applicationContext.getString( + when (it.isRental) { + true -> R.string.type_rent + false -> R.string.type_sale + })) + } + + init { + _selectedProperty.value = marsProperty + } } diff --git a/MarsRealEstate-Starter/app/src/main/java/com/example/android/marsrealestate/network/MarsApiService.kt b/MarsRealEstate-Starter/app/src/main/java/com/example/android/marsrealestate/network/MarsApiService.kt index 1e71148cc..77fbfc704 100644 --- a/MarsRealEstate-Starter/app/src/main/java/com/example/android/marsrealestate/network/MarsApiService.kt +++ b/MarsRealEstate-Starter/app/src/main/java/com/example/android/marsrealestate/network/MarsApiService.kt @@ -17,4 +17,38 @@ package com.example.android.marsrealestate.network -private const val BASE_URL = "https://android-kotlin-fun-mars-server.appspot.com/" +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import retrofit2.Call +import retrofit2.Retrofit +import retrofit2.converter.moshi.MoshiConverterFactory +import retrofit2.http.GET +import retrofit2.http.Query + +private const val BASE_URL = "https://android-kotlin-fun-mars-server.appspot.com" + +private val moshi = Moshi.Builder() + .add(KotlinJsonAdapterFactory()) + .build() + +private val retrofit = Retrofit.Builder() + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .baseUrl(BASE_URL) + .build() + +interface MarsApiService { + @GET("/realestate") + suspend fun getProperties(@Query("filter") type: String): List +} + +object MarsApi { + val retrofitService: MarsApiService by lazy { + retrofit.create(MarsApiService::class.java) + } +} + +enum class MarsApiFilter(val value: String) { + SHOW_RENT("rent"), + SHOW_BUY("buy"), + SHOW_ALL("all") +} \ No newline at end of file diff --git a/MarsRealEstate-Starter/app/src/main/java/com/example/android/marsrealestate/network/MarsProperty.kt b/MarsRealEstate-Starter/app/src/main/java/com/example/android/marsrealestate/network/MarsProperty.kt index 761b84c00..8ea83ea3b 100644 --- a/MarsRealEstate-Starter/app/src/main/java/com/example/android/marsrealestate/network/MarsProperty.kt +++ b/MarsRealEstate-Starter/app/src/main/java/com/example/android/marsrealestate/network/MarsProperty.kt @@ -17,4 +17,17 @@ package com.example.android.marsrealestate.network -class MarsProperty() +import android.os.Parcelable +import com.squareup.moshi.Json +import kotlinx.android.parcel.Parcelize + +@Parcelize +data class MarsProperty( + val id: String, + @Json(name = "img_src") val imgSrcUrl: String, + val type: String, + val price: Double +): Parcelable { + val isRental + get() = type == "rent" +} diff --git a/MarsRealEstate-Starter/app/src/main/java/com/example/android/marsrealestate/overview/OverviewFragment.kt b/MarsRealEstate-Starter/app/src/main/java/com/example/android/marsrealestate/overview/OverviewFragment.kt index 5c059be6a..ef86548ad 100644 --- a/MarsRealEstate-Starter/app/src/main/java/com/example/android/marsrealestate/overview/OverviewFragment.kt +++ b/MarsRealEstate-Starter/app/src/main/java/com/example/android/marsrealestate/overview/OverviewFragment.kt @@ -20,9 +20,13 @@ package com.example.android.marsrealestate.overview import android.os.Bundle import android.view.* import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController import com.example.android.marsrealestate.R import com.example.android.marsrealestate.databinding.FragmentOverviewBinding +import com.example.android.marsrealestate.databinding.GridViewItemBinding +import com.example.android.marsrealestate.network.MarsApiFilter /** * This fragment shows the the status of the Mars real-estate web services transaction. @@ -49,7 +53,17 @@ class OverviewFragment : Fragment() { // Giving the binding access to the OverviewViewModel binding.viewModel = viewModel + binding.photosGrid.adapter = PhotoGridAdapter(PhotoGridAdapter.OnClickListener{ + viewModel.displayPropertyDetails(it) + }) + viewModel.navigateToSelectedProperty.observe(this, Observer { + if ( null != it ) { + this.findNavController().navigate( + OverviewFragmentDirections.actionShowDetail(it)) + viewModel.displayPropertyDetailsComplete() + } + }) setHasOptionsMenu(true) return binding.root } @@ -61,4 +75,15 @@ class OverviewFragment : Fragment() { inflater.inflate(R.menu.overflow_menu, menu) super.onCreateOptionsMenu(menu, inflater) } + + override fun onOptionsItemSelected(item: MenuItem?): Boolean { + viewModel.updateFilter( + when(item?.itemId){ + R.id.show_rent_menu -> MarsApiFilter.SHOW_RENT + R.id.show_buy_menu -> MarsApiFilter.SHOW_BUY + else -> MarsApiFilter.SHOW_ALL + } + ) + return super.onOptionsItemSelected(item) + } } diff --git a/MarsRealEstate-Starter/app/src/main/java/com/example/android/marsrealestate/overview/OverviewViewModel.kt b/MarsRealEstate-Starter/app/src/main/java/com/example/android/marsrealestate/overview/OverviewViewModel.kt index 79392efde..ea995583c 100644 --- a/MarsRealEstate-Starter/app/src/main/java/com/example/android/marsrealestate/overview/OverviewViewModel.kt +++ b/MarsRealEstate-Starter/app/src/main/java/com/example/android/marsrealestate/overview/OverviewViewModel.kt @@ -20,10 +20,20 @@ package com.example.android.marsrealestate.overview import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.android.marsrealestate.network.MarsApi +import com.example.android.marsrealestate.network.MarsApiFilter +import com.example.android.marsrealestate.network.MarsProperty +import kotlinx.coroutines.launch +import retrofit2.Call +import retrofit2.Response /** * The [ViewModel] that is attached to the [OverviewFragment]. */ + +enum class MarsApiStatus { LOADING, ERROR, DONE } + class OverviewViewModel : ViewModel() { // The internal MutableLiveData String that stores the most recent response @@ -33,17 +43,55 @@ class OverviewViewModel : ViewModel() { val response: LiveData get() = _response + private val _properties = MutableLiveData>() + + val properties: LiveData> + get() = _properties + + private val _status = MutableLiveData() + + val status: LiveData + get() = _status + + private val _navigateToSelectedProperty = MutableLiveData() + val navigateToSelectedProperty: LiveData + get() = _navigateToSelectedProperty + /** * Call getMarsRealEstateProperties() on init so we can display status immediately. */ init { - getMarsRealEstateProperties() + getMarsRealEstateProperties(MarsApiFilter.SHOW_ALL) + } + + fun displayPropertyDetails(marsProperty: MarsProperty){ + _navigateToSelectedProperty.value = marsProperty + } + + fun displayPropertyDetailsComplete(){ + _navigateToSelectedProperty.value = null } /** * Sets the value of the status LiveData to the Mars API status. */ - private fun getMarsRealEstateProperties() { - _response.value = "Set the Mars API Response here!" + private fun getMarsRealEstateProperties(filter: MarsApiFilter) { + viewModelScope.launch { + _status.value = MarsApiStatus.LOADING + try { + val listResult = MarsApi.retrofitService.getProperties(filter.value) + if (listResult.isNotEmpty()) { + _properties.value = listResult + _status.value = MarsApiStatus.DONE + } + } catch (e: Exception) { + _status.value = MarsApiStatus.ERROR + _properties.value = ArrayList() + } + } + } + + fun updateFilter(filter: MarsApiFilter){ + getMarsRealEstateProperties(filter) } } diff --git a/MarsRealEstate-Starter/app/src/main/java/com/example/android/marsrealestate/overview/PhotoGridAdapter.kt b/MarsRealEstate-Starter/app/src/main/java/com/example/android/marsrealestate/overview/PhotoGridAdapter.kt index 0a53a2019..c52ec702a 100644 --- a/MarsRealEstate-Starter/app/src/main/java/com/example/android/marsrealestate/overview/PhotoGridAdapter.kt +++ b/MarsRealEstate-Starter/app/src/main/java/com/example/android/marsrealestate/overview/PhotoGridAdapter.kt @@ -17,4 +17,57 @@ package com.example.android.marsrealestate.overview +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.example.android.marsrealestate.databinding.GridViewItemBinding +import com.example.android.marsrealestate.network.MarsProperty +class PhotoGridAdapter(private val onClickListener: OnClickListener) : ListAdapter(DiffCallback) { + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): MarsPropertyViewHolder { + return MarsPropertyViewHolder( + GridViewItemBinding.inflate( + LayoutInflater.from(parent.context) + ) + ) + } + + override fun onBindViewHolder(holder: MarsPropertyViewHolder, position: Int) { + val marsProperty = getItem(position) + holder.itemView.setOnClickListener { + onClickListener.onClick(marsProperty) + } + holder.bind(marsProperty) + } + + class MarsPropertyViewHolder( + private val binding: GridViewItemBinding + ) : RecyclerView.ViewHolder(binding.root) { + + fun bind(marsProperty: MarsProperty) { + binding.property = marsProperty + binding.executePendingBindings() + } + } + + companion object DiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: MarsProperty, newItem: MarsProperty): Boolean { + return oldItem === newItem + } + + override fun areContentsTheSame(oldItem: MarsProperty, newItem: MarsProperty): Boolean { + return oldItem.id == newItem.id + } + } + + class OnClickListener(val clickListener: (marsProperty: MarsProperty) -> Unit) { + fun onClick(marsProperty: MarsProperty) = clickListener(marsProperty) + } +} \ No newline at end of file diff --git a/MarsRealEstate-Starter/app/src/main/res/layout/fragment_detail.xml b/MarsRealEstate-Starter/app/src/main/res/layout/fragment_detail.xml index 002f892ac..815395157 100644 --- a/MarsRealEstate-Starter/app/src/main/res/layout/fragment_detail.xml +++ b/MarsRealEstate-Starter/app/src/main/res/layout/fragment_detail.xml @@ -21,6 +21,13 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> + + + + + - + app:spanCount="2" + tools:itemCount="16" + android:layout_width="0dp" + android:layout_height="0dp"/> \ No newline at end of file diff --git a/MarsRealEstate-Starter/app/src/main/res/layout/grid_view_item.xml b/MarsRealEstate-Starter/app/src/main/res/layout/grid_view_item.xml index b6a23a1a6..acca4eb63 100644 --- a/MarsRealEstate-Starter/app/src/main/res/layout/grid_view_item.xml +++ b/MarsRealEstate-Starter/app/src/main/res/layout/grid_view_item.xml @@ -1,31 +1,41 @@ - - - - + + + + + + - + android:layout_height="match_parent"> + + + + + + + + \ No newline at end of file diff --git a/MarsRealEstate-Starter/app/src/main/res/navigation/nav_graph.xml b/MarsRealEstate-Starter/app/src/main/res/navigation/nav_graph.xml index 0f4c7c627..8fce01966 100644 --- a/MarsRealEstate-Starter/app/src/main/res/navigation/nav_graph.xml +++ b/MarsRealEstate-Starter/app/src/main/res/navigation/nav_graph.xml @@ -38,6 +38,10 @@ android:name="com.example.android.marsrealestate.detail.DetailFragment" android:label="fragment_detail" tools:layout="@layout/fragment_detail"> + diff --git a/TrackMySleepQuality-Starter/app/build.gradle b/TrackMySleepQuality-Starter/app/build.gradle index 25ed8d70e..1dacb3cae 100755 --- a/TrackMySleepQuality-Starter/app/build.gradle +++ b/TrackMySleepQuality-Starter/app/build.gradle @@ -80,5 +80,10 @@ dependencies { testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test.ext:junit:1.1.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' + + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0" + + // Kotlin Extensions and Coroutines support for Room + implementation "androidx.room:room-ktx:$room_version" } diff --git a/TrackMySleepQuality-Starter/app/src/androidTest/java/com/example/android/trackmysleepquality/SleepDatabaseTest.kt b/TrackMySleepQuality-Starter/app/src/androidTest/java/com/example/android/trackmysleepquality/SleepDatabaseTest.kt index 1e5d85669..5e857eeb4 100755 --- a/TrackMySleepQuality-Starter/app/src/androidTest/java/com/example/android/trackmysleepquality/SleepDatabaseTest.kt +++ b/TrackMySleepQuality-Starter/app/src/androidTest/java/com/example/android/trackmysleepquality/SleepDatabaseTest.kt @@ -15,57 +15,57 @@ */ package com.example.android.trackmysleepquality -// -//import androidx.room.Room -//import androidx.test.ext.junit.runners.AndroidJUnit4 -//import androidx.test.platform.app.InstrumentationRegistry -//import com.example.android.trackmysleepquality.database.SleepDatabase -//import com.example.android.trackmysleepquality.database.SleepDatabaseDao -//import com.example.android.trackmysleepquality.database.SleepNight -//import org.junit.Assert.assertEquals -//import org.junit.After -//import org.junit.Before -//import org.junit.Test -//import org.junit.runner.RunWith -//import java.io.IOException -// -// -///** -// * This is not meant to be a full set of tests. For simplicity, most of your samples do not -// * include tests. However, when building the Room, it is helpful to make sure it works before -// * adding the UI. -// */ -// -//@RunWith(AndroidJUnit4::class) -//class SleepDatabaseTest { -// -// private lateinit var sleepDao: SleepDatabaseDao -// private lateinit var db: SleepDatabase -// -// @Before -// fun createDb() { -// val context = InstrumentationRegistry.getInstrumentation().targetContext -// // Using an in-memory database because the information stored here disappears when the -// // process is killed. -// db = Room.inMemoryDatabaseBuilder(context, SleepDatabase::class.java) -// // Allowing main thread queries, just for testing. -// .allowMainThreadQueries() -// .build() -// sleepDao = db.sleepDatabaseDao -// } -// -// @After -// @Throws(IOException::class) -// fun closeDb() { -// db.close() -// } -// -// @Test -// @Throws(Exception::class) -// fun insertAndGetNight() { -// val night = SleepNight() -// sleepDao.insert(night) -// val tonight = sleepDao.getTonight() -// assertEquals(tonight?.sleepQuality, -1) -// } -//} \ No newline at end of file + +import androidx.room.Room +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.example.android.trackmysleepquality.database.SleepDatabase +import com.example.android.trackmysleepquality.database.SleepDatabaseDao +import com.example.android.trackmysleepquality.database.SleepNight +import org.junit.Assert.assertEquals +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import java.io.IOException + + +/** + * This is not meant to be a full set of tests. For simplicity, most of your samples do not + * include tests. However, when building the Room, it is helpful to make sure it works before + * adding the UI. + */ + +@RunWith(AndroidJUnit4::class) +class SleepDatabaseTest { + + private lateinit var sleepDao: SleepDatabaseDao + private lateinit var db: SleepDatabase + + @Before + fun createDb() { + val context = InstrumentationRegistry.getInstrumentation().targetContext + // Using an in-memory database because the information stored here disappears when the + // process is killed. + db = Room.inMemoryDatabaseBuilder(context, SleepDatabase::class.java) + // Allowing main thread queries, just for testing. + .allowMainThreadQueries() + .build() + sleepDao = db.sleepDatabaseDao + } + + @After + @Throws(IOException::class) + fun closeDb() { + db.close() + } + + @Test + @Throws(Exception::class) + fun insertAndGetNight() { + val night = SleepNight() + sleepDao.insert(night) + val tonight = sleepDao.getTonight() + assertEquals(tonight?.sleepQuality, -1) + } +} \ No newline at end of file diff --git a/TrackMySleepQuality-Starter/app/src/main/java/com/example/android/trackmysleepquality/database/SleepDatabase.kt b/TrackMySleepQuality-Starter/app/src/main/java/com/example/android/trackmysleepquality/database/SleepDatabase.kt index f29a7d352..3be0ce071 100755 --- a/TrackMySleepQuality-Starter/app/src/main/java/com/example/android/trackmysleepquality/database/SleepDatabase.kt +++ b/TrackMySleepQuality-Starter/app/src/main/java/com/example/android/trackmysleepquality/database/SleepDatabase.kt @@ -15,3 +15,37 @@ */ package com.example.android.trackmysleepquality.database + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase + +@Database(entities = [SleepNight::class], version = 1, exportSchema = false) +abstract class SleepDatabase : RoomDatabase() { + abstract val sleepDatabaseDao: SleepDatabaseDao + + companion object { + @Volatile + private var INSTANCE: SleepDatabase? = null + + fun getInstance(context: Context): SleepDatabase { + synchronized(this) { + var instance = INSTANCE + + if (instance == null) { + instance = Room.databaseBuilder( + context.applicationContext, + SleepDatabase::class.java, + "sleep_history_database" + ) + .fallbackToDestructiveMigration() + .allowMainThreadQueries() + .build() + INSTANCE = instance + } + return instance + } + } + } +} diff --git a/TrackMySleepQuality-Starter/app/src/main/java/com/example/android/trackmysleepquality/database/SleepDatabaseDao.kt b/TrackMySleepQuality-Starter/app/src/main/java/com/example/android/trackmysleepquality/database/SleepDatabaseDao.kt index 6410761f3..ba3d29a2e 100755 --- a/TrackMySleepQuality-Starter/app/src/main/java/com/example/android/trackmysleepquality/database/SleepDatabaseDao.kt +++ b/TrackMySleepQuality-Starter/app/src/main/java/com/example/android/trackmysleepquality/database/SleepDatabaseDao.kt @@ -16,7 +16,29 @@ package com.example.android.trackmysleepquality.database +import androidx.lifecycle.LiveData import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Update @Dao -interface SleepDatabaseDao +interface SleepDatabaseDao { + @Insert + fun insert(night: SleepNight) + + @Update + fun update(night: SleepNight) + + @Query("SELECT * from daily_sleep_quality_table WHERE nightId = :key") + fun get(key: Long): SleepNight? + + @Query("DELETE FROM daily_sleep_quality_table") + fun clear() + + @Query("SELECT * FROM daily_sleep_quality_table ORDER BY nightId DESC LIMIT 1") + fun getTonight(): SleepNight? + + @Query("SELECT * FROM daily_sleep_quality_table ORDER BY nightId DESC") + fun getAllNights(): LiveData> +} diff --git a/TrackMySleepQuality-Starter/app/src/main/java/com/example/android/trackmysleepquality/database/SleepNight.kt b/TrackMySleepQuality-Starter/app/src/main/java/com/example/android/trackmysleepquality/database/SleepNight.kt index e80e4b70b..fa06e0a50 100755 --- a/TrackMySleepQuality-Starter/app/src/main/java/com/example/android/trackmysleepquality/database/SleepNight.kt +++ b/TrackMySleepQuality-Starter/app/src/main/java/com/example/android/trackmysleepquality/database/SleepNight.kt @@ -16,3 +16,18 @@ package com.example.android.trackmysleepquality.database +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "daily_sleep_quality_table") +data class SleepNight( + @PrimaryKey(autoGenerate = true) + var nightId: Long = 0L, + @ColumnInfo(name = "start_time_milli") + val startTimeMilli: Long = System.currentTimeMillis(), + @ColumnInfo(name = "end_time_milli") + var endTimeMilli: Long = startTimeMilli, + @ColumnInfo(name = "quality_rating") + var sleepQuality: Int = -1 +) \ No newline at end of file diff --git a/TrackMySleepQuality-Starter/app/src/main/java/com/example/android/trackmysleepquality/sleepquality/SleepQualityViewModel.kt b/TrackMySleepQuality-Starter/app/src/main/java/com/example/android/trackmysleepquality/sleepquality/SleepQualityViewModel.kt index e66449235..f44abb05f 100755 --- a/TrackMySleepQuality-Starter/app/src/main/java/com/example/android/trackmysleepquality/sleepquality/SleepQualityViewModel.kt +++ b/TrackMySleepQuality-Starter/app/src/main/java/com/example/android/trackmysleepquality/sleepquality/SleepQualityViewModel.kt @@ -15,3 +15,8 @@ */ package com.example.android.trackmysleepquality.sleepquality + +import androidx.lifecycle.ViewModel + +class SleepQualityViewModel : ViewModel() { +} \ No newline at end of file diff --git a/TrackMySleepQuality-Starter/app/src/main/java/com/example/android/trackmysleepquality/sleepquality/SleepQualityViewModelFactory.kt b/TrackMySleepQuality-Starter/app/src/main/java/com/example/android/trackmysleepquality/sleepquality/SleepQualityViewModelFactory.kt index e66449235..44d027b83 100755 --- a/TrackMySleepQuality-Starter/app/src/main/java/com/example/android/trackmysleepquality/sleepquality/SleepQualityViewModelFactory.kt +++ b/TrackMySleepQuality-Starter/app/src/main/java/com/example/android/trackmysleepquality/sleepquality/SleepQualityViewModelFactory.kt @@ -15,3 +15,16 @@ */ package com.example.android.trackmysleepquality.sleepquality + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider + +class SleepQualityViewModelFactory : ViewModelProvider.Factory { + + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(SleepQualityViewModel::class.java)) { + return SleepQualityViewModel() as T + } + throw IllegalArgumentException("Unknown viewModel class") + } +} \ No newline at end of file diff --git a/TrackMySleepQuality-Starter/app/src/main/java/com/example/android/trackmysleepquality/sleeptracker/SleepTrackerFragment.kt b/TrackMySleepQuality-Starter/app/src/main/java/com/example/android/trackmysleepquality/sleeptracker/SleepTrackerFragment.kt index d395f78e9..7e548b8ba 100755 --- a/TrackMySleepQuality-Starter/app/src/main/java/com/example/android/trackmysleepquality/sleeptracker/SleepTrackerFragment.kt +++ b/TrackMySleepQuality-Starter/app/src/main/java/com/example/android/trackmysleepquality/sleeptracker/SleepTrackerFragment.kt @@ -22,7 +22,9 @@ import android.view.View import android.view.ViewGroup import androidx.databinding.DataBindingUtil import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider import com.example.android.trackmysleepquality.R +import com.example.android.trackmysleepquality.database.SleepDatabase import com.example.android.trackmysleepquality.databinding.FragmentSleepTrackerBinding /** @@ -37,12 +39,29 @@ class SleepTrackerFragment : Fragment() { * * This function uses DataBindingUtil to inflate R.layout.fragment_sleep_quality. */ - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle?): View? { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { // Get a reference to the binding object and inflate the fragment views. val binding: FragmentSleepTrackerBinding = DataBindingUtil.inflate( - inflater, R.layout.fragment_sleep_tracker, container, false) + inflater, R.layout.fragment_sleep_tracker, container, false + ) + + val application = requireNotNull(this.activity).application + + val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao + + val viewModelFactory = SleepTrackerViewModelFactory(dataSource, application) + + val sleepTrackerViewModel = + ViewModelProvider( + this, viewModelFactory + ).get(SleepTrackerViewModel::class.java) + + binding.lifecycleOwner = this + binding.sleepTrackerViewModel = sleepTrackerViewModel return binding.root } diff --git a/TrackMySleepQuality-Starter/app/src/main/java/com/example/android/trackmysleepquality/sleeptracker/SleepTrackerViewModel.kt b/TrackMySleepQuality-Starter/app/src/main/java/com/example/android/trackmysleepquality/sleeptracker/SleepTrackerViewModel.kt index 77fe6e337..3d59770a5 100755 --- a/TrackMySleepQuality-Starter/app/src/main/java/com/example/android/trackmysleepquality/sleeptracker/SleepTrackerViewModel.kt +++ b/TrackMySleepQuality-Starter/app/src/main/java/com/example/android/trackmysleepquality/sleeptracker/SleepTrackerViewModel.kt @@ -18,13 +18,80 @@ package com.example.android.trackmysleepquality.sleeptracker import android.app.Application import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Transformations +import androidx.lifecycle.viewModelScope import com.example.android.trackmysleepquality.database.SleepDatabaseDao +import com.example.android.trackmysleepquality.database.SleepNight +import kotlinx.coroutines.launch /** * ViewModel for SleepTrackerFragment. */ class SleepTrackerViewModel( - val database: SleepDatabaseDao, - application: Application) : AndroidViewModel(application) { + val database: SleepDatabaseDao, + application: Application +) : AndroidViewModel(application) { + + private val nights = database.getAllNights() + + val nightsString = Transformations.map(nights) { nights -> + "${nights.toString()}"// formatNights(nights, application.resources) + } + + private var tonight = MutableLiveData() + + init { + initializeTonight() + } + + private fun initializeTonight() { + viewModelScope.launch { +// tonight.value = getTonightFromDatabase() + } + } + + private suspend fun getTonightFromDatabase(): SleepNight? { + var night: SleepNight? = database.getTonight() ?: return null + if (night?.endTimeMilli != night?.startTimeMilli) { + night = null + } + return night + } + + fun onStartTracking() { + viewModelScope.launch { + val newNight = SleepNight() + insert(newNight) + tonight.value = getTonightFromDatabase() + } + } + + private suspend fun insert(night: SleepNight) { + database.insert(night) + } + + fun onStopTracking() { + viewModelScope.launch { + val oldNight = tonight.value ?: return@launch + oldNight.endTimeMilli = System.currentTimeMillis() + update(oldNight) + } + } + + private suspend fun update(night: SleepNight) { + database.update(night) + } + + fun onClear() { + viewModelScope.launch { + clear() + tonight.value = null + } + } + + suspend fun clear() { + database.clear() + } } diff --git a/TrackMySleepQuality-Starter/app/src/main/res/layout/fragment_sleep_tracker.xml b/TrackMySleepQuality-Starter/app/src/main/res/layout/fragment_sleep_tracker.xml index a54e99e5d..e5515fe56 100755 --- a/TrackMySleepQuality-Starter/app/src/main/res/layout/fragment_sleep_tracker.xml +++ b/TrackMySleepQuality-Starter/app/src/main/res/layout/fragment_sleep_tracker.xml @@ -24,6 +24,9 @@ click handlers, and state variables. --> + @@ -49,14 +52,30 @@ which keeps it displayed and updated in the TextView whenever it changes. --> - + android:orientation="vertical"> + + + + + +