مثال مبسط على ViewModel & LiveData في Android
بسم الله الرحمن الرحيم
مؤخرا, في google IO 17 تم الإعلان عن New Architecture Component وفي هذه المقالة سنتحدث عن اثنين منهم وهما ViewModel & LiveData.
بداية لنفرض أن لدينا تطبيق بهذا الشكل
عند الضغط على button Add one سيتم إضافة 1 للقيمة الموجودة في textView, وسيكون كود layout كالتالي
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
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"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="me.a3zcs.mvp.architecurecomponent.MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:id="@+id/number" />
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:layout_marginTop="0dp"
app:layout_constraintTop_toBottomOf="@+id/number" />
</android.support.constraint.ConstraintLayout>
أما Activity كالتالي
public class MainActivity extends AppCompatActivity {
TextView number;
Button addOne;
int i = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
number = findViewById(R.id.number);
addOne = findViewById(R.id.button);
setNumber(i);
addOne.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
setNumber(i);
}
});
}
private void setNumber(int value){
number.setText(String.valueOf(value));
}
}
الكود بسيط جدا, ويعمل بطريقة صحيحة, ولكن حينما تقوم بتغيير وضع الشاشة إلى landscape بدل portrait ستعود قيمة textView إلى 0, لأن Activity قد حصل لها destroy ثم create من جديد, سابقا يتم حل مثل هذه المشاكل التي يكون لها علاقة ب configChanges عن طريق استخدام onSaveInstanceState و onRestoreInstanceState, جميل جدا, سنتحدث عن أحد الحلول الجديدة لهذه المشكلة وهو ViewModel.
ViewModel:
هو class تم تصمميه لتعامل مع data الخاصة ب view, والميزة المضافة أنه يخزن البيانات في حالة configChanges, جميل جدا, لنبدأ بعمل refactor للكود السابق بإنشاء Model يقوم بعمل extends ل ViewModel
public class CounterViewModel extends ViewModel {
private int count;
public void setCount(int count) {
this.count = count;
}
public int getCount() {
return count;
}
}
جميل جدا الان سنقوم باستخدام هذا class في MainActivity كـ Composition Object ثم نقوم بإنشائه في onCreate, ليصبح شكل Activity كالتالي:
public class MainActivity extends AppCompatActivity {
TextView number;
Button addOne;
CounterViewModel counterViewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
counterViewModel = ViewModelProviders.of(this).get(CounterViewModel.class);
number = findViewById(R.id.number);
addOne = findViewById(R.id.button);
setNumber(counterViewModel.getCount());
addOne.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
counterViewModel.setCount(counterViewModel.getCount()+1);
setNumber(counterViewModel.getCount());
}
});
}
private void setNumber(int value){
number.setText(String.valueOf(value));
}
}
نلاحظ أننا نستخدم في الإنشاء ViewModelProviders, وهي ستقوم بحفظ وتخزين ViewModel ل activity المعطى, بمعنى, في حال حدثت حالة configChanges -كتغير orientation- ل activity سيقوم class ViewModelProviders بتخزين بيانات ViewModel ولذلك نقوم بإعطائه هذا المعلومات أثناء التعريف
ViewModelProviders.of(<UI controller>).get(<MyViewModel>.class)
الان في حال قمنا بعمل rotation لن تتأثر البيانات حيث تم الإحتفاظ بها عن طريق ViewModelProviders
جميل جدا, لنضيف ال component الأخرى للكود الخاص بنا.
LiveData:
LiveData تتيح ل lifeCycleOwner -سواء كان Activity or Fragment- أنه يستقبل الداتا من ViewModel عن طريق ال مراقبة -observing- أي تعديل يطرأ على ViewModel class, جميل جدا, إذا بدلا من أن نقوم بتحديث UI في كل مرة بإستخدام setters و getters, سنقوم بإسلوب جديد سنستعرضه في المثال التالي
أولا لنقوم بعمل refactor ل viewModel الخاص بناء ليصبح كالتالي
public class CounterViewModel extends ViewModel {
private MutableLiveData<Integer> count = new MutableLiveData<>();
public CounterViewModel(){
count.setValue(0);
}
public void setCount(int count) {
this.count.setValue(count);
}
public MutableLiveData<Integer> getCount() {
return count;
}
}
قمنا بتغيير نوع البيانات الذي نتعامل معه إلى MutableLiveData<Object> وتعديل set & get, جميل جدا لننتقل إلى Main
//public class MainActivity extends AppCompatActivity
public class MainActivity extends LifecycleActivity {
TextView number;
Button addOne;
CounterViewModel counterViewModel;
private final Observer<Integer> countObserver = new Observer<Integer>() {
@Override
public void onChanged(@Nullable Integer integer) {
setNumber(integer);
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
counterViewModel = ViewModelProviders.of(this).get(CounterViewModel.class);
counterViewModel.getCount().observe(this,countObserver);
number = findViewById(R.id.number);
addOne = findViewById(R.id.button);
//setNumber(counterViewModel.getCount());
addOne.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
counterViewModel.setCount(counterViewModel.getCount().getValue()+1);
//setNumber(counterViewModel.getCount());
}
});
}
private void setNumber(int value){
number.setText(String.valueOf(value));
}
}
قمت بعمل comment قبل كل سطر تم حذفه ليسهل عليك مشاهدة التعديل, بداية قمنا بعمل Observer<Object> لديه دالة واحدة فقط وهي onChanged, يتم استدعاؤها إذا حصل أي تعديل للبيانات, بعد ذلك قمنا بعمل Observe عن طريق هذا السطر في onCreate
counterViewModel.getCount().observe(<LifeCycleOwner>,<Observer>)
كما تلاحظ لدينا Observer سيستدعي دالة onChanged في كل مرة يتم تعديل البيانات,بشرط أن يكون State ل lifeCycleOwner في STARTED أو RESUMED, ويكون LifecycleOwner في حالة STARTED في حالتين الأولى بعد استدعاء دالة onStart, الثانية, قبل استدعاء دالة onPause, ويكون LifecycleOwner في حالة RESUMED بعد استدعاء دالة onResume.
ولذلك عندما قمنا بعمل Observe أعطينا الدالة Observer, بالإضافة إلى LifecycleOwner الذي هو مرتبط به.
ينبغي الإشارة إلى نقطة في غاية الأهمية, في حال كان التطبيق يعمل لكنه في Background وتعرض النظام لما نسميه Low Memory, سيقوم النظام بإعادة استخدام resources بالتخلص -kill- من التطبيقات الموجودة في Background, وفي حال كنت تعتمد فقط على ViewModel ببساطة ستفقد بياناتك, على عكس onSaveInstanceState التي كانت تحل هذه المشكلة.
إلى هنا أتمنى أن أكون وفقت في تبسيط المفهوم, ولاشك أنك ستحتاج لمزيد من القراءة لتعامل مع بيانات أعقد من المذكورة في المثال.
والسلام عليكم ورحمة الله وبركاته.
المصادر
Android Architecture Components by Example
Android Architecture Components
التعليقات (1)
جميل جدا شكرا على التوضيح و لكن عندى سؤال اذا ف حالة انى يستخدم view model و حدث Low Memory ايه الحل حضرتك مشكور وضحتها طيب عشان نحل حاجه زى كدا ايه الحل ..؟
لايوجد لديك حساب في عالم البرمجة؟
تحب تنضم لعالم البرمجة؟ وتنشئ عالمك الخاص، تنشر المقالات، الدورات، تشارك المبرمجين وتساعد الآخرين، اشترك الآن بخطوات يسيرة !