نمط تصميم Android MVVM

في هذا البرنامج التعليمي، سنناقش ونقوم بتنفيذ نمط التصميم المعماري MVVM في تطبيق Android الخاص بنا. لقد ناقشنا سابقًا نمط تقديم العرض والمعالجة (MVP) في Android.

لماذا نحتاج إلى هذه الأنماط؟ إضافة كل شيء في نشاط (Activity) أو فريقنت (Fragment) واحد من شأنه أن يؤدي إلى مشاكل في الاختبار وإعادة تنظيم الكود. لذلك، يُوصى باستخدام فصل الكود والتنظيم المعماري النظيف.

Android MVVM

يشير MVVM إلى نموذج، عرض، نموذج العرض.

  • النموذج: يحتوي على بيانات التطبيق. لا يمكنه التحدث مباشرة إلى العرض. عمومًا، يُوصى بتعريض البيانات لـ نموذج العرض من خلال Observables.
  • العرض: يمثل واجهة المستخدم للتطبيق خالية من أي منطق تطبيق. يراقب نموذج العرض.
  • نموذج العرض: يعمل كوصلة بين النموذج والعرض. وهو مسؤول عن تحويل البيانات من النموذج. يوفر تيارات البيانات للعرض. كما يستخدم خطافات أو استدعاءات لتحديث العرض. سيطلب البيانات من النموذج أيضًا.

التدفق التالي يوضح نمط MVVM الأساسي. كيف يختلف هذا عن MVP؟

  • ViewModel يحل محل Presenter في الطبقة الوسيطة.
  • المقدم يحتفظ بالمراجع إلى العرض. ViewModel لا يفعل ذلك.
  • المقدم يحدث العرض باستخدام الطريقة التقليدية (تشغيل الأساليب).
  • ViewModel يرسل تدفقات البيانات.
  • المقدم والعرض في علاقة 1 إلى 1.
  • العرض وViewModel في علاقة 1 إلى كثير.
  • ViewModel لا يعرف أن العرض يستمع إليه.

هناك طريقتان لتنفيذ MVVM في Android:

  • ربط البيانات
  • ‏RXJava

في هذا البرنامج التعليمي، سنستخدم ربط البيانات فقط. تم تقديم مكتبة ربط البيانات من قبل Google لربط البيانات مباشرة في تخطيط xml. لمزيد من المعلومات حول ربط البيانات، انظر هذا البرنامج التعليمي. سنقوم بإنشاء تطبيق مثالي بسيط لصفحة تسجيل الدخول الذي يطلب إدخالات المستخدم. سنرى كيف يخبر ViewModel العرض متى يظهر رسالة تنبيه دون الاحتفاظ بمرجع للعرض.

كيف يمكن إعلام فئة ما بدون وجود مرجع لها؟ يمكن القيام بذلك بثلاث طرق مختلفة:

  • استخدام ربط البيانات ذات الاتجاهين
  • ‏استخدام LiveData
  • ‏استخدام RxJava

الربط الثنائي للبيانات

الربط الثنائي للبيانات هو تقنية لربط الكائنات الخاصة بك بتخطيطات XML الخاصة بك بحيث يمكن للكائن والتخطيط إرسال البيانات إلى بعضهما البعض. في حالتنا، يمكن لـ ViewModel إرسال البيانات إلى التخطيط ومراقبة التغييرات أيضًا. لهذا، نحتاج إلى BindingAdapter وسم مخصص معرف في XML. سيستمع Binding Adapter إلى التغييرات في خاصية السمة. سنتعلم المزيد حول الربط الثنائي للبيانات من خلال المثال أدناه.

هيكل مشروع مثال Android MVVM

إضافة مكتبة Data Binding

أضف الكود التالي إلى ملف build.gradle لتطبيقك:

android {

    dataBinding {
        enabled = true
    }
}

يتيح هذا التمكين الربط بالبيانات في تطبيقك.

إضافة التبعيات

أضف الاعتمادات التالية في ملف build.gradle:

implementation 'android.arch.lifecycle:extensions:1.1.0'

Model

النموذج سيحتوي على بريد المستخدم وكلمة المرور. الفصل التالي User.java يفعل ذلك:

package com.journaldev.androidmvvmbasics.model;


public class User {
    private String email;
    private String password;

    public User(String email, String password) {
        this.email = email;
        this.password = password;
    }

    public void setEmail(String email) {
        this.email = email;
    }


    public String getEmail() {
        return email;
    }

    public void setPassword(String password) {
        this.password = password;
    }


    public String getPassword() {
        return password;
    }


}

الربط ثنائي الاتجاه يسمح لنا بربط الكائنات في تخطيطات XML بحيث يمكن للكائن إرسال البيانات إلى التخطيط، والعكس صحيح. الصيغة للربط الثنائي الاتجاه هي @={variable}

Layout

الكود لملف activity_main.xml يعطى أدناه:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="https://schemas.android.com/apk/res/android"
    xmlns:bind="https://schemas.android.com/tools">

    <data>

        <variable
            name="viewModel"
            type="com.journaldev.androidmvvmbasics.viewmodels.LoginViewModel" />
    </data>


    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_margin="8dp"
            android:orientation="vertical">

            <EditText
                android:id="@+id/inEmail"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:hint="Email"
                android:inputType="textEmailAddress"
                android:padding="8dp"
                android:text="@={viewModel.userEmail}" />


            <EditText
                android:id="@+id/inPassword"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:hint="Password"
                android:inputType="textPassword"
                android:padding="8dp"
                android:text="@={viewModel.userPassword}" />


            <Button
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="8dp"
                android:onClick="@{()-> viewModel.onLoginClicked()}"
                android:text="LOGIN"
                bind:toastMessage="@{viewModel.toastMessage}" />


        </LinearLayout>

    </ScrollView>

</layout>

يتطلب ربط البيانات منا تعيين علامة التخطيط في الأعلى. هنا ViewModel لدينا يربط البيانات بالعرض. ()-> viewModel.onLoginClicked() يستدعي المستمع لنقرة الزر المعرفة بواسطة اللامبدا في ViewModel لدينا. يحدث النص الفرعي للقيم في النموذج (عبر ViewModel). bind:toastMessage="@{viewModel.toastMessage}" هو سمة مخصصة قمنا بإنشائها للربط ثنائي الاتجاه. بناءً على التغييرات في toastMessage في ViewModel، ستُشغّل BindingAdapter في العرض.

ViewModel

الكود لـ LoginViewModel.java يعطى أدناه:

package com.journaldev.androidmvvmbasics.viewmodels;

import android.databinding.BaseObservable;
import android.databinding.Bindable;
import android.text.TextUtils;
import android.util.Patterns;

import com.android.databinding.library.baseAdapters.BR;
import com.journaldev.androidmvvmbasics.model.User;

public class LoginViewModel extends BaseObservable {
    private User user;


    private String successMessage = "Login was successful";
    private String errorMessage = "Email or Password not valid";

    @Bindable
    private String toastMessage = null;


    public String getToastMessage() {
        return toastMessage;
    }


    private void setToastMessage(String toastMessage) {

        this.toastMessage = toastMessage;
        notifyPropertyChanged(BR.toastMessage);
    }


    public void setUserEmail(String email) {
        user.setEmail(email);
        notifyPropertyChanged(BR.userEmail);
    }

    @Bindable
    public String getUserEmail() {
        return user.getEmail();
    }

    @Bindable
    public String getUserPassword() {
        return user.getPassword();
    }

    public void setUserPassword(String password) {
        user.setPassword(password);
        notifyPropertyChanged(BR.userPassword);
    }

    public LoginViewModel() {
        user = new User("","");
    }

    public void onLoginClicked() {
        if (isInputDataValid())
            setToastMessage(successMessage);
        else
            setToastMessage(errorMessage);
    }

    public boolean isInputDataValid() {
        return !TextUtils.isEmpty(getUserEmail()) && Patterns.EMAIL_ADDRESS.matcher(getUserEmail()).matches() && getUserPassword().length() > 5;
    }
}

الطرق التي تم استدعاؤها في التخطيط مُنفّذة في الكود أعلاه بنفس التوقيع. إذا لم يكن النظير الخاص بالـ XML للطريقة موجودًا، فإننا بحاجة لتغيير السمة إلى app:. يمكن للفئة أعلاه أيضًا تمديد ViewModel. لكننا بحاجة إلى BaseObservable نظرًا لأنه يحول البيانات إلى تيارات ويُعلم عندما يتم تغيير خاصية toastMessage. علينا تعريف المُحَدِّد والمُعيِّن لسمة toastMessage المخصصة المُعرفة في الـ XML. داخل المُعيِّن، نُعلِم المُراقب (الذي سيكون العرض في تطبيقنا) بأن البيانات قد تغيرت. يمكن للعرض (نشاطنا) تحديد الإجراء المناسب.

تم إنشاء فئة BR تلقائيًا من ربط البيانات عند إعادة بناء المشروع

يتم تقديم الكود لفئة MainActivity.java أدناه:

package com.journaldev.androidmvvmbasics.views;


import android.databinding.BindingAdapter;
import android.databinding.DataBindingUtil;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;


import com.journaldev.androidmvvmbasics.R;
import com.journaldev.androidmvvmbasics.databinding.ActivityMainBinding;
import com.journaldev.androidmvvmbasics.viewmodels.LoginViewModel;


public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityMainBinding activityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        activityMainBinding.setViewModel(new LoginViewModel());
        activityMainBinding.executePendingBindings();

    }

    @BindingAdapter({"toastMessage"})
    public static void runMe(View view, String message) {
        if (message != null)
            Toast.makeText(view.getContext(), message, Toast.LENGTH_SHORT).show();
    }
}

بفضل ربط البيانات، تتم إنشاء فئة ActivityMainBinding تلقائيًا من التخطيط. يُطلق الطريقة @BindingAdapter كلما تم تغيير سمة toastMessage المعرفة على الزر. يجب أن تستخدم نفس السمة كما هو معرف في الـ XML وفي ViewModel. لذلك، في التطبيق أعلاه، تحدث ViewModel تحديثًا في النموذج عن طريق الاستماع إلى التغييرات في العرض. أيضًا، يمكن للنموذج تحديث العرض من خلال ViewModel باستخدام notifyPropertyChanged يتم تقديم نتيجة التطبيق أعلاه في العمل أدناه: بهذا ينتهي هذا البرنامج التعليمي حول MVVM في Android باستخدام ربط البيانات. يمكنك تنزيل المشروع من الرابط الموجود أدناه.

أساسيات AndroidMVVM

رابط المشروع على Github

Source:
https://www.digitalocean.com/community/tutorials/android-mvvm-design-pattern