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

MVVMベストプラクティス

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
Avatar for cheesesand101 cheesesand101
February 08, 2018
9.5k

 MVVMベストプラクティス

Avatar for cheesesand101

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