본문 바로가기

[Android] AAC #1 - Android Architecture Components

AAC - Android Architecture Components

 

AAC 연관 글

[ 코드랩 : Android Room with a View - Kotlin ]

 

Android Room with a View - Kotlin

Your Room database class must be abstract and extend RoomDatabase. Usually, you only need one instance of a Room database for the whole app. Let's make one now. Create a Kotlin class file called WordRoomDatabase and add this code to it: In Android Studio,

codelabs.developers.google.com

 

 

[ 코드랩 : 1.Introducation ]

AAC - Android Architecture Component

  • Android Libraries - Architecture Components (라이브러리 집합)
  • Architecture Components ?

: App 아키텍처에 대한 가이드를 Lifecycle관리 및 data persistence와과 같은 일반적인 작업을 위한 라이브러리와 함께 제공하는 것, Architecture Component 라이브러리는 Android Jetpack에 속해있다

  • 앱이 더 적은 boilerplate code(보일러 플레이트 코드)로 견고하고 테스트가 좋고, 유지보수가 용이한 도움을 준다

* boilerplate code ? 

프로그래밍에서는 상용구 코드를 의미

즉, 수정하지 않거나 최소한의 수정만을 거쳐 여러 곳에 필수적으로 사용되는 코드를 뜻하는 단어

보일러 플레이트 코드는 최소환의 작업을 하기 위해 많은 분량의 코드를 작성해야 하는 언어에서 자주 사용

예를 들면,
    Java는 Getter/Setter 같은 모든 Data클래스에 작성하는 코드를 lombok을 통해 줄일 수 있다
    Kotlin은 lombok의 필요 없이 data class로 선언하면 자동으로 Getter/Setter를 생성

 

  • Architecture Components 구성요소

1. Lifecycle을 Handling 할 수 있는 방법

2. LiveData

3. ViewModel

4. Room Persistence Library (Persistence - 지속성)

 

 

 


Gradle 설정


build.gradle (Moudle: app)

: kapt 플러그인과 의존성(Dependencies) 추가

apply plugin: 'kotlin-kapt' // kapt 어노테이션 프로세서 플러그인 추가


dependencies {
    // Room components (Room DB 사용위해 추가)
    implementation "androidx.room:room-runtime:$rootProject.roomVersion"
    implementation "androidx.room:room-ktx:$rootProject.roomVersion"
    kapt "androidx.room:room-compiler:$rootProject.roomVersion"
    androidTestImplementation "androidx.room:room-testing:$rootProject.roomVersion"

    // Lifecycle components (DataBinding 위한 Lifecycle)
    implementation "androidx.lifecycle:lifecycle-extensions:$rootProject.archLifecycleVersion"
    kapt "androidx.lifecycle:lifecycle-compiler:$rootProject.archLifecycleVersion"
    androidTestImplementation "androidx.arch.core:core-testing:$rootProject.androidxArchVersion"

    // ViewModel Kotlin support (뷰모델)
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$rootProject.archLifecycleVersion"

    // Coroutines (코루틴 사용할 경우 추가)
    api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$rootProject.coroutines"

    // Testing	(Testing 사용할 경우 추가)
    androidTestImplementation "androidx.arch.core:core-testing:$rootProject.coreTestingVersion"
}


build.gradle (project: 프로젝트명) 

: Dependencies에 사용될 Componets Version추가, [Components 최신버전 확인]

ext {
    roomVersion = '2.2.5'
    archLifecycleVersion = '2.2.0'
    coreTestingVersion = '2.1.0'
    materialVersion = '1.1.0'
    coroutines = '1.3.4'
}

 


ViewModel + LiveData

ViewModel은 특정 Acitivty / Fragment에 데이터를 제공합니다 

데이터를 로딩하거나 (Repository에게 Data를 가져오는 경우), 데이터의 변경을 알려주는 (View에게 Data의 변경을 알림) 데이터 로직을 다루는 비즈니스 파트(여기선 LiveData)와 연동됩니다

ViewModelview에 대해서 알지 못하며, activity의 재생성(Recreated)이나 rotation(회전)같은 Configuration변경에 영향을 받지 않아 Data가 안전합니다

즉, ViewModel은 UI에 독립적인 구조



  • UserViewModel.kt : UI에서 사용할 data를 준비하는 class
// 프로퍼티 (name,age 기본값 설정)
class UserViewModel(name:String ="", age:Int =0) : ViewModel() {
    var name: MutableLiveData<String> = MutableLiveData(name)
    var age: MutableLiveData<Int> = MutableLiveData(age)

    fun minus() {
        age.value = age.value!! - 1
    }

    fun plus() {
        age.value = age.value!! + 1
    }
}


: LiveData는 데이터 변경 시 LiveData 인터페이스의
onChanged(T t) 메서드가 Callback되어 UI에 변경을 알려줘서 UI를 자동으로 Update합니다

[LiveData 더 자세히 알아보기]

 

  • UserViewModelFactory.kt : ViewModel에 초기값을 설정하기 위해 ViewModelProvider.Factory를 구현한 Factory
// Factory 생성인자로 name, age Param을 받아서 ViewModel 생성인자로 넘겨 초기값 설정이 가능
class UserViewModelFactory(var name:String, var age: Int) : ViewModelProvider.Factory {

	// ViewModelProvider.Factory 인터페이스의 추상메서드 create() 구현
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
    	// 생성하려는 ViewModel 클래스 검사
        return if (modelClass.isAssignableFrom(UserViewModel::class.java)) {
            UserViewModel(name, age) as T	// ViewModel 인스턴스 생성, create() 제네릭타입에 맞춰 T로 retrun
        } else {
            throw IllegalArgumentException()	// UserViewModel 클래스가 아니라면 예외 발생
        }
    }
}


:
ViewModelProvider.Factory 인터페이스는 하나의 추상메서드 create()를 구현해야합니다

create() 메서드에서 ViewModel을 생성해서 반환하는데, 이 때 ViewModel의 생성에 파라미터를 전달해서 사용자가 원하는 초기값을 설정할 수 있고, create() 내부에서 isAssignableFrom()으로 생성하려는 ViewModel의 타입을 검사해서 ViewModel을 생성하거나 IllegalArgument 예외를 발생할 수 있습니다

[ViewModel / ViewModelFactory 더 자세히 알아보기]

 

  • MainActivity.kt
class MainActivity : AppCompatActivity() {
    lateinit var binding: ActivityMainBinding
    lateinit var viewModel: UserViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // xml 인플레이션 + DataBinding
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)

        // binding 객체가 this의 액티비티 lifecycle을 참조하며서 데이터가 변경되면 refresh 하겠다는 의미
        binding.setLifecycleOwner(this)

        // ViewModel 초기값 설정위해 Factory 사용 - 프로퍼티(name,age) 값 설정
        val factory = UserViewModelFactory("장재종", 27)
        // ViewModel 인스턴스 가져오기 (tihs = MainActivity의 수명주기에 의존)
        viewModel = ViewModelProvider(this, factory)[UserViewModel::class.java]

        binding.activity = this     // xml <data> activity 바인딩
        binding.vmUser = viewModel  // xml <data> vmUser 바인딩


        // Fragment
        supportFragmentManager.beginTransaction()
            .replace(R.id.fragment, BlankFragment())    // xml id - fragment에 BlankFragment 연결
            .commit()
    }
}

 

  • activity_main.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="activity" type="com.jjj.viewmodel_test.MainActivity" />
        <variable name="vmUser" type="com.jjj.viewmodel_test.UserViewModel" />
    </data>


    <LinearLayout ...
        android:orientation="vertical">

        <androidx.constraintlayout.widget.ConstraintLayout
            ... >

            <!-- ViewModel의 name 프로퍼티, getName() -->
            <TextView ...
                android:text='@{vmUser.name, default="default"}' />

            <!-- ViewModel의 age 프로퍼티, getAge() -->
            <TextView ...
                android:text="@{String.valueOf(vmUser.age), default=0}"/>

            <!-- viewModel클래스의 minus() 메서드 -->
            <com.google.android.material.floatingactionbutton.FloatingActionButton
            	...
                android:onClick="@{() -> vmUser.minus() }" />

            <!-- viewModel클래스의 plus() 메서드 -->
            <com.google.android.material.floatingactionbutton.FloatingActionButton
                ...
                android:onClick="@{() -> vmUser.plus()}" />

        </androidx.constraintlayout.widget.ConstraintLayout>

        <!-- Fragment -->
        <FrameLayout ...
            tools:layout="@layout/fragment_blank" />

    </LinearLayout>
</layout>

 

  • BlankFragment.kt
class BlankFragment : Fragment() {
    lateinit var binding: FragmentBlankBinding

    override fun onCreateView(inflater: LayoutInflater,
        		container: ViewGroup?, savedInstanceState: Bundle?): View? {
        // fragment 인플레이션, 기존 inflater.inflate와 달리 DataBindingUtil.inflate 사용
        binding = DataBindingUtil.inflate(inflater, R.layout.fragment_blank, container, false)

        // requireActivity()로 부모 lifecycleOwner로 설정 - 부모의 ViewModel 인스턴스를 가져옴 (동일 객체)
        binding.vmUser = ViewModelProvider(requireActivity())[UserViewModel::class.java]

        // lifecyclerOwner를 부모 activity로 설정, getViewLifeCycleOwner() 메서드
        binding.lifecycleOwner = viewLifecycleOwner     // 생략할 경우 ViewModel의 변화를 Update 하지 못함 (클릭이벤트 무시됨)

        // view 반환, binding.getRoot() 메서드
        return binding.root
    }
}


: Fragment는 xml인플레이션(DataBinding) 방식이 기존의 Fragment 방식과 다릅니다

기존 : inflater.inflate(R.layout.LAYOUTID, container, false)

데이터바인딩 :DataBindingUtil.inflate(inflater, R.layout.LAYOUTID, container, false)

(데이터바인딩 Activity는 DataBindingUtil.setContentView() / Fragment는 DataBindingUtil.inflate())   

 

  • fragment_blank.xml
<layout
    ... >

    <data>
        <variable name="vmUser" type="com.jjj.viewmodel_test.UserViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        ... >

        <!-- ViewModel의 name 프로퍼티, getName() -->
        <TextView ...
            android:text='@{vmUser.name , default="default"}' />

        <!-- ViewModel의 age 프로퍼티, getAge() -->
        <TextView ...
            android:text="@{String.valueOf(vmUser.age)}" />

        <!-- viewModel클래스의 minus() 메서드 -->
        <com.google.android.material.floatingactionbutton.FloatingActionButton
            ...
            android:onClick="@{() -> vmUser.minus() }" />

        <!-- viewModel클래스의 plus() 메서드 -->
        <com.google.android.material.floatingactionbutton.FloatingActionButton
            ...
            android:onClick="@{() -> vmUser.plus()}"/>

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>


: Fragment에 Activity의 뷰들과 동일한 구조로 구성하였습니다.

ActvityFragment가 서로 같은 ViewModel을 참조하므로 Activity와 Fragment의 Click이벤트가 모두 같은 ViewModel인스턴스에 적용됩니다

Data가 변경되면 자동으로 UI가 Update되는데, 이건 Data의 Type을 LiveData로 선언하였기 때문에 값을 변경하면 자동으로 값 변경 noti를 줘서 UI(VIew)가 데이터를 업데이트하게 됩니다

* LiveData는 Observable Data Holder 타입으로 app안의 component들이 명시적으로 Dependency를 갖지 않도록 하면서, LiveData의 변경을 관찰 할 수 있도록 합니다

Fragment는 LifecycleOwner를 부모(Activity)로 설정(requireActivity())하였기 때문에 부모와 같이 이벤트가 반영되고 변경됩니다

 

 


Fecthing Data

REST API로 Web에서 데이터를 가져오거나, DB에서 데이터를 가져올 때 로직(Logic)이 간단하고 짧다면 ViewModel에 직접 구현 할 수도 있지만, 이 로직(Logic) 부분이 커질수록 유지관리가 힘들고 ViewModel이 큰 책임(reponsibililty)를 갖기 때문에 이런 구현방법은 피하는게 좋습니다

ViewModel의 Scope는 LifecycleOwner(Activity/Fragment)의 lifecycle에 의존적이므로 Owner가 Destroyed될 경우 ViewModel이 갖고있던 데이터는 모두 사라지게 됩니다.

이런 문제점을 해결하기 위해 ViewModel의 데이터를 Fetching하는 부분을 Repository Module위임(delegate)하는 방식을 추천합니다

Repository는 data operation을 handling하는 책임(responsivility)을 갖게됩니다. 그러므로 앱에 명확한 API를 제공 필요

Repository는 data를 어디서 조회하는지, update 시 호출해야할 API가 무엇인지, 여러 data Source를 중재하는 역할을 해야합니다

( data source란? persistent Model(DB), Web Service, cache, etc... 를 의미)

// UserRepository Module
public class UserRepository {
    // WebService API 객체
    private Webservice webservice;

    // WebService API로 REST API Data를 가져와 반환해주는 메서드
    public LiveData<User> getUser(int userId) {
        final MutableLiveData<User> data = new MutableLiveData<>();
        webservice.getUser(userId).enqueue(new Callback<User>() {

                    @Override
                    // REST API의 응답이 오면 호출되는 값을 포함한 Callback함수
                    public void onResponse(Call<User> call, Response<User> response) {
                        // MutableLiveData인 data에 REST API 응답값 저장
                        data.setValue(response.body());
                    }
                });
        // Repository의 getUser()메서드 반환
        return data;
    }
}


: 위 코드는 ViewModel이 Web의 User정보를 보여주기 위해 Repository를 통해web에 있는 User의 정보를 얻어오기 위한 UserRepository의 구성입니다

위 getUser() 내부의 REST API 통신 로직이 ViewModel에 구현해도 되지만, Repository를 사용함으로 data를 추상화하고, ViewModel은 data가 어떻게 fatch되었는지 알 필요가 없게되어 의존성을 분리하게 되고

Repository의 data fatch부분을 필요에 따라 다른 구현으로 swap해도 ViewModel은 방법을 알지 못하기 때문에 영향을 미치지 않습니다. (Dependency의 분리)

 

 


Connecting ViewModel and Repositoy

ViewModel과 repository의 연결은 간단히 아래처럼 구성합니다

class UserProfileViewModel(userRepo : UserRepository) : ViewModel() {
    private var user : LiveData<User> = LiveData()
    private var userRepo : UserRepository = userRepo

    fun init(userId : String) {
        if (this.user != null){
            return
        }
        user = userRepo.getUser(userId)
    }

    fun getUser() : LiveData<User> {
        return this.user
    }
}


ViewModel 객체 생성 시 userRepo 인스턴스를 등록함으로, Web의 User정보를
Repository인 userRepo를 통해 얻을 수 있게 되었고 ViewModel은 user데이터를 얻어오는 과정에 대해 모르게 되었습니다


 

 

  • 권장되는 Architecture Components 구조

  1. Entity : Room으로 DB 작업할 때 DB내 테이블을 의미하는 annotated 클래스 (annotation 선언된 클래스)
  2. SQLite Database : Device Storage에서 Room은 SQLite DB를 만들고 그 위에서 유지관리를 합니다
  3. DAO : Data Access Object 의미, 함수에 SQL Query를 매핑해줍니다. DAO를 통해 DB메서드를 실행하면 Room에서 매핑된 Query로 DB작업을 처리해줍니다
  4. Room Database : DB 작업을 단순화 시켜주는 라이브러리, 기본 SQLite DB에 Access point(진입점)의 역할을 합니다. DAO의 메서드를 SQLite DB에 Query형태로 변환해서 DB 작업을 요청합니다  
  5. Repository : 여러 Data Source를 관리하는 용도로 사용하는 클래스
  6. ViewModel : Repository (data)와 UI (View) 사이의 Communication center의 역할, VIewModel로 인해 UI(View)는 더 이상 Data의 출처에 대해 모르게 됩니다(의존성 분리) ViewModel 인스턴스는 Activity/Fragment의 Recreation의 상황에도 데이터를 유지합니다
  7. LiveData : 관찰할 수 있는 Data Holder Class, 항상 최신 버전의 데이터를 보관/캐시하고 데이터가 변경되었을 때 관찰자(Observer)에게 통지해서 자동 Update를 합니다
    LiveData는 Owner의 수명주기 사애변경을 인식하며 자동으로 관리하기 때문에 UI (View)는 Livedata를 관찰하기만 하고, 관찰을 중지하거나 다시 시작하지 않아도 알아서 LiveData가 처리합니다. 

 

 

 

ViewModel

  • UI(view)에 Data 제공, Data 로딩, 변경을 알려주는 DataLogic(Observer)을 다루는 비즈니스 파트와 연동
  • Repository(저장소)와 UI사이에 Communication 역할
  • ViewModel은 view에 대해 알지 못하며, acitvity 재생성이나 rotation 같은 Configuration 변경에 영향 X

     : 즉, ViewModel은 UI에 독립적인 상태

 

 

LiveData

  • Observable 패턴의 DataHolder 클래스, 항상 최신 데이터를 보유하거나 캐시
  • app안의 component들이 명시적으로 dependancy를 갖지 않도록 하면서,

    변경 사항을 LiveData객체에서 관찰 할 수 있도록 하는 역할
  • app Component의 LifeCycle State에 따라서 동작하므로 Memory Leak의 방지가 큰 장점

 

위에서 사용한 예제의 ViewModel에서 User객체를 반환하는 부분을 LiveData로 wrapping 하여 전달

class UserProfileViewModel(var userId: String, var user: LiveData<User>) : ViewModel()

 

ViewModel을 불러오는 부분도 LiveData에 맞게 수정

override fun onActivityCreated(savedInstanceState: Bundle?) {
    super.onActivityCreated(savedInstanceState)
    var userId = getArguments().getString(UID_KEY);
    viewModel = ViewModelProvider(this)[UserProfileViewModel::class.java]
    viewModel.userId(userId)
    viewModel.user.Observe(this) { user ->
         // UI Update 로직
    }
}

 

data가 변경되면 LiveData인 user를 구독하는 observe { ... } 부분이 호출되어 실행됩니다

Observe()의 파라미터 중 람다식 부분은 abstract void onChanged(T t)를 표현합니다

onChanged() - 해당 LiveData의 값이 변경되는 호출되는 CallBack 함수

 

LiveData에 정의된 Callback함수 onChanged는 Owner(위에선 Fragment)가 Active 상태에만 Callback수행

  • Active 상태 : Start, Resume의 상태
  • InActive 상태 : Pause, Stop, Destroy의 상태  (onChanged() 호출하지 않는 상태)

       (Destroyed가 되면 자동으로 LiveData의 자원을 해제)

 

 

Repository

  • 많은 데이터 소스(Data source)를 관리하기 위해 만든 클래스
  • Room Database, Web 서버와 같은 Remote Data Source도 포함
  • 모든 Data 처리하여, 관찰 가능한 LiveData로 변환하여 ViewModel에서 사용할 수 있도록 하는 책임을 가짐

 

 

Room Database

  • 로컬 데이터를 관리하는 SQLite Database 위에 있는 Database Layer(SQLite Mapping Library)
  • DAO를 사용하여 호출된 함수를 기반으로 SQLite에 Query를 실행
  • 관찰 가능한 LiveData가 포함 된 Query를 직접 반환할 수 있음
  • 구성 요소

    • Entity : Database 테이블을 표현하는 Annotated Class
    • SQLite Database : 안드로이드 Device의 로컬 데이터 저장공간
    • DAO : (Data Access Object) Database를 접근하는 함수들이 정의된 abstract class 혹은 interface