๐Android์ ๋ชจ๋ ๊ฒ : 1๏ธโฃMVVM์ ์ํ Databinding, LiveData ๊ฐ๋ ๊ณผ ์์
https://goni95.tistory.com/173
๐Android์ ๋ชจ๋ ๊ฒ | Droid Knights 2021
๋ชฉ ์ฐจ ์์ Android ๐Android์ ๋ชจ๋ ๊ฒ : 1๏ธโฃAndroid, 2๏ธโฃAndroid SDK, 3๏ธโฃPlatform Architecture ๐Android์ ๋ชจ๋ ๊ฒ ๊ฐ๋ : 1๏ธโฃApp Manifest, 2๏ธโฃAndroid Context, 3๏ธโฃApp Components 1 O ๐An..
goni95.tistory.com
Android์์ ํ๋ก์ ํธ๋ฅผ MVVM ์ํคํ ์ฒ ํจํด์ผ๋ก ์ค๊ณํ๊ธฐ ์ํด ์์๋ณด๋ฉด ViewModel์ด View์ ๋ํ ์์กด์ฑ์ ๊ฐ์ง ์๊ณ ๋์จํ๊ฒ ์ฐ๊ฒฐ๋๋๋ก Databinding ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ํ์์ ์ผ๋ก ์ฌ์ฉ๋๋ค๋ ๋ง๊ณผ ํจ๊ป ViewModel์ด ๋ฐ์ดํฐ์ ๋ณ๊ฒฝ์ ์๋ฆฌ๋ฉด View๊ฐ ์ด๋ฅผ ๊ด์ฐฐํ๊ณ ์๋ค UI๋ฅผ ์ ๋ฐ์ดํธ ํ๋๋ก LiveData, Rx, Coroutine Flow ๋ฑ์ด ์ธ๊ธ๋ฉ๋๋ค.
Databinding๊ณผ LiveData ๋ชจ๋ Jetpack์ ์ํคํ ์ฒ(Architecture) ์นดํ ๊ณ ๋ฆฌ์ ํฌํจ๋ฉ๋๋ค.
1๏ธโฃ Databinding(๋ฐ์ดํฐ ๋ฐ์ธ๋ฉ)
๋ฐ์ดํฐ ๋ฐ์ธ๋ฉ์ Android 4.0 (API Level : 14) ์ด์ ์๋๋ก์ด๋ ๊ธฐ๊ธฐ๋ถํฐ ์ง์ํฉ๋๋ค.
๋ฐ์ดํฐ ๋ฐ์ธ๋ฉ์ ์ฌ์ฉํ๋ ค๋ฉด ์๋๋ก์ด๋ ์คํ๋์ค 1.5.0 ์ด์์ ๋ฒ์ ์ ์ฌ์ฉํด์ผ ํ๋ฉฐ, ์ต์ ์ ๋ฐ์ดํฐ ๋ฐ์ธ๋ฉ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ ค๋ฉด ์๋๋ก์ด๋ ์คํ๋์ค์ SDK ๋ฒ์ ์ ์ต์ ์ํ๋ก ์ ์งํ๋ ๊ฒ์ด ์ข์ต๋๋ค.
๋ฐ์ดํฐ ๋ฐ์ธ๋ฉ์ ์ ์ธ์ ํ์์ผ๋ก ๋ ์ด์์์ UI ๊ตฌ์ฑ ์์๋ฅผ ์ดํ๋ฆฌ์ผ์ด์ ์ ๋ฐ์ดํฐ์ ๊ฒฐํฉํ ์ ์๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๋๋ค.
๋ฐ์ดํฐ ๋ฐ์ธ๋ฉ์ ํตํด boilerplate code๋ฅผ ์ค์ด๊ณ ์ฑ๋ฅ ํฅ์๊ณผ ๋ฉ๋ชจ๋ฆฌ ๋์ ๋ฐ Null Pointer Exception์ ๋ฐฉ์ง ๊ทธ๋ฆฌ๊ณ MVVM ์ํคํ ์ฒ ํจํด ์ค๊ณ๋ฅผ ์ํด ์ฌ์ฉํด ํ ์คํธ ์ฉ์ด์ฑ, ์ ์ง๋ณด์์ฑ์ ๋์ผ ์ ์์ง๋ง ํด๋์ค ํ์ผ ์๊ฐ ์ฆ๊ฐํ๊ณ ๋น๋ ์๋๊ฐ ๋๋ ค์ง ์ ์๋ ๋จ์ ์ด ์์ต๋๋ค.
๊ธฐ์กด์๋ TextView์ ViewModel๋ก ๋ถํฐ title์ด๋ผ๋ ๋ฌธ์์ด์ ํ๋กํผํฐ๋ฅผ ์ฐธ์กฐํ์ฌ ๋ณ๊ฒฝํด์ผ ํ์ต๋๋ค.
val tileTv : TextView = findViewById(R.id.titleTextView)
titleTv.text = viewModel.title
๋ฐ์ดํฐ ๋ฐ์ธ๋ฉ์ ์ฌ์ฉํ๋ฉด ๋ ์ด์์ ํ์ผ์์ ์ง์ ๋ฐ์ดํฐ ๋ฐ์ธ๋ฉ ํํ์์ ์ฌ์ฉํด ์ ์ธํ ํ๋ก๊ทธ๋๋ฐ ์ฝ๋๋ฅผ ์์ฑํ ์ ์์ต๋๋ค.
<?xml version="1.0" encoding="utf-8"?>
<layout
...>
<data>
<variable
name="viewModel"
type="sang.gondroid.databindingexample.MainViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
...>
<TextView
android:id="@+id/TitleTextView"
...
android:text="@{viewModel.title}"
... />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
1. ๋ฐ์ดํฐ ๋ฐ์ธ๋ฉ ์ค์
๋ชจ๋ ์์ค์ build.gradle์ ์ถ๊ฐํฉ๋๋ค.
// 1. ์๋๋ก์ด๋ ์คํ๋์ค 4.0 ๋ฏธ๋ง์ธ ๊ฒฝ์ฐ
android {
...
buildFeatures {
dataBinding = true
}
}
// 2. ์๋๋ก์ด๋ ์คํ๋์ค 4.0 ์ด์์ธ ๊ฒฝ์ฐ
plugins {
...
id 'kotlin-kapt' // databinding [you should apply the kotlin-kapt plugin. / bindingAdapter]
}
android {
...
buildFeatures {
dataBinding = true
}
}
2. ๋ฐ์ธ๋ฉ ํด๋์ค ์์ฑ
๋ฐ์ดํฐ ๋ฐ์ธ๋ฉ์ ๋ ์ด์์์ ๋ณ์์ ๋ทฐ๋ฅผ ์ฐธ์กฐํ ์ ์๋ ๋ฐ์ธ๋ฉ ํด๋์ค๋ฅผ ์์ฑํ๋ฉฐ ๋ฐ์ธ๋ฉ ํด๋์ค๋ ViewDataBinding์ ์์ํฉ๋๋ค.
xml ๋ ์ด์์ ํ์ผ์ ์์ ๋ ์ด์์์ ํ๊ทธ๋ก ๊ฐ์ธ๋ฉด ์๋์ผ๋ก ๋ฐ์ธ๋ฉ ํด๋์ค๊ฐ ์์ฑ๋๋๋ฐ ๋ฐ์ธ๋ฉ ํด๋์ค ์ด๋ฆ์ ๋ ์ด์์ ํ์ผ๋ช ์ ํ์ค์นผ ์ผ์ด์ค๋ก ๋ณ๊ฒฝํ ๋ค Binding ์ ๋ฏธ์ด๋ฅผ ๋ถ์ฌ ์์ฑ๋ฉ๋๋ค.
<?xml version="1.0" encoding="utf-8"?>
<layout>
<androidx.constraintlayout.widget.ConstraintLayout
...>
...
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
tip : ๋ ์ด์์์ ๋ํ ํํ์ ***Binding ํด๋์ค๋ก ์์ฑ๋์ง๋ง ์ค์ ๋น์ฆ๋์ค ๋ก์ง ์ถ์ ๋๋ ๋๋ฒ๊น ์ ํ๋ ค๋ฉด ***BindingImpl์ ์ฐธ์กฐํด์ผ ํฉ๋๋ค.
3. ๋ฐ์ธ๋ฉ ํด๋์ค๋ก ๋ฐ์ธ๋ฉ ๊ฐ์ฒด ์์ฑํ๊ธฐ
DataBindingUtil ํด๋์ค์ ๋ฉ์๋๋ฅผ ํ์ฉํด ๋ฐ์ธ๋ฉ ๊ฐ์ฒด ์์ฑ
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.lifecycleOwner = this
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
saveInstanceState : Bundle?) : View? {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_main, container, false)
binding.lifecycleOwner = this
return binding.root
}
inflate() ๋ฉ์๋๋ฅผ ์ฌ์ฉํ์ฌ ๋ ์ด์์ ์ ๊ฐ์ ํจ๊ป ๋ฐ์ธ๋ฉ ๊ฐ์ฒด๋ฅผ ์์ฑํ๋ ๋ฐฉ๋ฒ
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.lifecycleOwner = this
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
saveInstanceState : Bundle?) : View? {
binding = MainFragmentBinding.inflate(inflater, container, false)
// binding = MainFragmentBinding.inflate(inflater)
// binding = MainFragmentBinding.inflate(layoutInflater)
binding.lifecycleOwner = this
return binding.root
}
*์ ๊ฐ : xml ๋ ์ด์์ ํ์ผ๊ณผ ์์ View์ ์์ฑ์ ์ฝ์ด ์ค์ View ๊ฐ์ฒด๋ฅผ ์์ฑํ๋ ๋์์ ์๋ฏธํฉ๋๋ค.
4. ๋ฐ์ธ๋ฉ ํด๋์ค ์ด๋ฆ ์ ์
๊ธฐ๋ณธ์ ์ผ๋ก ๋ฐ์ธ๋ฉ ํด๋์ค ์ด๋ฆ์ ๋ ์ด์์ ํ์ผ๋ช ์ ๋ฐํ์ผ๋ก ์์ฑ๋์ง๋ง ์ด๋ฅผ ๋ณ๊ฒฝํ๊ณ ์ถ์ ๊ฒฝ์ฐ <data> ํ๊ทธ ๋ด์ class ์์ฑ์ ์ฌ์ฉํ์ฌ ๋ณ๊ฒฝํ๋ฉด ๋ฉ๋๋ค.
<?xml version="1.0" encoding="utf-8"?>
<layout
... >
<data class="MainActivityBindingClass"/>
<androidx.constraintlayout.widget.ConstraintLayout
... >
...
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
class MainActivity : AppCompatActivity() {
private lateinit var binding: MainActivityBindingClass
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.lifecycleOwner = this
}
}
4. ID๋ก View ์ฐธ์กฐ
๋ฐ์ธ๋ฉ ํด๋์ค ๋ด๋ถ์์ ๋ฏธ๋ฆฌ findViewByID()๋ฅผ ํธ์ถํ ๊ฒฐ๊ณผ๋ฅผ ์บ์ฑํด ๋๊ธฐ ๋๋ฌธ์ ๋ฐ์ธ๋ฉ ํด๋์ค๋ฅผ ์ฌ์ฉํ๋ฉด findViewById()๋ฅผ ํธ์ถํ ํ์๊ฐ ์์ต๋๋ค.
<?xml version="1.0" encoding="utf-8"?>
<layout
... >
<data class="MainActivityBindingClass"/>
<androidx.constraintlayout.widget.ConstraintLayout
... >
<TextView
android:id="@+id/titleTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.lifecycleOwner = this
binding.titleTextView.text = "์๋
"
}
}
5. ๋ ์ด์์ ๋ณ์ ์ ์ธ๊ณผ ํด๋์ค ์ฐธ์กฐ
๋ ์ด์์์ ๋ณ์๋ฅผ ์ ์ธํ๊ณ ๋ณ์์ ๊ฐ์ ๋์ ํ๋ ๊ฒ์ผ๋ก View์ ์ํ๋ฅผ ๋ณ๊ฒฝํ ์ ์์ต๋๋ค.
๋ณ์๋ฅผ ์ ์ธํ๊ธฐ ์ํด์ <layout> ํ๊ทธ ๋ด์ <data> ํ๊ทธ๋ฅผ ์ฌ์ฉํ๋ฉฐ <data> ํ๊ทธ ๋ด์์ ๋ณ์๋ฅผ <variable> ํ๊ทธ๋ฅผ ์ฌ์ฉํด ์ ์ธํ๋ฉด ๋ฉ๋๋ค.
<variable> ํ๊ทธ๋ ๋ณ์์ ์ด๋ฆ์ ์ ์ํ๋ name๊ณผ ๋ณ์์ ์๋ฃํ์ ์ ์ํ๋ type ๋ ์์ฑ์ ๊ฐ์ต๋๋ค.
์ฐธ์กฐํ๊ณ ์ถ์ ํด๋์ค๋ฅผ ๋ ์ด์์ ํ์ผ์ ๋ถ๋ฌ์ค๋ ค๋ฉด <import> ํ๊ทธ๋ฅผ ์ฌ์ฉํด ์ ์ธํ๋ฉฐ ์๋ฃํ์ ์ ์ํ๋ type ์์ฑ์ ๊ฐ์ต๋๋ค.
๋ง์ฝ ์ด๋ฆ์ด ๋์ผํ ์๋ก ๋ค๋ฅธ ํด๋์ค๋ฅผ ๋ถ๋ฌ์ค๋ ๊ฒฝ์ฐ alias ์์ฑ์ ํตํด ๋ ์ด์์ ๋ด์์ class ๋ช ๋์ ์ฌ์ฉํ ์ด๋ฆ์ ์ค์ ํ๋ฉด ๋ฉ๋๋ค.
ViewModel์ ์ฌ์ฉํด View์ ์ํ๋ฅผ ๋ณ๊ฒฝํ๋ ์์ ์ ๋๋ค.
<?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="viewModel"
type="sang.gondroid.databindingexample.MainViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/titleTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{viewModel.title}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: MainViewModel
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.lifecycleOwner = this
viewModel = ViewModelProvider(this) [MainViewModel::class.java]
binding.viewModel = viewModel
}
}
class MainViewModel : ViewModel() {
private val _title = MutableLiveData<String>()
val title : LiveData<String>
get() = _title
init {
viewModelScope.launch {
for (i in 0.. 1000) {
delay(1000)
_title.value = "ํ์ดํ $i"
}
}
}
}
6. ๋ฐ์ธ๋ฉ ํํ์
xml ๋ ์ด์์์์ ์ฌ์ฉํ ์ ์๋ ์ฐ์ฐ์, Collections ํด๋์ค๋ฅผ ์ฌ์ฉํ ํํ์์ ์์๋ณด๊ฒ ์ต๋๋ค.
์ค์ํ ์ ์ ๋ฐ์ธ๋ฉ ํํ์ ์์ฑ ์ <๋ < ๋ก ์ ๋ ฅํด์ผ ํฉ๋๋ค.
- ์ฐ์ ์ฐ์ฐ์ : + - / * %
- ๋ฌธ์์ด ์ฐ๊ฒฐ : +
- ๋ ผ๋ฆฌ ์ฐ์ฐ์ : && ||
- ๋นํธ ์ฐ์ฐ์ : & | ^
- ๋จํญ ์ฐ์ฐ์ : + - ! ~
- ๋นํธ ์ด๋ ์ฐ์ฐ์ : >> >>> <<
- ๋น๊ต ์ฐ์ฐ์ : == > < >= <=
- Null ๋ณํฉ ์ฐ์ฐ์ : ??
<data>
<variable
name="viewModel"
type="sang.gondroid.databindingexample.MainViewModel" />
<import type="android.view.View"/>
<import type="sang.gondroid.databindingexample.View" alias="MyView"/>
<variable
name="index"
type="Integer" />
<import type="java.util.List"/>
<variable
name="list"
type="List<String>" />
<import type="java.util.Map"/>
<variable
name="map"
type="Map<String, String>" />
</data>
android:text="@{String.valueOf(viewModel.index + 1}"
// import ํ๊ทธ๋ฅผ ํตํด View ํด๋์ค๋ฅผ ์ฐธ์กฐ
android:visibility="@{viewModel.num > 10 ? View.GONE : View.VISIBLE}"
android:text='@{"name : " + viewModel.name}'
android:text="@{viewModel.name ?? viewModel.nullValue}"
<!--
1. string ๋ฆฌ์์ค ์ฐธ์กฐํ๊ธฐ(strings.xml)
<string name="nameFormat">%s %s</string>
<string name="nameFormat">%2$s %1$s</string> // ๋งค๊ฐ๋ณ์ ์์ ๋ณ๊ฒฝ ๊ฐ๋ฅ
-->
android:text="@{@string/nameFormat(firstName, lastName)}"
android:text="@{list[index]}"
android:text="@{list[0]}"
android:text='@{map["ad"]}'
android:text="@{map[`ad`]}"
2๏ธโฃ LiveData
3๏ธโฃ