“MVVM 패턴과 Paging으로 수퍼 빠른 이미지 피커 만들기” · 2020. 5. 17. ·...
Transcript of “MVVM 패턴과 Paging으로 수퍼 빠른 이미지 피커 만들기” · 2020. 5. 17. ·...
“MVVM 패턴과 Paging으로 수퍼 빠른 이미지 피커 만들기”
Charles
발표순서�
- 요구사항 분석 - 구현에 필요한 기술검토(MVVM, Data Binding, Paging) - 핵심 코드 설명 - 퍼포먼스 비교
이미지�피커�요구사항
•사용자�기기에�저장된�이미지�목록을�불러온다.��
•불러온�이미지는�격자무늬(Grid)�형태로�표현�한다.��
•복수개의�이미지�선택할�수�있어야�한다.�
3
�->�ContentResolver를�통해�MediaStore에�질의(Cursor�반환)�
��->�RecyclerView�+�GridLayoutManager��
�->�선택된�아이템을�저장할�자료구조�고려�및�구현
개발자�관점에서�추가적으로�고려해야�할�내용
•변화에�능동적인�구조�만들기�(확장성↑,�재사용성↑)��
•빠른�퍼포먼스�보장하기
4
�->�아키텍처�패턴�적용��
�->�Paging�라이브러리�적용
4
MVVM�패턴이란?
Model,�View,�View�Model�약자를�딴�디자인�패턴의�한�종류
5
•Model�:�데이터�저장,수정,삭제�관리.�네트워크�통신�기타�등등��->�Entity,�Repository,�DB,�Util�클래스�등,�그�외�모든것�
•View�:�UI�조작을�담당��->�Activity,�Fragment,�View,�ViewDataBinding�등�
•ViewModel�:�View에�표현할�데이터를�Model로�부터�가져와�가공하고�관리한다.��->�AAC�ViewModel�클래스,�BaseObservable�등�
구성요소가 하는 일
MVVM�패턴�특징1
6
• View로부터 완전히 독립된 ViewModel을 사용하는 패턴
View ViewModel유닛 테스트, 재사용, 확장 쉬워짐
난 너 알아난 너 몰라!
MVVM�패턴�특징2
7
• View와 ViewModel은 1:N의 관계를 갖을 수 있다.
View ViewModel
View
View
MVVM�패턴�특징3
8
• UI변경에 강건한 구조
디자인 좀 바꾸고 싶은데요..
일정에 차질이 생깁니다. 안됩니다.네 가능합니다.
AAC�ViewModel�(Android�Architecture�Component�ViewModel)
9
val�imageViewModel�=�ViewModelProvider(LifecycleOwner,�ViewModelProvider.Factory).get(ImageViewModel::class.java)
AAC ViewModel 인스턴스 생성
LifecycleOwner�:�생명주기를�담당하는�주체,�일반적으로�액티비티�또는�프레그먼트
ViewModelProvider.Factory�:�ViewModel�인스턴스�생성을�담당
• AAC ViewModel은 Configuration Change가 발생해도 뷰모델의 상태를 유지해준다. • 범위내에서 데이터 공유가 쉽다 • LifecycleOwner의 생명주기를 알고있다.
특징
AAC�ViewModel�제약사항
• 절대로 Context를 참조해서는 안된다. (Activity도 Context의 한 종류)
10
ViewModelActivity
Garbage Collector
Activity재생성
유지
화면회전
메모리 누수!파괴
데이터�바인딩
데이터�바인딩�정의(in�Wiki)�
data�binding�is�a�general�technique�that�binds�data�sources�from�the�provider�and�consumer�together�and�synchronizes�them.
<layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <data> <variable name="viewmodel" type="com.myapp.data.ViewModel" /> </data> <ConstraintLayout... /> <!-- UI layout's root element --> </layout>
activity_main.xml 애노테이션 프로세싱에 의한 코드 생성 ActivityMainBinding.javaactivity_main.xml ActivityMainBinding.java
11
->�데이터소스와�UI컴포넌트의�상태를�일치시키는�기술,�(findViewById�대용�X)��
12
ViewModel ViewViewDataBinding
12
ViewModel에�View를�바인딩하자
UI컴포넌트(액티비티 or 프레그먼트)
class MainActivity : AppCompatActivity{ val viewModel : ViewModel val binding : ActivityMainBinding
@Override protected void onCreate(Bundle savedInstanceState) { … binding.viewModel = viewModel } }
구글에서�권장하는�아키텍처
Model
ViewModel
View
13
아키텍처
14
페이징�라이브러리�개요
•사용자에게�보여지는�일부�데이터만�로드��
•네트워크�대역폭,�시스템�리소스�사용량을�절감
15
페이징�라이브러리�핵심�컴포넌트
16
DataSource-> 요청된 데이터를 PagedList에 전달한다.
PagedList -> Immutable한 데이터를 Lazy로딩함
PagedListAdapter
-> 백그라운드에서 PagedList의 로딩을 관찰하고 비교한 뒤 RecyclerView에 데이터를 표현
DataSource의�종류(https://www.charlezz.com/?p=599)
PositionalDataSource��
포지션�기반으로�데이터들을�불러온다.�고정된�사이즈를�갖는�데이터셋을�페이징�하는데�적합
17
PageKeyedDataSource��
페이지�기반으로�데이터를�불러온다.�페이지�단위의�키를�통해�이전�페이지와�다음페이지를�가져온다.�
ItemKeyedDataSource��
아이템의�아이디를�기반으로�데이터들을�불러온다.�로드된�아이템의�키가�다음�및�이전�데이터를�페이징�하기�위해�참조된다.�
MediaStore에�질의하기(이미지�목록�가져오기)
19
val projection = arrayOf(
MediaStore.Images.ImageColumns._ID,
MediaStore.Images.ImageColumns.BUCKET_ID,
MediaStore.Images.ImageColumns.DATE_ADDED,
MediaStore.Images.ImageColumns.SIZE,
MediaStore.Images.ImageColumns.MIME_TYPE,
MediaStore.Images.ImageColumns.ORIENTATION)
class Image(
val id: Long,
val bucketId: String,
val dateAdded: Long,
val fileSize: Long,
val mediaType: Int,
val orientation: Int,
…
)val cursor: Cursor? = context.contentResolver.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
projection,
selection,
selectionArgs,
sortOrder)데이터 베이스 쿼리 결과에 접근할 수 있는 포인터
MediaStore에�질의하기(이미지�목록�가져오기)
val�id�=�cursor.getLong(cursor.getColumnIndex(MediaStore.Images.ImageColumns._ID))�
val�bucketId�=�cursor.getString(cursor.getColumnIndex(MediaStore.Images.ImageColumns.BUCKET_ID))�
val�dateAdded�=�cursor.getLong(cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATE_ADDED))�
val�fileSize�=�cursor.getLong(cursor.getColumnIndex(MediaStore.Images.ImageColumns.SIZE))�
val�mimeType�=�cursor.getString(cursor.getColumnIndex(MediaStore.Images.ImageColumns.MIME_TYPE))�
val�orientation�=�cursor.getInt(cursor.getColumnIndex(MediaStore.Images.ImageColumns.ORIENTATION))�
val�uri�=�ContentUris.withAppendedId(getContentUri(),�id)�
val�image�=�Image(�
������������id,�bucketId,�
������������dateAdded,�fileSize,�
������������MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE,�
������������mimeType,�orientation,�
������������uri�
)
20
레코드를 하나씩 읽으며 Image 인스턴스로 치환
DataSource�만들기
class�ImageDataSource(...)�:�PositionalDataSource<Image>()�{��
����private�var�cursor�=�...�
����override�fun�loadInitial(params:�LoadInitialParams,�callback:�LoadInitialCallback<Image>)�{�
��������val�list:List<Image>�=�getMediaList(cursor,�params.requestedLoadSize)�
��������callback.onResult(list,�0,�cursor.count)�����������
����}�
����override�fun�loadRange(params:�LoadRangeParams,�callback:�LoadRangeCallback<Image>)�{�
��������val�list:List<Image>�=�getMediaList(cursor,�params.loadSize)�
��������callback.onResult(list)�
����}�
����private�fun�getMediaList(cursor:�Cursor,�loadSize:�Int):�ArrayList<Image>�{�
��������val�imageList�=�ArrayList<Image>()�
��������…�//�loadSize�만큼�cursor�순회하며�mediaList에�Image�추가�
��������return�imageList�
����}�
}� 21
Position을 Key로 사용
아이템 리스트 전달, 내부에서 PagedList로 변경
DataSource.Factory�만들기
class�ImageDataSourceFactory(...)�:�DataSource.Factory<Int,�Image>()�{�
����override�fun�create():�DataSource<Int,�Image>�{�
��������val�dataSource�=�ImageDataSource(...)�
��������return�dataSource�
����}�
}
22
초기화 또는 무효화(invalited) 될 때마다 호출
Repository�만들기�(�LiveData<PagedList<Image>>�반환하기)
class�ImageRepository(…)�{�
����fun�queryImageList():�LiveData<PagedList<Image>>�{�
��������val�factory�=�ImageDataSourceFactory(…)�
��������val�pageSize�=�30�
��������return�LivePagedListBuilder(factory,�pageSize).build()�
����}�
}
23
LiveData<PagedList<T>>를 반환할 수 있도록 도와주는 유틸 클래스
ViewModel�만들기
class�ImageViewModel(application:�Application,�...)�:�AndroidViewModel(application)�{�
����private�val�imageRepository:�ImageRepository�=�...�
����val�items:�LiveData<PagedList<Image>>�by�lazy�{�
��������imageRepository.queryImageList()�
����}�
����...�
}
24
Repository로부터 데이터를 가져온다
RecyclerView에�데이터�표현하기
class�ImageFragment�:�Fragment()�{�
����...�
����lateinit�var�viewModel:ImageViewModel�
����lateinit�var�adapter:�ImageAdapter�
����lateinit�var�layoutManager:�GridLayoutManager�
����...�
����override�fun�onViewCreated(view:�View,�savedInstanceState:�Bundle?)�{�
��������super.onViewCreated(view,�savedInstanceState)�
��������binding.lifecycleOwner�=�viewLifecycleOwner�
��������binding.viewModel�=�viewModel�
��������binding.recyclerView.adapter�=�adapter�
��������binding.recyclerView.layoutManager�=�layoutManager�
��������viewModel.items.observe(viewLifecycleOwner,�Observer�{�adapter.submitList(it)�})�
��������...�
����}�
} 25
LiveData를 관찰하여 PagedList에 변경이 있을시 Adapter에 즉각 반영
선택한�아이템�관리하기
class�SelectionManager(…)�:�BaseObservable()�{�
����val�selectedMap�=�LinkedHashMap<Long,�Image>()�
����fun�setChecked(image:�Image,�checked:�Boolean)�{…}�
����fun�isChecked(id:�Long):�Boolean�{…}�
����fun�toggle(image:�Image)�{…}�
����fun�getCount():�LiveData<Int>�=�…�
}�
26
순서가 보장되는 HashMap
SelectionManager와�데이터�바인딩�표현식
<—RecyclerView에서�표현되는�ViewHolder용�레이아웃—>�
<layout>�
����<data>�
��������<variable�name=“item”�type=“Image”/>�
��������<variable�name=“selectionManager"�type="SelectionManager"�/>�
����</data>�
����<ConstraintLayout>�
��������<ImageView�…�
������������android:onClick="@{v->selectionManager.toggle(item.media)}"/>�
��������<ImageView�…�
������������android:src="@{selectionManager.isChecked(item.id)?�@drawable/checked_on:@drawable/checked_off}”/>�
����</ConstraintLayout>�
</layout>
27
View를 클릭하면 선택한 아이템으로 지정
선택된 아이템인지 확인하여 체크 유무를 결정
시연영상�및�퍼포먼스�비교�
Pickle Band Facebook Instagram
2.2초 3.0초 5초 7초
초기화에 걸리는 시간 비교
페이징 컴포넌트를 사용하면 사진 수에 크게 영향 받지 않고 일정한 퍼포먼스를 발휘한다.
Thank You
Questions?
31