[Android Kotlin 기초] 7-7. Interact with RecyclerView items

Badge 7-7. Interact with RecyclerView items

Task : Get the starter code and inspect the changes to the app

Step1. 먼저 준비된 앱을 다운로드 받습니다. 수면 세부 정보를 기입하기 위한 앱입니다.

images/Untitled.png

Step2. 수면 상세 화면을 위한 코드를 훑어보기

이 codelab 에서는 취침 시간을 위한 Click Handler를 구현합니다.

클릭하면 앱이 특정 수면에 대한 세부 정보를 보여주는 fragement로 이동합니다. 시작 코드에는 이에 대한 fragmentnavGraph가 이미 포함되어 있습니다.

우선, 스타터 프로젝트를 살펴보겠습니다. 스타터 프로젝트는 다음과 같은 구조로 되어 있습니다.

images/Untitled%201.png

패키지를 보면,

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {

				**/** DataBinding **/**
        // Get a reference to the binding object and inflate the fragment views.
        val binding: FragmentSleepDetailBinding = DataBindingUtil.inflate(
                inflater, R.layout.fragment_sleep_detail, container, false)

				**/** Bundle **/**
        val application = requireNotNull(this.activity).application
        val arguments = SleepDetailFragmentArgs.fromBundle(arguments)

				**/** Database **/**
        // Create an instance of the ViewModel Factory.
        val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
        val viewModelFactory = SleepDetailViewModelFactory(arguments.sleepNightKey, dataSource)

				**/** ViewModel : link with database **/**
        // Get a reference to the ViewModel associated with this fragment.
        val sleepDetailViewModel =
                ViewModelProvider(
                        this, viewModelFactory).get(SleepDetailViewModel::class.java)

				**/** Databinding link with viewmodel **/**
        // To use the View Model with data binding, you have to explicitly
        // give the binding object a reference to it.
        binding.sleepDetailViewModel = sleepDetailViewModel

        binding.setLifecycleOwner(this)

				**/** Navigation **/**
        // Add an Observer to the state variable for Navigating when a Quality icon is tapped.
        sleepDetailViewModel.navigateToSleepTracker.observe(viewLifecycleOwner, Observer {
            if (it == true) { // Observed state is true.
                this.findNavController().navigate(
                        SleepDetailFragmentDirections.actionSleepDetailFragmentToSleepTrackerFragment())
                // Reset state to make sure we only navigate once, even if the device
                // has a configuration change.
                sleepDetailViewModel.doneNavigating()
            }
        })

        return binding.root
    }

마치 다음과 같은 구조로 fragment를 설계하고 있습니다!

images/Untitled%202.png

ViewModel을 살펴보면 Database에서 LiveData를 가져오는 것을 알 수 있죠.

class SleepDetailViewModel(
        private val sleepNightKey: Long = 0L,
        dataSource: SleepDatabaseDao) : ViewModel() {

    /**
     * Hold a reference to SleepDatabase via its SleepDatabaseDao.
     */
    val database = dataSource

    private val night: LiveData<SleepNight>

    fun getNight() = night

    init {
        night=database.getNightWithId(sleepNightKey)
    }

		/**
     * Variable that tells the fragment whether it should navigate to [SleepTrackerFragment].
     *
     * This is `private` because we don't want to expose the ability to set [MutableLiveData] to
     * the [Fragment]
     */
    private val _navigateToSleepTracker = MutableLiveData<Boolean?>()

    /**
     * When true immediately navigate back to the [SleepTrackerFragment]
     */
    val navigateToSleepTracker: LiveData<Boolean?>
        get() = _navigateToSleepTracker

    /**
     * Call this immediately after navigating to [SleepTrackerFragment]
     */
    fun doneNavigating() {
        _navigateToSleepTracker.value = null
    }

    fun onClose() {
        _navigateToSleepTracker.value = true
    }
}
@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)

ViewModelFactory가 하는 일도 잠깐 살펴봅시다. 뷰모델을 생성해주는데, Key와 데이터소스를 넣어주고 있습니다.

class SleepDetailViewModelFactory(
        private val sleepNightKey: Long,
        private val dataSource: SleepDatabaseDao) : ViewModelProvider.Factory {
    @Suppress("unchecked_cast")
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(SleepDetailViewModel::class.java)) {
            return SleepDetailViewModel(sleepNightKey, dataSource) as T
        }
        throw IllegalArgumentException("Unknown ViewModel class")
    }
}

xml 부분도 데이터바인딩을 잘 해주고 있습니다.

<data>
    <variable
        name="sleepDetailViewModel"
        type="com.example.android.trackmysleepquality.sleepdetail.SleepDetailViewModel" />
</data>

Task : Make item clickable

자 이제 이 프로젝트에서 RecyclerView에 있는 Item에 대한 click이 가능하도록 해봅시다. RecyclerView는 앞서 살펴봤던 Fragment가 아닌 SleepTrackerFragment 에 있습니다.

Recycler에서 하나의 Item 을 가장 잘 나타내고 있는 것은 ViewHolder 입니다. 클릭을 들을 수 있는 좋은 장소이지만 처리하기에는 적합한 장소가 아닙니다.

Step1. Adapter에서 Click Listener

  1. SleepNightAdapter.kt 에서 리스너를 생성합니다.
class SleepNightListener(val clickListener: (sleepId: Long) -> Unit) {
   fun onClick(night: SleepNight) = clickListener(night.nightId)
}
  1. list_item_sleep_night.xml 에서 clicklistener를 바인딩 시킵니다.
<data>
		<variable
        name="sleep"
        type="com.example.android.trackmysleepquality.database.SleepNight"/>
    <variable
        name="clickListener"
        type="com.example.android.trackmysleepquality.sleeptracker.SleepNightListener" />
</data>

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:onClick="@{() -> clickListener.onClick(sleep)}"
    >

람다 표현식을 쓰면 굳이 onClick(view : View) 가 아니여도 함수를 넣을 수 있습니다. 아이템도 함께 바인딩되어 있기 때문에 어떤 항목이 click되었는지 쉽게 알 수 있습니다.

Step2. ViewModel에 ClickListener 전달

  1. 어뎁터 생성시 clickListener를 등록할 수 있도록 생성자에 넣어줍니다.
class SleepNightAdapter(val clickListener: SleepNightListener):
       ListAdapter<SleepNight, SleepNightAdapter.ViewHolder>(SleepNightDiffCallback()) {
  1. viewholder에서 데이터와 clicklistener를 바인딩시킵니다. 당연히 holder에서도 실제 바인딩하는 쪽에 연결시켜줘야겠죠?
// onBindViewHolder
holder.bind(getItem(position)!!, clickListener)

// ViewHolder
fun bind(item: SleepNight, clickListener: SleepNightListener) {
            binding.sleep = item
            binding.clickListener = clickListener
            binding.executePendingBindings()
        }

  1. Adapter 생성시 clicklistener를 넣어봅시다.
// SleepTrackerFragment
val adapter = SleepNightAdapter(SleepNightListener { nightId ->
            Toast.makeText(context, "${nightId}", Toast.LENGTH_LONG).show()
        })

Task : Handle item clicks

저희는 ViewModel에서 click에 대한 처리 로직을 담당하기로 했었죠. ViewModel에서 onclick을 처리해봅시다. 물론 그 값을 프로퍼티로 관리해야죠.

private val _navigateToSleepDetail = MutableLiveData<Long>()
val navigateToSleepDetail
    get() = _navigateToSleepDetail
fun onSleepNightClicked(id: Long) {
    _navigateToSleepDetail.value = id
}

기본 값 처리도 해봅시다.

fun onSleepDetailNavigated() {
    _navigateToSleepDetail.value = null
}

이제 fragment에서 adapter를 생성할때 이 메소드를 통해서 정의하면 되겠죠? Toast로 처리했던 부분을 변경시켜 줍시다.

val adapter = SleepNightAdapter(SleepNightListener { nightId ->
    sleepTrackerViewModel.onSleepNightClicked(nightId)
})

이제 아이템 뷰가 클릭되면, ViewModel의 메소드가 호출되고 값을 LiveData에 저장합니다. 이제 LiveData의 값이 변경될때의 처리만 해주면 끝나게 되겠죠!

우리 모두가 알고 있듯이 observe 함수는 이 일을 훌륭하게 해냅니다 😀

sleepTrackerViewModel.navigateToSleepDetail.observe(viewLifecycleOwner, Observer { night ->
    night?.let {
        this.findNavController().navigate(
                SleepTrackerFragmentDirections
                        .actionSleepTrackerFragmentToSleepDetailFragment(night))
        sleepTrackerViewModel.onSleepDetailNavigated()
    }
})

Answer these questions

Question 1

Assume that your app contains a RecyclerView that displays items in a shopping list. Your app also defines a click-listener class: 다음과 같은 클릭 리스너가 구현되어 있습니다.

class ShoppingListItemListener(val clickListener: (itemId: Long) -> Unit) {    
	fun onClick(cartItem: CartItem) = clickListener(cartItem.itemId)
}

How do you make the ShoppingListItemListener available to data binding? Select one.

데이터 바인딩으로 ShoppingListItemListener를 사용하도록 만들려면 어떻게 만들래요?

fun onBinding (cartItem: CartItem) {dataBindingEnable(true)}
fun onClick(cartItem: CartItem) = {     
	clickListener(cartItem.itemId)    
	dataBindingEnable(true)
}

Question 2

Where do you add the android:onClick attribute to make items in a RecyclerView respond to clicks? Select all that apply.

RecyclerView 의 항목이 클릭에 반응하기 위해서 android:onClick 속성을 어디에 추가해야합니까?