در قسمتهای قبلی این مطلب با الگوی معماری MVP آشنا شدیم:
آموزش اندروید-فصل ۲۸-۱: مقدمه معماری MVP در برنامههای اندروید
آموزش اندروید-فصل ۲۸-۲: معماری MVP در برنامههای اندروید
آموزش اندروید-فصل ۲۸-۳: تست برنامههای MVP
برای تکمیل آموزش MVP و آشنایی با روش گوگل برای پیادهسازی الگوی معماری MVP در این بخش به سراغ مثال گوگل برای MVP میرویم. گوگل در بخش مثالهای خود در سایت گیتهاب، برنامهای دارد به نام todo که آن را به شیوههای مختلف پیادهسازی کرده است.
این برنامه نمونه به روشهای زیر پیادهسازی شده است:
todo-mvp:
این برنامه اصول پایه الگوی معماری MVP را نشان میدهد.
todo‑mvp‑loaders:
همان برنامه قبلی است که این بار اطلاعات را به کمک Loaders API واکشی (fetch) میکند.
todo‑databinding:
همان برنامه todo-mvp است که این بار اطلاعات را به استفاده از Data Binding Library واکشی میکند.
todo‑mvp‑clean:
برنامه todo که با استفاده از مفاهیم معماری «پاکیزه» یا clean architecture پیادهسازی شده است.
todo‑mvp‑dagger:
همان برنامه todo-mvp است که برای تزریق وابستگی از Dagger2 استفاده میکند.
todo‑mvp‑contentproviders:
برنامهای بر اساس todo‑mvp‑loaders که از عرضهکنندگان محتوا یا Content Providers در کنار Loaders API استفاده میکند.
todo‑mvp‑rxjava:
همان برنامه todo-mvp است که برای پیادهسازی همزمانی (concurrency) و لایه داده انتزاعی (Abstract Data Layer) از کتابخانه RxJava استفاده میکند.
todo‑mvvm‑databinding:
برنامهای بر اساس todo‑databinding که از الگوی معماری MVVM استفاده میکند.
در این مطلب سعی میکنم نگاهی بیاندازیم به این برنامه و یک بار دیگر MVP را با هم مرور کنیم.
برنامه todo-mvp
این برنامه را میتوانید از گیتهاب دانلود کنید. این نسخه از برنامه به عنوان پایهای برای سایر مثالهای todo استفاده میشود. هدف از ارائه این برنامه نمونه:
- استفاده از معماری MVP بدون استفاده از هیچ چارچوب برنامهنویسی
- یک مرجع پایه برای بقیه مثالها
است. در این پروژه هر جا «نمایشگر» گفته میشود، منظور «نمایشگر» در معماری MVP است. این پروژه به کتابخانههای زیر وابسته است:
- Common Android support libraries: کلاسهایی که در بسته com.android.support.* قرار دارند. از این کتابخانه برای انتقال ویژگیهای جدید اندروید به نسخههای قدیمیتر استفاده میشود.
- Android Testing Support Library: چارچوبی برای پشتیبانی از تست UI با استفاده از Espresso و AndroidJUnitRunner.
- Mockito: کتابخانهای برای ساخت تست قالب (Mock)
- Guava: مجموعهای از کتابخانههای پایه برای جاوا که معمولاً در برنامههای اندروید از آنها استفاده میشود.
طراحی برنامه
همه نسخههای برنامه todo ویژگیهای یکسانی دارند. برنامه شامل چهار صفحه است:
- Tasks: لیستی از وظایف را نمایش میدهد.
- TaskDetail: جزئیات یک وظیفه را نمایش میدهد.
- AddEditTask: صفحهای که از آن برای ایجاد وظیفه جدید یا ویرایش وظیفه استفاده میشود.
- Statistics: آمار وظایف را نشان میدهد.
در این مثالها هر صفحه با این کلاسها و اینترفیسها پیادهسازی شده است:
- یک کلاس contract که ازتباط بین نمایشگر و معرف را تعریف میکند.
- یک اکتیویتی که معرف و فرگمنتها را میسازد.
- یک فرگمنت که اینترفیس نمایشگر را پیادهسازی میکند.
- یک «معرف» که اینترفیس معرف را پیادهسازی میکند.
برای درک بهتر اجزای برنامه به تصویر زیر دقت کنید:
منطق برنامه معمولاً در معرف قرار دارد و نمایشگر مرتبط کارهای مربوط به UI را انجام میدهد. نمایشگر هیچ به منطق برنامه دسترسی ندارد و کارش تبدیل فرمانهای معرف به عملیات UI است. همچنین عملیات کاربر را به معرف انتقال میدهد.
پیادهسازی برنامه
قبل از هر چیزی باید ببینیم پروژه چطور ساختاربندی شده است. هر چند که این ساختار اجباری نیست و میتوان به شیوههای متفاوتی ساختار پروژه را ساخت، اما این شیوهای است که گوگل استفاده میکند و به نظر من باعث دستهبندی راحتتر اجزای پروژه میشود:
همانطور که در تصویر میبینید، دو کلاس عمومی که در همه صفحات از آنها استفاده میشود در ریشه پروژه قرار دارند: BasePresenter و BaseView. برای هر صفحه/اکتیویتی برنامه یک بسته/پکیج جدید ساخته شده و تمام کلاسهای مرتبط با آن در آن در همان بسته قرار دارد. مثلا در بسته taskdetil چهار کلاس زیر قرار دارند:
- TaskDetailActivity: این اکتیویتی کارش ساختن «معرف» و «نمایشگر» است.
- TaskDetailContract: برای دسترسی راحتتر، یک اینترفیس ساختهایم که اینترفیسهای مربوط به «نمایشگر» و «معرف» را در آن تعریف کردهایم.
- TaskDetailFragment: این فرگمنت نقش «نمایشگر» را در معماری MVP بر عهده دارد.
- TaskDetailPresenter: همانطور که از نامش پیداست، پیادهسازی «معرف» در این کلاس است.
برای آشنایی بیشتر با این کلاسها نگاهی به کد آنها میاندازیم:
کلاس TaskDetailContract
با این اصلیترین قسمت کار شروع میکنیم:
public interface TaskDetailContract { interface View extends BaseView<Presenter> { void setLoadingIndicator(boolean active); void showMissingTask(); void hideTitle(); void showTitle(String title); void hideDescription(); void showDescription(String description); void showCompletionStatus(boolean complete); void showEditTask(String taskId); void showTaskDeleted(); void showTaskMarkedComplete(); void showTaskMarkedActive(); boolean isActive(); } interface Presenter extends BasePresenter { void editTask(); void deleteTask(); void completeTask(); void activateTask(); } }
ساختار این کلاس بسیار ساده است: یک اینترفیس که داخل آن دو اینترفیس دیگر قرار دارد. هر کدام از این دو اینترفیس، نقش کلیدی در معماری MVP ایفا میکنند.
اینترفیس TaskDetailContract.View همان اینترفیس «نمایشگر» در معماری MVP است. از آنجایی که TaskDetailFragment قرار است نقش «نمایشگر» را ایفا کند، پس باید این اینترفیس را پیادهسازی کند.
اینترفیس TaskDetailContract.Presenter هم همانطور که از نامش پیدا است نقش «معرف» معماری MVP را ایفا میکند و کلاس TaskDetailPresenter این اینترفیس را پیادهسازی میکند.
اگر به اسم متدهای اینترفیس TaskDetailContract.View دقت کنید دقیقاً میفهمید که در این صفحه برنامه چه اتفاقاتی قرار است بیافتد و همین یکی از مزیتهای اصلی «جدایی موضوعات» و الگوی معماری MVP است.
کلاس TaskDetailPresenter
این کلاس پیادهسازی اینترفیس معرف است و نقش اصلی آن برقراری ارتباط بین مدل و نمایشگر است:
public class TaskDetailPresenter implements TaskDetailContract.Presenter { private final TasksRepository mTasksRepository; private final TaskDetailContract.View mTaskDetailView; @Nullable private String mTaskId; public TaskDetailPresenter(@Nullable String taskId, @NonNull TasksRepository tasksRepository, @NonNull TaskDetailContract.View taskDetailView) { mTaskId = taskId; mTasksRepository = checkNotNull(tasksRepository, "tasksRepository cannot be null!"); mTaskDetailView = checkNotNull(taskDetailView, "taskDetailView cannot be null!"); mTaskDetailView.setPresenter(this); } @Override public void start() { openTask(); } private void openTask() { if (Strings.isNullOrEmpty(mTaskId)) { mTaskDetailView.showMissingTask(); return; } mTaskDetailView.setLoadingIndicator(true); mTasksRepository.getTask(mTaskId, new TasksDataSource.GetTaskCallback() { @Override public void onTaskLoaded(Task task) { // The view may not be able to handle UI updates anymore if (!mTaskDetailView.isActive()) { return; } mTaskDetailView.setLoadingIndicator(false); if (null == task) { mTaskDetailView.showMissingTask(); } else { showTask(task); } } @Override public void onDataNotAvailable() { // The view may not be able to handle UI updates anymore if (!mTaskDetailView.isActive()) { return; } mTaskDetailView.showMissingTask(); } }); } @Override public void editTask() { if (Strings.isNullOrEmpty(mTaskId)) { mTaskDetailView.showMissingTask(); return; } mTaskDetailView.showEditTask(mTaskId); } @Override public void deleteTask() { if (Strings.isNullOrEmpty(mTaskId)) { mTaskDetailView.showMissingTask(); return; } mTasksRepository.deleteTask(mTaskId); mTaskDetailView.showTaskDeleted(); } @Override public void completeTask() { if (Strings.isNullOrEmpty(mTaskId)) { mTaskDetailView.showMissingTask(); return; } mTasksRepository.completeTask(mTaskId); mTaskDetailView.showTaskMarkedComplete(); } @Override public void activateTask() { if (Strings.isNullOrEmpty(mTaskId)) { mTaskDetailView.showMissingTask(); return; } mTasksRepository.activateTask(mTaskId); mTaskDetailView.showTaskMarkedActive(); } private void showTask(@NonNull Task task) { String title = task.getTitle(); String description = task.getDescription(); if (Strings.isNullOrEmpty(title)) { mTaskDetailView.hideTitle(); } else { mTaskDetailView.showTitle(title); } if (Strings.isNullOrEmpty(description)) { mTaskDetailView.hideDescription(); } else { mTaskDetailView.showDescription(description); } mTaskDetailView.showCompletionStatus(task.isCompleted()); } }
همانطور که گفتم این کلاس قرار است ارتباط بین مدل و نمایشگر را برقرار کند. برای این کار باید هم به نمایشگر دسترسی داشته باشد و هم به مدل. برای انجام این کار و برقراری ارتباط بین این دو، نمونه مدل و نمایشگر را از طریق متد سازنده میگیرد:
public TaskDetailPresenter(@Nullable String taskId, @NonNull TasksRepository tasksRepository, @NonNull TaskDetailContract.View taskDetailView) { mTaskId = taskId; mTasksRepository = checkNotNull(tasksRepository, "tasksRepository cannot be null!"); mTaskDetailView = checkNotNull(taskDetailView, "taskDetailView cannot be null!"); mTaskDetailView.setPresenter(this); }
پارامترهای سازنده را ببینید:
taskId: شناسه Taskی که میخواهیم جزئیات آن را ببینیم.
tasksRepository: این شی همان مدل برنامه است. در ادامه مطلب به این کلاس هم خواهیم پرداخت.
taskDetailView: شیای از کلاسی است که اینترفیس TaskDetailContract.View را پیادهسازی کرده است. در این برنامه، نمونهای از فرگمنت TaskDetailFragment است.
حالا احتمالاً میپرسید که کار اصلی معرف که ارتباط بین مدل و نمایشگر است چطور انجام میشود؟ برای درک فرایند کار به متد deleteTask دقت کنید:
@Override public void deleteTask() { if (Strings.isNullOrEmpty(mTaskId)) { mTaskDetailView.showMissingTask(); return; } mTasksRepository.deleteTask(mTaskId); mTaskDetailView.showTaskDeleted(); }
عملکرد این متد این است که ابتدا چک میکند که ببیند آیا taskId معبر است یا نه؟ اگر taskId نامعتبر باشد به نمایشگر میگوید که عملی را انجام بدهد که باید در زمان موجود نبودن یک Task انجام بدهد: showMissingTask.
اگر taskId معبر باشد، به مدل (mTasksRepository) میگوید که این Taskرا حذف کند و بعد به نمایشگر میگوید که Task حذف شده است و کاری را انجام بدهد که در زمان حذف Task باید انجام بدهد.
همانطور که میبینید معرف اصلاً نمیداند که مدل چطور Task را حذف میکند و نمایشگر بعد از حذف Task چه کار میکند. این همان جدایی موضوعات است. مدل و نمایشگر مسئول پیادهسازی کارهای محول شده هستند و معرف اینجا فقط واسطه بین اینها است. مدل از وجود نمایشگر و پیادهسازی آن هیچ اطلاعی ندارد و نمایشگر هم نمیداند که چطور قرار است یک Task حذف شود! این جدایی باعث میشود که هر بخشی از برنامه فقط کاری را انجام دهد که بر عهدهاش است و از سایر بخشها جدا شود. تغییر و نگهداری برنامه هم بسیار راحتتر میشود، آفرین به MVP!
کلاس TaskDetailFragment
بعد از این که دیدم معرف چطور کار میکند، حالا نوبت این است که ببینیم نمایشگر ما چطور کار میکند:
public class TaskDetailFragment extends Fragment implements TaskDetailContract.View { @NonNull private static final String ARGUMENT_TASK_ID = "TASK_ID"; @NonNull private static final int REQUEST_EDIT_TASK = 1; private TaskDetailContract.Presenter mPresenter; private TextView mDetailTitle; private TextView mDetailDescription; private CheckBox mDetailCompleteStatus; public static TaskDetailFragment newInstance(@Nullable String taskId) { Bundle arguments = new Bundle(); arguments.putString(ARGUMENT_TASK_ID, taskId); TaskDetailFragment fragment = new TaskDetailFragment(); fragment.setArguments(arguments); return fragment; } @Override public void onResume() { super.onResume(); mPresenter.start(); } @Nullable @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View root = inflater.inflate(R.layout.taskdetail_frag, container, false); setHasOptionsMenu(true); mDetailTitle = (TextView) root.findViewById(R.id.task_detail_title); mDetailDescription = (TextView) root.findViewById(R.id.task_detail_description); mDetailCompleteStatus = (CheckBox) root.findViewById(R.id.task_detail_complete); // Set up floating action button FloatingActionButton fab = (FloatingActionButton) getActivity().findViewById(R.id.fab_edit_task); fab.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mPresenter.editTask(); } }); return root; } @Override public void setPresenter(@NonNull TaskDetailContract.Presenter presenter) { mPresenter = checkNotNull(presenter); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_delete: mPresenter.deleteTask(); return true; } return false; } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.taskdetail_fragment_menu, menu); } @Override public void setLoadingIndicator(boolean active) { if (active) { mDetailTitle.setText(""); mDetailDescription.setText(getString(R.string.loading)); } } @Override public void hideDescription() { mDetailDescription.setVisibility(View.GONE); } @Override public void hideTitle() { mDetailTitle.setVisibility(View.GONE); } @Override public void showDescription(@NonNull String description) { mDetailDescription.setVisibility(View.VISIBLE); mDetailDescription.setText(description); } @Override public void showCompletionStatus(final boolean complete) { Preconditions.checkNotNull(mDetailCompleteStatus); mDetailCompleteStatus.setChecked(complete); mDetailCompleteStatus.setOnCheckedChangeListener( new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { if (isChecked) { mPresenter.completeTask(); } else { mPresenter.activateTask(); } } }); } @Override public void showEditTask(@NonNull String taskId) { Intent intent = new Intent(getContext(), AddEditTaskActivity.class); intent.putExtra(AddEditTaskFragment.ARGUMENT_EDIT_TASK_ID, taskId); startActivityForResult(intent, REQUEST_EDIT_TASK); } @Override public void showTaskDeleted() { getActivity().finish(); } public void showTaskMarkedComplete() { Snackbar.make(getView(), getString(R.string.task_marked_complete), Snackbar.LENGTH_LONG) .show(); } @Override public void showTaskMarkedActive() { Snackbar.make(getView(), getString(R.string.task_marked_active), Snackbar.LENGTH_LONG) .show(); } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == REQUEST_EDIT_TASK) { // If the task was edited successfully, go back to the list. if (resultCode == Activity.RESULT_OK) { getActivity().finish(); } } } @Override public void showTitle(@NonNull String title) { mDetailTitle.setVisibility(View.VISIBLE); mDetailTitle.setText(title); } @Override public void showMissingTask() { mDetailTitle.setText(""); mDetailDescription.setText(getString(R.string.no_data)); } @Override public boolean isActive() { return isAdded(); } }
در توضیح این کلاس اولین نکتهای که باید به آن توجه کنید این است که این کلاس اینترفیس TaskDetailContract.View را پیادهسازی کرده است. بنابراین از منظر MVP این کلاس نقش نمایشگر را بر عهده دارد.
دومین نکته این است که این کلاس متدی دارد به نام setPresenter که کار اصلی آن این است که یک نمونه از TaskDetailContract.Presenter را در اختیار این کلاس قرار میدهد تا نمایشگر ما بتواند عملیات کاربر را از طریق آن به مدل منتقل کند.
@Override public void setPresenter(@NonNull TaskDetailContract.Presenter presenter) { mPresenter = checkNotNull(presenter); }
پیش از این دیدید که معرف چطور به عنوان واسطه بین مدل و نمایشگر، یک Task را حذف میکند. حالا ببینیم واقعا چه اتفاقی میافتد که یک Task حذف میشود و نقش نمایشگر و معرف و مدل چیست؟
فرایند حذف یک Task:
ابتدا کاربر از منوی صفحه جزئیات Task بر روی دکمه Delete کلیک میکند. همانطور که میدانید نمایشگر کارش این است که عملیات کاربر را پردازش کرده و به معرف اطلاع بدهد که کاربر چه عملی انجام داده است. بنابراین هر وقت که کاربر بر روی منوی Delete کلیک میکند، نمایشگر آن را به معرف اطلاع میدهد:
@Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_delete: mPresenter.deleteTask(); return true; } return false; }
حالا نوبت معرف است که این درخواست کاربر را که از نمایشگر دریافت کرده است، پردازش و کار مناسب را انجام دهد:
@Override public void deleteTask() { if (Strings.isNullOrEmpty(mTaskId)) { mTaskDetailView.showMissingTask(); return; } mTasksRepository.deleteTask(mTaskId); mTaskDetailView.showTaskDeleted(); }
همانطور که میبینید معرف درخواست را به مدل انتقال میدهد و حذف Task در مدل اتفاق میافتد:
@Override public void deleteTask(@NonNull String taskId) { mTasksRemoteDataSource.deleteTask(checkNotNull(taskId)); mTasksLocalDataSource.deleteTask(checkNotNull(taskId)); mCachedTasks.remove(taskId); }
بعد از حذف Task معرف از نمایشگر میخواهد به کاربر اطلاع دهد که Task مورد نظر حذف شده است:
@Override public void showTaskDeleted() { getActivity().finish(); }
این سناریویی است که در همه کارها و بخشهای برنامه اتفاق میافتد.
حالا میرسیم به یکی از مهمترین بخشهای برنامه: جایی که باید همه این اجزای پراکنده را به هم وصل کنیم تا برنامه را بتوانیم اجرا کنیم! درست حدس زدید، حالا نوبت بررسی کلاس TaskDetailActivity است.
کلاس TaskDetailActivity
کار اصلی این کلاس این است که نمایشگر، معرف و مدل را بسازد و ارتباط بین آنها را برقرار کند:
public class TaskDetailActivity extends AppCompatActivity { public static final String EXTRA_TASK_ID = "TASK_ID"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.taskdetail_act); // Set up the toolbar. Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); ActionBar ab = getSupportActionBar(); ab.setDisplayHomeAsUpEnabled(true); ab.setDisplayShowHomeEnabled(true); // Get the requested task id String taskId = getIntent().getStringExtra(EXTRA_TASK_ID); TaskDetailFragment taskDetailFragment = (TaskDetailFragment) getSupportFragmentManager() .findFragmentById(R.id.contentFrame); if (taskDetailFragment == null) { taskDetailFragment = TaskDetailFragment.newInstance(taskId); ActivityUtils.addFragmentToActivity(getSupportFragmentManager(), taskDetailFragment, R.id.contentFrame); } // Create the presenter new TaskDetailPresenter( taskId, Injection.provideTasksRepository(getApplicationContext()), taskDetailFragment); } @Override public boolean onSupportNavigateUp() { onBackPressed(); return true; } }
همانطور که میدانید، کلاس TaskDetailFragment نقش نمایشگر را دارد. در متد onCreate اکتیویتی در خطهای زیر نمایشگر ما آمادهسازی میشود:
TaskDetailFragment taskDetailFragment = (TaskDetailFragment) getSupportFragmentManager() .findFragmentById(R.id.contentFrame); if (taskDetailFragment == null) { taskDetailFragment = TaskDetailFragment.newInstance(taskId); ActivityUtils.addFragmentToActivity(getSupportFragmentManager(), taskDetailFragment, R.id.contentFrame); }
خطوط زیر هم معرف را آمادهسازی میکند:
new TaskDetailPresenter( taskId, Injection.provideTasksRepository(getApplicationContext()), taskDetailFragment);
مدل هم توسط کلاس Injection ساخته میشود و به سازنده معرف ارسال میشود و … بعله کار تمام است!
این چرخه برای همه صفحات/اکتیویتیهای دیگر برنامه هم تکرار میشود.
در بخش بعدی این مطلب به سراغ تزریق وابستگی و Dagger میرویم تا همین کاری که بر عهده اکتیویتی TaskDetailActivity گذاشتیم را هم از دوشش برداریم و کاری کنیم که نمایشگر و معرف هیچ ارتباطی با چرخه زندگی اکتیویتی نداشته باشند.
kheili seri post haye MVP khoob boodan mamnonam azatoon