Lesson 8: App architecture (UI Layer)

HanbinΒ·2021λ…„ 9μ›” 1일
0

Teach Android Development

λͺ©λ‘ 보기
8/13
post-thumbnail

πŸ’‘ Teach Android Development

κ΅¬κΈ€μ—μ„œ μ œκ³΅ν•˜λŠ” ꡐ윑자료λ₯Ό μ •λ¦¬ν•˜κΈ° μœ„ν•œ ν¬μŠ€νŠΈμž…λ‹ˆλ‹€.

Android Development Resources for Educators

Android app architecture

Avoid short-term hacks

  • μ΄‰λ°•ν•œ 일정 같은 μ™ΈλΆ€ μš”μΈμœΌλ‘œ 인해 μ•± λ””μžμΈ 및 ꡬ쑰에 λŒ€ν•΄ 잘λͺ»λœ 결정이 λ‚΄λ €μ§ˆ 수 μžˆμŠ΅λ‹ˆλ‹€.
  • 결정은 ν–₯ν›„ μž‘μ—…μ— 영ν–₯을 λ―ΈμΉ©λ‹ˆλ‹€.(앱을 μž₯기적으둜 μœ μ§€ν•˜κΈ° μ–΄λ €μšΈ 수 있음.)
  • 일정을 λ§žμΆ”λŠ” 것과 μœ μ§€λ³΄μˆ˜ λΆ€λ‹΄κ°„μ˜ κ· ν˜•μ΄ ν•„μš”ν•©λ‹ˆλ‹€.

Examples of short-term hacks

단기 결정에 λŒ€ν•œ μ΄λŸ¬ν•œ μ˜ˆλŠ” 기술적 λΆ€μ±„λ‘œ μ΄μ–΄μ§ˆ 수 있으며 μœ μ—°ν•˜κ³  효율적인 λ°©λ²•μœΌλ‘œ 앱을 ν™•μž₯ν•˜λŠ” 것을 μ–΄λ ΅κ²Œ λ§Œλ“€ 수 μžˆμŠ΅λ‹ˆλ‹€.

  • λ§Žμ€ κΈ°κΈ°λ₯Ό κ³ λ €ν•˜μ§€ μ•Šκ³  νŠΉμ • 기기에 맞게 μ•± μ‘°μ •.
  • third party codeλ₯Ό μ™„μ „νžˆ μ΄ν•΄ν•˜μ§€ μ•Šκ³  λ³΅μ‚¬ν•˜μ—¬ λΆ™μ—¬λ„£κΈ°.
  • Acitivty νŒŒμΌμ— λͺ¨λ“  λΉ„μ§€λ‹ˆμŠ€ 둜직 배치.
  • μ½”λ“œμ—μ„œ μ‚¬μš©μžμ—κ²Œ ν‘œμ‹œλ˜λŠ” λ¬Έμžμ—΄ ν•˜λ“œμ½”λ”©.

Why you need good app architecture

μš°μˆ˜ν•œ app architectureλŠ” 앱이 ν™•μž₯ κ°€λŠ₯ν•˜κ³  μ•ˆμ •μ μ΄λ©° κ΄€λ¦¬ν•˜κΈ° μ‰½μŠ΅λ‹ˆλ‹€.

  • νŠΉμ • λΉ„μ§€λ‹ˆμŠ€ 둜직이 μ†ν•˜λŠ” μœ„μΉ˜λ₯Ό λͺ…ν™•ν•˜κ²Œ μ •μ˜.
  • κ°œλ°œμžκ°€ 더 μ‰½κ²Œ ν˜‘μ—…ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
  • μ½”λ“œλ₯Ό 더 μ‰½κ²Œ ν…ŒμŠ€νŠΈν•  수 μžˆμŠ΅λ‹ˆλ‹€.
  • 이미 ν•΄κ²°λœ 문제λ₯Ό ν™œμš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
  • μ•± ν™•μž₯에 λ”°λ₯Έ μ‹œκ°„ μ ˆμ•½ 및 기술적 뢀채 κ°μ†Œ.

Android Jetpack

  • Best practiceλ₯Ό ν†΅ν•©ν•˜κ³  μ•±μ—μ„œ 이전 λ²„μ „κ³Όμ˜ ν˜Έν™˜μ„±μ„ μ œκ³΅ν•˜λŠ” Android 라이브러리.
  • Jetpack은 androidx.* package libraries둜 κ΅¬μ„±λ©λ‹ˆλ‹€.

Separation of concerns

Acitivty, FragmentλŠ” 데이터 ν‘œμ‹œ, μ‚¬μš©μž μž…λ ₯, Android μ‹œμŠ€ν…œ 이벀트λ₯Ό λ‹΄λ‹Ήν•©λ‹ˆλ‹€.

ViewModelμ—λŠ” UIλ₯Ό κ·Έλ¦¬λŠ” 데 ν•„μš”ν•œ λͺ¨λ“  데이터와 이λ₯Ό μ΅œμ‹  μƒνƒœλ‘œ μœ μ§€ν•˜λŠ” κΈ°λŠ₯이 ν¬ν•¨λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€.

Architecture components

  • MVVM, MVI와 같은 λ””μžμΈ νŒ¨ν„΄μ€ μ•±μ˜ ꡬ쑰에 λŒ€ν•œ νŒŒμ•…μ„ μš©μ΄ν•˜κ²Œ ν•©λ‹ˆλ‹€.
  • Jetpack architecture componentsλŠ” κ°•λ ₯ν•˜κ³  ν…ŒμŠ€νŠΈ κ°€λŠ₯ν•˜λ©° μœ μ§€κ΄€λ¦¬ κ°€λŠ₯ν•œ 앱을 μ„€κ³„ν•˜λŠ”λ° 도움이 λ©λ‹ˆλ‹€.

ViewModel

UI μ»¨νŠΈλ‘€λŸ¬λŠ” 주둜 μ‚¬μš©μžμ—κ²Œ 정보λ₯Ό ν‘œμ‹œν•˜κ³  이벀트λ₯Ό 처리 역할을 ν•˜κΈ° λ•Œλ¬Έμ— 데이터λ₯Ό κ΄€λ¦¬λŠ” ViewModelμ—μ„œ μ§„ν–‰ν•©λ‹ˆλ‹€.

Gradle: lifecycle extensions

app/build.gradle νŒŒμΌμ— μΆ”κ°€ν•΄ μ€λ‹ˆλ‹€.

dependencies {
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
    implementation "androidx.activity:activity-ktx:$activity_version"
}

ViewModel

  • UI용 데이터 μ€€λΉ„
  • Activity, Fragment λ˜λŠ” Viewλ₯Ό μ°Έμ‘°ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.
  • lifecycle둜 scopeκ°€ μ§€μ •λ©λ‹ˆλ‹€.(Activity, Fragment에 쑴재.)
  • configuration λ³€κ²½ μ‹œμ—λ„ 데이터가 μœ μ§€λ©λ‹ˆλ‹€.
  • scopeκ°€ μ‚΄μ•„μžˆλŠ”ν•œ μ‘΄μž¬ν•©λ‹ˆλ‹€.

Lifetime of a ViewModel

ViewModel class

ViewModel ν΄λž˜μŠ€λŠ” 좔상 ν΄λž˜μŠ€μ΄λ―€λ‘œ μ„œλΈŒν΄λž˜μ‹±ν•΄μ•Ό ν•©λ‹ˆλ‹€. 더 이상 μ‚¬μš©λ˜μ§€ μ•Šκ³  μ†Œλ©Έλ  λ•Œ ν˜ΈμΆœλ˜λŠ” onCleared() λ©”μ„œλ“œκ°€ μžˆμŠ΅λ‹ˆλ‹€.

Implement a ViewModel

class ScoreViewModel : ViewModel() {
    var scoreA : Int = 0
    var scoreB : Int = 0
    fun incrementScore(isTeamA: Boolean) {
        if (isTeamA) {
            scoreA++
        }
        else {
            scoreB++
        }
    }
}

Load and use a ViewModel

activity-ktx artifact ν¬ν•¨λ˜μ–΄μžˆλŠ” by viewModels()λ₯Ό μ‚¬μš©ν•˜μ—¬ κ°€μ Έμ˜΅λ‹ˆλ‹€.

class MainActivity : AppCompatActivity() {
    // Delegate provided by androidx.activity.viewModels
    val viewModel: ScoreViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        val scoreViewA: TextView = findViewById(R.id.scoreA)
        scoreViewA.text = viewModel.scoreA.toString()
    }

Data binding

ViewModels and data binding

  • data binding 없이 μ‚¬μš©ν•  경우.

    • 쀑간 UI Controllerλ₯Ό μ˜μ‘΄ν•˜μ—¬ 톡신해야 ν•©λ‹ˆλ‹€.
  • data binding μ‚¬μš©ν•  경우.

    • viewModel 객체λ₯Ό dataBinding에 μ „λ‹¬ν•˜λ©΄ view와 viewModelκ°„μ˜ 일뢀 톡신을 μžλ™ν™” ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

Data binding in XML revisited

data ν…Œκ·Έ μ•ˆμ— ViewModel을 μ§€μ •ν•©λ‹ˆλ‹€.

<layout>
   <data>
       <variable>
           name="viewModel"
           type="com.example.kabaddikounter.ScoreViewModel" />
   </data>
   <ConstraintLayout ../>
</layout>

Attaching a ViewModel to a data binding

class MainActivity : AppCompatActivity() {

    val viewModel: ScoreViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding: ActivityMainBinding = DataBindingUtil.setContentView(this,
            R.layout.activity_main)

        binding.viewModel = viewModel
        ...

Using a ViewModel from a data binding

<TextView
    android:id="@+id/scoreViewA"
    android:text="@{viewModel.scoreA.toString()}" />
        ...

ViewModels and data binding

Viewκ°€ μΈμŠ€ν„΄μŠ€ν™”λ  λ•Œλ§Œ μ„€μ •λ˜κΈ° λ•Œλ¬Έμ— 클릭 μ΄λ²€νŠΈκ°€ 일어났을 λ•Œ μ—…λ°μ΄νŠΈκ°€ ν•„μš”ν•©λ‹ˆλ‹€. λ‹€μŒ LiveDataμ—μ„œ 이 문제λ₯Ό λ‹€λ£° κ²ƒμž…λ‹ˆλ‹€.

override fun onCreate(savedInstanceState: Bundle?) {
    ...
   val binding: ActivityMainBinding = DataBindingUtil.setContentView(this,
        R.layout.activity_main)

   binding.plusOneButtonA.setOnClickListener {
       viewModel.incrementScore(true)
       binding.scoreViewA.text = viewModel.scoreA.toString()
   }
}

LiveData

Observer design pattern

  • observer 객체가 주체에 μƒνƒœ 변경이 μžˆμ„ λ•Œ μ•Œλ¦¬κΈ° μœ„ν•΄ 객체 λͺ©λ‘μ„ μœ μ§€ν•˜λŠ” κ³³μž…λ‹ˆλ‹€.
  • observerλŠ” μ£Όμ²΄λ‘œλΆ€ν„° μƒνƒœ 변경을 μˆ˜μ‹ ν•˜μ—¬ μ μ ˆν•œ μ½”λ“œλ₯Ό μ‹€ν–‰ν•©λ‹ˆλ‹€.
  • observerλŠ” μ–Έμ œλ“ μ§€ μΆ”κ°€ν•˜κ±°λ‚˜ μ œκ±°ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

Observer design pattern diagram

LiveData

  • observeν•  수 μžˆλŠ” lifecycle-aware 데이터 홀더
  • λͺ¨λ“  데이터와 ν•¨κ»˜ μ‚¬μš©ν•  수 μžˆλŠ” Wrapper
  • ViewModelμ—μ„œ κ°œλ³„ 데이터 ν•„λ“œλ₯Ό λ³΄κ΄€ν•˜λŠ” 데 자주 μ‚¬μš©ν•©λ‹ˆλ‹€.
  • observer(Activity, Fragmentμ—μ„œ μ‚¬μš©)λ₯Ό μΆ”κ°€ν•˜κ±°λ‚˜ μ œκ±°ν•  수 μžˆλŠ” λ©”μ„œλ“œ.
    • observe(owner: LifecycleOwner, observer: Observer)
    • removeObserver(observer: Observer)

LiveData versus MutableLiveData

LiveData(immutable): getValue()

MutableLiveData: getValue(), postValue(value: T), setValue(value: T)

일반적으둜 MutableLiveDataλŠ” ViewModelμ—μ„œ private ν•˜κ²Œ μ‚¬μš©ν•˜μ—¬ λ…ΈμΆœ μ‹œν‚€μ§€ μ•Šκ³  λ³€κ²½ λΆˆκ°€λŠ₯ν•œ LiveDataλ₯Ό observerμ—κ²Œ λ…ΈμΆœν•©λ‹ˆλ‹€.

Use LiveData in ViewModel

class ScoreViewModel : ViewModel() {

    private val _scoreA = MutableLiveData<Int>(0)
    val scoreA: LiveData<Int>
        get() = _scoreA

    fun incrementScore(isTeamA: Boolean) {
        if (isTeamA) {
            _scoreA.value = _scoreA.value!! + 1
        } 
        ...

Add an observer on LiveData

ViewModel λ©”μ„œλ“œλ₯Ό ν˜ΈμΆœν•˜λŠ” 클릭 이벀트λ₯Ό μ„€μ •ν•©λ‹ˆλ‹€.

binding.plusOneButtonA.setOnClickListener {
    viewModel.incrementScore(true)
}

νŒ€ A의 점수λ₯Ό μ—…λ°μ΄νŠΈν•˜κΈ° μœ„ν•΄ observerλ₯Ό μƒμ„±ν•©λ‹ˆλ‹€.

val scoreA_Observer = Observer<Int> { newValue ->
    binding.scoreViewA.text = newValue.toString()
}

ViewModel의 scoreA LiveData에 observeλ₯Ό μΆ”κ°€ν•©λ‹ˆλ‹€.

viewModel.scoreA.observe(this, scoreA_Observer)

Two-way data binding

  • XMLμ—μ„œ LiveData에 λ°”μΈλ”©ν•˜λ©΄ μ½”λ“œμ—μ„œ observerκ°€ ν•„μš”ν•˜μ§€ μ•Šμ•„ μ½”λ“œμ˜ 양을 쀄 일 수 μžˆμŠ΅λ‹ˆλ‹€.

Example layout XML

<layout>
   <data>
       <variable>
           name="viewModel"
           type="com.example.kabaddikounter.ScoreViewModel" />
   </data>
   <ConstraintLayout ..>
       <TextView ...
           android:id="@+id/scoreViewA"
           android:text="@{viewModel.scoreA.toString()}" />
       ...
   </ConstraintLayout>
</layout>

Example Activity

class MainActivity : AppCompatActivity() {
    val viewModel: ScoreViewModel by viewModels()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding: ActivityMainBinding = DataBindingUtil
             .setContentView(this, R.layout.activity_main)
        binding.viewModel = viewModel
        binding.lifecycleOwner = this
        binding.plusOneButtonA.setOnClickListener {
            viewModel.incrementScore(true)
        }
        ...

Example ViewModel

class ScoreViewModel : ViewModel() {
    private val _scoreA = MutableLiveData<Int>(0)
    val scoreA : LiveData<Int>
        get() = _scoreA
    private val _scoreB = MutableLiveData<Int>(0)
    val scoreB : LiveData<Int>
        get() = _scoreB
    fun incrementScore(isTeamA: Boolean) {
        if (isTeamA) {
            _scoreA.value = _scoreA.value!! + 1
        } else {
            _scoreB.value = _scoreB.value!! + 1
        }
    }
}

Transform LiveData

observerμ—κ²Œ μ „λ‹¬ν•˜κΈ° 이전에 LiveData 객체에 μ €μž₯된 값을 λ³€κ²½ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
λ‹€λ₯Έ 값을 기반으둜 ν•˜λŠ” λ‹€λ₯Έ LiveData μΈμŠ€ν„΄μŠ€λ₯Ό λ°˜ν™˜ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

Manipulating LiveData with transformations

LiveDataλŠ” μƒˆλ‘œμš΄ LiveData 객체둜 λ³€ν™˜λ  수 μžˆμŠ΅λ‹ˆλ‹€.

observerκ°€ λ°˜ν™˜λœ κ²°κ³Όλ₯Ό observingν•˜μ§€ μ•ŠλŠ” ν•œ λ³€ν™˜μ€ κ³„μ‚°λ˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.

  • map()
  • switchMap()

Example LiveData with transformations

val result: LiveData<String> = Transformations.map(viewModel.scoreA) { 
    x -> if (x > 10) "A Wins" else ""
}

0개의 λŒ“κΈ€