Android
MVVM (आर्किटेक्चर)
खोज…
टिप्पणियों
सिंटेक्स डेटाबाइंडिंग के साथ विचित्र है
की तरह एक्सएमएल निश्चित समारोह उपसर्गों में एक संपत्ति के लिए एक ViewModel समारोह बाध्यकारी जब get
या is
गिरा दिया जाता है। उदाहरण के लिए। ViewModel::getFormattedText
पर @{viewModel.formattedText}
हो जाएगा जब इसे xml में किसी प्रॉपर्टी से @{viewModel.formattedText}
जाएगा। इसी तरह ViewModel::isContentVisible
साथ ViewModel::isContentVisible
-> @{viewModel.contentVisible}
(जावा @{viewModel.contentVisible}
)
की तरह उत्पन्न बाध्यकारी कक्षाएं ActivityMainBinding
एक्सएमएल वे, नहीं जावा वर्ग के लिए बाइंडिंग बना रहे हैं के नाम पर रखा गया है।
कस्टम बाइंडिंग
Activity_main.xml में मैंने app
पर TextColor विशेषता सेट की है न कि android
नामस्थान। ऐसा क्यों है? चूँकि विशेषता textColor
लिए एक कस्टम सेटर निर्धारित है जो एक ColorRes रिसोर्स आईडी को दिखाता है जो ViewModel द्वारा वास्तविक रंग में भेजा जाता है।
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);
}
}
यह कैसे काम करता है, इसकी जानकारी के लिए डेटाबाइंडिंग लाइब्रेरी: कस्टम सेटर्स देखें
रुको ... आपके xml में तर्क है !!!
आप तर्क दे सकते हैं कि मैं android:visibility
लिए xml में जो चीजें करता हूं android:visibility
और app:textColor
MVVM संदर्भ में गलत / विरोधी पैटर्न हैं क्योंकि मेरे विचार में व्यू लॉजिक है। हालाँकि, मैं तर्क दूंगा कि परीक्षण कारणों के लिए मेरे ViewModel से Android निर्भरता को बनाए रखना मेरे लिए अधिक महत्वपूर्ण है।
इसके अलावा, वास्तव में app:textColor
क्या करता है app:textColor
? यह केवल इसके साथ जुड़े वास्तविक रंग के लिए एक रिसोर्स पॉइंटर को हल करता है। इसलिए ViewModel अभी भी तय करता है कि कुछ स्थिति के आधार पर कौन सा रंग दिखाया गया है।
android:visibility
मुझे लगता है कि इस पद्धति का नाम होने के कारण वास्तव में यहां टर्नरी ऑपरेटर का उपयोग करना ठीक है। नाम के कारण isLoadingVisible
और isContentVisible
है वास्तव में इस बारे में कोई संदेह नहीं है कि प्रत्येक परिणाम को दृश्य में क्या हल करना चाहिए। इसलिए मुझे लगता है कि यह ViewModel द्वारा दी गई कमांड को निष्पादित कर रहा है जो वास्तव में व्यू लॉजिक कर रहा है।
दूसरी ओर मैं सहमत हूँ कि viewModel.isLoading ? View.VISIBLE : View.GONE
का उपयोग viewModel.isLoading ? View.VISIBLE : View.GONE
करना एक बुरी बात होगी क्योंकि आप दृश्य में यह धारणा बना रहे हैं कि दृश्य के लिए उस स्थिति का क्या अर्थ है।
उपयोगी सामग्री
इस अवधारणा को समझने की कोशिश में निम्नलिखित संसाधनों ने मेरी बहुत मदद की है:
- जेरेमी समानता - मॉडल-व्यू- व्यूमॉडल (एमवीवीएम) समझाया (C #) (08.2010)
- शामलिया शुकुर - एमवीवीएम डिजाइन पैटर्न (C #) (03.2013) की मूल बातें समझना
- फ्रोड निल्सन - एंड्रॉइड डेटाबाइंडिंग: अलविदा प्रस्तुतकर्ता, हैलो व्यूमॉडल! (०७.२,०१५)
- जो बर्च - एमवीवीएम (09.2015) के साथ Android का अनुमोदन
- फ्लोरिना मुंटेस्क्यू - एंड्रॉइड आर्किटेक्चर पैटर्न भाग 3: मॉडल-व्यू- व्यूमॉडल (10.2016)
एमवीवीएम उदाहरण डेटाबाइंडिंग लाइब्रेरी का उपयोग करना
MVVM का पूरा बिंदु व्यू लेयर से तर्क युक्त परतों को अलग करना है।
एंड्रॉइड पर हम इसके साथ हमारी मदद करने के लिए डेटाबाइंडिंग लाइब्रेरी का उपयोग कर सकते हैं और एंड्रॉइड निर्भरताओं के बारे में चिंता किए बिना हमारे अधिकांश तर्क यूनिट-परीक्षण योग्य बना सकते हैं।
इस उदाहरण में मैं मूर्खतापूर्ण सरल ऐप के लिए केंद्रीय घटक दिखाऊंगा जो निम्न कार्य करता है:
- शुरू में नकली एक नेटवर्क कॉल और एक लोडिंग स्पिनर दिखा
- एक क्लिक काउंटर टेक्स्ट व्यू, एक संदेश टेक्स्ट व्यू और एक बटन के साथ काउंटर को बढ़ाने के लिए एक दृश्य दिखाएं
- यदि काउंटर कुछ संख्या तक पहुंचता है तो बटन पर अपडेट काउंटर पर क्लिक करें और काउंटर कलर और मैसेज टेक्स्ट को अपडेट करें
आइए दृश्य परत से शुरू करें:
activity_main.xml
:
यदि आप इस बात से अपरिचित हैं कि डेटाबाइंडिंग कैसे काम करती है, तो शायद आपको खुद को इससे परिचित होने में 10 मिनट लगने चाहिए। जैसा कि आप देख सकते हैं, सभी क्षेत्र जो आप आमतौर पर बसने वालों के साथ अपडेट करते हैं, दृश्यमॉडल चर पर कार्यों के लिए बाध्य हैं।
यदि आपको android:visibility
के बारे में एक प्रश्न मिला है 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>
मॉडल परत के आगे। यहाँ मेरे पास है:
- दो फ़ील्ड जो ऐप की स्थिति का प्रतिनिधित्व करते हैं
- क्लिक की संख्या और उत्साह की स्थिति को पढ़ने के लिए
- मेरी क्लिक गणना बढ़ाने के लिए एक विधि
- कुछ पिछली स्थिति को पुनर्स्थापित करने के लिए एक विधि (अभिविन्यास परिवर्तनों के लिए महत्वपूर्ण)
इसके अलावा मैं यहां 'उत्तेजना की स्थिति' को परिभाषित करता हूं जो क्लिक की संख्या पर निर्भर है। यह बाद में व्यू पर रंग और संदेश को अपडेट करने के लिए उपयोग किया जाएगा।
यह ध्यान रखना महत्वपूर्ण है कि मॉडल में कोई भी धारणा नहीं है कि उपयोगकर्ता को राज्य कैसे प्रदर्शित किया जा सकता है!
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 के आगे।
यह मॉडल पर परिवर्तनों को ट्रिगर करेगा और उन्हें दृश्य पर दिखाने के लिए मॉडल से डेटा प्रारूपित करेगा। ध्यान दें कि यह यहाँ है जहाँ हम मूल्यांकन करते हैं कि कौन सा GUI प्रतिनिधित्व मॉडल द्वारा दिए गए राज्य के लिए उपयुक्त है ( resolveCounterColor
और resolveLabelText
)। इसलिए हम उदाहरण के लिए आसानी से एक को लागू कर सकता है UnderachieverClickerModel
ViewModel या ध्यान में रखते हुए किसी भी कोड को छुए बिना उत्साह के राज्य के लिए कम थ्रेसहोल्ड है।
यह भी ध्यान दें कि ViewModel वस्तुओं को देखने के लिए कोई संदर्भ नहीं रखता है। सभी संपत्तियाँ @Bindable
एनोटेशन के माध्यम से @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 के आरंभिक होने के बाद यह DataBindingUtil (कृपया जेनरेट किए गए वर्गों के नामकरण के लिए 'सिंटैक्स' अनुभाग की जाँच करें) के माध्यम से xml लेआउट के लिए बाध्य है।
नोट की सदस्यता इस परत पर दी गई है क्योंकि हमें मेमोरी लीक और एनपीई से बचने के लिए गतिविधि को रोकने या नष्ट होने पर उन्हें अनसब्सक्राइब करना होगा। ओरिएंटेशन चेंजेस पर व्यूस्टेट को जारी रखने और पुनः लोड करने के लिए भी यहां ट्रिगर किया गया है
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;
}
}
कार्रवाई में सब कुछ देखने के लिए इस उदाहरण परियोजना को देखें ।