github.com/google-developer-training/android-kotlin-fundamentals-apps/tree/master/MarsRealEstateGrid
Add "for sale" images to the overview
Step 1: MarsProperty가 형식을 포함하도록 업데이트 하기
MarsProperty.kt
data class MarsProperty(
val id: String,
@Json(name = "img_src") val imgSrcUrl: String,
val type: String,
val price: Double) {
val isRental
get() = type == "rent"
}
Step 2: 그리드 아이템 레이아웃 업데이트 하기
grid_view_item.xml
<layout ...>
<data>
<import type="android.view.View" />
<variable
... />
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="170dp">
<ImageView
android:id="@+id/mars_image"
... />
<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:scaleType="fitCenter"
android:src="@drawable/ic_for_sale_outline"
android:visibility="@{property.rental ? View.GONE : View.VISIBLE}"
tools:src="@drawable/ic_for_sale_outline"/>
</FrameLayout>
</layout>
레이아웃 파일의 데이터 베인딩 구문 안에서 클래스의 컴포넌트를 사용하고 싶을 때 import를 사용한다.
Filter the results
Mars 웹 서비스는 타입이 rent이거나 buy인 값만 얻을 수 있게 해주는 filter라는 쿼리 매개변수/옵션이 있다.
https://android-kotlin-fun-mars-server.appspot.com/realestate?filter=buy
Step 1: Mars API 서비스 업데이트 하기
MarsApiService.kt
enum class MarsApiFilter(val value: String) {
SHOW_RENT("rent"),
SHOW_BUY("buy"),
SHOW_ALL("all") }
...
interface MarsApiService {
@GET("realestate")
suspend fun getProperties(@Query("filter") type: String): List<MarsProperty>
}
...
@Query는 getProperties() 메서드(그리고 Retrofit)에게 filter 옵션을 사용해 웹 서비스 요청을 하라고 알려준다. getProperties()가 호출될 때마다 요청 URL은 ?filter=type 부분을 포함한다. 이는 웹 서비스가 그 쿼리에 맞는 결과와 함께 응답하도록 지시한다.
Step 2: overview 뷰 모델 업데이트 하기
OverviewViewModel.kt
init {
getMarsRealEstateProperties(MarsApiFilter.SHOW_ALL)
}
private fun getMarsRealEstateProperties(filter: MarsApiFilter) {
viewModelScope.launch {
_status.value = MarsApiStatus.LOADING
try {
_properties.value = MarsApi.retrofitService.getProperties(filter.value)
...
} ...
}
}
fun updateFilter(filter: MarsApiFilter) {
getMarsRealEstateProperties(filter)
}
Step 3: 프래그먼트를 옵션 메뉴에 연결하기
OverviewFragment.kt
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 true
}
Create a detail page and set up navigation
Step 1: detail 뷰 모델을 생성하고 detail 레이아웃 업데이트 하기
DetailViewModel.kt
class DetailViewModel(marsProperty: MarsProperty, app: Application) : AndroidViewModel(app) {
private val _selectedProperty = MutableLiveData<MarsProperty>()
val selectedProperty: LiveData<MarsProperty>
get() = _selectedProperty
init {
_selectedProperty.value = marsProperty
}
}
fragment_detail.xml
<layout ...>
<data>
<variable
name="viewModel"
type="com.example.android.marsrealestate.detail.DetailViewModel" />
</data>
<ScrollView
...>
<androidx.constraintlayout.widget.ConstraintLayout
...>
<ImageView
android:id="@+id/main_photo_image"
...
app:imageUrl="@{viewModel.selectedProperty.imgSrcUrl}"
... />
Glide를 사용해 이미지를 불러오는 바인딩 어댑터는 모든 app:imageUrl 속성을 감지하므로 여기에서도 자동으로 사용된다.
Step 2: overview 뷰 모델에 내비게이션 정의하기
OverviewViewModel.kt
class OverviewViewModel : ViewModel() {
...
private val _navigateToSelectedProperty = MutableLiveData<MarsProperty>()
val navigateToSelectedProperty: LiveData<MarsProperty>
get() = _navigateToSelectedProperty
...
fun displayPropertyDetails(marsProperty: MarsProperty) {
_navigateToSelectedProperty.value = marsProperty
}
fun displayPropertyDetailsComplete() {
_navigateToSelectedProperty.value = null
}
}
Step 3: 그리드 어댑터와 프래그먼트에 클릭 리스너 설정하기
PhotoGridAdapter.kt
class PhotoGridAdapter( private val onClickListener: OnClickListener ) :
ListAdapter<MarsProperty,
PhotoGridAdapter.MarsPropertyViewHolder>(DiffCallback) {
...
override fun onBindViewHolder(holder: PhotoGridAdapter.MarsPropertyViewHolder, position: Int) {
val marsProperty = getItem(position)
holder.itemView.setOnClickListener {
onClickListener.onClick(marsProperty)
}
holder.bind(marsProperty)
}
...
class OnClickListener(val clickListener: (marsProperty:MarsProperty) -> Unit) {
fun onClick(marsProperty:MarsProperty) = clickListener(marsProperty)
}
}
OverviewFragment.kt
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
...
binding.photosGrid.adapter = PhotoGridAdapter(PhotoGridAdapter.OnClickListener {
viewModel.displayPropertyDetails(it)
})
...
}
Step 4: 내비게이션 그래프를 수정하고 MarsProperty를 포장이 가능하게(parcelable) 만들기
nav_graph.xml
<navigation ...>
...
<fragment
android:id="@+id/detailFragment"
...>
<argument
android:name="selectedProperty"
app:argType="com.example.android.marsrealestate.network.MarsProperty"
/>
</fragment>
</navigation>
Parcelable 인터페이스는 객체가 연속적이게해서 그 객체의 데이터가 프래그먼트와 액티비티 사이사이에 전달될 수 있다.
MarsProperty.kt
@Parcelize
data class MarsProperty(
val id: String,
@Json(name = "img_src") val imgSrcUrl: String,
val type: String,
val price: Double) : Parcelable {
...
}
@Parcelize는 이 클래스를 위해 Parcelable 인터페이스의 메서드를 자동으로 구현하려고 Kotlin Android 상속(extension)을 사용한다.
Step 5: 프래그먼트 연결하기
OverviewFragment.kt
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
...
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()
}
})
...
}
DetailFragment.kt
class DetailFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
...
val marsProperty = DetailFragmentArgs.fromBundle(arguments!!).selectedProperty
val viewModelFactory = DetailViewModelFactory(marsProperty, application)
binding.viewModel = ViewModelProvider(
this, viewModelFactory).get(DetailViewModel::class.java)
return binding.root
}
}
Create a more useful detail page
DetailViewModel.kt
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
}))
}
fragment_detail.xml
<TextView
android:id="@+id/property_type_text"
...
android:text="@{viewModel.displayPropertyType}"
... />
<TextView
android:id="@+id/price_value_text"
...
android:text="@{viewModel.displayPropertyPrice}"
... />
Homework
Answer these questions
Q1. 4 / Q2. 2
Connect to the internet quiz
1. 2 / 2. 3 / 3. 2 / 4. 1 / 5. 2 / 6. 4 / 7. 4 / 8. 1, 2
Retrofit call adapter가 뭔지 모르겠다..ㅠㅠ
'안드로이드 Android > Android Study Jams' 카테고리의 다른 글
9. Repository and workManager - WorkManager (0) | 2021.01.29 |
---|---|
9. Repository and workManager - Add a Repository (0) | 2021.01.29 |
8. Connect to the internet - Load and display images from the internet (0) | 2021.01.28 |
8. Connect to the internet - Get data from the internet (0) | 2021.01.28 |
7. Databases and RecyclerView - RecyclerView Fundamentals (0) | 2021.01.21 |