classMainActivityextendsLifecycleActivity{privateMyLocationListenermyLocationListener;publicvoidonCreate(BundlesavedInstanceState){myLocationListener=newMyLocationListener(this,getLifecycle(),location->{// update UI});}}classMyLocationListener{privatebooleanenabled=false;privateLifecyclelifecycle;publicMyLocationListener(Contextcontext,Lifecyclelifecycle,Callbackcallback){}publicvoidenable(){enabled=true;if(lifecycle.getCurrentState().isAtLeast(STARTED)){// connect if not connected}}}
(2) Lifecycle Observer
화면밖에서도 생명주기에 따른 동작을 정의하기 위해서 원하느 클래스에 LifecycleObserver 인터페이스를 구현하고, 넘겨받은 Lifecycle Owner객체에 구현한 LifecyclerObserver를 등록해야 한다.
annotaion을 이용하여 Lifecycle Owner의 생명주기에 따른 동작할 메소드를 정의할 수 있다.
Lifecycles를 통해 화면 밖에서 화면의 생명주기를 모니터링하고, 동작을 정의할 수 있다. -> 더 직관적인 생명주기 프로그래밍을 가능하게 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
classMyLocationListenerimplementsLifecycleObserver{publicMyLocationListener(Contextcontext,Lifecyclelifecycle,Callbackcallback){lifecycle.addObserver(this);}@OnLifecycleEvent(Lifecycle.Event.ON_START)voidstart(){// Do something}@OnLifecycleEvent(Lifecycle.Event.ON_STOP)voidstop(){// Do something}}
2. LiveData
데이터를 개선된 Observable로 wrapping하여 생명주기와 데이터 변경을 인지할 수 있도록 한다.
데이터 모델을 wrapping해서 생명주기와 데이터 변경을 자연스럽게 모델 스스로 인지할 수 있도록 한다.
위에서 예시로 든 MyLocationListener를 LiveData로 다시 구현할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
classMyLocationListenerextendsLiveData<Location>{publicMyLocationListener(Contextcontext){}// 1개 이상의 active observer@OverrideprotectedvoidonActive(){// Do something}// 0개의 active observer@OverrideprotectedvoidonInactive(){// Do something}}
LiveData는 Active observer의 개수에 따라 onActive(), onInactive()가 불린다.
1
2
3
4
5
6
7
8
9
classMainActivityextendsLifecycleActivity{publicvoidonCreate(BundlesavedInstanceState){LiveData<Location>myLocationListener=newMyLocationListener();// Active, Inactive를 판단하기 위해 observe() 할 때 Lifecycle를 넘긴다.myLocationListener.observe(this,location->{// Data가 변경되면 동작할 콜백을 등록});}}
Observer는 LiveData.observe()로 등록하며, 이 메소드로 데이터 변경을 구독한다.
Active observer는 생명주기가 최소 Resumed, Started에 있는 Observer를 말한다.
Observer가 생명주기에 따라 active, inactive 상태를 판단하기 위해 observer()를 호출할 때 Lifecycle을 넘긴다.
LiveData는 Active observer 개수로 생명주기를 간접적으로 인지한다.
1
2
3
4
5
6
7
8
9
10
classMyLocationListenerextendsLiveData<Location>{privateLocationManagerlocationManager;privateLocationListenerlistener=newLocationListener(){@OverridepublicvoidonLocationChanged(Locationlocation){// setValue()로 데이터를 변경하고 구독하는 Observer들에게 이벤트를 전달setValue(location);}};}
LiveData는 observe()에서 넘어온 Lifecycle로 생명주기를 모니터링하고, 함께 받은 콜백으로 데이터 변경 이벤트를 구독한다.
콜백은 LiveData.setValue()로 데이터를 변경하면 호출된다.
3. ViewModel
앱의 생명주기를 고려하여 UI 관련 데이터를 저장하고 관리하는 컴포넌트이다.
데이터를 쉽게 생명주기와 분리하여 관리할 수 있도록 돕는다.
AAC의 ViewModeel을 상속받은 뷰모델은 ViewModelProviders로 Scpoe를 관리할 수 있다.
해당 Scope내에서는 하나의 인스턴스만을 유지하여 작업이 중복되거나 데이터가 소실되지 않도록 한다.
classMyViewModelextendsViewModel{privateLiveData<User>userData;publicLiveData<User>getUser(StringuserId){if(userData==null)userData=webservice.fetchUser(userId);returnuserData;}}classMainActivityextendsLifecycleActivity{publicvoidonCreate(BundlesavedInstanceState){StringuserId="userId@gmail.com";// 처음이면 this Scope에 종속된 MyViewModel를 생성한다.// this Scope에 종속된 MyViewModel가 이미 있다면 불러온다.ViewModelProviders.of(this).get(MyViewModel.class)// 화면회전이 일어나 다시 호출되어도 같은 인스턴스 이므로 중복작업이 일어나지 않는다..getUser(userId).observe(this,user->{// update UI});}}
4. Room
ORM(Object Relation Mapping. Cursor 단위로 통신하는 쿼리를 객체 단위로 통신할 수 있도록 돕는다) 라이브러리 중 하나로, Annotation 기반이다.
// Database 정의. 테이블 및 버전을 함께 적는다.// RoomDatabase를 상속받는다.@Database(entities={User.class},version=1)publicabstractclassMyDatabaseextendsRoomDatabase{// Dao를 선언한다.publicabstractUserDaouserDao();}// 정의한 Database 객체를 가져온다.publicMyDatabasegetMyDatabase(){MyDatabasedb=Room.databaseBuilder(getApplicationContext(),MyDatabase.class).build();}// Entity Annotation으로 테이블 정의. 인스턴스 변수들이 곧 Column이다.@EntitypublicclassUser{// PrimaryKey Annotation으로 키를 정의한다.@PrimaryKeyprivateintuid;privateStringfirstName;privateStringlastName;}// DAO 정의@DaointerfaceUserDao{// Query Annotation으로 쿼리를 정의한다.// 파라미터로 전달할 값을 : 기호 다음에 같은 이름으로 선언한다. 여기서는 :first, :last 이다.// FROM 절로 넘긴 테이블과 매칭되는 모델로 반환값을 선언하면 알아서 맞는 객체로 매핑해준다. 여기서는 User이다.@Query("SELECT * FROM user WHERE first_name :first AND last_name :last")UserfindByName(Stringfirst,Stringlast);// 파라미터로 객체 그대로를 넘깁니다. 값 매칭은 Room이 인스턴스 변수를 보고 알아서 해줍니다.@InsertvoidinsertAll(User...users);@Deletevoiddelete(Useruser);@UpdatevoidupdateAll(List<User>users);}
(2) 컴파일 타임 쿼리 검증
1
2
3
// User 테이블인데 실수로 FROM에 users로 적었다. 이럴경우 Room은 컴파일 타임에 에러를 뱉어 실수를 빨리 발견하도록 돕는다.@Query("SELECT uid, firstName, lastName FROM users WHERE uid :uid")UserfindByUid(Stringuid);
Room은 원래 런타임으로 테스트해야만 제대로 동작하는지 알 수 있는 쿼리를 컴파일 타임에 검증하여, 정확한 쿼리를 빨리 짤 수 있도록 돕는다.
// User를 LiveData로 Wrapping하여 반환한다.@Query("SELECT uid, firstName, lastName FROM user WHERE uid :uid")LiveData<User>findByUid(Stringuid);/*
* Dao에서 LiveData로 반환된 값을 ViewModel을 거쳐 뷰에서 observe()하면 데이터베이스의 값이 변경될 때 실시간으로 추적할 수 있다.
*/classMyViewModelextendsViewModel{privateUserDaouserDao;privateLiveData<User>userData;publicMyViewModel(UserDaouserDao){this.userDao=userDao;}// Dao에서 LiveData로 반환된 값을 반환한다.publicLiveData<User>getUser(StringuserId){if(userData==null)userData=userDao.findByUid("userId");returnuserData;}}classMainActivityextendsLifecycleActivity{publicvoidonCreate(BundlesavedInstanceState){StringuserId="userId@gmail.com";ViewModelProviders.of(this).get(MyViewModel.class).getUser(userId).observe(this,user->{// Room의 Dao가 반환한 LiveData를 구독함으로써 데이터베이스 변경을 실시간으로 추적한다.});}}
Room은 LiveData와 연계하여 데이터베이스의 값을 실시간으로 추적할 수 있다.
다른 ORM은 Observability를 구현하려면 많은 고민이 필요하지만, Room은 반환 타입을 LiveData로 바꾸기만 하면 쉽게 구현할 수 있다.
5. Paging
리스트 뷰에서 컨텐츠를 특정 기준으로 범위를 나누고, 스크롤을 따라 범위 단위로 로드되도록 하는 것을 Paging이라고 한다.
Paging에는 세 가지 작업이 필요하다.
데이터를 Page 단위로 가져오는 쿼리
데이터를 특정 기준으로 Page 나누기
중복 아이템 검사
위 세 가지를 구현하는 것은 귀찮은 일이 많은데, Paging Library는 이를 쉽게 구현할 수 있도록 도와준다.
Paging Library는 세 가지로 구성되어 있다. DataSource, PagedList, PagedListAdapter
(1) DataSource
1
2
3
// 반환 타입을 DataSource로 하면 자동으로 PositionalDataSource를 생성한다.@Query("select * from users WHERE age > :age order by name DESC, id ASC")DataSource.Factory<Integer,User>usersOlderThan(intage);
Local 또는 Network에서 데이터를 가져오는 쿼리를 담고 있다.
PagedKeyedDataSource, ItemKeyedDataSource, PositionalDataSource 3가지가 있으며, 키 속성에 따라 맞는 클래스를 상속받아 쿼리를 구현해야 한다.