Skip to content
This repository was archived by the owner on Jan 11, 2024. It is now read-only.

[코드랩] Feature internet filter #88

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions GuessTheWord-Starter/app/build.gradle
Original file line number Diff line number Diff line change
@@ -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'
}
Original file line number Diff line number Diff line change
@@ -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<String>

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)
}
}
Original file line number Diff line number Diff line change
@@ -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<String>

/**
* 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!!")
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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")
}
}
Original file line number Diff line number Diff line change
@@ -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 <T : ViewModel?> create(modelClass: Class<T>): T {
if(modelClass.isAssignableFrom(ScoreViewModel::class.java)) {
return ScoreViewModel(finalScore) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
20 changes: 20 additions & 0 deletions MarsRealEstate-Starter/app/build.gradle
Original file line number Diff line number Diff line change
@@ -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"
}
2 changes: 2 additions & 0 deletions MarsRealEstate-Starter/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -19,6 +19,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.marsrealestate">

<uses-permission android:name="android.permission.INTERNET" />

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
Original file line number Diff line number Diff line change
@@ -17,3 +17,54 @@

package com.example.android.marsrealestate

import android.view.View
import android.widget.ImageView
import androidx.core.net.toUri
import androidx.databinding.BindingAdapter
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.example.android.marsrealestate.network.MarsProperty
import com.example.android.marsrealestate.overview.MarsApiStatus
import com.example.android.marsrealestate.overview.PhotoGridAdapter

@BindingAdapter("imageUrl")
fun ImageView.bindImage(imgUrl: String?) {
imgUrl?.let {
val imgUri = it.toUri().buildUpon().scheme("https").build()
Glide.with(this.context)
.load(imgUri)
.apply(
RequestOptions()
.placeholder(R.drawable.loading_animation)
.error(R.drawable.ic_broken_image)
)
.into(this)
}
}

/**
* When there is no Mars property data (data is null), hide the [RecyclerView], otherwise show it.
*/
@BindingAdapter("listData")
fun bindRecyclerView(recyclerView: RecyclerView, data: List<MarsProperty>?) {
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
}
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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<MarsProperty>()
val selectedProperty: LiveData<MarsProperty>
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
}
}
Original file line number Diff line number Diff line change
@@ -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<MarsProperty>
}

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")
}
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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<String>
get() = _response

private val _properties = MutableLiveData<List<MarsProperty>>()

val properties: LiveData<List<MarsProperty>>
get() = _properties

private val _status = MutableLiveData<MarsApiStatus>()

val status: LiveData<MarsApiStatus>
get() = _status

private val _navigateToSelectedProperty = MutableLiveData<MarsProperty>()
val navigateToSelectedProperty: LiveData<MarsProperty>
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)
}
}
Original file line number Diff line number Diff line change
@@ -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<MarsProperty,
PhotoGridAdapter.MarsPropertyViewHolder>(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<MarsProperty>() {
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)
}
}
10 changes: 10 additions & 0 deletions MarsRealEstate-Starter/app/src/main/res/layout/fragment_detail.xml
Original file line number Diff line number Diff line change
@@ -21,6 +21,13 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">

<data>

<variable
name="viewModel"
type="com.example.android.marsrealestate.detail.DetailViewModel" />
</data>

<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -36,6 +43,7 @@
android:layout_width="0dp"
android:layout_height="266dp"
android:scaleType="centerCrop"
app:imageUrl="@{viewModel.selectedProperty.imgSrcUrl}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
@@ -46,6 +54,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@{viewModel.displayPropertyType}"
android:textColor="#de000000"
android:textSize="39sp"
app:layout_constraintStart_toStartOf="parent"
@@ -57,6 +66,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@{viewModel.displayPropertyPrice}"
android:textColor="#de000000"
android:textSize="20sp"
app:layout_constraintStart_toStartOf="parent"
Original file line number Diff line number Diff line change
@@ -31,14 +31,21 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.android.marsrealestate.MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{viewModel.response}"

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/photos_grid"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
android:padding="6dp"
app:listData="@{viewModel.properties}"
android:clipToPadding="false"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:spanCount="2"
tools:itemCount="16"
android:layout_width="0dp"
android:layout_height="0dp"/>

</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
62 changes: 36 additions & 26 deletions MarsRealEstate-Starter/app/src/main/res/layout/grid_view_item.xml
Original file line number Diff line number Diff line change
@@ -1,31 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>

<!--
~ Copyright 2019, The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
~
-->

<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<ImageView
android:id="@+id/mars_image"

<data>
<import type="android.view.View" />

<variable
name="property"
type="com.example.android.marsrealestate.network.MarsProperty" />
</data>

<FrameLayout
android:layout_width="match_parent"
android:layout_height="170dp"
android:scaleType="centerCrop"
android:adjustViewBounds="true"
android:padding="2dp"
tools:src="@tools:sample/backgrounds/scenic"/>
</layout>
android:layout_height="match_parent">

<ImageView
android:id="@+id/mars_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:adjustViewBounds="true"
android:padding="2dp"
android:scaleType="centerCrop"
app:imageUrl="@{property.imgSrcUrl}" />

<ImageView
android:id="@+id/mars_property_type"
android:layout_width="wrap_content"
android:layout_height="45dp"
android:layout_gravity="bottom|end"
android:adjustViewBounds="true"
android:padding="5dp"
android:visibility="@{property.rental ? View.GONE : View.VISIBLE}"
android:scaleType="fitCenter"
android:src="@drawable/ic_for_sale_outline"
tools:src="@drawable/ic_for_sale_outline" />

</FrameLayout>

</layout>
Original file line number Diff line number Diff line change
@@ -38,6 +38,10 @@
android:name="com.example.android.marsrealestate.detail.DetailFragment"
android:label="fragment_detail"
tools:layout="@layout/fragment_detail">
<argument
android:name="selectedProperty"
app:argType="com.example.android.marsrealestate.network.MarsProperty"
/>
</fragment>

</navigation>
5 changes: 5 additions & 0 deletions TrackMySleepQuality-Starter/app/build.gradle
Original file line number Diff line number Diff line change
@@ -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"
}

Original file line number Diff line number Diff line change
@@ -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)
// }
//}

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)
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<List<SleepNight>>
}
Original file line number Diff line number Diff line change
@@ -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
)
Original file line number Diff line number Diff line change
@@ -15,3 +15,8 @@
*/

package com.example.android.trackmysleepquality.sleepquality

import androidx.lifecycle.ViewModel

class SleepQualityViewModel : ViewModel() {
}
Original file line number Diff line number Diff line change
@@ -15,3 +15,16 @@
*/

package com.example.android.trackmysleepquality.sleepquality

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider

class SleepQualityViewModelFactory : ViewModelProvider.Factory {

override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(SleepQualityViewModel::class.java)) {
return SleepQualityViewModel() as T
}
throw IllegalArgumentException("Unknown viewModel class")
}
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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<SleepNight?>()

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()
}
}

Original file line number Diff line number Diff line change
@@ -24,6 +24,9 @@
click handlers, and state variables. -->
<data>

<variable
name="sleepTrackerViewModel"
type="com.example.android.trackmysleepquality.sleeptracker.SleepTrackerViewModel" />
</data>

<!-- Start of the visible fragment layout using ConstraintLayout -->
@@ -49,14 +52,30 @@
which keeps it displayed and updated in the TextView
whenever it changes. -->

<TextView
android:id="@+id/textview"
android:layout_width="match_parent"
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin"
android:layout_marginTop="@dimen/margin"
android:layout_marginEnd="@dimen/margin"
android:text="@string/how_was_hour_sleep" />
android:orientation="vertical">

<TextView
android:id="@+id/textview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin"
android:layout_marginTop="@dimen/margin"
android:layout_marginEnd="@dimen/margin"
android:text="@string/how_was_hour_sleep" />

<TextView
android:id="@+id/format_Nights"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin"
android:layout_marginTop="@dimen/margin"
android:layout_marginEnd="@dimen/margin"
android:text="@{sleepTrackerViewModel.nightsString}" />

</androidx.appcompat.widget.LinearLayoutCompat>
</ScrollView>

<!-- With data binding and LiveData, we can track the buttons' visibility states
@@ -69,6 +88,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin"
android:onClick="@{()->sleepTrackerViewModel.onStartTracking()}"
android:text="@string/start"
app:layout_constraintBaseline_toBaselineOf="@id/stop_button"
app:layout_constraintEnd_toStartOf="@+id/stop_button"
@@ -82,6 +102,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin"
android:layout_marginEnd="@dimen/margin"
android:onClick="@{() -> sleepTrackerViewModel.onStopTracking()}"
android:text="@string/stop"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/start_button"
@@ -95,10 +116,12 @@
android:layout_marginStart="@dimen/margin"
android:layout_marginEnd="@dimen/margin"
android:layout_marginBottom="@dimen/margin"
android:onClick="@{() -> sleepTrackerViewModel.onClear()}"
android:text="@string/clear"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

</layout>