๊ฐœ๋ฐœ/Android

[Android] Kotlin Delegates in Android: Android ๊ฐœ๋ฐœ์— Properties Delegate ํ™œ์šฉํ•˜๊ธฐ

๋„๋ฆฌ ๐ŸŸ 2021. 4. 8. 23:27

์ž‘์„ฑ์ผ: 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๋ฅผ ์–ด๋–ป๊ฒŒ ์‚ฌ์šฉํ• ์ง€ ์ƒ๊ฐํ•˜๋Š” ๊ฒƒ์— ํ‘น ๋น ์กŒ์œผ๋ฉด ์ข‹๊ฒ ์Šต๋‹ˆ๋‹ค!