آموزش اندروید-فصل ۲۸-۲: معماری MVP در برنامه‌های اندروید

در بخش قبلی درباره معماری MVP و تفاوت آن با معماری پیش‌فرض اندروید و MVC صحبت کردیم. در این بخش می‌خواهیم درباره پیاده‌سازی معماری MVP در پروژه‌های اندروید صحبت کنیم.

۱- معماری MVP

الگوی معماری MVP یک الگوی طراحی بر مبنای MVC است که جدایی موضوعات را افزایش داده و آزمون‌شوندگی برنامه را تسهیل می‌کند. این الگو سه لایه مدل، نمایشگر و معرف با وظایف و نقش‌های کاملاً تعریف شده ایجاد می‌کند.

ch-28-05-MVP-Android

مدل مسئول منطق کاری برنامه است و نحوه ایجاد، ذخیره‌سازی و تغییر داده‌ها را کنترل می‌کند. نمایشگر (View) یک رابط کاربری منفعل است که داده‌ها را نمایش داده و عملیات کاربر را به معرف انتقال می‌دهد. معرف (Presenter) به عنوان میانجی عمل می‌کند و رابط بین مدل و نمایشگر است و همچنین عملیات کاربر را که نمایشگر فرستاده است پردازش می‌کند.

۲- برنامه‌ریزی و آماده‌سازی پروژه

می‌خواهیم یک پروژه ساده یادداشت نویسی بسازیم تا به کمک آن MVP را بهتر درک کنید. این برنامه به کاربر امکان می‌دهد تا یادداشت بنویسد، آن‌ها را در یک پایگاه داده یا دیتابیس محلی (بر روی خود گوشی) ذخیره کند و همچنین بتواند یادداشت‌ّا را حذف کند. برای ساده کردن کار، این برنامه فقط یک اکتیویتی دارد.

ch-28-06-mvp_sample_app-copia-corrected

در این آموزش، ما به طور خاصی بر روی پیاده‌سازی MVP تمرکز می‌کنیم و سایر عملکردها، مانند تنظیم دیتابیس اسکیولایت SQLite)، ساخت DAO (اشیای دسترسی به داده‌ها یا Data Access Object) و پردازش عملیات کاربر را رها می‌کنیم. اگر در هر کدام از این موضوعات مشکل داشتید، اسمارت‌لب برای همه آن‌ها آموزش‌های مناسب را قبلاً تدارک دیده است!

نمودار فرایند کار و لایه‌های MVP

بیایید با ساخت یک یادداشت جدید شروع کنیم. اگر ما این کار را به اجزای کوچکتر بشکنیم، جریان کار بر اساس معماری MVP این شکلی خواهد شد:

  • کاربر متن یادداشت را می‌نویسد و بر روی دکمه اضافه کردن تپ می‌کند.
  • معرف یا Presenter یک شی Note با متنی که کاربر وارد کرده است می‌سازد و از مدل می‌خواهد که آن را در دیتابیس ذخیره کند.
  • مدل یادداشت جدید را در دیتابیس ذخیره کرده و به معرف اطلاع می‌دهد که لیست یادداشت‌ها تغییر کرده است.
  • معرف فیلد مربوط به یادداشت جدید را خالی کرده و از نمایشگر می‌خواهد که لیست یادداشت‌ها را تازه کند تا یادداشت جدید در آن نمایش داده شود.

ch-28-07-action_diagram

حالا بیایید و عملیات مورد نیاز این کار را به کمک MVP از هم جدا کنیم. برای این که این اشیا را تا حد ممکن از هم جدا کنیم، ارتباط بین این اجزا را از طریق اینترفیس‌ها انجام می‌دهیم. ما به چهار اینترفیس نیاز داریم:

  • RequiredViewOps: عملیات ضروری برای نمایشگر که در اختیار معرف (Presenter) قرار می‌گیرد و معرف هر کاری که با نمایشگر داشته باشد باید از طریق این اینترفیس انجام دهد.
  • ProvidedPresenterOps: عملیاتی که معرف در اختیار نمایشگر می‌گذارد تا نمایشگر از طریق آن با معرف ارتباط برقرار کند.
  • RequiredPresenterOps: عملیات ضروری معرف که در اختیار مدل قرار می‌گیرد و مدل از طریق آن با معرف ارتباط برقرار می‌کند.
  • ProvidedModelOps: عملیاتی که مدل در اختیار معرف می‌گذارد.

ch-28-08-MVP_interfaces

۳- پیاده‌سازی MVP در اندروید

حالا که می‌دانیم چطور باید متدهای مختلف را سازمان دهی کنیم، می‌توانیم ساخت برنامه را آغاز کنیم. ما برای سادگی فقط بر روی پیاده‌سازی عملیات اضافه کردن یک یادداشت جدید متمرکز می‌شویم. کد برنامه در  گیت‌هاب در اختیار شما قرار خواهد گرفت.

ما فقط از یک اکتیویتی استفاده می‌کنیم که شامل ویوهای زیر است:

  • یک ادیت تکست برای یادداشت جدید
  • دکمه‌ای برای ذخیره یادداشت جدید
  • یک ریسایکلر ویر برای نمایش فهرست همه یادداشت‌ها
  • دو تکست‌ویو و یک دکمه برای حذف یادداشت در هر آیتم RecyclerView

اینترفیس‌ها

با ساختن اینترفیس‌ها شروع می‌کنیم. برای این که همه چیز منظم بماند همه اینترفیس‌ها را یک جا تعریف می‌کنیم. باز هم یادآوری می‌کنیم که در این مثال فقط بر روی ایجاد یک یادداشت جدید تمرکز می‌کنیم.

public interface MVP_Main {
    /**
     * Required View methods available to Presenter.
     * A passive layer, responsible to show data
     * and receive user interactions
     */
    interface RequiredViewOps {
        // View operations permitted to Presenter
        Context getAppContext();
            Context getActivityContext();
        void notifyItemInserted(int layoutPosition);
            void notifyItemRangeChanged(int positionStart, int itemCount);
    }

    /**
     * Operations offered to View to communicate with Presenter.
     * Processes user interactions, sends data requests to Model, etc.
     */
    interface ProvidedPresenterOps {
        // Presenter operations permitted to View
        void clickNewNote(EditText editText);
            // setting up recycler adapter
            int getNotesCount();
            NotesViewHolder createViewHolder(ViewGroup parent, int viewType);
            void bindViewHolder(NotesViewHolder holder, int position);
    }

    /**
     * Required Presenter methods available to Model.
     */
    interface RequiredPresenterOps {
        // Presenter operations permitted to Model
        Context getAppContext();
            Context getActivityContext();
    }

    /**
     * Operations offered to Model to communicate with Presenter
     * Handles all data business logic.
     */
    interface ProvidedModelOps {
            // Model operations permitted to Presenter
            int getNotesCount();
            Note getNote(int position);
        int insertNote(Note note);
            boolean loadData();
    }
}

لایه نمایشگر

حالا وقت آن است که لایه‌های مدل، نمایشگر و معرف را بسازیم. از آنجایی که MainActivity نقش نمایشگر را بر عهده دارد، باید اینترفیس RequiredViewOps را پیاده‌سازی کند.

public class MainActivity
        extends AppCompatActivity
    implements View.OnClickListener, MVP_Main.RequiredViewOps {

    private MVP_Main.ProvidedPresenterOps mPresenter;
    private EditText mTextNewNote;
    private ListNotes mListAdapter;

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.fab:{
                // Adds a new note
                mPresenter.clickNewNote(mTextNewNote);
            }
        }
    }
    @Override
    public Context getActivityContext() {
        return this;
    }

    @Override
    public Context getAppContext() {
        return getApplicationContext();
    }
    // Notify the RecyclerAdapter that a new item was inserted
    @Override
    public void notifyItemInserted(int adapterPos) {
        mListAdapter.notifyItemInserted(adapterPos);
    }
    // notify the RecyclerAdapter that items has changed
    @Override
    public void notifyItemRangeChanged(int positionStart, int itemCount){
        mListAdapter.notifyItemRangeChanged(positionStart, itemCount);
    }
    // notify the RecyclerAdapter that data set has changed
    @Override
    public void notifyDataSetChanged() {
        mListAdapter.notifyDataSetChanged();
    }
    // Recycler adapter
    // This class could have their own Presenter, but for the sake of
    // simplicity, will use only one Presenter.
    // The adapter is passive and all the processing occurs
    // in the Presenter layer.
    private class ListNotes extends RecyclerView.Adapter<NotesViewHolder>
    {
        @Override
        public int getItemCount() {
            return mPresenter.getNotesCount();
        }

        @Override
        public NotesViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            return mPresenter.createViewHolder(parent, viewType);
        }

        @Override
        public void onBindViewHolder(NotesViewHolder holder, int position) {
            mPresenter.bindViewHolder(holder, position);
        }
    }
}

لایه معرف:

معرف میانجی است و باید دو تا اینترفیس را پیاده‌سازی کند:

  • ProvidedPresenterOps تا به نمایشگر اجازه بدهد از این طریق با معرف ارتباط برقرار کند
  • RequiredPresenterOps تا بتواند نتایج را از مدل بگیرد

به اینترفیس لایه نمایشگر توجه ویژه بکنید. ما باید از WeakReference<MVP_Main.RequiredViewOps> استفاده کنیم چون MainActivity ممکن است در هر لحظه نابود شود و ما باید جلوی نشت حافظه را بگیریم. همچنین لایه مدل هنوز ساخته نشده است. این کار را بعداً وقتی که لایه‌های مخالف MVP را به هم وصل می‌کنیم، انجام خواهیم داد.

public class MainPresenter implements MVP_Main.ProvidedPresenterOps, MVP_Main.RequiredPresenterOps {

    // View reference. We use as a WeakReference
    // because the Activity could be destroyed at any time
    // and we don't want to create a memory leak
    private WeakReference<MVP_Main.RequiredViewOps> mView;
    // Model reference
    private MVP_Main.ProvidedModelOps mModel;

    /**
     * Presenter Constructor
     * @param view  MainActivity
     */
    public MainPresenter(MVP_Main.RequiredViewOps view) {
        mView = new WeakReference<>(view);
    }

    /**
     * Return the View reference.
     * Throw an exception if the View is unavailable.
     */
    private MVP_Main.RequiredViewOps getView() throws NullPointerException{
        if ( mView != null )
            return mView.get();
        else
            throw new NullPointerException("View is unavailable");
    }

    /**
     * Retrieves total Notes count from Model
     * @return  Notes list size
     */
    @Override
    public int getNotesCount() {
        return mModel.getNotesCount();
    }

    /**
     * Creates the RecyclerView holder and setup its view
     * @param parent    Recycler viewGroup
     * @param viewType  Holder type
     * @return          Recycler ViewHolder
     */
    @Override
    public NotesViewHolder createViewHolder(ViewGroup parent, int viewType) {
        NotesViewHolder viewHolder;
        LayoutInflater inflater = LayoutInflater.from(parent.getContext());

        View viewTaskRow = inflater.inflate(R.layout.holder_notes, parent, false);
        viewHolder = new NotesViewHolder(viewTaskRow);

        return viewHolder;
    }

    /**
     * Binds ViewHolder with RecyclerView
     * @param holder    Holder to bind
     * @param position  Position on Recycler adapter
     */
    @Override
    public void bindViewHolder(final NotesViewHolder holder, int position) {
        final Note note = mModel.getNote(position);
        holder.text.setText( note.getText() );
        holder.date.setText( note.getDate() );
        holder.btnDelete.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                clickDeleteNote(note, holder.getAdapterPosition(), holder.getLayoutPosition());
            }
        });

    }

    /**
     * @return  Application context
     */
    @Override
    public Context getAppContext() {
        try {
            return getView().getAppContext();
        } catch (NullPointerException e) {
            return null;
        }
    }

    /**
     * @return  Activity context
     */
    @Override
    public Context getActivityContext() {
        try {
            return getView().getActivityContext();
        } catch (NullPointerException e) {
            return null;
        }
    }

    /**
     * Called by View when user clicks on new Note button.
     * Creates a Note with text typed by the user and asks
     * Model to insert it in DB.
     * @param editText  EditText with text typed by user
     */
    @Override
    public void clickNewNote(final EditText editText) {
        getView().showProgress();
        final String noteText = editText.getText().toString();
        if ( !noteText.isEmpty() ) {
            new AsyncTask<Void, Void, Integer>() {
                @Override
                protected Integer doInBackground(Void... params) {
                    // Inserts note in Model, returning adapter position
                    return mModel.insertNote(makeNote(noteText));
                }

                @Override
                protected void onPostExecute(Integer adapterPosition) {
                    try {
                        if (adapterPosition > -1) {
                            // Note inserted
                            getView().clearEditText();
                            getView().notifyItemInserted(adapterPosition + 1);
                            getView().notifyItemRangeChanged(adapterPosition, mModel.getNotesCount());
                        } else {
                            // Informs about error
                            getView().hideProgress();
                            getView().showToast(makeToast("Error creating note [" + noteText + "]"));
                        }
                    } catch (NullPointerException e) {
                        e.printStackTrace();
                    }
                }
            }.execute();
        } else {
            try {
                getView().showToast(makeToast("Cannot add a blank note!"));
            } catch (NullPointerException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * Creates a Note object with given text
     * @param noteText  String with Note text
     * @return  A Note object
     */
    public Note makeNote(String noteText) {
        Note note = new Note();
        note.setText( noteText );
        note.setDate(getDate());
        return note;

    }
}

لایه مدل

لایه مدل مسئول انجام منطق کاری برنامه است. این کلاس یک ArrayList با یادداشت‌هایی که به دیتابیس اضافه شده‌اند و یک DAO برای انجام عملیات دیتابیس و یک ارجاع به معرف (Presenter) دارد.

۴- وصل کردن اجزا به هم

حالا که لایه‌های مختلف MVP را داریم وقت آن است که از روی آن‌ها شی‌ بسازیم و آن‌ها را به هم وصل کنیم. اما قبل از آن باید چند موضوع را مستقیماً به اندروید ربط دارند بررسی کنیم.

نمونه‌سازی از لایه‌ها

از آنجایی که اندروید اجازه نمی‌دهد یک شی از روی اکتیویتی ساخته شود، لایه نمایشگر توسط اندروید نمونه‌سازی خواهد شد. ما باید از لایه‌های معرف و مدلنمونه یا شی بسازیم. متأسفانه ساختن اشیا بیرون از اکتیویتی ممکن است مشکلاتی برای ما ایجاد کند.

توصیه می‌کنیم که از نوعی «تزریق وابستگی» (dependency injection) استفاده کنید. از آنجایی که تمرکز ما بر روی پیاده‌سازی MVP است، از روش ساده‌تری استفاده می‌کنیم، اما این سادگی فقط در درک مسأله است. در ادامه این مبحث به تزریق وابستگی هم خواهیم پرداخت.

  • نمونه‌سازی از روی معرف و مدل در اکتیویتی به عنوان متغیرهای محلی
  • تنظیم RequiredViewOps و ProvidedModelOps در معرف
  • تنظیم RequiredPresenterOps در مدل
  • نگهداری ProvidedPresenterOps به عنوان یک ارجاع (reference) در نمایشگر
/**
* Setup Model View Presenter pattern
*/
private void setupMVP() {
     // Create the Presenter
     MainPresenter presenter = new MainPresenter(this);
     // Create the Model
     MainModel model = new MainModel(presenter);
     // Set Presenter model
     presenter.setModel(model);
     // Set the Presenter as a interface
     mPresenter = presenter;
}

مدیریت تغییر تنظیمات

نکته دیگری که باید همیشه به آن توجه کنیم چرخه زندگی اکتیویتی است. اکتیویتی‌های اندروید ممکن است در هر زمانی نابود شوند و لایه‌های معرف و مدل هم به همراه آن نابود می‌شوند. ما باید برای حل این مشکل و نگهداری حالت برنامه در زمان تغییر تنظیمات دستگاه (مانند تغییر جهت صفحه نمایش از عمودی به افقی و مانند آن) از نوعی ماشین نگهداری حالت کمک بگیریم.

برای این کار ما از یک کلاس دیگر به نام StateMaintainer استفاده می‌کنیم که شامل یک فرگمنت است که حالت خودش را نگهداری می‌کند و ما از این فرگمنت برای ذخیره‌سازی و بازیابی اشیامان استفاده می‌کنیم. کد این کلاس در سورس کدهای پروژه هست و می‌توانید آن را ببینید.

ما باید متد onDestroy را به معرف و مدل اضافه کنیم تا آن‌ها را از وضعیت فعلی اکتیویتی مطلع کنیم. همچنین باید متد setView را به معرف اضافه کنیم که مسئول گرفتن ارجاع به یک نمایشگر جدید بعد از بازسازی اکتیویتی است.

ch-28-09-mvp_view-lifecycle

public class MainActivity
        extends AppCompatActivity
    implements View.OnClickListener, MVP_Main.RequiredViewOps
{
    // …
    private void setupMVP() {
        // Check if StateMaintainer has been created
        if (mStateMaintainer.firstTimeIn()) {
            // Create the Presenter
            MainPresenter presenter = new MainPresenter(this);
            // Create the Model
            MainModel model = new MainModel(presenter);
            // Set Presenter model
            presenter.setModel(model);
            // Add Presenter and Model to StateMaintainer
            mStateMaintainer.put(presenter);
            mStateMaintainer.put(model);

            // Set the Presenter as a interface
            // To limit the communication with it
            mPresenter = presenter;

        }
        // get the Presenter from StateMaintainer
        else {
            // Get the Presenter
            mPresenter = mStateMaintainer.get(MainPresenter.class.getName());
            // Updated the View in Presenter
            mPresenter.setView(this);
        }
    }
    // …
}

موخره

معماری MVP می‌تواند بعضی از مسائلی که در معماری پیش‌فرض اندروید وجود دارند را حل کند. این معماری نگهداری از کد و تست برنامه را آسان می‌کند. ممکن است در ابتدای کار پیاده‌سازی MVP مشکل به نظر برسد اما وقتی که منطق پشت آن را فهمیدید، کل ماجرا برایتان ساده می‌شود.

الان می‌توانید کتابخانه MVP خودتان را بسازید یا از راه‌حل‌های موجود مانند Mosby یا Simple-mvp استفاده کنید. حالا می‌فهمید که این کتابخانه‌ها در پشت صحنه چه کار می‌کنند.

ما تقریباً به انتهای سفر خودمان در MVP رسیدیم. در بخش سوم و آخر این سری، تست واحد (Unit Test) و تزریق وابستگی (dependency Injection) را به برنامه اضافه می‌کنیم. همچنان ما را دنبال کنید.

7 فکر می‌کنند “آموزش اندروید-فصل ۲۸-۲: معماری MVP در برنامه‌های اندروید

  1. امیر

    سلام
    ممنون از مقالتون
    ولی بهتر نیس یه سری کلمات به فارسی ترجمه نشن – مثل معرف یا نمایشگر یا آزمون‌شوندگی

    پاسخ
  2. عباس

    سلام . به نظرم این نوع پیاده سازی درست نیست شما حتی setOnClickListener ها رو هم در لایه presenter تعریف کردین و یا recyclerview و اینها از اساس view هستن و ربطی به منطق ندارن . میتونستین منطق رو بهشون تزریغ کنید از presenter و در view عملیات ساخت رو اضافه کنید .

    مشکل بعدی که مشکل نمیشه گفت بهش این هست که از Callback داریم استفاده میکنیم بهتره به نظرم در ارتباط بین لایه های Model و Presenter از callback استفاده نشه چون نیازی نیست اما در ارتباط با View نیاز هست چون نمیتونیم از این کلاس نمونه سازی کنیم .

    نکته بعدی این هست که اصلا activity رو نمیشه view حساب کرد .

    پاسخ
  3. ریحانه فرش باف

    سلام. ممنون از آموزش های خوبتون. اسمارت لب یکی از بهترین منابع آموزشی فارسی زبان اندروید هست که من خیلی اوقات بهش سر میزنم و ازش خیلی چیزا یاد گرفتم. در رابطه با این آموزشتون، با توجه به اینکه مدت زیادی ازش میگذره، بنظرتون الان با وجود الگوی معماری MVVM باز هم نیازی به MVP تو اندروید هست؟ و اگر هست چطور باید تشخیص بدیم کدوم یکی برای کار ما مناسبه؟

    پاسخ
  4. احسان

    سلام
    من یه سوال برام پیش اومده؛ چرا به جای این که توی اکتیویتی بیایم از presenter و model شی بسازیم یا تزریق وابستگی انجام بدیم، نمیایم از Singleton استفاده کنیم، یعنی presenter و model رو بیایم Singleton کنیم.
    به دلیل اینکه ما یه شی از اونا بیشتر نیاز نداریم و یکی باشه بهتره و همینطور خودشون object خودشون رو نگه میدارن.
    هر جا لازمه صداشون بزنیم getInstance میکنیم.

    پاسخ

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *