2017년 2월 16일 목요일

Android architecture를 통한 MVP, MVVM의 이해

android-architecture 를 통한 MVP, MVVM의 이해

MVP

  • TasksActivity
    • TasksFragment와 TasksPresenter를 생성하고 연결하는 controller의 역할을 한다.
  • TasksFragment와 TasksPresenter간의 연결은 TasksContract의 View와 Presenter에 의해 이루어진다.
  • TasksFragment
    • newInstance 함수를 통해 생성함수를 제공한다.
    • setPresenter를 통해 presenter를 입력받는다.
    • presenter의 start함수를 호출한다.
  • TasksPresenter
    • 초기화시 view의 setPresenter를 호출하여 자기자신을 View에 등록한다.

MVP with databinding

  • TasksActivity
    • TasksFragment와 TasksPresenter와 TasksViewModel을 생성한다.
    • TasksPresenter 생성시 TasksFragment를 파라메타로 넣어준다: new TasksPresenter(.., tasksFragment);
    • TasksViewModel 생성시 TasksPresenter를 파라케타로 넣어준다: new TasksViewModel(getApplicationContext(), mTasksPresenter);
    • TasksFragment에 TasksViewModel을 설정한다: tasksFragment.setViewModel(tasksViewModel);
  • TasksPresenter
    • TasksFragment가 바라보는 TasksContract.Presenter를 구현한다.
    • 생성자에서 TasksContract.View를 받는다: 여기서는 TasksFragment이다.
    • 생성자에서 받은 View에 presenter를 설정해준다: mTasksView.setPresenter(this);
    • start 함수를 구현한다.
    • xml 로부터의 이벤트를 받는다: addNewTask()
  • TasksFragment
    • TasksPresenter가 바라보는 TasksContract.View를 구현한다.
    • setPresenter를 구현하여 TasksContract.Presenter를 받는다: 여기서는 TasksPresenter이다.
    • 시작시 presenter의 start 함수를 호출한다: mPresenter.start();
    • layout file의 이름이 tasks_frag.xml이라면 TasksFragBinding으로 databinding을 한다: TasksFragBinding.inflate(inflater, container, false);
    • xml에 지정한 data의 값을 설정해준다: tasksFragBinding.setTasks(mTasksViewModel); and tasksFragBinding.setActionHandler(mPresenter);
    • TasksFragBinding에서 생성된 View는 getRoot 함수를 통해 얻어온다: tasksFragBinding.getRoot();
    • ListView를 위한 adapter에 presenter를 넘겨준다: ListView에서 처리하고자 하는 이벤트 발생시 presenter의 관련 함수를 호출해준다.
  • TasksViewModel
    • BaseObservable을 상속한다.
    • 생성자에서 TasksContract.Presenter를 받는다: 여기서는 TasksPresenter이다.
    • @Bindable을 통해 xml 파일과 연결한다.
    • 가져올 필요가 있는 값을 presenter를 통해 얻어온다.

MVVM

TasksActivity를 만든다. TasksActivity는 TasksFragment와 TasksViewModel을 생성한다. TasksFragment와 TasksViewModel는 서로 상호 참조를 해야 한다. 상호참조를 위해 TasksFragment는 setPresenter 함수를 통해 TasksViewModel를 입력받고, TasksViewModel은 생성시 TasksFragment를 입력받는다. 이때 서로를 직접적으로 입력받는 것이 아닌 인터페이스를 입력받는다.
TasksFragment의 기본 구성은 아래와 같다.
public class TasksFragment extends Fragment implements TasksNavigator {
  private TasksViewModel tasksViewModel;

  // 생성 함수
  public static TasksFragment newInstance() {
    return new TasksFragment();
  }

  // view model을 받는다.
  public void setViewModel(TasksViewModel viewModel) {
    tasksViewModel = viewModel;
  }
}
그리고 시작시 ViewModel의 start 함수를 호출하여 데이타 로딩같은 초기 작업이 이루어지도록 한다.
// 여기서는 onResume에서 start를 호출하지만 필요에 따라 onCreate에서 호출할 수도 있을 것이다.
@Override
public void onResume() {
  super.onResume();
  tasksViewModel.start();
}
databinding을 통해 View와 ViewModel을 연결한다.
// xml 파일의 이름이 tasks_frag.xml이므로 TasksFragBinding으로 연결이 된다.
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
                         Bundle savedInstanceState) {
  tasksFragBinding = TasksFragBinding.inflate(inflater, container, false);
  tasksFragBinding.setViewModel(tasksViewModel);
  View root = tasksFragBinding.getRoot();
  return root;
}
TasksViewModel의 기본 구성은 다음과 같다.
public class TasksViewModel extends BaseObservable {
  private final TasksNavigator navigator;

  public TasksViewModel(TasksNavigator navigator) {
    this.navigator = navigator;
  }

  public void start() {
    // 데이타를 로드한다.
    loadTasks(false);
  }
}
View와의 databinding을 위한 변수들을 설정한다. ObservableList, ObservableBoolean, ObservableField등의 다양한 databinding타입을 사용한다.(이 타입들의 변수들에 대한 설정은 get/set함수를 통해 이루어진다.) 또는 @Bindable을 사용해서 연결할 수도 있다.
// list view에서 보여지는 데이타들을 가지고 있는 변수
public final ObservableList<Task> items = new ObservableArrayList<>();
// 현재의 필터링 레벨을 보여주는 변수
public final ObservableField<String> currentFilteringLabel = new ObservableField<>();

// Bindable을 사용하는 경우에는 필요한 곳에서 아래의 형태로 호출을 해주어야 한다.
// notifyPropertyChanged(BR.empty); // It's a @Bindable so update manually
@Bindable
public boolean isEmpty() {
  return items.isEmpty();
}
여기까지가 기본적인 TasksFragment와 TasksViewModel의 구현이다. 이를 기반으로 실제 비지니스 로직이 들어가는 함수들을 구현하면 된다. 예를 들면, TasksNavigator에 필요한 인터페이스를 정의하여 TasksFragment가 이를 구현하게 하고 TasksViewModel은 View에 변경이 가해지거나(버튼 클릭등) Model이 바뀌는 경우 TasksNavigator의 필요한 함수를 호출하게 한다.
TasksFragment는 ListView를 포함하고 있다. 이 경우 ListView용의 ViewModel을 따로 만들어준다. 이의 구현을 살펴보자. ListView의 Adapter에 별도의 MVVM이 있다고 보면 될듯하다.
public static class TasksAdapter extends BaseAdapter {
  // Fragment의 TaskNavigator와 같은 역할을 하는 것
  private final TaskItemNavigator taskItemNavigator;
  // ListView에서의 처리에 TasksViewModel를 사용할 필요가 있는 경우도 있으므로 필요하면 이처럼 내부에 가지고 있는다.
  private final TasksViewModel tasksViewModel;

  public TasksAdapter(List<Task> tasks, TaskItemNavigator taskItemNavigator, TasksViewModel tasksViewModel) {
    this.taskItemNavigator = taskItemNavigator;
    this.tasksViewModel = tasksViewModel;
    // 데이타 로딩
    setList(tasks);
  }

  // TasksViewModel의 items에 변경이 일어나면 호출되기 위한 함수
  public void replaceData(List<Task> tasks) {
    setList(tasks);
  }

  @Override
  public View getView(int i, View view, ViewGroup viewGroup) {
    Task task = getItem(i);
    // xml 파일의 이름이 task_item.xml이다.
    TaskItemBinding binding;
    if (view == null) {
      // Inflate
      LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext());
      // Create the binding
      binding = TaskItemBinding.inflate(inflater, viewGroup, false);
    } else {
      // Recycling view
      binding = DataBindingUtil.getBinding(view);
    }

    // ViewModel을 생성한다.
    final TaskItemViewModel viewModel = new TaskItemViewModel(taskItemNavigator);
    // View와 ViewModel을 연결한다.
    binding.setViewModel(viewmodel);
    // To save on PropertyChangedCallbacks, wire the item's snackbar text observable to the
    // fragment's.
    viewModel.snackbarText.addOnPropertyChangedCallback(
        new Observable.OnPropertyChangedCallback() {
      @Override
      public void onPropertyChanged(Observable observable, int i) {
        tasksViewModel.snackbarText.set(viewmodel.getSnackbarText());
      }
    });
    viewModel.setTask(task);
    return binding.getRoot();
  }

  private void setList(List<Task> tasks) {
      this.tasks = tasks;
      notifyDataSetChanged();
  }
}
TasksViewModel이 task item의 리스트를 가지고 있다. 이의 변경이 일어나면 어떻게 ListView에 적용이 되는 걸까? tasks_frag.xml의 ListView를 보면 app:items="@{viewmodel.items}" property가 적용되어 있다. 여기에 추가하여 아래의 사용자 지정 바인딩을 사용한다.
public class TasksListBindings {

  // app:items의 값, 즉 TasksViewModel의 items에 변경이 일어나면 이 함수가 호출된다.
  // adapter의 내용을 items로 변경해준다.
  @SuppressWarnings("unchecked")
  @BindingAdapter("app:items")
  public static void setItems(ListView listView, List<Task> items) {
    TasksFragment.TasksAdapter adapter = (TasksFragment.TasksAdapter) listView.getAdapter();
    if (adapter != null) {
      adapter.replaceData(items);
    }
  }
}

Dagger2

  • Component와 Module을 정의한다.
  • Component
    • TasksRepositoryComponent에 dependency를 가지며 TasksPresenterModule을 사용하는 Component
@FragmentScoped
@Component(dependencies = TasksRepositoryComponent.class, modules = TasksPresenterModule.class)
public interface TasksComponent {
  void inject(TasksActivity activity);
}
  • Module
@Module
public class TasksPresenterModule {
  private final TasksContract.View mView;

  public TasksPresenterModule(TasksContract.View view) {
    mView = view;
  }

  @Provides
  TasksContract.View provideTasksContractView() {
    return mView;
  }
}
  • Component와 Module의 정의 후 아래와 같이 사용한다.
public class TasksActivity extends AppCompatActivity {
  @Inject TasksPresenter mTasksPresenter;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    ...
    DaggerTasksComponent.builder()
        .tasksRepositoryComponent(((ToDoApplication) getApplication()).getTasksRepositoryComponent())
        .tasksPresenterModule(new TasksPresenterModule(tasksFragment)).build()
        .inject(this);
  }
}

Building asynchronous views in SwiftUI 정리

Handling loading states within SwiftUI views self loading views View model 사용하기 Combine을 사용한 AnyPublisher Making SwiftUI views refreshable r...