: 이 포스트를 보기 전에 "안드로이드 mvp와 mvvm 예제로 알아보자(1) - 기본 구현" 포스트 와 "안드로이드 mvp와 mvvm 예제로 알아보자(2) - MVP" 포스트 를 대충이라도 훑어봐야 한다. 그렇지 않으면 해당 포스트 이해가 힘들 것이다.
※ MVVM(Model - View - ViewModel) 이란?
: MVVM은 Model, View, ViewModel을 일컫는 말로 Model과 View, ViewModel 3가지 역할을 가진다. MVVM은 사용자 인터페이스와 데이터를 연결(바인딩)하는 메커니즘인 데이터 바인딩을 사용한다. 데이터 바인딩에 대해 모른다면 "예제를 통해 안드로이드 데이터 바인딩에 대해 알아보자" 포스트에서 데이터 바인딩에 대해서 조금이라도 알고 이 포스트를 봐야한다. 데이터 바인딩을 모른다면 해당 포스트 이해가 불가능하다. MVVM은 MVP의 몇몇 단점을 보완하기 위해 등장했다. 여기서 기술한 내용에 대해서 처음에는 이해가 안 갈 수 있다. 다음에 나오는 예제를 본 후 다시 이 부분을 본다면 이해가 갈 것이다. MVVM에서 Model은 MVP와 마찬가지로 데이터와 비즈니스 로직이 들어간다. 뷰 또한 마찬가지로 데이터를 표시한다. 하지만 MVP와 달리 데이터 바인딩을 사용하기 때문에 MVP처럼 UI를 보여주는 로직을 구현할 필요가 없다. ViewModel에서 모델에서 가져온 데이터를 바인딩된 UI에 바로 적용한다. 때문에 mvp 보단 mvvm에서 뷰는 간소화된다. 단, 애니메이션이나 액티비티 전환 등은 ViewModel에서 구현이 어려워 그런 부분은 View에서 구현한다. 기본적인 View의 상태와 UI 로직은 ViewModel에서 구현한다. 처음 접한 사람은 이 내용을 이해하기 어렵다. 다음의 예를 보고 이 내용을 다시 보면 이해가 갈 것이다. 다음 예제는 "안드로이드 mvp와 mvvm 예제로 알아보자(1) - 기본 구현" 포스트와 "안드로이드 mvp와 mvvm 예제로 알아보자(2) - mvp" 포스트에 나온 소스를 mvvm 설계 방식으로 변형한 것이다. 예제 소스는 https://github.com/wikibook/advanced-android-book/tree/master/tech05/NewGitHubRepos 이다.
Ex) mvvm을 이용한 예제(예제 소스의 app-mvvm)
: mvvm은 model, view, contract, viewmodel로 구성된다. contract는 mvp와 마찬가지로 뷰와 프레젠터가 구현할 인터페이스를 구현한다.
1. activity_repository_list.xml
: mvvm 에서는 데이터 바인딩을 사용한다 데이터 바인딩에 연결되는 클래스는 viewmodel이다. 아래를 보면 메인 액티비티에 ~.RepositoryListViewModel을 바인딩 한 걸 알 수 있다. 또한 Spinner에 리스너를 달았다. Spinner에 이벤트가 오면 RepositoryListViewModel의 onLanguageSpinnerItemSelected 메서드가 호출된다. RepositoryListViewModel에서는 progressBarVisibility 변수를 통해 ProgressBar의 visibility 속성을 변경할 수 있다.
<layout .... <data> <!-- RepositoryListViewModel에 바인딩한다 --> <variable name="viewModel" type="com.github.advanced_android.newgithubrepo.viewmodel.RepositoryListViewModel"/> </data> .... <!-- ViewModelのonLanguageSpinnerItemSelected()를 onItemSelectedListener로 이용한다 --> <Spinner ... android:onItemSelected="@{viewModel::onLanguageSpinnerItemSelected}" /> <!-- ViewModelのprogressBarVisibility를 View의 visibility(View 표시 여부)로 이용한다 --> <ProgressBar ..... android:visibility="@{viewModel.progressBarVisibility}" /> </layout>
2. RepositoryListViewModel
public class RepositoryListViewModel { public final ObservableInt progressBarVisibility = new ObservableInt(View.VISIBLE); public RepositoryListViewModel(RepositoryListViewContract repositoryListView, GitHubService gitHubService) { this.repositoryListView = repositoryListView; this.gitHubService = gitHubService; } public void onLanguageSpinnerItemSelected(AdapterView<?> parent, View view, int position, long id) { // 스피너의 선택 내용이 바뀌면 호출된다 loadRepositories((String) parent.getItemAtPosition(position)); } /** * 지난 일주일간 만들어진 라이브러리를 인기순으로 가져온다 */ private void loadRepositories(String langugae) { // 로딩 중이므로 진행바를 표시한다 progressBarVisibility.set(View.VISIBLE); // 일주일 전 날짜 문자열 지금이 2016-10-27이라면 2016-10-20이라는 문자열을 얻는다 final Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.DAY_OF_MONTH, -7); String text = DateFormat.format("yyyy-MM-dd", calendar).toString(); // Retrofit을 이용해 서버에 액세스한다 // 지난 일주일간 만들어졌고 언어가 language인 것을 쿼리로 전달한다 Observable<GitHubService.Repositories> observable = gitHubService.listRepos("language:" + langugae + " " + "created:>" + text); // 입출력(IO)용 스레드로 통신하고, 메인스레드로 결과를 받도록 한다 observable.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(new Subscriber<GitHubService.Repositories>() { @Override public void onNext(GitHubService.Repositories repositories) { // 로딩이 끝났으므로 진행바를 표시하지 않는다 progressBarVisibility.set(View.GONE); // 가져온 아이템을 표시하고자, RecyclerView에 아이템을 설정해 갱신한다 repositoryListView.showRepositories(repositories); } @Override public void onError(Throwable e) { // 통신에 실패하면 호출된다 // 여기서는 스낵바를 표시한다(아래에 표시되는 바) repositoryListView.showError(); } @Override public void onCompleted() { // 아무것도 하지 않는다 } }); } }
3. RepositoryListViewContract 인터페이스와 RepositoryListActivity 액티비티
public interface RepositoryListViewContract { void showRepositories(GitHubService.Repositories repositories); void showError(); void startDetailActivity(String fullRepositoryName); }
위 인터페이스를 View에서 구현한다. 구현한 메서드들은 데이터 바인딩으로는 할 수 없는 작업들을 하는 걸 알 수 있다. onCreate를 보면 binding.setVeiwModel로 레이아웃을 실제 인스턴스와 연결한 걸 알 수 있다. 이전 포스트 mvp와 비교해보면 뷰에서 하는 일이 현저히 줄어들 걸 알 수 있다. 다음은 RecyclerView의 각 목록을 mvvm으로 어떻게 구현했는 지 알아보자.
public class RepositoryListActivity extends AppCompatActivity implements RepositoryListViewContract { .... @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ActivityRepositoryListBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_repository_list); final GitHubService gitHubService = ((NewGitHubReposApplication) getApplication()).getGitHubService(); binding.setViewModel(new RepositoryListViewModel((RepositoryListViewContract) this, gitHubService)); // 뷰를 셋업 setupViews(); } /** * 목록 등 화면 요소를 만든다 */ private void setupViews() { // 툴바 설정 Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); // Recycler View RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_repos); recyclerView.setLayoutManager(new LinearLayoutManager(this)); repositoryAdapter = new RepositoryAdapter((Context) this, (RepositoryListViewContract) this); recyclerView.setAdapter(repositoryAdapter); // SnackBar 표시에서 이용한다 coordinatorLayout = (CoordinatorLayout) findViewById(R.id.coordinator_layout); // Spinner languageSpinner = (Spinner) findViewById(R.id.language_spinner); ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item); adapter.addAll("java", "objective-c", "swift", "groovy", "python", "ruby", "c"); adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); languageSpinner.setAdapter(adapter); } // =====RepositoryListViewContract 구현===== // 여기서 Presenter로부터 지시를 받아 뷰의 변경 등을 한다 @Override public void startDetailActivity(String full_name) { DetailActivity.start(this, full_name); } @Override public void showRepositories(GitHubService.Repositories repositories) { repositoryAdapter.setItemsAndRefresh(repositories.items); } @Override public void showError() { Snackbar.make(coordinatorLayout, "읽을 수 없습니다", Snackbar.LENGTH_LONG) .setAction("Action", null).show(); } }
4. repo_item.xml
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" ... <data> <variable name="viewModel" type="com.github.advanced_android.newgithubrepo.viewmodel.RepositoryItemViewModel" /> </data> <android.support.v7.widget.CardView android:clickable="true" android:onClick="@{viewModel::onItemClick}" <RelativeLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="8dp"> <ImageView bind:imageUrl="@{viewModel.repoImageUrl}" /> <TextView android:text="@{viewModel.repoName}"/> <TextView android:text="@{viewModel.repoDetail}" /> <TextView android:text="@{viewModel.repoStar}"/> </RelativeLayout> </android.support.v7.widget.CardView> </layout>
bind:imageUrl = "@{viewModel.repoImageUrl} 은 @BindingAdpater({"imageUrl"}) 애노테이션이 붙은 메서드를 호출한다. 따라서 아래의 메서드가 호출된다. 이전 mvp 에서는 별도의 ViewHolder에서 별도의 loadImage 메서드를 호출했었다. 데이터 바인딩을 사용하면 아래와 같이 하면 된다.
public class BindingAdapters { @BindingAdapter({"imageUrl"}) public static void loadImage(final ImageView imageView, final String imageUrl) { // 이미지는 Glide라는 라이브러리를 사용해 데이터를 설정한다 Glide.with(imageView.getContext()) .load(imageUrl) .asBitmap().centerCrop().into(new BitmapImageViewTarget(imageView) { @Override protected void setResource(Bitmap resource) { // 이미지를 동그랗게 오려낸다 RoundedBitmapDrawable circularBitmapDrawable = RoundedBitmapDrawableFactory.create(imageView.getResources(), resource); circularBitmapDrawable.setCircular(true); imageView.setImageDrawable(circularBitmapDrawable); } }); } }
5. RepositoryItemViewModel
public class RepositoryItemViewModel { public ObservableField<String> repoName = new ObservableField<>(); public ObservableField<String> repoDetail = new ObservableField<>(); public ObservableField<String> repoStar = new ObservableField<>(); public ObservableField<String> repoImageUrl = new ObservableField<>(); RepositoryListViewContract view; private String fullName; public RepositoryItemViewModel(RepositoryListViewContract view) { this.view = view; } public void loadItem(GitHubService.RepositoryItem item) { fullName = item.full_name; repoDetail.set(item.description); repoName.set(item.name); repoStar.set(item.stargazers_count); repoImageUrl.set(item.owner.avatar_url); } public void onItemClick(View itemView) { view.startDetailActivity(fullName); } }
6. RepositoryAdaper
public class RepositoryAdapter extends RecyclerView.Adapter<RepositoryAdapter.RepoViewHolder> { public RepositoryAdapter(Context context, RepositoryListViewContract view) { this.context = context; this.view = view; } ..... /** * RecyclerView의 아이템의 뷰 작성과 뷰를 보존할 ViewHolder를 생성 */ @Override public RepoViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { RepoItemBinding binding = DataBindingUtil.inflate(LayoutInflater.from(context), R.layout.repo_item, parent, false); binding.setViewModel(new RepositoryItemViewModel(view)); return new RepoViewHolder(binding.getRoot(), binding.getViewModel()); } /** * onCreateViewHolder에서 만든 ViewHolder의 뷰에 * setItemsAndRefresh(items)에서 설정된 데이터를 넣는다 */ @Override public void onBindViewHolder(final RepoViewHolder holder, final int position) { final GitHubService.RepositoryItem item = getItemAt(position); holder.loadItem(item); } /** * 뷰를 보존해 둘 클래스 * 여기서는 ViewModel을 가진다 */ static class RepoViewHolder extends RecyclerView.ViewHolder { private final RepositoryItemViewModel viewModel; public RepoViewHolder(View itemView, RepositoryItemViewModel viewModel) { super(itemView); this.viewModel = viewModel; } public void loadItem(GitHubService.RepositoryItem item) { viewModel.loadItem(item); } } }
'안드로이드 > 기본' 카테고리의 다른 글
안드로이드 유용한 라이브러리 배울 수 있는 링크 모음 (0) | 2017.08.23 |
---|---|
안드로이드 mvp와 mvvm 예제로 알아보자(2) - MVP (0) | 2017.08.08 |
안드로이드 mvp와 mvvm 예제로 알아보자(1) - 기본 구현 (0) | 2017.08.07 |
안드로이드 build.gradle을 이용한 버전 설정(versionCode, versionName) (0) | 2017.08.06 |
안드로이드 스튜디오 쓸모 있는 단축키 모음 및 활용/사용법 (0) | 2017.08.06 |