์์ฑ์ผ: 2019.12.02
์๋ฌธ Kotlin Delegates in Android: Utilizing the power of Delegated Properties in Android development์ ๋ฒ์ญํ ๊ธ์ ๋๋ค.
Delegate์ ๋ํ ์ค๋ช ๊ณผ ํจ๊ป Android ๊ฐ๋ฐ์์ ์์ฃผ ์ฌ์ฉ๋๋ ์ฝ๋๋ค์ ์์ ๋ก ์ฝ๊ฒ ์ค๋ช ํด์ค๋๋ค. ์ค๋ณต๋๋ ์ฝ๋๋ค์ Delegate๋ฅผ ํตํด ๊ฐ์ ํด๋ณผ ์ ์์ ๊ฒ ๊ฐ๋ค์ ๐
Kotlin์ ์ดํ๋ฆฌ์ผ์ด์ ๊ฐ๋ฐ์ ๋์์ด ๋๋ ๊ธฐ๋ฅ์ ๊ฐ์ง ์์ฃผ ๋ฉ์ง ์ธ์ด์ ๋๋ค. ๊ทธ ์ค ํ๋๊ฐ Delegate Propertis์ ๋๋ค. ์ด ๊ธ์์ Delegate๊ฐ ์๋๋ก์ด๋ ๊ฐ๋ฐ์ ์ด๋ค ๋์์ด ๋๋์ง ์ดํด๋ณด๊ฒ ์ต๋๋ค.
- Basics
- Fragment arguments
- SharedPreferences delegates
- View delegates
- Conclusion
Basics
๋จผ์ Delegate๋ ๋ฌด์์ด๊ณ , ์ด๋ป๊ฒ ๋์ํ ๊น์? ์ด๋ ค์ ๋ณด์ผ ์๋ ์์ง๋ง ์ ๋ง ๋ณต์กํ์ง ์์ต๋๋ค.
Delegate๋ ํ๋กํผํฐ์ ๊ฐ์ ์ ๊ณตํ๊ณ , ๊ทธ ๊ฐ์ ๋ณํ๋ฅผ ์ฒ๋ฆฌํ๋ ํด๋์ค์ผ ๋ฟ์ ๋๋ค. ์ด๋ฅผ ํตํด ํ๋กํผํฐ์ getter-setter ๋ก์ง์ ๋ถ๋ฆฌ๋ ํด๋์ค๋ก ์ด๋ ๋๋ ์์(delegate)ํ ์ ์๊ณ , ๊ทธ ๋ก์ง์ ์ฌ์ฌ์ฉํ ์๋ ์์ต๋๋ค.
param์ด๋ผ๋ String ํ๋กํผํฐ๊ฐ ํญ์ ์, ๋ค ๊ณต๋ฐฑ์ด ์ ๊ฑฐ๋ ๋ฌธ์์ด์ ๊ฐ์ ธ์ผ ํ๋ค๊ณ ํด๋ด ์๋ค. ๊ทธ๋ผ ํ๋กํผํฐ์ setter๋ฅผ ์๋์ฒ๋ผ ๊ตฌํํ ์ ์์ต๋๋ค:
class Example {
var param: String = ""
set(value) {
field = value.trim()
}
}
Kotlin syntax์ ๋ํด ์ ๋ชจ๋ฅด๊ฒ ๋ค๋ฉด, Kotlin ๋ฌธ์์ Properties๋ฅผ ์ฐธ๊ณ ํด์ฃผ์ธ์.
๊ทธ๋ผ ์ด ๋ก์ง์ ๋ค๋ฅธ ํด๋์ค์์ ์ฌ์ฌ์ฉํ๊ณ ์ถ์ ๊ฒฝ์ฐ ์ด๋ป๊ฒ ํด์ผ ํ ๊น์? ์ด์ Delegate๊ฐ ๋ฑ์ฅํ ๋์ ๋๋ค:
class TrimDelegate : ReadWriteProperty<Any?, String> {
private var trimmedValue: String = ""
override fun getValue(
thisRef: Any?,
property: KProperty<*>
): String {
return trimmedValue
}
override fun setValue(
thisRef: Any?,
property: KProperty<*>, value: String
) {
trimmedValue = value.trim()
}
}
์์ ๊ฐ์ด Delegate๋ ํ๋กํผํฐ์ getter, setter 2๊ฐ์ ๋ฉ์๋๋ฅผ ๊ฐ๋ ํด๋์ค์ ๋๋ค. KProperty์ ์ธ์คํด์ค์ธ property์ ์ด ํ๋กํผํฐ๋ฅผ ๊ฐ๋ ๊ฐ์ฒด๋ฅผ ๋ํ๋ด๋ thisRef๋ฅผ ์ ๊ณตํฉ๋๋ค. ์ด๊ฒ ๋ค์์! ๊ทธ๋ฆฌ๊ณ ์๋์ฒ๋ผ ์๋ก ์์ฑํ Delegate๋ฅผ ์ฌ์ฉํ ์ ์์ต๋๋ค.
class Example {
var param: String by TrimDelegate()
}
๋์ผํ ๋์์ ์๋์ฒ๋ผ ์ธ ์ ์์ต๋๋ค.
class Example {
private val delegate = TrimDelegate()
var param: String
get() = delegate.getValue(this, ::param)
set(value) {
delegate.setValue(this, ::param, value)
}
}
::param์ ํ๋กํผํฐ์ ๋ํด KProperty์ธ์คํด์ค๋ฅผ ๋ฐํํฉ๋๋ค.
๋ณด์๋ ๊ฒ์ฒ๋ผ Delegate์๋ ๋ชจํธํ ๋ถ๋ถ์ด ์์ต๋๋ค. ๊ฐ๋จํ๋ฉด์๋ ์์ฃผ ์ ์ฉํฉ๋๋ค. ์ด์ ๋ช ๊ฐ์ง Android ์ฌ์ฉ ์์ ๋ฅผ ๋ณด๊ฒ ์ต๋๋ค.
๊ณต์ ๋ฌธ์์์ Delegate์ ๋ํด ๋ ์์๋ณผ ์ ์์ต๋๋ค.
Fragment arguments
Fragment์ ํ๋ผ๋ฏธํฐ๋ค์ ๋๊ฒจ์ผ ํ ๋๊ฐ ์์ฃผ ์์ต๋๋ค. ๋ณดํต ์๋์ ๊ฐ์ ์ฝ๋๋ก ๊ตฌํ๋ฉ๋๋ค.
class DemoFragment : Fragment() {
private var param1: Int? = null
private var param2: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let { args ->
param1 = args.getInt(Args.PARAM1)
param2 = args.getString(Args.PARAM2)
}
}
companion object {
private object Args {
const val PARAM1 = "param1"
const val PARAM2 = "param2"
}
fun newInstance(param1: Int, param2: String): DemoFragment =
DemoFragment().apply {
arguments = Bundle().apply {
putInt(Args.PARAM1, param1)
putString(Args.PARAM2, param2)
}
}
}
}
static method์ธ newInstance ์์์ fragment๋ฅผ ์์ฑํฉ๋๋ค. fragment์ argument์ ํ๋ผ๋ฏธํฐ๋ค์ ๋ฃ๊ณ , onCreate ์์ ์ ๋ค์ ๊บผ๋ ๋๋ค.
Argument ๊ด๋ จ ๋ก์ง์ ํ๋กํผํฐ getter, setter๋ก ๋ถ๋ฆฌํด์ ์ฝ๋๋ฅผ ์ข ๋ ๊น๋ํ๊ฒ ๋ง๋ค ์ ์์ต๋๋ค.
class DemoFragment : Fragment() {
private var param1: Int?
get() = arguments?.getInt(Args.PARAM1)
set(value) {
value?.let {
arguments?.putInt(Args.PARAM1, it)
} ?: arguments?.remove(Args.PARAM1)
}
private var param2: String?
get() = arguments?.getString(Args.PARAM2)
set(value) {
arguments?.putString(Args.PARAM2, value)
}
companion object {
private object Args {
const val PARAM1 = "param1"
const val PARAM2 = "param2"
}
fun newInstance(param1: Int, param2: String): DemoFragment =
DemoFragment().apply {
this.param1 = param1
this.param2 = param2
}
}
}
ํ์ง๋ง ์ฌ์ ํ ๊ฐ์ ์ฝ๋๋ฅผ ํ๋กํผํฐ๋ง๋ค ์จ์ค์ผ ํ๊ณ , ํ๋กํผํฐ๊ฐ ๋ง์์ง์๋ก ๋ฒ๊ฑฐ๋ก์ ์ง๋๋ค. ๊ฒ๋ค๊ฐ argument์ ๋ํ ๋ช ์์ ์ธ ๋์๋ค์ด ์กฐ๊ธ ๋ณต์กํด ๋ณด์ด๊ธฐ๋ ํฉ๋๋ค.
์ฝ๋๋ฅผ ์ข ๋ ์์๊ฒ ๋ง๋ค ์ ์์๊น์? ๋น์ฐํ ์์ฃ ! ์์ํ์ ๊ฒ์ฒ๋ผ property delegate๋ฅผ ์ฌ์ฉํ ๊ฒ์ ๋๋ค.
๋จผ์ ์ค๋น๊ฐ ์กฐ๊ธ ํ์ํฉ๋๋ค. Fragment์ argument๋ Bundle ๊ฐ์ฒด์ ์ ์ฅ๋๋๋ฐ, ๊ฐ์ ํ์ ์ ๋ฐ๋ผ ์ ์ฅํ๋ ๋ฉ์๋๋ ๋ถ๋ฆฌ๋์ด ์์ต๋๋ค. ์์์ ํ์ ์ธ ๊ฐ์ Bundle์ ๋ฃ๊ณ , ์ง์ํ์ง ์๋ ํ์ ์ด๋ฉด ์์ธ๋ฅผ ๋์ง๋ ํ์ฅ ํจ์๋ฅผ ๋ง๋ค์ด ๋ด ์๋ค.
fun <T> Bundle.put(key: String, value: T) {
when (value) {
is Boolean -> putBoolean(key, value)
is String -> putString(key, value)
is Int -> putInt(key, value)
is Short -> putShort(key, value)
is Long -> putLong(key, value)
is Byte -> putByte(key, value)
is ByteArray -> putByteArray(key, value)
is Char -> putChar(key, value)
is CharArray -> putCharArray(key, value)
is CharSequence -> putCharSequence(key, value)
is Float -> putFloat(key, value)
is Bundle -> putBundle(key, value)
is Parcelable -> putParcelable(key, value)
is Serializable -> putSerializable(key, value)
else -> throw IllegalStateException("Type of property $key is not supported")
}
}
์ ์ด์ Delegate๋ฅผ ์์ฑํ ์ค๋น๊ฐ ๋์์ต๋๋ค:
class FragmentArgumentDelegate<T : Any> :
ReadWriteProperty<Fragment, T> {
@Suppress("UNCHECKED_CAST")
override fun getValue(
thisRef: Fragment,
property: KProperty<*>
): T {
val key = property.name
return thisRef.arguments
?.get(key) as? T
?: throw IllegalStateException("Property ${property.name} could not be read")
}
override fun setValue(
thisRef: Fragment,
property: KProperty<*>, value: T
) {
val args = thisRef.arguments
?: Bundle().also(thisRef::setArguments)
val key = property.name
args.put(key, value)
}
}
Delegate๋ Fragment argument๋ก๋ถํฐ ํ๋กํผํฐ ๊ฐ์ ์ฝ์ต๋๋ค. ๊ทธ๋ฆฌ๊ณ ํ๋กํผํฐ ๊ฐ์ด ๋ณ๊ฒฝ๋๋ฉด, Delegate๋ Fragment argument๋ฅผ ๋ค์ ๊ฐ์ ธ์ต๋๋ค (๋๋ Fragment๊ฐ ์์ง Bundle์ ๊ฐ์ง์ง ์์ผ๋ฉด ์๋ก์ด Bundle์ ์์ฑํ๊ณ setํฉ๋๋ค). ์ด arguments์ ์์์ ๋ง๋ ํ์ฅ ํจ์ Bundle.put์ ์ด์ฉํ์ฌ ์ ๊ฐ์ ์๋๋ค.
ReadWriteProperty๋ ๋ ๊ฐ์ง ํ์ ํ๋ผ๋ฏธํฐ๋ฅผ ๊ฐ๋ ์ ๋ค๋ฆญ ์ธํฐํ์ด์ค์ ๋๋ค. ์์ ์์๋ ์ฒซ ๋ฒ์งธ๋ฅผ Fragment๋ก ์ง์ ํ๋๋ฐ, ์ด๋ ์ด Delegate๊ฐ Fragment ๋ด๋ถ ํ๋กํผํฐ์ ๋ํด์๋ง ์ฌ์ฉ๊ฐ๋ฅํ๋ค๋ ๊ฒ์ ์๋ฏธํฉ๋๋ค. thisRef๋ฅผ ํตํด Fragment ์ธ์คํด์ค์ ์ ๊ทผํ๊ณ , ๊ทธ arguments๋ฅผ ๊ด๋ฆฌํ ์ ์์ต๋๋ค.
ํ๋กํผํฐ์ ์ด๋ฆ์ Argument์ ํค๋ก ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์, ๋ ์ด์ ํค๋ฅผ ์์๋ก ์ ์ฅํ์ง ์์๋ ๋ฉ๋๋ค.
ReadWriteProperty์ ๋ ๋ฒ์งธ ํ๋ผ๋ฏธํฐ๋ ํ๋กํผํฐ์ ๊ฐ์ ํ์ ์ ๋ํ๋ ๋๋ค. ์ ์์ ์์ ๋ช ์์ ์ผ๋ก non-nullable ํ์ ์ผ๋ก ์ง์ ํ๊ณ , ์ฝ์ ์ ์๋ ๊ฐ์ด๋ฉด ์์ธ๋ฅผ ๋ฐ์์์ผฐ์ต๋๋ค. Fragment์์ non-nullable ํ๋กํผํฐ๋ฅผ ์ฌ์ฉํ๋๋ก ๊ฐ์ ํ๊ณ , ๊ท์ฐฎ์ null ์ฒดํฌ๋ฅผ ํ์ง ์์๋ ๋ฉ๋๋ค.
ํ์ง๋ง nullable ํ๋กํผํฐ๊ฐ ํ์ํ ๋๋ ์์ต๋๋ค. ๊ทธ๋ผ ๋ค๋ฅธ Delegate๋ฅผ ๋ง๋ค์ด์, argument๋ฅผ ์ฝ์ ์ ์์ผ๋ฉด ์์ธ๋ฅผ ๋ฐ์์ํค์ง ์๊ณ null์ ๋ฐํํ๋๋ก ๋ง๋ค์ด ๋ด ์๋ค
class FragmentNullableArgumentDelegate<T : Any?> :
ReadWriteProperty<Fragment, T?> {
@Suppress("UNCHECKED_CAST")
override fun getValue(
thisRef: Fragment,
property: KProperty<*>
): T? {
val key = property.name
return thisRef.arguments?.get(key) as? T
}
override fun setValue(
thisRef: Fragment,
property: KProperty<*>, value: T?
) {
val args = thisRef.arguments
?: Bundle().also(thisRef::setArguments)
val key = property.name
value?.let { args.put(key, it) } ?: args.remove(key)
}
}
ํธ๋ฆฌ๋ฅผ ์ํด ๋ช ๊ฐ์ง ํจ์๋ฅผ ์์ฑํด ๋ณด๊ฒ ์ต๋๋ค.(ํ์๋ ์๋๊ณ ๋จ์ง ๋ณด๊ธฐ ์ข๊ฒ ํ๋ ค๋ ๋ชฉ์ ์ ๋๋ค.)
fun <T : Any> argument(): ReadWriteProperty<Fragment, T> =
FragmentArgumentDelegate()
fun <T : Any> argumentNullable(): ReadWriteProperty<Fragment, T?> =
FragmentNullableArgumentDelegate()
๊ทธ๋ผ Delegate๋ฅผ ์ฌ์ฉํ๋ ์ฝ๋๋ ์๋์ ๊ฐ์ต๋๋ค:
class DemoFragment : Fragment() {
private var param1: Int by argument()
private var param2: String by argument()
companion object {
fun newInstance(param1: Int, param2: String): DemoFragment =
DemoFragment().apply {
this.param1 = param1
this.param2 = param2
}
}
}
๊ฝค ๊น๋ํ์ง ์๋์?
SharedPreferences delegate
๋ค์ ์ฑ ์คํ์ ๋น ๋ฅด๊ฒ ๊ฐ์ ๊ฐ์ ธ์ค๊ธฐ ์ํด ๋ฉ๋ชจ๋ฆฌ์ ๊ฐ์ ์ ์ฅํ๋ ๊ฒฝ์ฐ๋ ์์ฃผ ์์ต๋๋ค. ์๋ฅผ ๋ค์ด, ์ฌ์ฉ์๊ฐ ์ฑ์ ์ปค์คํฐ๋ง์ด์ฆํ ์ ์๋ ๋ช ๊ฐ์ง ์ค์ ์ ์ ์ฅํด์ผ ํฉ๋๋ค. ํํ ๋ฐฉ๋ฒ ์ค ํ๋๊ฐ SharedPreferences๋ฅผ ์ฌ์ฉํ์ฌ key-value ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๋ ๋ฐฉ๋ฒ์ ๋๋ค.
3๊ฐ์ง์ ๊ฐ์ ์ ์ฅํ๊ณ ๊ฐ์ ธ์ค๋ ์ญํ ์ ํ๋ ํด๋์ค๊ฐ ์๋ค๊ณ ๊ฐ์ ํด๋ด ์๋ค.
class Settings(context: Context) {
private val prefs: SharedPreferences =
PreferenceManager.getDefaultSharedPreferences(context)
fun getParam1(): String? {
return prefs.getString(PrefKeys.PARAM1, null)
}
fun saveParam1(param1: String?) {
prefs.edit().putString(PrefKeys.PARAM1, param1).apply()
}
fun getParam2(): Int {
return prefs.getInt(PrefKeys.PARAM2, 0)
}
fun saveParam2(param2: Int) {
prefs.edit().putInt(PrefKeys.PARAM2, param2).apply()
}
fun getParam3(): String {
return prefs.getString(PrefKeys.PARAM3, null)
?: DefaulsValues.PARAM3
}
fun saveParam3(param3: String) {
prefs.edit().putString(PrefKeys.PARAM2, param3).apply()
}
companion object {
private object PrefKeys {
const val PARAM1 = "param1"
const val PARAM2 = "param2"
const val PARAM3 = "special_key_param3"
}
private object DefaulsValues {
const val PARAM3 = "defaultParam3"
}
}
}
์ฌ๊ธฐ์ Default SharedPreferences๋ฅผ ๊ฐ์ ธ์ค๋ ๋ก์ง๊ณผ ๊ฐ ๊ฐ๋ค์ ๊ฐ์ ธ์ค๊ณ ์ ์ฅํ๋ ๋ฉ์๋๋ฅผ ์ ๊ณตํ๊ณ ์์ต๋๋ค. ๋ํ param3๋ ๋ค๋ฅธ ๊ฐ๋ค๊ณผ๋ ์กฐ๊ธ ๋ค๋ฅด๊ฒ ๋ค๋ฅธ ํ์์ key์ default ๊ฐ์ ์ฌ์ฉํ๋๋ก ํ์ต๋๋ค.
์ฝ๋๋ฅผ ๋ณด๋ฉด ์ค๋ณต๋ ๋ถ๋ถ๋ค์ด ์ข ๋ณด์ ๋๋ค. ๋ฌผ๋ก ์ค๋ณต๋๋ ๋ถ๋ถ๋ค์ private method๋ก ์ฎ๊ธธ ์๋ ์์ต๋๋ค. ํ์ง๋ง ๊ทธ๋ ๊ฒ ํ๋๋ผ๋ ์ฌ์ ํ ๊ฑฐ์ถ์ฅ์ค๋ฌ์ด ์ฝ๋๊ฐ ๋จ์์๊ฒ ๋ฉ๋๋ค. ๊ทธ๋ฆฌ๊ณ ๋ค๋ฅธ ํด๋์ค์์ ์ด ๋ก์ง์ ์ฌ์ฌ์ฉํ๊ณ ์ถ์ ๊ฒฝ์ฐ์ ์ด๋ป๊ฒ ํด์ผ ํ ๊น์? Delegate๊ฐ ์ฝ๋๋ฅผ ์ผ๋ง๋ ๋ ๊น๋ํ๊ฒ ๋ง๋ค ์ ์๋์ง ์ดํด๋ณด๊ฒ ์ต๋๋ค.
์ข ๋ ํฅ๋ฏธ๋กญ๊ฒ ํ๊ธฐ ์ํด, ์กฐ๊ธ ๋ค๋ฅธ ๋ฐฉ์์ ์จ ๋ณผ๊น์? ์ด๋ฒ์๋ Object expressions๋ฅผ ์ฌ์ฉํ์ฌ SharedPreferences ํด๋์ค์ ํ์ฅ ํจ์๋ฅผ ๋ง๋ค์ด ๋ณด๊ฒ ์ต๋๋ค.
fun SharedPreferences.string(
defaultValue: String = "",
key: (KProperty<*>) -> String = KProperty<*>::name
): ReadWriteProperty<Any, String> =
object : ReadWriteProperty<Any, String> {
override fun getValue(
thisRef: Any,
property: KProperty<*>
) = getString(key(property), defaultValue)
override fun setValue(
thisRef: Any,
property: KProperty<*>,
value: String
) = edit().putString(key(property), value).apply()
}
SharedPreferences ํ์ฅ ํจ์๋ ์ต๋ช ์ ReadWriteProperty๋ฅผ ๋ฐํํฉ๋๋ค.
Delegate๋ key ํจ์๋ฅผ ์ฌ์ฉํ์ฌ Preferences๋ก๋ถํฐ String์ผ๋ก ๊ฐ์ ์ฝ์ต๋๋ค. ๊ธฐ๋ณธ์ผ๋ก ํค๋ ํ๋กํผํฐ์ ์ด๋ฆ์ด์ด์ ์์์ ์ ์ฅํ๊ฑฐ๋ ์ ๋ฌํ ํ์๊ฐ ์์ต๋๋ค. ํ์ง๋ง Preferences ๋ด๋ถ์์์ ํค ์ถฉ๋์ด ๊ฑฑ์ ๋๊ฑฐ๋ ๋ช ์์ ์ผ๋ก ํค์ ์ ๊ทผํ๊ณ ์ถ์ ๊ฒฝ์ฐ, ์ปค์คํ ํค๋ฅผ ์ ๋ฌํ ์ ์๋ ์ต์ ์ด ์์ต๋๋ค. ๋ํ Preferences์์ ๊ฐ์ ์ฐพ์ง ๋ชปํ ๊ฒฝ์ฐ์ default ๊ฐ๋ ์ ๋ฌํ ์ ์์ต๋๋ค. Settings ์์ ๊ฐ ๋์ํ๋๋ก ํ๋ ค๋ฉด String?๊ณผ Int ํ์ ์ ์ํ ๊ฑฐ์ ๋์ผํ ๋ก์ง์ Delegate๊ฐ 2๊ฐ ๋ ํ์ํฉ๋๋ค.
fun SharedPreferences.stringNullable(
defaultValue: String? = null,
key: (KProperty<*>) -> String = KProperty<*>::name
): ReadWriteProperty<Any, String?> =
object : ReadWriteProperty<Any, String?> {
override fun getValue(
thisRef: Any,
property: KProperty<*>
) = getString(key(property), defaultValue)
override fun setValue(
thisRef: Any,
property: KProperty<*>,
value: String?
) = edit().putString(key(property), value).apply()
}
fun SharedPreferences.int(
defaultValue: Int = 0,
key: (KProperty<*>) -> String = KProperty<*>::name
): ReadWriteProperty<Any, Int> =
object : ReadWriteProperty<Any, Int> {
override fun getValue(
thisRef: Any,
property: KProperty<*>
) = getInt(key(property), defaultValue)
override fun setValue(
thisRef: Any,
property: KProperty<*>,
value: Int
) = edit().putInt(key(property), value).apply()
}
๋ง์ง๋ง์ผ๋ก Settings ํด๋์ค๋ฅผ ์ด๋ ๊ฒ ์ ๋ฆฌํ ์ ์์ต๋๋ค.
class Settings(context: Context) {
private val prefs: SharedPreferences =
PreferenceManager.getDefaultSharedPreferences(context)
var param1 by prefs.stringNullable()
var param2 by prefs.int()
var param3 by prefs.string(
key = { "KEY_PARAM3" },
defaultValue = "default"
)
}
์ด์ ํจ์ฌ ๋ณด๊ธฐ ์ข๋ค์. ๋์ค์ ์๋ก์ด ๊ฐ์ ์ถ๊ฐํด์ผ ํ๋ค๋ฉด ํ ์ค์ ์ฝ๋๋ง์ผ๋ก ์ถ๊ฐํ ์ ์์ต๋๋ค!
View delegates
3๊ฐ์ ํ ์คํธ ํ๋(title, subtitle, description)๋ฅผ ๊ฐ๋ ์ปค์คํ ๋ทฐ๊ฐ ์๋ค๊ณ ๊ฐ์ ํด๋ณด๊ฒ ์ต๋๋ค:
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/tvTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/tvSubtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/tvDescription"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
๊ทธ๋ฆฌ๊ณ ๊ฐ ํ๋์ ํ ์คํธ์ ์ ๊ทผํ๊ณ ์์ ํ๋ ๋ฉ์๋๊ฐ ํ์ํ๋ค๊ณ ํด๋ด ์๋ค.
class CustomView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : FrameLayout(context, attrs) {
var title: String
get() = tvTitle.text.toString()
set(value) {
tvTitle.text = value
}
var subtitle: String
get() = tvSubtitle.text.toString()
set(value) {
tvSubtitle.text = value
}
var description: String
get() = tvDescription.text.toString()
set(value) {
tvDescription.text = value
}
init {
inflate(context, R.layout.custom_view, this)
}
}
์ฌ๊ธฐ์ ๋ ์ด์์์ ๊ฐ ๋ทฐ์ ์ ๊ทผํ๊ธฐ ์ํด Kotlin Android Extensions์ View binding์ ์ฌ์ฉํ์ต๋๋ค.
๋ค๋ฅธ ํด๋์ค๋ก ์ฝ๊ฒ ๋ถ๋ฆฌํ ์ ์๋ ์ฝ๋๊ฐ ๋ช ํํ ๋ณด์ ๋๋ค. ์ด์ Delegate์ ๋์์ ๋ฐ์์ ๋ถ๋ฆฌํด๋ด ์๋ค!
TextView์ ์์ ์ text์ ๋ํ Delegate๋ฅผ ๋ฐํํ๋ ํ์ฅ ํจ์๋ฅผ ์ถ๊ฐํ์ต๋๋ค.
fun TextView.text(): ReadWriteProperty<Any, String> =
object : ReadWriteProperty<Any, String> {
override fun getValue(
thisRef: Any,
property: KProperty<*>
): String = text.toString()
override fun setValue(
thisRef: Any,
property: KProperty<*>, value: String
) {
text = value
}
}
๊ทธ๋ผ CustomView์์ ์ด๋ ๊ฒ ์ฌ์ฉํ ์ ์์ต๋๋ค
class CustomView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : FrameLayout(context, attrs) {
init {
inflate(context, R.layout.custom_view, this)
}
var title by tvTitle.text()
var subtitle by tvSubtitle.text()
var description by tvDescription.text()
}
View๋ null์ด ๋๋ฉด ์ ๋๊ธฐ ๋๋ฌธ์, init ๋ธ๋ก์์ View inflating์ด ์๋ฃ๋ ์ดํ์ ํ๋กํผํฐ๋ฅผ ์ด๊ธฐํํด์ผ ํฉ๋๋ค.
๊ธฐ์กด ์ฝ๋๋ฅผ ์์ฒญ๋๊ฒ ๊ฐ์ ํ๋ค๊ณ ๋ณผ ์๋ ์๊ฒ ์ง๋ง, ์์ ์ Delegate์ ์ฅ์ ์ ๋ํ๋ด๋ ๊ฒ์ ๋๋ค.
๋ฌผ๋ก TextView๋ก ํ์ ์ง์ง ์์๋ ๋ฉ๋๋ค. ์๋ฅผ ๋ค์ด, view visibility์ ๋ํ Delegate๋ ๋ง๋ค ์ ์์ต๋๋ค(keepBounds๋ View๊ฐ ๋ ์ด์์์์ ์์ง ๊ณต๊ฐ์ ์ฐจ์งํ๊ณ ์๋์ง, ์๋์ง ๊ฒฐ์ ํฉ๋๋ค).
fun View.isVisible(keepBounds: Boolean): ReadWriteProperty<Any, Boolean> =
object : ReadWriteProperty<Any, Boolean> {
override fun getValue(
thisRef: Any,
property: KProperty<*>
): Boolean = visibility == View.VISIBLE
override fun setValue(
thisRef: Any,
property: KProperty<*>,
value: Boolean
) {
visibility = when {
value -> View.VISIBLE
keepBounds -> View.INVISIBLE
else -> View.GONE
}
}
}
ProgressBar์ progress๋ฅผ ์ํ Delegate๋ ๋ง๋ค ์ ์์ต๋๋ค.
fun ProgressBar.progress(): ReadWriteProperty<Any, Float> =
object : ReadWriteProperty<Any, Float> {
override fun getValue(
thisRef: Any,
property: KProperty<*>
): Float = if (max == 0) 0f else progress / max.toFloat()
override fun setValue(
thisRef: Any,
property: KProperty<*>, value: Float
) {
progress = (value * max).toInt()
}
}
์๋ ์ฝ๋๋ CustomView์ ProgressBar๊ฐ ์๋ค๋ฉด ์์ Delegate๋ค์ ์ด๋ป๊ฒ ์ธ ์ ์๋์ง ๋ณด์ฌ์ค๋๋ค.
class CustomView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : FrameLayout(context, attrs) {
init {
inflate(context, R.layout.custom_view, this)
}
var title by tvTitle.text()
var subtitle by tvSubtitle.text()
var description by tvDescription.text()
var progress by progressBar.progress()
var isProgressVisible by progressBar.isVisible(keepBounds = false)
}
๋ณด์๋ ๊ฒ์ฒ๋ผ ์ํ๋ ๊ฒ์ ์ผ๋ง๋ ์ง ์์ํ ์ ์์ต๋๋ค!
Conclusion
Android ๊ฐ๋ฐ์์ Kotlin property delegate๋ฅผ ์ฌ์ฉํ๋ ๋ช ๊ฐ์ง ์์ ๋ฅผ ์ดํด๋ดค์ต๋๋ค. ๋ฌผ๋ก ์ฌ๋ฌ๋ถ์ ์ดํ๋ฆฌ์ผ์ด์ ์์ ์ด๋ฅผ ์ฌ์ฉํ ๋ ๋ค๋ฅธ ๋ง์ ๋ฐฉ๋ฒ์ ์๊ฐํด๋์ ์๋ ์์ต๋๋ค(๋ง์ฝ ์๋ค๋ฉด ์ธ์ ๋ ์ง ๋๊ธ๋ก ์๋ ค์ฃผ์ธ์). ์ด ํฌ์คํ ์ ๋ชฉ์ ์ ํ๋กํผํฐ ์์์ด ์ผ๋ง๋ ๊ฐ๋ ฅํ ๋๊ตฌ์ด๊ณ ์ด๋ป๊ฒ ์ฌ์ฉํ๋์ง ์๋ฆฌ๊ธฐ ์ํจ์ด์์ต๋๋ค. ๊ทธ๋ฆฌ๊ณ ์ฌ๋ฌ๋ถ์ด ์ด์ Delegate๋ฅผ ์ด๋ป๊ฒ ์ฌ์ฉํ ์ง ์๊ฐํ๋ ๊ฒ์ ํน ๋น ์ก์ผ๋ฉด ์ข๊ฒ ์ต๋๋ค!
'๊ฐ๋ฐ > Android' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
Android ๊ฐ๋ฐ์๋ฅผ ์ํ ํจ์ํ ํ๋ก๊ทธ๋๋ฐ Part 2 (0) | 2021.04.08 |
---|---|
Android ๊ฐ๋ฐ์๋ฅผ ์ํ ํจ์ํ ํ๋ก๊ทธ๋๋ฐ Part 1 (0) | 2021.04.08 |
[Android] Kotlin Extension Function์ ์ฌ์ฉํ์ฌ ๋๋ธ ํด๋ฆญ ๋ฐฉ์งํ๊ธฐ (0) | 2021.04.08 |
[Android] Jetpack - LiveData (0) | 2021.04.08 |
[Android] 100% ์ด๋ณด์๋ฅผ ์ํ RxJava(RxJava for 100% beginners)-part 1 (0) | 2021.04.08 |