- DataBinding
xml ์ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์ธ๋ฉํ์ฌ ๋ถํ์ํ ์ฝ๋๋ฅผ ์ค์ด๋ ๋ฐฉ๋ฒ์ผ๋ก, ๋ณดํต MVVM ํจํด์ ๊ตฌํ ํ ๋ ์ฌ์ฉ๋๋ค.
์ฆ, ๋ฐ์ดํฐ๋ฐ์ธ๋ฉ์ ์ ํ๋ฆฌ์ผ์ด์
๋ก์ง๊ณผ ๋ ์ด์์์ bindingํ๋ ๋ฐ ํ์ํ ๋ถํ์ํ ์ฝ๋๋ฅผ ์ต์ํํ๋ค.
// Android Studio 4.0
android {
buildFeatures {
dataBinding = true
}
}
ํํ์ ์ธ์ด๋ก ๋ ์ด์์์ ๋ทฐ์ ๋ณ์๋ฅผ ์ฐ๊ฒฐํ๋ ํํ์์ ์์ฑํ ์ ์๋ค.
DataBinding Library๋ ๋ ์ด์์์ ๋ทฐ๋ฅผ ๋ฐ์ดํฐ ๊ฐ์ฒด์ ๊ฒฐํฉํ๋ ๋ฐ ํ์ํ ํด๋์ค๋ฅผ ์๋์ผ๋ก ์์ฑํ๋ค.
DataBinding Library๋ import, variable ๋ฐ includes๊ณผ ๊ฐ์ด ๋ ์ด์์์์ ์ฌ์ฉํ ์ ์๋ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ค.
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="viewmodel"
type="com.myapp.data.ViewModel" />
</data>
<ConstraintLayout... /> <!-- UI layout's root element -->
</layout>
๋ ์ด์์ ๋ด์ ํํ์์ '@{}'
๊ตฌ๋ฌธ์ ์ฌ์ฉํ์ฌ ํน์ฑ ์์ฑ์ ์์ฑ๋๋ค.
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{viewmodel.getName()}" />
layout ํ๊ทธ๋ก xml์ ๊ฐ์ธ๋ฉด ๊ฐ ๋ ์ด์์์ Binding Class๊ฐ ์์ฑ๋๋ค.
๊ธฐ๋ณธ์ ์ผ๋ก ํด๋์ค ์ด๋ฆ์ ๋ ์ด์์ ํ์ผ ์ด๋ฆ์ ๊ธฐ๋ฐ์ผ๋ก ํ์ฌ ํ์ค์นผ ํ๊ธฐ๋ฒ์ผ๋ก ๋ณํํ๊ณ Binding ์ ๋ฏธ์ฌ๋ฅผ ์ถ๊ฐํ๋ค.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: ActivityMainBinding = DataBindingUtil.setContentView(
this, R.layout.activity_main)
// ์์ ๋ ์ด์์ ํ์ผ ์ด๋ฆ์ activity_main.xml์ด๋ฏ๋ก ์์ฑ๋๋ ํด๋์ค๋ ActivityMainBinding์ด๋ค
binding.viewmodel = MainViewModel()
}
LayoutInflator๋ฅผ ์ฌ์ฉํ ์๋ ์๋ค.
val binding: ActivityMainBinding = ActivityMainBinding.inflate(getLayoutInflater())
๋ํ Fragment
, RecyclerView
์ด๋ํฐ ๋ด์์ ๋ฐ์ดํฐ ๊ฒฐํฉ ํญ๋ชฉ์ ์ฌ์ฉํ๊ณ ์๋ค๋ฉด ๋ค์ ์ฝ๋ ์์์์ ๊ฐ์ด ๊ฒฐํฉ ํด๋์ค ๋๋ BindingClass์ ํ์
์ ๋ฏธ๋ฆฌ ์ ์ ์๋๊ฒฝ์ฐ DataBindingUtil
ํด๋์ค์ inflate()
๋ฉ์๋๋ฅผ ์ฌ์ฉํ ์๋ ์๋ค.
val listItemBinding = ListItemBinding.inflate(layoutInflater, viewGroup, false)
// or
val listItemBinding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false)
๊ตฌ์ฒด์ ์ธ ๊ฒฐํฉ ํด๋์ค๋ฅผ ์ ์ ์๋ ๋๋ ์๋ค.
์๋ฅผ ๋ค์ด ์์์ ๋ ์ด์์์ ์๋ํ๋ RecyclerView.Adapter๋ ํน์ ๊ฒฐํฉ ํด๋์ค๋ฅผ ์ธ์ํ์ง ๋ชปํ๋ค.
๋ฐ๋ผ์ ์ด ์ด๋ํฐ๋ onBindViewHolder()
๋ฉ์๋๋ฅผ ํธ์ถํ๋ ๋์์๋ ๊ณ์ํด์ ๊ฒฐํฉ ๊ฐ์ ํ ๋นํด์ผ ํ๋ค.
override fun onBindViewHolder(holder: BindingHolder, position: Int) {
item: T = items.get(position)
holder.binding.setVariable(BR.item, item);
holder.binding.executePendingBindings();
}
- ์ฐ์
+ - / * %
- ๋ฌธ์์ด ์ฐ๊ฒฐ
+
- ๋
ผ๋ฆฌ
&& ||
- ๋ฐ์ด๋๋ฆฌ
& | ^
- ๋จํญ
+ - ! ~
- ์ ํ
>> >>> <<
- ๋น๊ต
== > < >= <=(<๋ <์ผ๋ก ์ด์ค์ผ์ดํ ์ฒ๋ฆฌํด์ผ ํจ)
instanceof
- ๋ฆฌํฐ๋ด - ๋ฌธ์, ๋ฌธ์์ด, ์ซ์,
null
- ๋ณํ
- ๋ฉ์๋ ํธ์ถ
- ํ๋ ์ก์ธ์ค
- ๋ฐฐ์ด ์ก์ธ์ค
[]
- ์ผํญ ์ฐ์ฐ์
?:
android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age > 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'
this
super
new
- ๋ช ์์ ์ ๋ค๋ฆญ(Generic) ํธ์ถ
null ๋ณํฉ ์ฐ์ฐ์(??
)๋ ์ผ์ชฝ ํผ์ฐ์ฐ์๊ฐ null
์ด ์๋๋ฉด ์ผ์ชฝ ํผ์ฐ์ฐ์๋ฅผ ์ ํํ๊ณ null
์ด๋ฉด ์ค๋ฅธ์ชฝ ํผ์ฐ์ฐ์๋ฅผ ์ ํํ๋ค.
android:text="@{user.displayName ?? user.lastName}"
์์ฑ๋ ๋ฐ์ดํฐ ๊ฒฐํฉ ์ฝ๋๋ ์๋์ผ๋ก null
๊ฐ์ ํ์ธํ๊ณ null
ํฌ์ธํฐ ์์ธ๋ฅผ ๋ฐฉ์งํ๋ค.
์๋ฅผ ๋ค์ด @{user.name}
ํํ์์์ user
๊ฐ null์ด๋ฉด user.name
์ null์ด ๊ธฐ๋ณธ๊ฐ์ผ๋ก ํ ๋น๋ฉ๋๋ค. age์ ์ ํ์ด int
์ธ user.age
๋ฅผ ์ฐธ์กฐํ๋ฉด ๋ฐ์ดํฐ ๊ฒฐํฉ์ 0์ ๊ธฐ๋ณธ๊ฐ์ ์ฌ์ฉํ๋ค.
Array
, List
, Map
๊ณผ ๊ฐ์ ์ผ๋ฐ ์ปฌ๋ ์
์๋ ํธ์์ []
์ฐ์ฐ์๋ฅผ ์ฌ์ฉํ์ฌ ์ก์ธ์คํ ์ ์๋ค.
<data>
<import type="android.util.SparseArray"/>
<import type="java.util.Map"/>
<import type="java.util.List"/>
<variable name="list" type="List<String>"/>
<variable name="sparse" type="SparseArray<String>"/>
<variable name="map" type="Map<String, String>"/>
<variable name="index" type="int"/>
<variable name="key" type="String"/>
</data>
โฆ
android:text="@{list[index]}"
โฆ
android:text="@{sparse[index]}"
โฆ
android:text="@{map[key]}"
๋ค์ ์์์์ ๊ฐ์ด ์์๋ฐ์ดํ๋ฅผ ์ฌ์ฉํ์ฌ ์์ฑ ๊ฐ์ ๋ฌถ์ด ํํ์์ ํฐ๋ฐ์ดํ๋ฅผ ์ฌ์ฉํ ์ ์๋ค.
android:text='@{map["firstName"]}'
๋ํ ํฐ๋ฐ์ดํ๋ฅผ ์ฌ์ฉํ์ฌ ์์ฑ ๊ฐ์ ๋ฌถ์ ์๋ ์๋ค.
android:text="@{map[`firstName`]}"
ํํ์์ ๋ค์ ๊ตฌ๋ฌธ์ ์ฌ์ฉํ์ฌ ์ฑ ๋ฆฌ์์ค๋ฅผ ์ฐธ์กฐํ ์ ์๋ค.
android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
๋ณต์ํ ๋งค๊ฐ๋ณ์๋ ์ ๋ฌํ๋ ค ๋ฆฌ์์ค๋ฅผ ์ฐธ์กฐ ๊ฐ๋ฅํ๋ค.
android:text="@{@string/nameFormat(firstName, lastName)}"
android:text="@{@plurals/banana(bananaCount)}"
๋ฆฌ์ค๋๋ ๋ฐ์ธ๋ฉ ๊ฐ๋ฅํ๋ค.
์ด๋ฒคํธ ์์ฑ ์ด๋ฆ์ ๋ช ๊ฐ์ง ์์ธ๋ฅผ ์ ์ธํ๊ณ ๋ฆฌ์ค๋ ๋ฉ์๋์ ์ด๋ฆ์ ๋ฐ๋ผ ๊ฒฐ์ ๋๋ค.
setOnXXXListener์์ set์ ๋นผ๊ณ O๋ฅผ ์๋ฌธ์ o๋ก ๋ฐ๊ฟ์ด๋ค.
<!-- setOnNavigationSelectListener-->
app:NavigationItemSelectedListener="@{viewModel::NavigationItemSelectedListener}"
<!-- onRangeSelectListener -->
app:onRangeSelectedListener="@{calendarDialogViewModel::onRangeSelectedListener}"
<!-- setOnRefreshListener -->
app:onRefreshListener="@{() -> homeVM.onRefresh()}"
class MyHandlers {
fun onClickFriend(view: View) { ... }
}
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="handlers" type="com.example.MyHandlers"/>
<variable name="user" type="com.example.User"/>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}"
android:onClick="@{handlers::onClickFriend}"/>
</LinearLayout>
</layout>
๋งค๊ฐ๋ณ์๋ ์ ๋ฌ ๊ฐ๋ฅํ๋ค
class Presenter {
fun onSaveClick(task: Task){}
}
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="task" type="com.android.example.Task" />
<variable name="presenter" type="com.android.example.Presenter" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{() -> presenter.onSaveClick(task)}" />
</LinearLayout>
</layout>
๋ค์๊ณผ ๊ฐ์ด ๋ ์ด์์ ๋งค๊ฐ๋ณ์์ ํจ๊ป ๋๋ค ํํ์์ ์ฌ์ฉํ ์ ์๋ค.
class Presenter {
fun onCompletedChanged(task: Task, completed: Boolean){}
}
<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onCheckedChanged="@{(cb, isChecked) -> presenter.completeChanged(task, isChecked)"/>
import๋ฅผ ์ฌ์ฉํ๋ฉด ๋ ์ด์์ ํ์ผ ๋ด์์ ํด๋์ค๋ฅผ ์ฝ๊ฒ ์ฐธ์กฐํ ์ ์๋ค.
<data>
<import type="android.view.View"/>
</data>
...
<TextView
android:text="@{user.lastName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>
ํด๋์ค ์ด๋ฆ ์ถฉ๋์ด ๋ฐ์ํ๋ฉด ํด๋์ค ์ค ํ๋์ ์ด๋ฆ์ ๋ณ์นญ์ผ๋ก ๋ฐ๊ฟ ์ ์๋ค.
๋ค์ ์๋ com.example.real.estate
ํจํค์ง์ View ํด๋์ค ์ด๋ฆ์ Vista๋ก ๋ฐ๊พผ๋ค.
<import type="android.view.View"/>
<import type="com.example.real.estate.View" alias="Vista"/>
<data>
<import type="com.example.User"/>
<import type="java.util.List"/>
<variable name="user" type="User"/>
<variable name="userList" type="List<User>"/>
</data>
๋ํ ๊ฐ์ ธ์จ ์ ํ์ ์ฌ์ฉํ์ฌ ํํ์์ ์ผ๋ถ๋ฅผ ๋ณํํ ์๋ ์๋ค. ๋ค์ ์๋ connection ์์ฑ์ User ์ ํ์ผ๋ก ๋ณํ
<TextView
android:text="@{((User)(user.connection)).lastName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
ํํ์์์ ์ ์ ํ๋ ๋ฐ ๋ฉ์๋๋ฅผ ์ฐธ์กฐํ ๋ ๊ฐ์ ธ์จ ์ ํ์ ์ฌ์ฉํ ์๋ ์๋ค.
๋ค์ ์ฝ๋๋ MyStringUtils
ํด๋์ค๋ฅผ ๊ฐ์ ธ์์ capitalize
๋ฉ์๋๋ฅผ ์ฐธ์กฐํ๋ค.
<data>
<import type="com.example.MyStringUtils"/>
<variable name="user" type="com.example.User"/>
</data>
โฆ
<TextView
android:text="@{MyStringUtils.capitalize(user.lastName)}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
์์ฑ์ ์ฑ ๋ค์์คํ์ด์ค ๋ฐ ๋ณ์ ์ด๋ฆ์ ์ฌ์ฉํจ์ผ๋ก์จ ํฌํจํ๋ ๋ ์ด์์์์ ํฌํจ๋ ๋ ์ด์์์ ๊ฒฐํฉ์ผ๋ก ๋ณ์๋ฅผ ์ ๋ฌํ ์ ์๋ค.
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:bind="http://schemas.android.com/apk/res-auto">
<data>
<variable name="user" type="com.example.User"/>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/name" bind:user="@{user}"/>
<include layout="@layout/contact" bind:user="@{user}"/>
</LinearLayout>
</layout>
BindingAdapter๋ ์ ์ ํ ํ๋ ์์ํฌ๋ฅผ ํธ์ถํ์ฌ ๊ฐ์ ์ค์ ํ๋ ์์
์ ๋ด๋นํ๋ค.
์ผ๋ถ ์์ฑ์๋ CustomBindingLogic์ด ํ์ํ๋ค
์๋ฅผ ๋ค์ด, android:paddingLeft
์์ฑ์๋ ์ฐ๊ฒฐ๋ setter๊ฐ ์๋ค. ๋์ setPadding(left, top, right, bottom)
๋ฉ์๋๊ฐ ์ ๊ณต๋๋ BindingAdapter ์ด๋
ธํ
์ด์
์ด ์๋ static BindingAdapter ๋ฉ์๋๋ฅผ ์ฌ์ฉํ๋ฉด ์์ฑ์ setter๊ฐ ํธ์ถ๋๋ ๋ฐฉ์์ ๋ง์ถค์ค์ ํ ์ ์๋ค.
@BindingAdapter("android:paddingLeft")
fun setPaddingLeft(view: View, padding: Int) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom())
}
@BindingAdapter("app:goneUnless")
fun goneUnless(view: View, visible: Boolean) {
view.visibility = if (visible) View.VISIBLE else View.GONE
}
@BindingAdapter("loadImage")
fun bindLoadImageFromUri(view: ImageView, uri: Uri?) {
uri?.let {
Glide.with(view.context)
.load(it.toString())
.into(view)
}
}
๋งค๊ฐ๋ณ์ ์ ํ์ ์ค์ํ๋ค.
์ฒซ ๋ฒ์งธ ๋งค๊ฐ๋ณ์๋ ์์ฑ๊ณผ ์ฐ๊ฒฐ๋ ๋ทฐ์ ์ ํ์ ๊ฒฐ์ ํ๋ค.
๋ ๋ฒ์งธ ๋งค๊ฐ๋ณ์๋ ์ง์ ๋ ์์ฑ์ ๊ฒฐํฉ ํํ์์์ ํ์ฉ๋๋ ์ ํ์ ๊ฒฐ์ ํ๋ค.
๊ฐ๋ฐ์๊ฐ ์ ์ํ๋ BindingAdapter๋ ์ถฉ๋์ด ๋ฐ์ํ๋ฉด Android ํ๋ ์์ํฌ์์ ์ ๊ณตํ๋ ๊ธฐ๋ณธ ์ด๋ํฐ๋ณด๋ค ์ฐ์ ์ ์ฉ๋๋ค.
๋ํ ์ฌ๋ฌ๊ฐ์ง ์์ฑ์ ๋ฐ๋ ์ด๋ํฐ๋ ์ ์ํ ์ ์๋ค.
@BindingAdapter("imageUrl", "error")
fun loadImage(view: ImageView, url: String, error: Drawable) {
Picasso.get().load(url).error(error).into(view)
}
<ImageView
app:imageUrl="@{venue.imageUrl}"
app:error="@{@drawable/venueError}"/>
ํ๋๋ผ๋ ์ค์ ๋ ๋ ์ด๋ํฐ๊ฐ ํธ์ถ๋๋๋ก ํ๋ ค๋ฉด ๋ค์ ์์์ ๊ฐ์ด ์ด๋ํฐ์ ์ ํ์ requireAll
ํ๋๊ทธ๋ฅผ false
๋ก ์ค์ ํ ์ ์๋ค.
@BindingAdapter(value = ["imageUrl", "placeholder"], requireAll = false)
fun setImageUrl(imageView: ImageView, url: String?, placeHolder: Drawable?) {
if (url == null) {
imageView.setImageDrawable(placeholder);
} else {
MyImageLoader.loadInto(imageView, url, placeholder);
}
}
BindingAdapter ๋ฉ์๋๋ ์ ํ์ ์ผ๋ก ํธ๋ค๋ฌ์ ์ด์ ๊ฐ์ ์ฌ์ฉํ ์ ์๋ค.
์ด์ ๊ฐ๊ณผ ์ ๊ฐ์ ์ฌ์ฉํ๋ ๋ฉ์๋๋ ์๋ ์์ ๊ฐ์ด ์์ฑ์ ๋ชจ๋ ์ด์ ๊ฐ์ ๋จผ์ ์ ์ธํ ํ ์ ๊ฐ์ ์ ์ธํด์ผ ํ๋ค.
@BindingAdapter("android:paddingLeft")
fun setPaddingLeft(view: View, oldPadding: Int, newPadding: Int) {
if (oldPadding != newPadding) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom())
}
}
๊ฐ๋จํ ๊ธฐ์กด ๊ฐ์ฒด๋ฅผ ๋ฐ์ดํฐ ๊ฒฐํฉ์ ์ฌ์ฉํ ์๋ ์์ง๋ง ๊ฐ์ฒด๋ฅผ ์์ ํด๋ UI๊ฐ ์๋์ผ๋ก ์
๋ฐ์ดํธ๋์ง๋ ์๋๋ค.
Observable data๋ฅผ ์ฌ์ฉํ๋ฉด ๋ฐ์ดํฐ ๋ณ๊ฒฝ ์ ๋ฆฌ์ค๋๋ผ๋ ๋ค๋ฅธ ๊ฐ์ฒด์ ์๋ฆฌ๋ ๊ธฐ๋ฅ์ ๋ฐ์ดํฐ ๊ฐ์ฒด์ ์ ๊ณตํ ์ ์๋ค.
Observable data object์๋ ์ธ ๊ฐ์ง ์ ํ, ์ฆ ๊ฐ์ฒด(object), ํ๋(field) ๋ฐ ์ปฌ๋ ์
(collection), LiveData, Flow๋ฑ์ด ์๋ค.
Observable
ํด๋์ค ๋ฐ ๋ค์๊ณผ ๊ฐ์ ํ๋ฆฌ๋ฏธํฐ๋ธ(Primitive) ๊ด๋ จ ํด๋์ค๋ฅผ ์ฌ์ฉํ์ฌ ํ๋๋ฅผ ์๋ณ ๊ฐ๋ฅํ๊ฒ ๋ง๋ค ์ ์๋ค.
- ObservableField
- ObservableBoolean
- ObservableByte
- ObservableChar
- ObservableShort
- ObservableInt
- ObservableLong
- ObservableFloat
- ObservableDouble
- ObservableParcelable
val firstName = ObservableField<String>()
val lastName = ObservableField<String>()
val age = ObservableInt()
๋ค์๊ณผ ๊ฐ์ด field ๊ฐ์ ์ก์ธ์คํ๋ค.
user.firstName = "Google"
val age = user.age
์ผ๋ถ ์ฑ์ ๋์ ๊ตฌ์กฐ๋ฅผ ์ฌ์ฉํ์ฌ ๋ฐ์ดํฐ๋ฅผ ๋ณด์ ํฉ๋๋ค.
ObservableCollection ํตํด ํค๋ฅผ ์ฌ์ฉํ์ฌ ์ด๋ฌํ ๊ตฌ์กฐ์ ์ก์ธ์คํ ์ ์๋ค.
๋ค์ ์์ ๊ฐ์ด ํค๊ฐ String๊ณผ ๊ฐ์ ์ฐธ์กฐ ์ ํ์ผ ๋๋ ObservableArrayMap
ํด๋์ค๊ฐ ์ ์ฉํ๋ค.
ObservableArrayMap<String, Any>().apply {
put("firstName", "Google")
put("lastName", "Inc.")
put("age", 17)
}
<data>
<import type="android.databinding.ObservableMap"/>
<variable name="user" type="ObservableMap<String, Object>"/>
</data>
โฆ
<TextView
android:text="@{user.lastName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:text="@{String.valueOf(1 + (Integer)user.age)}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
๋ ๋ค๋ฅธ ์์
ObservableArrayList<Any>().apply {
add("Google")
add("Inc.")
add(17)
}
<data>
<import type="android.databinding.ObservableList"/>
<import type="com.example.my.app.Fields"/>
<variable name="user" type="ObservableList<Object>"/>
</data>
โฆ
<TextView
android:text='@{user[Fields.LAST_NAME]}'
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:text='@{String.valueOf(1 + (Integer)user[Fields.AGE])}'
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
Observable ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํ๋ ํด๋์ค๋ฅผ ์ฌ์ฉํ๋ฉด ์๋ณ ๊ฐ๋ฅํ ๊ฐ์ฒด์ ์์ฑ ๋ณ๊ฒฝ์ ๊ดํด ์๋ฆผ์ ๋ฐ์ผ๋ ค๋ ๋ฆฌ์ค๋๋ฅผ ๋ฑ๋กํ ์ ์๋ค.
Observable ์ธํฐํ์ด์ค์ ๋ฆฌ์ค๋๋ฅผ ์ถ๊ฐ ๋ฐ ์ญ์ ํ๋ ๋ฉ์ปค๋์ฆ์ด ์์ง๋ง ์๋ฆผ์ด ์ ์ก๋๋ ์์ ์ ๊ฐ๋ฐ์๊ฐ ์ง์ ๊ฒฐ์ ํด์ผ ํ๋ค.
๋ ์ฝ๊ฒ ๊ฐ๋ฐํ ์ ์๋๋ก ๋ฐ์ดํฐ ๊ฒฐํฉ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ๋ฆฌ์ค๋ ๋ฑ๋ก ๋ฉ์ปค๋์ฆ์ ๊ตฌํํ๋ BaseObservable ํด๋์ค๋ฅผ ์ ๊ณต.
BaseObservable์ ๊ตฌํํ๋ ๋ฐ์ดํฐ ํด๋์ค๋ ์์ฑ์ด ๋ณ๊ฒฝ๋ ๋ ์๋ฆฌ๋ ์ญํ ์ ํ๋ค.
๋ค์ ์์ ๊ฐ์ด Bindable ์ฃผ์์ getter์ ํ ๋นํ๊ณ setter์ notifyPropertyChanged()
๋ฉ์๋๋ฅผ ํธ์ถํจ์ผ๋ก์จ ์ด ์์
์ ์๋ฃํ๋ค.
class User : BaseObservable() {
@get:Bindable
var firstName: String = ""
set(value) {
field = value
notifyPropertyChanged(BR.firstName)
}
@get:Bindable
var lastName: String = ""
set(value) {
field = value
notifyPropertyChanged(BR.lastName)
}
}
ObservableField์ ๊ฐ์ด Observable
์ ๊ตฌํํ๋ ๊ฐ์ฒด์ ๋ฌ๋ฆฌ LiveData ๊ฐ์ฒด๋ ๋ฐ์ดํฐ ๋ณ๊ฒฝ์ ๊ตฌ๋
ํ๋ ๊ด์ฐฐ์์ ์๋ช
์ฃผ๊ธฐ๋ฅผ ์๊ณ ์๋ค.
์ด ์๋ช
์ฃผ๊ธฐ๋ฅผ ์๋ฉด LiveData ์ฌ์ฉ์ ์ด์ ์ ์ค๋ช
๋ ๋ง์ ์ด์ ์ ํ์ฉํ ์ ์๋ค.
LiveData ๊ฐ์ฒด๋ฅผ ์ฌ์ฉํ๋ ค๋ฉด ์๋ช
์ฃผ๊ธฐ ์์ ์๋ฅผ ์ง์ ํ์ฌ LiveData ๊ฐ์ฒด์ ๋ฒ์๋ฅผ ์ ์ํด์ผ ํ๋ค.
๋ค์ ์์์๋ ๊ฒฐํฉ ํด๋์ค๋ฅผ ์ธ์คํด์คํํ ํ ํ๋์ ์๋ช
์ฃผ๊ธฐ ์์ ์๋ก ์ง์ ํ๋ค.
class ViewModelActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// Inflate view and obtain an instance of the binding class.
val binding: UserBinding = DataBindingUtil.setContentView(this, R.layout.user)
// Specify the current activity as the lifecycle owner.
binding.setLifecycleOwner(this)
}
}
private val _homeCategories = MutableLiveData<List<HomeCategory>>()
val homeCategories: LiveData<List<HomeCategory>>
get() = _homeCategories
<layout>
<data>
<variable
name="viewmodel"
type="com.example.main.MainViewModel"/>
<variable
name="adapter"
type="com.example.main.MainAdapter" />
</data>
...
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:adapter="@{adapter}"
app:adapterHomeCategories="@{viewmodel.homeCategories}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:spanCount="2"
tools:listitem="@layout/item_home_category" />
...
</layout>
object RecyclerViewBindingAdapter {
@JvmStatic
@BindingAdapter("adapterHomeCategories")
fun bindAdapterHomeItemList(recyclerView: RecyclerView, homeCategories: List<HomeCategory>?) {
homeCategories?.let { (recyclerView.adapter as MainAdapter).addItems(homeCategories) }
}
}
๋จ๋ฐฉํฅ DataBinding์ ์ฌ์ฉํ๋ฉด ์๋์๊ฐ์ด ์ฌ์ฉํ๊ฒ๋๋ค.
์์ฑ์ ๊ฐ์ ์ค์ ํ๊ณ ์ด ์์ฑ์ ๋ณ๊ฒฝ์ ๋ฐ์ํ๋ ๋ฆฌ์ค๋๋ฅผ ์ค์ ํ๋ค.
<CheckBox
android:id="@+id/rememberMeCheckBox"
android:checked="@{viewmodel.rememberMe}"
android:onCheckedChanged="@{viewmodel.rememberMeChanged}"/>
์ด๊ฒ์ ์๋ฐฉํฅ DataBinding์ผ๋ก ๋ณ๊ฒฝํ๋ฉด ์๋์ ๊ฐ๋ค
<CheckBox
android:id="@+id/rememberMeCheckBox"
android:checked="@={viewmodel.rememberMe}"/>
'=' ๊ธฐํธ๊ฐ ํฌํจ๋ @={}
ํ๊ธฐ๋ฒ์ ์์ฑ๊ณผ ๊ด๋ จ๋ ๋ฐ์ดํฐ ๋ณ๊ฒฝ์ฌํญ์ ๋ฐ๋ ๋์์ ์ฌ์ฉ์ ์
๋ฐ์ดํธ๋ฅผ ์์ ํ๋ค.
์ด๊ธฐ ๊ฐ์ ์ค์ ํ๊ณ ๊ฐ์ด ๋ณ๊ฒฝ๋ ๋ ์
๋ฐ์ดํธํ๋ ๋ฉ์๋์ @BindingAdapter
๋ฅผ ์ฌ์ฉํ์ฌ ์ด๋
ธํ
์ด์
์ ์ถ๊ฐ
@BindingAdapter("time")
@JvmStatic fun setTime(view: MyView, newValue: Time) {
// Important to break potential infinite loops.
if (view.time != newValue) {
view.time = newValue
}
}
๋ทฐ์์ ๊ฐ์ ์ฝ๋ ๋ฉ์๋์ @InverseBindingAdapter
๋ฅผ ์ฌ์ฉํ์ฌ ์ด๋
ธํ
์ด์
์ ์ถ๊ฐ
@InverseBindingAdapter("time")
@JvmStatic fun getTime(view: MyView) : Time {
return view.getTime()
}
์ด๋ ๋ฐ์ดํฐ ๊ฒฐํฉ์ ๋ฐ์ดํฐ ๋ณ๊ฒฝ ์ ์งํํ ์์
(@BindingAdapter
๋ฅผ ์ฌ์ฉํ์ฌ ์ด๋
ธํ
์ด์
์ ์ถ๊ฐํ ๋ฉ์๋๋ฅผ ํธ์ถํจ) ๊ณผ ๋ทฐ ์์ฑ ๋ณ๊ฒฝ ์ ํธ์ถํ ํญ๋ชฉ(InverseBindingListener๋ฅผ ํธ์ถํจ)์ ํ์
ํ๊ฒ ๋ค๋ค. ํ์ง๋ง ์์ฑ์ด ์ธ์ ๋๋ ์ด๋ป๊ฒ ๋ณ๊ฒฝ๋๋์ง๋ ์ธ์ํ์ง ๋ชปํ๋ค.
์์ฑ์ ๋ณ๊ฒฝ ์๊ธฐ ๋๋ ๋ฐฉ์์ ์๊ธฐ ์ํด์๋ ๋ทฐ์ ๋ฆฌ์ค๋๋ฅผ ์ค์ ํด์ผ ํ๋ค.
๋ฆฌ์ค๋๋ ๋ง์ถค ๋ทฐ์ ์ฐ๊ฒฐ๋ ๋ง์ถค ๋ฆฌ์ค๋์ด๊ฑฐ๋ ํฌ์ปค์ค ์์ค ๋๋ ํ
์คํธ ๋ณ๊ฒฝ๊ณผ ๊ฐ์ ์ผ๋ฐ ์ด๋ฒคํธ์ผ ์ ์๋ค.
@BindingAdapter("app:timeAttrChanged")
@JvmStatic fun setListeners(
view: MyView,
attrChange: InverseBindingListener
) {
// Set a listener for click, focus, touch, etc.
}
๋ฆฌ์ค๋์๋ InverseBindingListener๊ฐ ๋งค๊ฐ๋ณ์๋ก ํฌํจํ๋ค.
InverseBindingListener๋ฅผ ์ฌ์ฉํ๋ฉด ๋ฐ์ดํฐ ๊ฒฐํฉ ์์คํ
์ ์์ฑ์ด ๋ณ๊ฒฝ๋์์์ ์๋ฆด ์ ์๋ค.
๊ทธ๋ฌ๋ฉด ์์คํ
์ @InverseBindingAdapter
๋ฅผ ์ฌ์ฉํ์ฌ ์ด๋
ธํ
์ด์
์ด ์ถ๊ฐ๋ ๋ฉ์๋ ํธ์ถ์ ์์ํ ์ ์๋ค.
๋ฆฌ์ธ์ดํด๋ฌ๋ทฐ ๊ฐ ์์ดํ ์๋ DataBinding์ ์ฌ์ฉํ ์ ์๋ค.
// HomeAdapter.kt
class HomeAdapter(
private val clickListener: HomeItemViewHolder.OnClickListener
) : BindingRecyclerViewAdapter<HomeAdapter.HomeItemViewHolder>() {
private val items = mutableListOf<HomeCategory>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HomeItemViewHolder {
val binding = ItemHomeCategoryBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return HomeItemViewHolder(binding)
}
override fun onBindViewHolder(holder: HomeItemViewHolder, position: Int) {
holder.bind(items[position], clickListener)
holder.binding.executePendingBindings()
}
override fun getItemCount(): Int = items.size
fun addItems(items: List<HomeCategory>) {
this.items.clear()
this.items.addAll(items)
notifyDataSetChanged()
}
class HomeItemViewHolder(val binding: ItemHomeCategoryBinding) : RecyclerView.ViewHolder(binding.root) {
interface OnClickListener {
fun onItemClick(item: HomeCategory)
}
fun bind(item: HomeCategory, clickListener: OnClickListener) {
binding.item = item
binding.clickListener = clickListener
}
}
}
// HomFragment.kt
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
super.onCreateView(inflater, container, savedInstanceState)
return binding {
homeVM = homeViewModel
adapter = HomeAdapter(object : HomeAdapter.HomeItemViewHolder.OnClickListener {
override fun onItemClick(item: HomeCategory) {
startActivity(Intent(context, CategoryActivity::class.java))
}
})
}.root
}
<!-- home_fragment.xml-->
<com.beomjo.whitenoise.ui.view.GridRecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:adapter="@{adapter}"
app:adapterHomeCategories="@{homeVM.homeCategories}"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:spanCount="2"
tools:listitem="@layout/item_home_category" />
<!-- item_home_category.xml-->
<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="item"
type="com.beomjo.whitenoise.model.HomeCategory" />
<variable
name="clickListener"
type="com.beomjo.whitenoise.ui.adapters.HomeAdapter.HomeItemViewHolder.OnClickListener" />
</data>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="24dp"
app:cardElevation="0dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="180dp"
android:background="@{item.getPrimaryColor()}"
android:onClick="@{()->clickListener.onItemClick(item)}"
android:padding="30dp"
tools:background="@android:color/holo_blue_bright">
<ImageView
android:layout_width="36dp"
android:layout_height="36dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:loadImage="@{item.iconUrl}" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@{item.name}"
android:textColor="@color/white"
android:textSize="24sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:text="afaf" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</layout>
object RecyclerViewBindingAdapter {
@JvmStatic
@BindingAdapter("adapterHomeCategories")
fun bindAdapterHomeItemList(recyclerView: RecyclerView, homeCategories: List<HomeCategory>?) {
homeCategories?.let { (recyclerView.adapter as HomeAdapter).addItems(homeCategories) }
}
}