수색…


비고

데이터 바인딩을 사용하여 구문 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:visibilityapp:textColor android:visibility XML에서 수행하는 작업이 MVVM 컨텍스트의 잘못된 / 안티 패턴이라고 주장 할 수 있습니다. 그러나 테스트 용으로 ViewModel에서 안드로이드 종속성을 유지하는 것이 더 중요하다고 주장 할 것입니다.

게다가, 정말로 app:textColor 는 무엇을합니까? ressource 포인터를 연관된 실제 색상으로 만 해결합니다. 따라서 ViewModel은 어떤 조건에 따라 어떤 색상이 표시 될지 결정합니다.

android:visibility 에 관해서는 메소드가 어떻게 이름 지어 졌는지에 따라 여기에서 삼항 연산자를 사용하는 것이 실제로 가능하다고 느낍니다. isLoadingVisibleisContentVisible 이라는 이름 때문에 각 결과가 뷰에서 해결되어야하는 것에 대해서는 의심의 여지가 없습니다. 그래서 저는 ViewModel에 의해 주어진 명령을 실제로보기 논리를 수행하는 것보다 오히려 실행한다고 생각합니다.

반면에 viewModel.isLoading ? View.VISIBLE : View.GONE 을 사용하는 것에 동의 viewModel.isLoading ? View.VISIBLE : View.GONE 은보기에 해당 상태의 의미를 가정 할 때 수행하기에 좋지 않습니다.

유용한 자료

다음 리소스는이 개념을 이해하는 데 많은 도움이되었습니다.

데이터 바인딩 라이브러리를 사용하는 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.

그러면 모델의 변경 사항이 트리거되고 모델의 데이터 형식이 변경되어 뷰에 표시됩니다. 모델에 의해 주어진 상태 ( resolveCounterColorresolveLabelText )에 적합한 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;
    }
}

실행중인 모든 것을 보려면이 예제 프로젝트를 확인하십시오.



Modified text is an extract of the original Stack Overflow Documentation
아래 라이선스 CC BY-SA 3.0
와 제휴하지 않음 Stack Overflow