본문 바로가기

안드로이드 Android/Android Study Jams

8. Connect to the internet - Filter and create views with internet data

github.com/google-developer-training/android-kotlin-fundamentals-apps/tree/master/MarsRealEstateGrid

 

google-developer-training/android-kotlin-fundamentals-apps

android-kotlin-fundamentals-apps. Contribute to google-developer-training/android-kotlin-fundamentals-apps development by creating an account on GitHub.

github.com

 

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가 뭔지 모르겠다..ㅠㅠ