반응형

이번 게시물은 지난 게시물에서 배웠던 ViewModel을 RecyclerView에 적용해보았다.

지난번엔 단방향 바인딩을 사용했는데, 이번에는 양방향 바인딩을 사용하기 위해 EditText를 사용했다.

아래 게시물 내용들을 보고 오면 이해가 빠르다.

 

[안드로이드 스튜디오 독학#46] ViewModelExample

이번 게시물은 이론으로만 배웠던 ViewModel을 실제로 적용해서 0~100사이의 숫자 값을 넣을 수 있는 어플을 만들었다. 리모콘의 볼륨 조절같은 기능이라고 생각하면 될 것 같다. 이번 예제를 따라

seminzzang.tistory.com

 

[안드로이드 스튜디오 정리#4] RecyclerView

이 게시물은 다음 링크를 참조하여 학습했습니다. 안드로이드 리사이클러뷰 기본 사용법. (Android RecyclerView) 1. 안드로이드 리사이클러뷰(RecyclerView) 리사이클러뷰(RecyclerView)는, "많은 수의 데이터

seminzzang.tistory.com

 

[안드로이드 스튜디오 독학#18] Custom RecyclerView

이 게시물은 다음 링크를 참조하여 학습했습니다. 안드로이드 리사이클러뷰 기본 사용법. (Android RecyclerView) 1. 안드로이드 리사이클러뷰(RecyclerView) 리사이클러뷰(RecyclerView)는, "많은 수의 데이터

seminzzang.tistory.com

이번 예제를 만들면서 RecyclerView를 배울 때에 대해 다시 생각해보게 되었다.

RecyclerView를 처음 커스텀할 때 이해하는데 정말 힘들었는데, 그때 이해를 다 하고 넘어간게 이번에 DataBinding을 적용하는데 큰 도움이 된 것 같다.

기본적인 이론은 다 안다고 가정하고 설명된 글인 점을 참고해줬으면 좋겠다.

 

1. activity_main

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<?xml version="1.0" encoding="utf-8"?>
<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">
 
    <data>
        <variable
            name="mainViewModel"
            type="com.example.recyclerviewexamplekt.MainViewModel" />
    </data>
 
 
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity"
        android:orientation="vertical">
 
 
        <EditText
            android:id="@+id/et_name"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_margin="10dp"
            android:hint="name"
            android:text="@={mainViewModel.input_name}"/>
 
        <EditText
            android:id="@+id/et_age"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_margin="10dp"
            android:hint="age"
            android:text="@={mainViewModel.input_age}"/>
 
        <Button
            android:id="@+id/btn_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="입력"
            android:layout_margin="10dp"
            android:onClick="@{() -> mainViewModel.buttonClick()}"
            android:layout_gravity="center"/>
 
        <androidx.recyclerview.widget.RecyclerView
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
            android:id="@+id/rv_test"
            android:layout_width="wrap_content"
            android:layout_height="0dp"
            android:layout_weight="1"/>
 
    </LinearLayout>
</layout>
cs

지난번 게시물과 비슷하게 데이터 바인딩을 위해 레이아웃을 조금 수정해주었다.

먼저 눈여겨볼 부분은 EditText에서 android:text 속성이다.

기존에 TextView에서 바인딩한 것과 다르게 "@={ }" 형태로 되어있는데, 이는 양방향 바인딩을 한다는 의미이다.

그 다음으로는 RecyclerView에서 app:layoutManager속성을 지정해준 부분이다.

이 부분은 사실 RecyclerView를 초기화하는 부분에서도 할 수 있는 작업이다.

이번 공부가 최종적으로 MVVM 패턴 적용 + 클린코드 작성을 목표로 하고 있기 때문에, 액티비티의 코드를 최소화 하기위해 레이아웃에서 설정해주었다.

 

2. RecyclerItem

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?xml version="1.0" encoding="utf-8"?>
<layout>
    <data>
        <variable
            name="data"
            type="com.example.recyclerviewexamplekt.MainItem" />
    </data>
 
    <RelativeLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
 
        <ImageView
            android:id="@+id/iv"
            android:layout_width="70dp"
            android:layout_height="70dp"
            android:layout_alignParentLeft="true"
            android:layout_alignParentTop="true"
            android:src="@drawable/ic_launcher_foreground"/>
 
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center_vertical"
            android:layout_alignParentTop="true"
            android:layout_toRightOf="@+id/iv"
            android:orientation="vertical"
            android:padding="5dp">
 
            <TextView
                android:id="@+id/tv_name"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@{data.str1}"
                android:textSize="20dp"/>
            <TextView
                android:id="@+id/tv_text"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@{data.str2}"/>
        </LinearLayout>
 
    </RelativeLayout>
</layout>
 
 
cs

이 부분은 RecyclerView의 각 요소에 들어갈 View를 그린 것인데, 내용은 지난번의 ViewModel 예제와 다른게 거의 없다.

 

3. MainItem

1
2
3
4
5
6
package com.example.recyclerviewexamplekt
 
data class MainItem(
    val str1: String,
    val str2: String
    )
cs

RecyclerView의 Item이 될 MainItem이다. 

일반적인 data class이고, String 2개를 담았다.

 

4. MainAdapter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.example.recyclerviewexamplekt
 
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.example.recyclerviewexamplekt.databinding.RecyclerItemBinding
 
class MainAdapter() : RecyclerView.Adapter<MainAdapter.ViewHolder>(){
    var lists = mutableListOf<MainItem>()
// 1. ViewHolder
    class ViewHolder(private val binding: RecyclerItemBinding) :RecyclerView.ViewHolder(binding.root){
        fun bind(item: MainItem) = with(binding){
            data = item
        }
    }
// 2. onCreateViewHolder
    override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder {
        val binding = RecyclerItemBinding.inflate(LayoutInflater.from(viewGroup.context),viewGroup,false)
        return ViewHolder(binding)
    }
// 3. onBindViewHolder
    override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
        viewHolder.bind(lists.get(position))
    }
// 4. getItemCount
    override fun getItemCount(): Int = lists.size
// 5. setData
    fun setData(data: ArrayList<MainItem>){
        lists = data
        notifyDataSetChanged()
    }
 
}
cs

이 부분이 설명할 점이 많은데, 설명을 잘 할 자신이 없어서.... 이해가 잘 안된다면 아래 링크와 비교하면서 보면 이해가 쉬울 것이다.

 

[안드로이드 스튜디오 독학#18] Custom RecyclerView

이 게시물은 다음 링크를 참조하여 학습했습니다. 안드로이드 리사이클러뷰 기본 사용법. (Android RecyclerView) 1. 안드로이드 리사이클러뷰(RecyclerView) 리사이클러뷰(RecyclerView)는, "많은 수의 데이터

seminzzang.tistory.com

1) 먼저 ViewHolder를 만들어 주었는데, 기존에 여기서 View를 초기화해주는 작업( findViewByID() 작업 )을 해주었는데, 우리는 데이터 바인딩을 사용하므로 따로 초기화 작업을 해줄 필요가 없다. 

ViewHolder 안에 있는 bind 메서드는 매개변수로 입력받은 Model을 ViewModel의 데이터에 연결해주는 역할을 한다.

2) onCreateViewHolder() 에서는 원래는 inflate작업을 하고 root View를 ViewHolder로 넘겨주는 작업을 했었는데, 이 부분도 마찬가지로 데이터 바인딩을 사용하니까 똑같이 inflate작업을 하고 Binding 객체를 넘겨주면 된다.

3) onBindViewHolder() 에서는 원래 실제 데이터들을 View에 대입하는 작업을 했는데, 이부분 역시 데이터 바인딩 객체가 알아서 해주기 때문에 ViewHolder에서 미리 만들어놓은 bind를 호출한다.

4) getItemCount() 부분은 말 그대로 리스트의 사이즈를 반환해주면 된다. 

5) setData() 부분은 MainActivity에서 데이터를 넣는 역할을 한다.

 

5. MainViewModel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.example.recyclerviewexamplekt
 
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
 
class MainViewModel :ViewModel(){
    private val listsLiveData= MutableLiveData<ArrayList<MainItem>>()
 
    val lists : LiveData<ArrayList<MainItem>>
        get() = listsLiveData
 
    private var items = ArrayList<MainItem>()
/*  // 잘못 선언한 부분
    private val _str = MutableLiveData<String>()
 
    val str : LiveData<String>
        get() = _str
*/
 
    val input_name = MutableLiveData<String>()
    val input_age = MutableLiveData<String>()
 
    init {
        items = arrayListOf(
            MainItem("seminzzang1""21"),
            MainItem("seminzzang2""28")
        )
        listsLiveData.value = items
    }
 
    fun buttonClick(){
 
        val mainItem = MainItem(input_name.value.toString(), input_age.value.toString())
        items.add(mainItem)
        listsLiveData.value = items
 
        input_name.value = ""
    }
}
cs

먼저 저번과 다른 첫번째 부분은 데이터의 타입이 ArrayList<>타입으로 LiveData에 들어간 점이다.

RecyclerView는 리스트 형태의 자료를 넘겨줘야 하기 때문에 위와 같이 작성했다.

EditText의 LiveData가 MutableLiveData로 되어있다.

아래 글을 보고 온 사람이라면 알겠지만(이미 알고 있을수도 있고....), MutableLiveData는 수정 가능한 데이터이고, LiveData는 수정이 불가능한 데이터이다.

 

[안드로이드 스튜디오 정리#15] View Model, Live Data

이 게시물은 다음 링크를 참조하여 학습했습니다. Android 아키텍처 구성요소  | Android 개발자  | Android Developers 앱 작업 developer.android.com ViewModel 개요  | Android 개발자  | Android Develop..

seminzzang.tistory.com

또한 지난 게시물에서 사용자가 보이는 데이터는 LiveData로 하고, 내부 로직에서 사용하는 데이터는 MutableLiveData로 해야 된다고 설명했다.

하지만 EditText를 이용해서 양방향 바인딩을 하기 위해서는 View의 데이터를 ViewModel을 거쳐서 Model로 수정해주어야 하는데, LiveData를 사용하면 수정이 불가능하므로 MutableLiveData를 사용했다.

 

6. MainActivity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package com.example.recyclerviewexamplekt
 
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import androidx.activity.viewModels
import androidx.lifecycle.Observer
import com.example.recyclerviewexamplekt.databinding.ActivityMainBinding
 
class MainActivity : AppCompatActivity() {
    private lateinit var mBinding: ActivityMainBinding
    private val binding get() = mBinding!!
 
    private val viewModel: MainViewModel by viewModels()
 
    private lateinit var mainAdapter: MainAdapter
    val lists = mutableListOf<MainItem>()
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
 
        binding.mainViewModel = viewModel
 
        initRecycler()
        initObserver()
    }
 
    private fun initRecycler(){
        mainAdapter = MainAdapter()
        binding.rvTest.adapter = mainAdapter
    }
 
    private fun initObserver(){
        viewModel.lists.observe(this, Observer {
            mainAdapter.setData(it)
        })
 
        viewModel.input_age.observe(this, Observer {
            if(it.length > 0 && !checkNum(it.get(it.length-1))){
 
                viewModel.input_age.value = it.substring(0,it.length-1)
                binding.etAge.setText(viewModel.input_age.value)
                binding.etAge.setSelection(viewModel.input_age.value.toString().length)
                Toast.makeText(this,"숫자만 입력해주세요.",Toast.LENGTH_SHORT).show()
            }
        })
    }
 
    fun checkNum(ch:Char) : Boolean{
        if(ch.code < 48 || ch.code > 57return false
        return true
    }
}
cs

일단은 이해가 안된다면 onCreate() 부분부터 보자!

이전 게시물과 마찬가지로 Data Binding을 해주고, ViewModel을 연결해주었다.

그리고 initRecycler(), initObserver() 두가지 메서드가 호출되었다.

initRecycler는 우리가 커스텀한 어댑터를 연결한 것이고,

initObserver는 Live Data의 변경에 따른 UI 조작을 위한 옵저버 기능들을 모아서 정리했다.

 

input_age.observe() 안의 Observer는 나이 값인 input_age값을 숫자로만 받기 위해서 문자를 받을때마다 숫자인지 판별해주는 알고리즘이다.

이번 게시물과 직접적으로 연관있는 내용은 아니고, 알고리즘적인 내용이니 이해가 안가거나 필요없으면 넘겨도 괜찮다.

 

7. 결과물

초기 화면은 아래와 같다.

현재 화면은 내가 텍스트를 입력한 것인데, 원래는 텍스트는 입력이 안되어있다.

아래와 같이 숫자를 입력하면 숫자를 다시 지워주면서 Toast메세지를 출력하도록 했다.

마지막으로 '입력'버튼을 누르면 아래와 같이 RecyclerView에 데이터가 추가된다.

 

===

이번 게시물을 작성하면서 뜬구름같던 DataBinding + View Model을 대강 정리할 수 있었고, 기본적인 기능에는 이제 활용할 수 있을 것 같다.

시간이 된다면 

1. RecyclerView의 리스트 데이터 Room으로 구현

2. 숫자 입력 안되는 알고리즘 보완

--> 중간문자에 숫자가 아닌 값이 들어가면 판별이 안된다. 이 부분을 보완하려면 매 입력마다 모든 문자열을 탐색해야 되는데, 어떻게 해야할지 좀더 고민해봐야겠다....

ex) EditText의 커서 변경 불가능이라던지....

반응형

+ Recent posts