Android Kotlin Fundamentals Course 코드랩 하면서 노트. Android Kotlin Fundamentals and detail views with internet data 부터.

Add "for sale" images to the overview

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

Update grid_view_item.xml.

<layout ...>
  <FrameLayout
    android:layout_width="match_parent"
    android:layout_height="170dp">
    <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}"
      tools:src="@tools:sample/backgrounds/scenic" />
    <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>
  <data>
    <import type="android.view.View" />
    <variable
      name="property"
      type="com.example.android.marsrealestate.network.MarsProperty" />
  </data>
</layout>

Filter the results

The URL will be:

// https://android-kotlin-fun-mars-server.appspot.com/realestate?filter=buy

Update the Mars API service.

// 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>
}

Update the view model.

// OverviewViewModel.kt
class OverviewViewModel : ViewModel() {
  // ...
  init {
    getMarsRealEstateProperties(MarsApiFilter.SHOW_ALL)
  }

  fun updateFilter(filter: MarsApiFilter) {
    getMarsRealEstateProperties(filter)
  }

  private fun getMarsRealEstateProperties(filter: MarsApiFilter) {
    viewModelScope.launch {
      _status.value = MarsApiStatus.LOADING
      try {
        _properties.value = MarsApi.retrofitService.getProperties(filter.value)
        _status.value = MarsApiStatus.DONE

      } catch (e: Exception) {
        _status.value = MarsApiStatus.ERROR
        _properties.value = ArrayList()
      }
    }
  }
}
<!-- overflow_menu.xml -->
<menu ...>
  <item
    android:id="@+id/show_all_menu"
    android:title="@string/show_all" />
  <item
    android:id="@+id/show_rent_menu"
    android:title="@string/show_rent" />
  <item
    android:id="@+id/show_buy_menu"
    android:title="@string/show_buy" />
</menu>

Then, update the view model when user choose the menu.

// OverviewFragment.kt
class OverviewFragment : Fragment() {
  // ...
  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 with navigation

When the user taps the tile, pass the data to the detail page.

// 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
  }
}

Update the fragment_detail.xml.

<ImageView
  android:id="@+id/main_photo_image"
  ...
  app:imageUrl="@{viewModel.selectedProperty.imgSrcUrl}" />

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

Then, add navigation with passing property.

// 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
  }
}

Add click listeners in the grid adapter and fragment.

// PhotoGridAdapter.kt
class PhotoGridAdapter(private val onClickListener: OnClickListener) : ListAdapter<MarsProperty, PhotoGridAdapter.MarsPropertyViewHolder>(DiffCallback) {
  // ...
  override fun onBindViewHolder(holder: 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)
  }
}

Then, update the OverviewFragment.kt.

binding.photosGrid.adapter = PhotoGridAdapter(PhotoGridAdapter.OnClickListener {
    viewModel.displayPropertyDetails(it)
})

MarsProperty is unable to pass through the navigation yet. The class need to be Parcelable via @parcelize.

Open MarsProperty.kt and add @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"
}

Add argument at detail fragment in nav_graph.xml.

<fragment
  android:id="@+id/detailFragment"
  ...>
  <argument
    android:name="selectedProperty"
    app:argType="com.example.android.marsrealestate.network.MarsProperty"
    />
</fragment>

Finally, add an observer to navigateToSelectedProperty.

//OverviewFragment.kt
// in `onCreateView()`
viewModel.navigateToSelectedProperty.observe(this, Observer {
  if (null != it) {
    this.findNavController()
      .navigate(OverviewFragmentDirections.actionShowDetail(it))
    viewModel.displayPropertyDetailsComplete()
  }
})

Add initial logic for the detail fragment.

// DetailFragment.kt
class DetailFragment : Fragment() {
  override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
      savedInstanceState: Bundle?): View? {
    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
  }
}

To update the detail page, add these string resources in strings.xml.

<string name="type_rent">Rent</string>
<string name="type_sale">Sale</string>
<string name="display_type">For %s</string>
<string name="display_price_monthly_rental">$%,.0f/month</string>
<string name="display_price">$%,.0f</string>

Then, add Transformations in the view model.

// DetailViewModel.kt
// In the detail viewmodel class
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(
    when (it.isRental) {
      true -> R.string.type_rent
      false -> R.string.type_sale
    })
}

Add two textviews on the detail layout.

<TextView
  android:id="@+id/property_type_text"
  ...
  android:text="@{viewModel.displayPropertyType}"
  tools:text="To Rent" />

<TextView
  android:id="@+id/price_value_text"
  ...
  android:text="@{viewModel.displayPropertyPrice}"
  tools:text="$100,000" />

다음 챕터: Android Kotlin Fundamentals

색상을 바꿔요

눈에 편한 색상을 골라보세요 :)

Darkreader 플러그인으로 선택한 색상이 제대로 표시되지 않을 수 있습니다.