ما ویژگیهای الگوی معماری MVP را در بخش اول مطلب آموزش دادیم و در بخش دوم هم این الگو را در برنامه خودمان پیادهسازی کردیم. حالا وقت آن است که کمی بیشتر به اعماق برویم. در این آموزش بر روی موضوعات زیر تمرکز میکنیم:
- آمادهسازی محیط تست برنامه و نوشتن تست واحد (Unit Test) برای کلاسهای MVP
- پیادهسازی MVP به همراه تزریق وابستگی با استفاده از Dagger2
- و در اخر درباره مشکلات متداول در اندروید که باید از آنها اجتناب کرد صحبت خواهیم کرد.
۱- تست واحد (Unit Test)
یکی از بزرگترین نتایج استفاده از الگوی MVP ساده شدن فرایند تست برنامه است. پس بیایید برای کلاسهای مدل و معرفی که در قسمت قبل ساختیم تست بنویسیم. تستها را با استفاده از Robolectric اجرا میکنیم که یک چهارچوب تست واحد است که امکانات خیلی خوبی برای کلاسهای اندروید فراهم آورده است. برای ساخت اشیای قالب (Mock Objects) از Mockito استفاده خواهیم کرد که به ما اجازه میدهد بررسی کنیم که آیا کتدهای خاصی صدا زده شدهاند یا نه.
گام اول: آمادهسازی
فایل build.gradle مربوط به ماژول app را ویرایش کنید و وابستگیهای زیر را به آن اضافه کنید:
dependencies { //… testCompile 'junit:junit:4.12' // Set this dependency if you want to use Hamcrest matching testCompile 'org.hamcrest:hamcrest-library:1.1' testCompile "org.robolectric:robolectric:3.0" testCompile 'org.mockito:mockito-core:1.10.19' }
در شاخه src پروژه شاخهای با ساختار زیر بسازید: test/java/[package-name]/[app-name] بعد از آن تنظیمات مربوط به تست را به برنامه اضافه کنید. برای این کار دکمه Edit configuration را کلیک کنید: بر روی دکمه + کلیک کنید و از لیست JUnit را انتخاب کنید:
مقدار Working Directory را $MODULE_DIR$ بگذارید:
این تنظیمات برای اجرای همه تستها است بنابراین در قسمت Test Kind مقدار All in package را انتخاب کنید و بعد نام بسته یا پکیج برنامه را در قسمت Package بنویسید.
گام ۲: تست کردن مدل
بیایید تست خود را با کلاس مدل شروع کنیم. تست را کلاس RobolectricGradleTestRunner اجرا خواهد کرد که لوازم و منابع مورد نیاز برای عملیات ویژه اندروید را فراهم کرده است. ضروری است که حاشیهنوشت @Config را با مقادیر زیر تنظیم کنیم:
@RunWith(RobolectricGradleTestRunner.class) // Change what is necessary for your project @Config(constants = BuildConfig.class, sdk = 21, manifest = "/src/main/AndroidManifest.xml") public class MainModelTest { // write the tests }
میخواهیم که از یک DAO واقعی استفاده کنیم تا ببینیم آیا دادهها به درستی مدیریت میشوند یا نه. برای دسترسی به شی Context از کلاس RuntimeEnvironment.application استفاده میکنیم.
private DAO mDAO; // To test the Model you can just // create the object and and pass // a Presenter mock and a DAO instance @Before public void setup() { // Using RuntimeEnvironment.application will permit // us to access a Context and create a real DAO // inserting data that will be saved temporarily Context context = RuntimeEnvironment.application; mDAO = new DAO(context); // Using a mock Presenter will permit to verify // if certain methods were called in Presenter MainPresenter mockPresenter = Mockito.mock(MainPresenter.class); // We create a Model instance using a construction that includes // a DAO. This constructor exists to facilitate tests mModel = new MainModel(mockPresenter, mDAO); // Subscribing mNotes is necessary for tests methods // that depends on the arrayList mModel.mNotes = new ArrayList<>(); // We’re reseting our mock Presenter to guarantee that // our method verification remain consistent between the tests reset(mockPresenter); }
حالا وقت آن است که متدهای کلاس Model را تست کنیم.
// Create Note object to use in the tests private Note createNote(String text) { Note note = new Note(); note.setText(text); note.setDate("some date"); return note; } // Verify loadData @Test public void loadData(){ int notesSize = 10; // inserting data directly using DAO for (int i =0; i<notesSize; i++){ mDAO.insertNote(createNote("note_" + Integer.toString(i))); } // calling load method mModel.loadData(); // verify if mNotes, an ArrayList that receives the Notes // have the same size as the quantity of Notes inserted assertEquals(mModel.mNotes.size(), notesSize); } // verify insertNote @Test public void insertNote() { int pos = mModel.insertNote(createNote("noteText")); assertTrue(pos > -1); } // Verify deleteNote @Test public void deleteNote() { // We need to add a Note in DB Note note = createNote("testNote"); Note insertedNote = mDAO.insertNote(note); // add the same Note inside mNotes ArrayList mModel.mNotes = new ArrayList<>(); mModel.mNotes.add(insertedNote); // verify if deleteNote returns the correct results assertTrue(mModel.deleteNote(insertedNote, 0)); Note fakeNote = createNote("fakeNote"); assertFalse(mModel.deleteNote(fakeNote, 0)); }
حالا میتوانیم تست کلاس Model را اجرا و نتایج آن را بررسی کنیم. بقیه ویژگیهای کلاس را هم با خیال راحت تست کنید.
گام ۳: تست معرف (Presenter)
حالا بیایید بر روی تست معرف تمرکز کنیم. برای این تست به Robolectric احتیاج داریم تا بتوانیم از بعضی کلاسهای Android مانند AsyncTask استفاده کنیم. تنظیمات این تست بسیار شبیه تست کلاس Model است. ما از نمایشگر و مدل قالبی (Mock) برای تأیید صدا زدن متدها و مقادیر برگشتی استفاده میکنیم.
@RunWith(RobolectricGradleTestRunner.class) @Config(constants = BuildConfig.class, sdk = 21, manifest = "/src/main/AndroidManifest.xml") public class MainPresenterTest { private MainPresenter mPresenter; private MainModel mockModel; private MVP_Main.RequiredViewOps mockView; // To test the Presenter you can just // create the object and pass the Model and View mocks @Before public void setup() { // Creating the mocks mockView = Mockito.mock( MVP_Main.RequiredViewOps.class ); mockModel = Mockito.mock( MainModel.class, RETURNS_DEEP_STUBS ); // Pass the mocks to a Presenter instance mPresenter = new MainPresenter( mockView ); mPresenter.setModel(mockModel); // Define the value to be returned by Model // when loading data when(mockModel.loadData()).thenReturn(true); reset(mockView); } }
برای تست متدهای کلاس معرف بیایید از عملیات clickNewNote() شروع کنیم که مسئولیت ایجاد یک یادداشت جدید و ثبت آن در دیتابیس با استفاده از AsyncTask بر عهده دارد.
@Test public void testClickNewNote() { // We need to mock a EditText EditText mockEditText = Mockito.mock(EditText.class, RETURNS_DEEP_STUBS); // the mock should return a String when(mockEditText.getText().toString()).thenReturn(“Test_true"); // we also define a fake position to be returned // by the insertNote method in Model int arrayPos = 10; when(mockModel.insertNote(any(Note.class))).thenReturn(arrayPos); mPresenter.clickNewNote(mockEditText); verify(mockModel).insertNote(any(Note.class)); verify(mockView).notifyItemInserted( eq(arrayPos+1) ); verify(mockView).notifyItemRangeChanged(eq(arrayPos), anyInt()); verify(mockView, never()).showToast(any(Toast.class)); }
ما میتوانیم یک سناریوی تست طراحی کنیم که در آن متد insertNote() خطا برمیگرداند.
@Test public void testClickNewNoteError() { EditText mockEditText = Mockito.mock(EditText.class, RETURNS_DEEP_STUBS); when(mockModel.insertNote(any(Note.class))).thenReturn(-1); when(mockEditText.getText().toString()).thenReturn("Test_false"); when(mockModel.insertNote(any(Note.class))).thenReturn(-1); mPresenter.clickNewNote(mockEditText); verify(mockView).showToast(any(Toast.class)); }
در آخر هم متد deleteNote() را در هر دو حالت اجرای درست و با مقدار برگشتی خطا تست میکنیم.
@Test public void testDeleteNote(){ when(mockModel.deleteNote(any(Note.class), anyInt())).thenReturn(true); int adapterPos = 0; int layoutPos = 1; mPresenter.deleteNote(new Note(), adapterPos, layoutPos); verify(mockView).showProgress(); verify(mockModel).deleteNote(any(Note.class), eq(adapterPos)); verify(mockView).hideProgress(); verify(mockView).notifyItemRemoved(eq(layoutPos)); verify(mockView).showToast(any(Toast.class)); } @Test public void testDeleteNoteError(){ when(mockModel.deleteNote(any(Note.class), anyInt())).thenReturn(false); int adapterPos = 0; int layoutPos = 1; mPresenter.deleteNote(new Note(), adapterPos, layoutPos); verify(mockView).showProgress(); verify(mockModel).deleteNote(any(Note.class), eq(adapterPos)); verify(mockView).hideProgress(); verify(mockView).showToast(any(Toast.class)); }
۲- تزریق وابستگی با استفاده از Dagger 2
تزریق وابستگی ابزاری عالی برای توسعهدهندگان است. اگر با تزریق وابستگی آشنا نیستید، عمیقاً توصیه میکنم مقاله «کری» درباره تزریق وابستگی بخوانید.
تزریق وابستگی روشی برای تنظیم مقادیر یک شی و همکاران آن توسط یک نهاد خارجی است. به بیان دیگر اشیا را یک نهاد بیرونی تنظیم میکند. تزریق وابستگی روش جایگزین این است که یک شی خودش خودش را تنظیم کند. یاکوب ینکوف
در مثال ما، تزریق وابستگی امکان میدهد که مدل و معرف بیرون از نمایشگر ایجاد شوند و این باعث میشود که لایههای معماری MVP کمتر به هم وابستگی داشته باشند و با این کار جدایی موضوعات بیشتری در پروژه اعمال میشود. ما از Dagger 2 استفاده میکنیم، کتابخانهای فوقالعاده از طرف گوگل، تا در تزریق وابستگی به ما کمک کند. با این که آمادهسازی و تنظیم Dagger 2 بسیار ساده سر راست است، ولی این کتابخانه امکانات عالی فراوانی دارد که آن را کتابخانه سودمند ولی پیچیدهای میکند. ما فقط بر روی قسمتهایی از این کتابخانه که برای پیادهسازی MVP به آنها احتیاج داریم میپردازیم و خیلی وارد جزئیات این کتابخانه نمیشویم. اگر میخواهید بیشتر درباره Dagger بدانید، مقاله «کری» را بخوانید یا به مستنداتی که گوگل فراهم آورده است سری بزنید.
گام ۱: تنظیم Dagger 2
با تغییر در فایل build.gradle پروژه و افزودن وابستگی زیر آغاز کنید:
dependencies { // ... classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' }
سپس فایل build.dagger پروژه را مانند نمونه زیر تغییر بدهید.
apply plugin: 'com.neenbedankt.android-apt' dependencies { // apt command comes from the android-apt plugin apt 'com.google.dagger:dagger-compiler:2.0.2' compile 'com.google.dagger:dagger:2.0.2' provided 'org.glassfish:javax.annotation:10.0-b28' // ... }
پروژه را همکام یا Sync کنید و تا انتهای این کار صبر کنید.
گام ۲: پیادهسازی MVP با Dagger 2
بیایید با ساخت @Scope برای کلاسهای اکتیویتی شروع کنیم. ActivityScope را همانند کد زیر بسازید:
@Scope public @interface ActivityScope { }
حالا باید MainActivity را به عنوان یک @Module معرفی کنیم. اگر بیشتر از یک اکتیویتی داشته باشید، باید برای همه آنها این کار را بکنید.
@Module public class MainActivityModule { private MainActivity activity; public MainActivityModule(MainActivity activity) { this.activity = activity; } @Provides @ActivityScope MainActivity providesMainActivity() { return activity; } @Provides @ActivityScope MVP_Main.ProvidedPresenterOps providedPresenterOps() { MainPresenter presenter = new MainPresenter( activity ); MainModel model = new MainModel( presenter ); presenter.setModel( model ); return presenter; } }
ما همچنین به یک @Subcomponent نیاز داریم که پلی باشد برای @Component برنامه که باید آن را هم بسازیم.
@ActivityScope @Subcomponent( modules = MainActivityModule.class ) public interface MainActivityComponent { MainActivity inject(MainActivity activity); }
باید یک @Module و یک @Component برای Application بسازیم.
@Module public class AppModule { private Application application; public AppModule(Application application) { this.application = application; } @Provides @Singleton public Application providesApplication() { return application; } }
@Singleton @Component( modules = AppModule.class) public interface AppComponent { Application application(); MainActivityComponent getMainComponent(MainActivityModule module); }
در انتها، ما یک کلاس Application میخواهیم تا تزریق وابستگی را راهاندازی کند.
public class SampleApp extends Application { public static SampleApp get(Context context) { return (SampleApp) context.getApplicationContext(); } @Override public void onCreate() { super.onCreate(); initAppComponent(); } private AppComponent appComponent; private void initAppComponent(){ appComponent = DaggerAppComponent.builder() .appModule(new AppModule(this)) .build(); } public AppComponent getAppComponent() { return appComponent; } }
ثبت کلاس Application در فایل مانیفست را فراموش نکنید.
<application android:name=".SampleApp" </application>
گام ۳: تزریق کلاسهای MVP
بالاخره میتوانیم میتوانیم به کمک @Inject کلاسهای MVP را تزریق کنیم. تغییرات مورد نیاز باید در کلاس MainActivity صورت بگیرد. ما روش مقدار دهی کلاسهای مدل و معرف را تغییر میدهیم. قدم اول تغییر اعلان متغیر MVP_Main.ProvidedPresenterOps است. باید آن را عمومی یا public اعلام کنیم و باید @Inject به آن اضافه کنیم.
@Inject public MVP_Main.ProvidedPresenterOps mPresenter;
برای تنظیم MainActivityComponent کد زیر را اضافه کنید.
/** * Setup the {@link com.tinmegali.tutsmvp_sample.di.component.MainActivityComponent} * to instantiate and inject a {@link MainPresenter} */ private void setupComponent(){ Log.d(TAG, "setupComponent"); SampleApp.get(this) .getAppComponent() .getMainComponent(new MainActivityModule(this)) .inject(this); }
تنها چیزی که باقی مانده است مقداردهی یا بازمقداردهی معرف بر اساس وضعیتش در StateMaintainer است. برای این کار متد setupMVP() را تغییر داده و کدهای زیر را اضافه میکنیم.
/** * Setup Model View Presenter pattern. * Use a {@link StateMaintainer} to maintain the * Presenter and Model instances between configuration changes. */ private void setupMVP(){ if ( mStateMaintainer.firstTimeIn() ) { initialize(); } else { reinitialize(); } } /** * Setup the {@link MainPresenter} injection and saves in <code>mStateMaintainer</code> */ private void initialize(){ Log.d(TAG, "initialize"); setupComponent(); mStateMaintainer.put(MainPresenter.class.getSimpleName(), mPresenter); } /** * Recover {@link MainPresenter} from <code>mStateMaintainer</code> or creates * a new {@link MainPresenter} if the instance has been lost from <code>mStateMaintainer</code> */ private void reinitialize() { Log.d(TAG, "reinitialize"); mPresenter = mStateMaintainer.get(MainPresenter.class.getSimpleName()); mPresenter.setView(this); if ( mPresenter == null ) setupComponent(); }
حالا کلیه عناصر MVP به طور مستقل از نمایشگر تنظیم میشوند و به کمک تزریق وابستگی کد ساختار بهتری دارد. میتواند کد را بیشتر از این هم ارتقا داد و از تزریق وابستگی برای تزریق کلاسهای دیگر مانند DAO هم استفاده کنید.
۳- جلوگیری از اشتباهات متداول
من در اینجا تعدادی از خطاهای متداول را فهرست کردهام که باید در زمان استفاده از MVP از آنها اجتناب کنید:
- همیشه قبل از صدا زدن نمایشگر چک کنید که نمایشگر آماده و در دسترس باشد. نمایشگر شدیداً وابسته به چرخه زندگی برنامه است و ممکن است در زمانی که شما درخواستی را به آن میفرستید، نابود شده باشد.
- فراموش نکنید که پس از ساخت شدن دوباره نمایشگر، یک ارجاع جدید برایش بفرستید.
- هر زمان که نمایشگر نابود شد، متد onDestroy() را در معرف صدا بزنید. بعضی وقتها، باید رویدادهای onPause() و onStop() را هم به معرف اطلاع بدهید.
- هر زمان که با نمایشگرهای پیچیده کار میکنید، سعی کنید از چند معرف استفاده کنید.
- وقتی که از چند معرف استفاده میکنید، بهترین روش برای جابجایی اطلاعات بین آنها استفاده از نوعی «گذرگاه رویداد» یا Event Bus است.
- برای این که تا حد ممکن لایه نمایشگر را منفعل بسازید، از تزریق وابستگی برای ساخت لایههای معرف و مدل در بیرون از لایه نمایشگر استفاده کنید.
مؤخره
به انتهای این سری آموزش MVP رسیدیم. حالا باید بتوانید الگوی معماری MVP را در پروژههایتان استفاده کنید، آن را تست کنید و از تزریق وابستگی استفاده کنید. امیدوارم این مطالب برای شما مفید باشد.
کد برنامه نمونه
برنامهای که در سری آموزشی نوشتیم را میتوانید در گیتهاب ببینید، یا آن را بگیرید و خودتان از نزدیک ببینید که چه اتفاقاتی در برنامه افتاده است.
بسیار ممنون هستم ،،، از مطالب کاربردی که در داخل سایت به اشتراک میگذارید.
میخواستم اگر لطف کنید ،،، مقاله «کری» درباره تزریق وابستگی رو هم ترجمه کنید و به اشتراک بگذارید.
با تشکر و خسته نباشید.
به امید خدا در فرصت مناسب!
مطلب کری هم ترجمه شده و در سایت قرار گرفت:
آموزش اندروید-فصل ۲۸-۵: تزریق وابستگی با Dagger2
سلام خسته نباشید، ممنون بابت وبلاگ خوب تون.
اگه میشه یه مطلب در مورد مقایسه معماری های مختلف اندروید مثل MVP, MVC , MVVM , Android architecture component بنویسید. بازم ممنون بابت مطالب مفید و کاربردی تون
سلام. امیدوارم تا قبل از تعطیلات نوروزی بتونم یه مطلب درباره کامپوننتهای معماری اندروید بنویسم.