Android
MVVM (아키텍처)
수색…
비고
데이터 바인딩을 사용하여 구문 quirks
viewModel 함수를 xml의 속성에 바인딩 할 때 get
또는 is
와 같은 특정 함수 접두사가 삭제됩니다. 예 : ViewModel::getFormattedText
는 xml의 속성에 바인딩 할 때 @{viewModel.formattedText}
됩니다. 마찬가지로 ViewModel::isContentVisible
-> @{viewModel.contentVisible}
(Java Bean 표기법)
ActivityMainBinding
과 같은 생성 된 바인딩 클래스는 자바 클래스가 아닌 바인딩을 생성하는 XML의 이름을 따서 명명됩니다.
사용자 정의 바인딩
activity_main.xml에서 android
네임 스페이스가 아닌 app
에 textColor 특성을 설정했습니다. 왜 그런가요? ViewModel에서 실제 색상으로 보내는 ColorRes 자원 ID를 확인하는 속성 textColor
대해 정의 된 사용자 정의 설정자가 있기 때문입니다.
public class CustomBindings {
@TargetApi(23)
@BindingAdapter({"bind:textColor"})
public static void setTextColor(TextView textView, int colorResId) {
final Context context = textView.getContext();
final Resources resources = context.getResources();
final int apiVersion = Build.VERSION.SDK_INT;
int color;
if (apiVersion >= Build.VERSION_CODES.M) {
color = resources.getColor(colorResId, context.getTheme());
} else {
color = resources.getColor(colorResId);
}
textView.setTextColor(color);
}
}
이 작업 방법에 대한 자세한 내용은 DataBinding Library : Custom Setters를 확인하십시오 .
기다려 ... 당신의 XML에 논리가있다 !!!?
내보기에는 뷰 로직이 있기 때문에 android:visibility
및 app:textColor
android:visibility
XML에서 수행하는 작업이 MVVM 컨텍스트의 잘못된 / 안티 패턴이라고 주장 할 수 있습니다. 그러나 테스트 용으로 ViewModel에서 안드로이드 종속성을 유지하는 것이 더 중요하다고 주장 할 것입니다.
게다가, 정말로 app:textColor
는 무엇을합니까? ressource 포인터를 연관된 실제 색상으로 만 해결합니다. 따라서 ViewModel은 어떤 조건에 따라 어떤 색상이 표시 될지 결정합니다.
android:visibility
에 관해서는 메소드가 어떻게 이름 지어 졌는지에 따라 여기에서 삼항 연산자를 사용하는 것이 실제로 가능하다고 느낍니다. isLoadingVisible
및 isContentVisible
이라는 이름 때문에 각 결과가 뷰에서 해결되어야하는 것에 대해서는 의심의 여지가 없습니다. 그래서 저는 ViewModel에 의해 주어진 명령을 실제로보기 논리를 수행하는 것보다 오히려 실행한다고 생각합니다.
반면에 viewModel.isLoading ? View.VISIBLE : View.GONE
을 사용하는 것에 동의 viewModel.isLoading ? View.VISIBLE : View.GONE
은보기에 해당 상태의 의미를 가정 할 때 수행하기에 좋지 않습니다.
유용한 자료
다음 리소스는이 개념을 이해하는 데 많은 도움이되었습니다.
- Jeremy Likness - MVVM (Model-View-ViewModel) 설명 (C #) (2010 년 8 월 8 일)
- Shamlia Shukkur - MVVM 디자인 패턴의 기초 이해 (C #) (03.2013)
- Frode Nilsen - 안드로이드 데이터 바인딩 : 안녕히 주무십시오, 안녕하세요 ViewModel! (07.2015)
- Joe Birch - MVVM을 사용한 안드로이드 접근 (09.2015)
- Florina Muntenescu - Android 아키텍처 패턴 파트 3 : Model-View-ViewModel (10.2016)
데이터 바인딩 라이브러리를 사용하는 MVVM 예제
MVVM의 핵심은 로직을 포함하는 레이어를 뷰 레이어에서 분리하는 것입니다.
Android에서는 DataBinding Library 를 사용하여이 문제를 해결하고 Android 의존성에 대해 걱정하지 않고도 대부분의 로직을 Unit-testable로 만들 수 있습니다.
이 예제에서는 다음을 수행하는 어리석은 간단한 App의 중심 구성 요소를 보여줍니다.
- 시작시 네트워크 호출을 위조하여 로딩 스피너 표시
- 클릭 카운터 TextView, 메시지 TextView 및 카운터를 증가시키는 버튼이있는보기 표시
- 버튼이 업데이트 번호를 클릭하면 업데이트 카운터를 클릭하고 카운터 색상과 메시지 텍스트를 업데이트합니다.
뷰 레이어부터 시작해 보겠습니다.
activity_main.xml
:
DataBinding의 작동 방식에 익숙하지 않은 경우 10 분이 소요될 것입니다. 보시다시피 보통 setter를 사용하여 업데이트 할 모든 필드는 viewModel 변수의 함수에 바인딩됩니다.
android:visibility
또는 app:textColor
속성에 대한 질문이있는 경우 '비고'섹션을 확인하십시오.
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="android.view.View" />
<variable
name="viewModel"
type="de.walled.mvvmtest.viewmodel.ClickerViewModel"/>
</data>
<RelativeLayout
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/activity_horizontal_margin"
tools:context="de.walled.mvvmtest.view.MainActivity">
<LinearLayout
android:id="@+id/click_counter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_marginTop="60dp"
android:visibility="@{viewModel.contentVisible ? View.VISIBLE : View.GONE}"
android:padding="8dp"
android:orientation="horizontal">
<TextView
android:id="@+id/number_of_clicks"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/ClickCounter"
android:text="@{viewModel.numberOfClicks}"
android:textAlignment="center"
app:textColor="@{viewModel.counterColor}"
tools:text="8"
tools:textColor="@color/red"
/>
<TextView
android:id="@+id/static_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="4dp"
android:layout_marginStart="4dp"
style="@style/ClickCounter"
android:text="@string/label.clicks"
app:textColor="@{viewModel.counterColor}"
android:textAlignment="center"
tools:textColor="@color/red"
/>
</LinearLayout>
<TextView
android:id="@+id/message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/click_counter"
android:layout_centerHorizontal="true"
android:visibility="@{viewModel.contentVisible ? View.VISIBLE : View.GONE}"
android:text="@{viewModel.labelText}"
android:textAlignment="center"
android:textSize="18sp"
tools:text="You're bad and you should feel bad!"
/>
<Button
android:id="@+id/clicker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/message"
android:layout_centerHorizontal="true"
android:layout_marginTop="8dp"
android:visibility="@{viewModel.contentVisible ? View.VISIBLE : View.GONE}"
android:padding="8dp"
android:text="@string/label.button"
android:onClick="@{() -> viewModel.onClickIncrement()}"
/>
<android.support.v4.widget.ContentLoadingProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="90dp"
android:layout_centerHorizontal="true"
style="@android:style/Widget.ProgressBar.Inverse"
android:visibility="@{viewModel.loadingVisible ? View.VISIBLE : View.GONE}"
android:indeterminate="true"
/>
</RelativeLayout>
</layout>
다음 모델 레이어. 나는 여기있다 :
- 앱 상태를 나타내는 두 개의 입력란
- getters는 클릭 수와 흥분 상태를 읽습니다.
- 클릭 수를 늘리는 방법
- 이전 상태를 복원하는 방법 (방향 변경에 중요)
또한 여기에서는 클릭 수에 따라 '흥분 상태'를 정의합니다. 나중에보기에서 색상과 메시지를 업데이트하는 데 사용됩니다.
상태가 사용자에게 표시되는 방법에 대한 모델에 가정이 없다는 점에 유의해야합니다.
ClickerModel.java
import com.google.common.base.Optional;
import de.walled.mvvmtest.viewmodel.ViewState;
public class ClickerModel implements IClickerModel {
private int numberOfClicks;
private Excitement stateOfExcitement;
public void incrementClicks() {
numberOfClicks += 1;
updateStateOfExcitement();
}
public int getNumberOfClicks() {
return Optional.fromNullable(numberOfClicks).or(0);
}
public Excitement getStateOfExcitement() {
return Optional.fromNullable(stateOfExcitement).or(Excitement.BOO);
}
public void restoreState(ViewState state) {
numberOfClicks = state.getNumberOfClicks();
updateStateOfExcitement();
}
private void updateStateOfExcitement() {
if (numberOfClicks < 10) {
stateOfExcitement = Excitement.BOO;
} else if (numberOfClicks <= 20) {
stateOfExcitement = Excitement.MEH;
} else {
stateOfExcitement = Excitement.WOOHOO;
}
}
}
다음 ViewModel.
그러면 모델의 변경 사항이 트리거되고 모델의 데이터 형식이 변경되어 뷰에 표시됩니다. 모델에 의해 주어진 상태 ( resolveCounterColor
및 resolveLabelText
)에 적합한 GUI 표현을 평가하는 곳은 여기에 있습니다. 예를 들어 ViewModel 또는 뷰에서 코드를 건드리지 않고도 흥분 상태에 대한 임계 값이 낮은 UnderachieverClickerModel
을 쉽게 구현할 수 있습니다.
또한 ViewModel은 뷰 객체에 대한 참조를 보유하지 않습니다. 모든 속성은 @Bindable
주석을 통해 바인딩되며 notifyChange()
(모든 속성을 업데이트해야한다고 신호 함) 또는 notifyPropertyChanged(BR.propertyName)
(이 속성을 업데이트해야한다고 신호를 notifyPropertyChanged(BR.propertyName)
업데이트됩니다.
ClickerViewModel.java
import android.databinding.BaseObservable;
import android.databinding.Bindable;
import android.support.annotation.ColorRes;
import android.support.annotation.StringRes;
import com.android.databinding.library.baseAdapters.BR;
import de.walled.mvvmtest.R;
import de.walled.mvvmtest.api.IClickerApi;
import de.walled.mvvmtest.model.Excitement;
import de.walled.mvvmtest.model.IClickerModel;
import rx.Observable;
public class ClickerViewModel extends BaseObservable {
private final IClickerApi api;
boolean isLoading = false;
private IClickerModel model;
public ClickerViewModel(IClickerModel model, IClickerApi api) {
this.model = model;
this.api = api;
}
public void onClickIncrement() {
model.incrementClicks();
notifyChange();
}
public ViewState getViewState() {
ViewState viewState = new ViewState();
viewState.setNumberOfClicks(model.getNumberOfClicks());
return viewState;
}
public Observable<ViewState> loadData() {
isLoading = true;
return api.fetchInitialState()
.doOnNext(this::initModel)
.doOnTerminate(() -> {
isLoading = false;
notifyPropertyChanged(BR.loadingVisible);
notifyPropertyChanged(BR.contentVisible);
});
}
public void initFromSavedState(ViewState savedState) {
initModel(savedState);
}
@Bindable
public String getNumberOfClicks() {
final int clicks = model.getNumberOfClicks();
return String.valueOf(clicks);
}
@Bindable
@StringRes
public int getLabelText() {
final Excitement stateOfExcitement = model.getStateOfExcitement();
return resolveLabelText(stateOfExcitement);
}
@Bindable
@ColorRes
public int getCounterColor() {
final Excitement stateOfExcitement = model.getStateOfExcitement();
return resolveCounterColor(stateOfExcitement);
}
@Bindable
public boolean isLoadingVisible() {
return isLoading;
}
@Bindable
public boolean isContentVisible() {
return !isLoading;
}
private void initModel(final ViewState viewState) {
model.restoreState(viewState);
notifyChange();
}
@ColorRes
private int resolveCounterColor(Excitement stateOfExcitement) {
switch (stateOfExcitement) {
case MEH:
return R.color.yellow;
case WOOHOO:
return R.color.green;
default:
return R.color.red;
}
}
@StringRes
private int resolveLabelText(Excitement stateOfExcitement) {
switch (stateOfExcitement) {
case MEH:
return R.string.label_indifferent;
case WOOHOO:
return R.string.label_excited;
default:
return R.string.label_negative;
}
}
}
그것을 활동에 모두 묶어 라!
여기서는 필요한 모든 종속성을 가진 viewModel을 초기화하는보기를 볼 수 있습니다.이보기는 안드로이드 컨텍스트에서 인스턴스화해야합니다.
viewModel이 초기화 된 후에는 DataBindingUtil을 통해 xml 레이아웃에 바인딩됩니다 (생성 된 클래스의 이름을 지정하려면 '구문'섹션을 확인하십시오.).
참고 구독은 메모리 누수 및 NPE를 방지하기 위해 활동이 일시 중지되거나 삭제 될 때 구독 취소를 처리해야하기 때문에이 계층에서 구독합니다. 또한 OrientationChanges에 대한 viewState의 유지 및 다시로드가 여기에서 트리거됩니다.
MainActivity.java
import android.databinding.DataBindingUtil;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import de.walled.mvvmtest.R;
import de.walled.mvvmtest.api.ClickerApi;
import de.walled.mvvmtest.api.IClickerApi;
import de.walled.mvvmtest.databinding.ActivityMainBinding;
import de.walled.mvvmtest.model.ClickerModel;
import de.walled.mvvmtest.viewmodel.ClickerViewModel;
import de.walled.mvvmtest.viewmodel.ViewState;
import rx.Subscription;
import rx.subscriptions.Subscriptions;
public class MainActivity extends AppCompatActivity {
private static final String KEY_VIEW_STATE = "state.view";
private ClickerViewModel viewModel;
private Subscription fakeLoader = Subscriptions.unsubscribed();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// would usually be injected but I feel Dagger would be out of scope
final IClickerApi api = new ClickerApi();
setupViewModel(savedInstanceState, api);
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
binding.setViewModel(viewModel);
}
@Override
protected void onPause() {
fakeLoader.unsubscribe();
super.onPause();
}
@Override
protected void onDestroy() {
fakeLoader.unsubscribe();
super.onDestroy();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
outState.putSerializable(KEY_VIEW_STATE, viewModel.getViewState());
}
private void setupViewModel(Bundle savedInstance, IClickerApi api) {
viewModel = new ClickerViewModel(new ClickerModel(), api);
final ViewState savedState = getViewStateFromBundle(savedInstance);
if (savedState == null) {
fakeLoader = viewModel.loadData().subscribe();
} else {
viewModel.initFromSavedState(savedState);
}
}
private ViewState getViewStateFromBundle(Bundle savedInstance) {
if (savedInstance != null) {
return (ViewState) savedInstance.getSerializable(KEY_VIEW_STATE);
}
return null;
}
}
실행중인 모든 것을 보려면이 예제 프로젝트를 확인하십시오.