Upgrade to Pro — share decks privately, control downloads, hide ads and more …

MVVMベストプラクティス

cheesesand101
February 08, 2018
8.9k

 MVVMベストプラクティス

cheesesand101

February 08, 2018
Tweet

Transcript

  1. MVVM概要 • アプリケーションをModel / View / ViewModel に分割するアーキテクチャパターン • MicrosoftのKen

    CooperとTed Petersが開発 • WPF/Silverlightで使われていたが、最近は他 のプラットフォームでも採用例が多い
  2. カスタムセッター(画像) <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を記述可能
  3. ViewModelとModel • ViewModel -> Model ◦ Modelの状態を変更する • Model ->

    ViewModel ◦ ViewModelがModelの状態を監視する ◦ 監視は、RxJava等のインターフェースを使う ◦ ※本発表ではRxJavaを使います。説明を単純にするた め、エラー処理やライフサイクルの管理は省略します。
  4. 悪い例(UserSettingViewModel) class UserSettingViewModel(private val context: Context) { val settingA =

    ObservableField<String>() fun load(){ val pref = context.getSharedPreferences("UserSetting", Context.MODE_PRIVATE) val value = pref.getString("SettingA", "") this.settingA.set(value) } } ViewModelがSharedPreferenceを直接参照
  5. UserSettingRepository class UserSettingRepository(private val context:Context) { private val settingASubject =

    PublishSubject.create<String>() fun getSettingA() : Observable<String>{ return this.settingASubject } fun loadSettingA(){ val pref = context.getSharedPreferences("UserSetting", Context.MODE_PRIVATE) val value = pref.getString("SettingA", "") this.settingASubject.onNext(value) } } PublishSubjectをObservableとして返すだけ データを取得してPublisSubjectにonNext
  6. UserSettingViewModel class UserSettingViewModel(private val repository: UserSettingRepository) { val settingA =

    ObservableField<String>() init { repository.getSettingA().subscribe { value -> settingA.set(value) } } fun load(){ repository.loadSettingA() } } 状態を監視し続け、変更があっ たらsettingAを更新 ※将来的に設定の変更機能をつ けたときも、変更はここに流れて くる repositoryにデータを読み込ませる
  7. Navigatorの実装例 (DroidKaigi/conference-app-2017) @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)); } https://github.com/DroidKaigi/conference-app-2017/blob/master/app/src/main/java/io/github/droidkaigi/confsched2017/view/helper/Navi gator.java
  8. Context (android.content.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(); ・・・・・・・ 多い!!!
  9. ViewModelの定義 class TasksViewModel(...){ val items : ObservableField<List<TaskItem>> } sealed class

    TaskItemViewModel{ class Task(...) : TaskItemViewModel(){ val name : String } class Ad(...) : TaskItemViewModel(){ ... } }
  10. Activity class TasksActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?)

    { ... val binding = DataBindingUtil.setContentView<ActivityTasksBinding>(this, R.layout.activity_tasks) binding.recyclerView.adapter = TasksAdapter(repository) Adapterを生成
  11. BindingAdapter + バインド @BindingAdapter("taskItems") fun RecyclerView.setTaskItems(items:List<TaskItem>>?){ (this.adapter as TasksAdapter).setData(items ?:

    listOf()) } <android.support.v7.widget.RecyclerView android:id="@+id/recyclerView" ... app:taskItems="@{viewModel.items}" /> activity_tasks.xml RecyclerViewExtension.kt
  12. TasksAdapter class TasksAdapter(private val repository: TaskRepository) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { private

    var items = listOf<TaskItem>() fun setData(items:List<TaskItem>){ this.items = items this.notifyDataSetChanged() } override fun getItemCount(): Int { return this.items.size } override fun getItemViewType(position: Int): Int { val item = this.items.get(position) return when(item){ is TaskItem.Ad-> ViewType.Ad.type is TaskItem.Task -> ViewType.Task.type else -> TODO() } } 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)) } } 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() } } } } enum class ViewType(val type : Int){ Ad(0), Task(1); companion object { fun from(value:Int) : ViewType{ return ViewType.values().first { it.type == value } } } } class TaskAdViewHolder(val binding: ItemTaskAdBinding) : RecyclerView.ViewHolder(binding.root) class TaskViewHolder(val binding: ItemTaskBinding) : RecyclerView.ViewHolder(binding.root) } 長い!!のでポイントだけ・・・
  13. TasksAdapter.onCreateViewHolder 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の作成
  14. TasksAdapter.onBindViewHolder 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の接続
  15. ObservableListの定義 public interface ObservableList<T> extends List<T> { void addOnListChangedCallback(ObservableList.OnListChangedCallback<? extends

    ObservableList<T>> listener); void removeOnListChangedCallback(ObservableList.OnListChangedCallback<? extends ObservableList<T>> listener); }
  16. OnListChangedCallback 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); } 新規追加はこれが呼ばれる
  17. TasksAdapter 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を更新
  18. 差分検知 ViewModel ViewModel View View Model Model 差分を計算 差分検知機能 Model

    Model Model 新しいコレクションが流れてくる NEW
  19. TaskRepositoryの定義 class TaskRepository { fun getTasks() : Single<TaskItem> fun complete(id:Long)

    : Single<TaskItem> } RxJavaのSingle リポジトリが内部的にWebAPIにアクセスし、 返ってきた結果を1回だけ流す
  20. 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 -> parent.onComplete(models) }, {error -> //(省略) }) } } } TaskItemViewModel.Task Modelの更新 TasksViewModelを更新
  21. class TasksViewModel(private val repository: TaskRepository){ val items = ObservableField<List<TaskItem>>() val

    count = ObservableInt() fun onComplete(models:List<TaskItem>){ items.set(models) count.set(models.count { it is TaskItem.Task }) } } TasksViewModel Item, タスク数の更新
  22. class TaskRepository { fun getTask() : Observable<TaskItem> fun complete(id:Long) }

    TaskRepository Modelの状態変更を通知 何回も流れ続けるため、 Single -> Observableに変更 Modelの状態変更を要求 返り値がない
  23. sealed class TaskItemViewModel{ class Task(private val repository: TaskRepository, private val

    model:TaskItem.Task) : TaskItemViewModel(){ fun delete(){ repository.complete(model.id) } } } TaskItemViewModel.Task Modelの状態変更を要求
  24. TasksViewModel class TasksViewModel(private val repository: TaskRepository) { val items =

    ObservableField<List<TaskItem>>() val count = ObservableInt() init { repository.getTask().subscribe { models -> items.set(models) count.set(models.count { it is TaskItem.Task }) } } } Modelの状態変更を監視し続ける Modelの状態変更が起きるたびに、常に最新の状態に書き換わる
  25. まとめ • 関心の分離を意識してView - ViewModel - Model を適切に分離し、ViewModelの肥大化 を防ぐ •

    RecyclerViewは基本的な実装パターンをベー スに変更検知や効率化を考慮 • データフローを単純化して複雑化しないように
  26. Fin