본 포스팅은 DroidKaigi 2018 ~ MVVMベストプラクティス 을 기본으로 번역하여 작성했습니다
제 일본어 실력으로 인하여 오역이나 오타가 발생할 수 있습니다.
DroidKaigi 2018
Yasuhiko Sakamoto
이 발표에 대해서
실제로 만들어 보면…
MVVM 어떻게 만들었던가?
ViewModel이 View와 Model을 침식하여 비대화 → 「관심 분리」를 생각한다
관심 분리란
by 마틴 파울러
「가장 유용한 설계 원칙으로 프로그램 (사용자 인터페이스) 의 Presentation 층과 그 밖의 기능을 잘 나눈다
, 라는 것이 있습니다」
MVVM에 있어서 관심 분리
View – 이벤트 알림 –> ViewModel
ViewModel – 변경 알림 –> View
ViewModel은 View에 대한 참조를 가지지 않는다
ViewModel – 상태 변경을 요구 –> Model
Model – 변경 알림 –> ViewModel
Model은 ViewModel에 대한 참조를 가지지 않는다
Android 개발에 있어서 실천적인 방법
Testability 를 기준으로 사용
View와 ViewModel
Data binding expression
<TextView
...
android:text=”@{viewModel.name}”
/>
class ViewModel{
val name : ObservableField<String>
}
View -> ViewModel
<Button
...
android:onClick="@{viewModel::onClick}"
class XXViewModel{
fun onClick(view:View){ // ViewModel에 View가 섞여버린다
##36p, View -> ViewModel
<Button
...
android:onClick="@{() -> viewModel.onClick()}" <!-- 람다를 사용하는 작성법이 가능 -->
class XXViewModel{
fun onClick(){
ViewModel -> VIew
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
>
...
<ImageView
...
app:imageUrl="@{viewModel.imageUrl}"
/>
@BindingAdapter("imageUrl")
fun ImageView.setImageUrl(imageUrl:String){
Picasso.with(this.context).load(imageUrl).into(this)
}
Kotlin 의 경우, 확장함수를 사용해 BindingAdapter를 작성 가능
Android 개발에 있어서 실천적인 방법
ViewModel과 Model
class UserSettingViewModel(private val context: Context) {
val settingA = ObservableField<String>()
fun load(){
val pref = context.getSharedPreferences("UserSetting", Context.MODE_PRIVATE) // ViewModel이 SharedPreference를 직접 참조
val value = pref.getString("SettingA", "")
this.settingA.set(value)
}
}
class UserSettingRepository(private val context:Context) {
private val settingASubject = PublishSubject.create<String>()
fun getSettingA() : Observable<String>{
return this.settingASubject // PublishSubject를 Observable로서 반환
}
fun loadSettingA(){
val pref = context.getSharedPreferences("UserSetting", Context.MODE_PRIVATE)
val value = pref.getString("SettingA", "")
this.settingASubject.onNext(value) // 데이터를 얻어 PublishSubject 에 onNext
}
}
class UserSettingViewModel(private val repository: UserSettingRepository) {
val settingA = ObservableField<String>()
init {
repository.getSettingA().subscribe { value ->
settingA.set(value)
}
// 상태를 감시하여 변경이 있으면 settingA를 갱신
// ※ 추후에 설정 변경 기능을 붙인 경우에도 변경은 여기로 흘러들어온다
}
fun load(){
repository.loadSettingA() // repository에 데이터를 읽게함
}
}
Dialog 표시・화면 이동
@ActivityScope
public class Navigator {
private final Activity activity;
@Inject
public Navigator(AppCompatActivity activity) {
this.activity = activity;
}
public void navigateToSessionDetail(@NonNull Session session, @Nullable Class<? extends Activity> parentClass) {
activity.startActivity(SessionDetailActivity.createIntent(activity, session.id, parentClass));
}
Context
public abstract class Context {
public final String getString(int resId);
public final String getString(int resId, Object... formatArgs) ;
public final int getColor(int id);
public final Drawable getDrawable(int id);
public final TypedArray obtainStyledAttributes(AttributeSet set, int[] attrs, int defStyleAttr, int
defStyleRes);
public abstract ClassLoader getClassLoader();
public abstract String getPackageName();
public abstract ApplicationInfo getApplicationInfo();
public abstract String getPackageResourcePath();
public abstract String getPackageCodePath();
public abstract SharedPreferences getSharedPreferences(String var1, int var2);
public abstract boolean moveSharedPreferencesFrom(Context var1, String var2);
public abstract boolean deleteSharedPreferences(String var1);
public abstract FileInputStream openFileInput(String var1) throws FileNotFoundException;
public abstract FileOutputStream openFileOutput(String var1, int var2) throws FileNotFoundException;
public abstract boolean deleteFile(String var1);
public abstract File getFileStreamPath(String var1);
public abstract File getDataDir();
・・・・・・・
많아!!!
MVVM로 어떻게 만들지?
기본적인 구현 패턴
광고가 있는 TODO 앱
sealed class TaskItem{
class Task(val name:String, ...) : TaskItem()
class Ad(...) : TaskItem()
}
class TasksViewModel(...){
val items : ObservableField<List<TaskItem>>
}
sealed class TaskItemViewModel{
class Task(...) : TaskItemViewModel(){
val name : String
}
class Ad(...) : TaskItemViewModel(){
...
}
}
RecyclerView → Adapter
class TasksActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
...
val binding = DataBindingUtil.setContentView<ActivityTasksBinding>(this,
R.layout.activity_tasks)
binding.recyclerView.adapter = TasksAdapter(repository)
Adapter 생성
데이터 바인딩으로 전달
// RecyclerViewExtension.kt
@BindingAdapter("taskItems")
fun RecyclerView.setTaskItems(items:List<TaskItem>>?){
(this.adapter as TasksAdapter).setData(items ?: listOf())
}
<!-- activity_tasks.xml -->
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView"
...
app:taskItems="@{viewModel.items}"
/>
데이터 바인딩으로 전달
길어!! 그러니 포인트만…
class TaskViewHolder(val binding:ItemTaskBinding) :
RecyclerView.ViewHolder(binding.root)
class TaskAdViewHolder(val binding:ItemTaskAdBinding) :
RecyclerView.ViewHolder(binding.root)
Binding을 ViewHolder가 가진다
override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int):
RecyclerView.ViewHolder {
val viewTypeEnum = ViewType.from(viewType)
val layoutInflater = LayoutInflater.from(parent!!.context)
return when(viewTypeEnum){
ViewType.Ad -> TaskAdViewHolder(DataBindingUtil.inflate(layoutInflater,
R.layout.item_task_ad, parent, false))
ViewType.Task -> TaskViewHolder(DataBindingUtil.inflate(layoutInflater,
R.layout.item_task, parent, false))
}
}
ViewHolder 생성
override fun onBindViewHolder(holder: RecyclerView.ViewHolder?, position: Int) {
if(holder != null){
val item = this.items.get(position)
when (holder) {
is TaskViewHolder -> {
val viewModel = TaskItemViewModel.Task(item as TaskItem.Task, repository)
holder.binding.viewModel = viewModel
holder.binding.executePendingBindings()
}
is TaskAdViewHolder -> {
val viewModel = TaskItemViewModel.Ad(item as TaskItem.Ad)
holder.binding.viewModel = viewModel
holder.binding.executePendingBindings()
}
}
}
}
ViewModel 의 생성 & View와 ViewModel 의 접속
변경 알림
탭해서 TODO를 추가
public interface ObservableList<T> extends List<T> {
void addOnListChangedCallback(ObservableList.OnListChangedCallback<? extends
ObservableList<T>> listener);
void removeOnListChangedCallback(ObservableList.OnListChangedCallback<? extends
ObservableList<T>> listener);
}
public abstract static class OnListChangedCallback<T extends ObservableList> {
public OnListChangedCallback() {
}
public abstract void onChanged(T var1);
public abstract void onItemRangeChanged(T var1, int var2, int var3);
public abstract void onItemRangeInserted(T var1, int var2, int var3); // 신규 추가는 이 메소드가 불려짐
public abstract void onItemRangeMoved(T var1, int var2, int var3, int var4);
public abstract void onItemRangeRemoved(T var1, int var2, int var3);
}
class TasksAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
fun setData(items:ObservableList<TaskItemViewModel>){
items.addOnListChangedCallback([長い䛾で省略]...{
override fun onItemRangeInserted(list: ObservableList<TaskItemViewModel>?,
positionStart: Int, itemCount: Int) {
notifyItemRangeInserted(positionStart, itemCount) // 변경 부분만 Adapter를 갱신
}
……
Diff 감시 기능 → Diff 를 계산 → 새로운 Collection이 흘러들어온다
Diff 감시 기능 → Diff 를 전달 → ADapter → 생성 → View
구현의 효율화
class TaskRepository {
fun getTasks() : Single<TaskItem>
fun complete(id:Long) : Single<TaskItem>
}
RxJava의 Single, Repository가 내부적으로 WebAPI에 접속해 반환된 결과를 1회만 반환한다
sealed class TaskItemViewModel{
class Task(private val parent:TasksViewModel, private val
repository: TaskRepository, private val model:TaskItem.Task) :
TaskItemViewModel(){
fun complete(){
repository.complete(model.id).subscribe ({ models -> // Model 갱신
parent.onComplete(models) // TaskViewModel을 갱신
}, {error ->
//(생략)
})
}
}
}
class TasksViewModel(private val repository: TaskRepository){
val items = ObservableField<List<TaskItem>>()
val count = ObservableInt()
fun onComplete(models:List<TaskItem>){ // Item, Task 개수 갱신
items.set(models)
count.set(models.count { it is TaskItem.Task })
}
}
Model의 상태를 변경 → Model의 상태 변경에 따라 ViewModel이 자동으로 갱신하는 형태라면 ViewModel 간의 소통이 사라지고, 데이터 흐름도 단순하게 된다
class TaskRepository {
fun getTask() : Observable<TaskItem> // Model의 상태 변경을 알림, 몇번이라도 알리기때문에, Single → Observable로 변경
fun complete(id:Long) // Model의 상태 변경을 요구, 반환값이 없다
}
sealed class TaskItemViewModel{
class Task(private val repository: TaskRepository, private val
model:TaskItem.Task) : TaskItemViewModel(){
fun delete(){
repository.complete(model.id) // Model 상태 변경을 요구
}
}
}
class TasksViewModel(private val repository: TaskRepository) {
val items = ObservableField<List<TaskItem>>()
val count = ObservableInt()
init {
repository.getTask().subscribe { models -> // Model의 상태 변경을 계속 감시한다
items.set(models)
count.set(models.count { it is TaskItem.Task }) // Model의 상태 변경이 일어날때마다, 매번 최신 상태로 변경한다
}
}
}
comments powered by Disqus
Subscribe to this blog via RSS.