آموزش اندروید-فصل ۲۸-۶: برنامه نمونه todo و Dagger2

در فصل ۲۸ و در چند مطلب به بحث معماری در برنامه‌های اندروید پرداختیم:

مقدمه معماری MVP در برنامه‌های اندروید

معماری MVP در برنامه‌های اندروید

تست برنامه‌های MVP

در ادامه این مطالب با برنامه todo گوگل آشنا شدیم:

برنامه نمونه todo از گوگل برای آشنایی با MVP

در ادامه این مطالب به موضوع تزریق وابستگی یا dependency injection پرداختیم:

تزریق وابستگی با Dagger2

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

مروری بر برنامه

برنامه todo mvp + Dagger2 را از گیت‌هاب دانلود کنید یا آن را با گیت کلون کنید. اگر نگاهی به ساختار برنامه بیاندازید مشابهت بسیار زیادی با برنامه todo-mvp دارد. تنها تفاوت اصلی برنامه، تزریق وابستگی‌ها توسط Dagger2 است.

اولین تغییر این برنامه تنظیمات گریدل پروژه و ماژول اپ است. ابتدا باید در بخش وابستگی‌های گریدل پروژه وابستگی به کتابخانه android-apt را اضافه کنیم:

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.3.1'
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
    }
}

بعد در بیلد گریدل مربوط به ماژول اپ باید دو تغییر بدهیم:

اضافه کردن android-apt و اضافه کردن Dagger:

apply plugin: 'com.android.application'

android {
    compileSdkVersion rootProject.ext.compileSdkVersion
    buildToolsVersion rootProject.ext.buildToolsVersion
//...
}

و

dependencies {
    // Dagger dependencies
    apt "com.google.dagger:dagger-compiler:$rootProject.daggerVersion"
    provided 'org.glassfish:javax.annotation:10.0-b28'
    compile "com.google.dagger:dagger:$rootProject.daggerVersion"
    // ...
}

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

نمایشگر:

TasksFragment tasksFragment =
		(TasksFragment) getSupportFragmentManager().findFragmentById(R.id.contentFrame);
if (tasksFragment == null) {
	// Create the fragment
	tasksFragment = TasksFragment.newInstance();
	ActivityUtils.addFragmentToActivity(
			getSupportFragmentManager(), tasksFragment, R.id.contentFrame);
}

مدل:

TasksRepository taskRepository = Injection.provideTasksRepository(getApplicationContext());

البته کمی کد را تغییر دادم تا خوانایی آن بالاتر برود.

معرف:

mTasksPresenter = new TasksPresenter(taskRepository, tasksFragment);

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

افزودن تزریق وابستگی به پروژه

همانطور که پیش از این و در مطلب «تزریق وابستگی با Dagger2» گفتیم، اجزای اصلی Dagger کامپوننت و ماژول هستند. برای شروع کار یک کلاس به اسم ToDoApplication به ریشه پروژه اضافه می‌کنیم و در مانیفست برنامه آن را به عنوان کلاس Application برنامه معرفی می‌کنیم:

<application
    // ...
    android:name="com.example.android.architecture.blueprints.todoapp.ToDoApplication">

حالا یک ماژول Dagger به نام ApplicationModule به برنامه اضافه می‌کنیم. کار اصلی این ماژول تزریق وابستگی به Context در هر جایی است که به این شی نیاز دارد:

@Module
public final class ApplicationModule {

    private final Context mContext;

    ApplicationModule(Context context) {
        mContext = context;
    }

    @Provides
    Context provideContext() {
        return mContext;
    }
}

اگر می‌دانید که این کلاس چه کاری می‌کند اعلام می‌کنم که Dagger را خوب یاد گرفته‌اید! این کلاس یک شی از نوع Context را اصطلاحا provide یا عرضه می‌کند. بنابراین در جاهایی که به Context نیاز داریم می‌توانیم از این ماژول استفاده کنیم. این شی Context از کجا می‌آید؟ بله درست حدس زدید از کلاس ToDoApplication:

public class ToDoApplication extends Application {

    private TasksRepositoryComponent mRepositoryComponent;

    @Override
    public void onCreate() {
        super.onCreate();

        mRepositoryComponent = DaggerTasksRepositoryComponent.builder()
                .applicationModule(new ApplicationModule((getApplicationContext())))
                .build();

    }

    public TasksRepositoryComponent getTasksRepositoryComponent() {
        return mRepositoryComponent;
    }

}

همانطور که می‌بینید در این کلاس با کمک Dagger یک نمونه از کامپوننت TasksRepositoryComponent ساخته می‌شود و یک نمونه از ApplicationModule به آن فرستاده می‌شود.

ساختار این کامپوننت به شکل زیر است:

@Singleton
@Component(modules = {TasksRepositoryModule.class, ApplicationModule.class})
public interface TasksRepositoryComponent {

    TasksRepository getTasksRepository();
}

حالا به ساختار پروژه یک نگاهی بیاندازیم:

28-6-01-android-todo-mvp-dagger-structure

همانطور که می‌بینید در هر شاخه که مربوط به یک اکتیویتی برنامه است، دو کلاس Component و Module اضافه شده است. این کلاس‌ها مربوط به Dagger هستند.

ما کلاس‌های TasksPresenterModule و TasksComponent را برای بررسی بیشتر باز می‌کنیم:

@Module
public class TasksPresenterModule {

    private final TasksContract.View mView;

    public TasksPresenterModule(TasksContract.View view) {
        mView = view;
    }

    @Provides
    TasksContract.View provideTasksContractView() {
        return mView;
    }

}

کار اصلی این ماژول تزریق نمایشگر یا View مرتبط با این صفحه است. تنها «ویژگی» این کلاس از نوع TasksContract.View است. یک متد سازنده دارد که این ویژگی را مقداردهی می‌کند و یک متد که با حاشیه‌نوشت @Provide مشخص شده است، این نمایشگر را تزریق می‌کند، به همین سادگی!

حالا نگاهی می‌اندازیم به کلاس TasksComponent:

@FragmentScoped
@Component(dependencies = TasksRepositoryComponent.class, modules = TasksPresenterModule.class)
public interface TasksComponent {

    void inject(TasksActivity activity);
}

فعلا به حاشیه‌نوشت @FragmentScoped کاری نداریم. کامپوننت‌های Dagger حتما باید با حاشیه‌نوشت @Component معرفی شوند. همانطور که می‌بینید این کامپوننت یک وابستگی دارد به کامپوننت TasksRepositoryComponent که کارش تزریق مدل است و یک وابستگی دارد به ماژول TasksPresenterModule که کارش تزریق نمایشگر است. همه این‌ها قرار است در اکتیویتی TasksActivity تزریق شوند. حالا سری می‌زنیم به TasksActivity تا ببینیم پایان قصه تزریق وابستگی به کجا می‌رسد:

import javax.inject.Inject;

public class TasksActivity extends AppCompatActivity {

    @Inject TasksPresenter mTasksPresenter;
	// ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.tasks_act);

        // Create the presenter
        DaggerTasksComponent.builder()
                .tasksRepositoryComponent(((ToDoApplication) getApplication()).getTasksRepositoryComponent())
                .tasksPresenterModule(new TasksPresenterModule(tasksFragment)).build()
                .inject(this);

				// ...
    }

	// ...
}

مهم‌ترین نکته این کلاس تعریف mTasksPresenter با حاشیه‌نوشت @Inject است. ما دیگر نیازی نیست از روی کلاس معرف یک شی بسازیم و این کار را بر عهده Dagger گذاشته‌ایم تا برای ما این شی را بسازد و به اکتیویتی تزریق کند. برای این کار باید کامپوننت را آماده کنیم و این دومین بخش مهم و متفاوت این کلاس است:

@Override
protected void onCreate(Bundle savedInstanceState) {
    // ...
    // Create the presenter
    ToDoApplication application = (ToDoApplication) getApplication();
    DaggerTasksComponent
        .builder()
        .tasksRepositoryComponent(application.getTasksRepositoryComponent())
        .tasksPresenterModule(new TasksPresenterModule(tasksFragment))
        .build()
        .inject(this);
        // ...
    }
    // ...
}

در این جا از Dagger می‌خواهیم که TasksComponent را به همراه تمام وابستگی‌هایش به اکتیویتی تزریق کند. اولین وابستگی این کامپوننت به یک کامپوننت دیگر به نام TasksRepositoryComponent است. این کامپوننت نمونه‌ای از TaskRepository را تزریق می‌کند. وابستگی دوم به ماژول TasksPresenterModule است که نمایشگر را تزریق می‌کند. پس حالا هم معرف و هم نمایشگر و هم مدل را به کمک Dagger به اکتیویتی تزریق کرده‌ایم بدون این که در اکتیویتی از روی آن‌ها نمونه یا شی‌ای ساخته باشیم و این کار Dagger است.

حالا یک بار با هم سناریو را از اول بررسی می‌کنیم:

۱- طبق معماری MVP باید یک شی مدل (M) داشته باشیم. مدل در اینجا TasksRepository است. بنابراین یک کلاس ساختیم به نام TasksRepositoryComponent. این کامپوننت باید بتواند دو شی متفاوت برای دسترسی به دیتای محلی و نیز دسترسی به دیتای اینترنتی یا remote را برای ما فراهم کند. بنابراین یک کلاس ماژول هم می‌سازیم که این دو نمونه را به ما عرضه (provide) کند:

@Module
abstract class TasksRepositoryModule {

    @Singleton
    @Binds
    @Local
    abstract TasksDataSource provideTasksLocalDataSource(TasksLocalDataSource dataSource);

    @Singleton
    @Binds
    @Remote
    abstract TasksDataSource provideTasksRemoteDataSource(FakeTasksRemoteDataSource dataSource);
}

۲- طبق معماری MVP باید کلاسی داشته باشیم که نقش نمایشگر (M) را بازی کند. بنابراین به ازای هر صفحه برنامه (فرگمنت یا اکتیویتی) یک کلاس ماژول و یک کامپوننت می‌سازیم. کلاس ماژول، نمایشگر را به ما عرضه می‌کند:

@Module
public class TasksPresenterModule {

    private final TasksContract.View mView;

    public TasksPresenterModule(TasksContract.View view) {
        mView = view;
    }

    @Provides
    TasksContract.View provideTasksContractView() {
        return mView;
    }

}

۳- طبق معماری MVP باید کلاسی داشته باشیم که نقش معرف (P) را بازی کند. این کلاس به TasksRepository و پیاده‌سازی‌های Local و Remote آن (همان کلاس‌های مدل) و نیز نمایشگر نیاز دارد. TasksComponent این‌ها را به کلاس TasksActivity تزریق می‌کند:

@FragmentScoped
@Component(dependencies = TasksRepositoryComponent.class, modules = TasksPresenterModule.class)
public interface TasksComponent {

    void inject(TasksActivity activity);
}

۴- در TasksActivity از Dagger می‌خواهیم که این وابستگی‌ها را به اکتیویتی تزریق کند:

@Override
protected void onCreate(Bundle savedInstanceState) {
    // ...
    // Create the presenter
    ToDoApplication application = (ToDoApplication) getApplication();
    DaggerTasksComponent
        .builder()
        .tasksRepositoryComponent(application.getTasksRepositoryComponent())
        .tasksPresenterModule(new TasksPresenterModule(tasksFragment))
        .build()
        .inject(this);
        // ...
    }
    // ...
}

و این کار را در تمام قسمت‌های برنامه تکرار می‌کنیم!

17 فکر می‌کنند “آموزش اندروید-فصل ۲۸-۶: برنامه نمونه todo و Dagger2

  1. بهزاد شیرین دلی

    سلام.خسته نباشید
    در خصوص mvp موردی که بیشتر از همه مشکل ساز هست ارتباط بین لایه های مختلف هست.بعضی از مواقع دو لایه presenter نیاز به تبادل دیتا دارند. فرض بفرمایید پرزنتز لاگین، با پرزنتر رجیستر باید تبادل دیتا انجام بدهند. یا اصلا ارتباط بین دو لایه مدل از یوزکیس های متفاوت کار صحیحی می باشد؟و یا اصلا قاعده خاصی برای تبادل دیتا در mvp وجود دارد؟
    در روش سنتی ما از  intent برای تبادل دیتا خیلی استفاده می کنیم.آیا این کار در mvp هم درست می باشد ما لایه view رو درگیر ارسال تبادل دیتا بکنیم. در حالی که این کار شاید بیشتر وظیفه لایه model باشد.
    ممنون میشم نظرتون رو در این خصوص بگین.

    پاسخ
    1. علی بهزادیان نژاد نویسنده

      سلام، پیاده‌سازی MVP در پروژه‌های پیچیده کمی سختی و ظرافت داره و باید خیلی با وسواس پیاده‌سازی بشه.
      ارتباط‌ها در MVP عمودی هستن، یعنی دو تا پرزنتر (معرف) نمی‌تونن مستقیم با هم ارتباط داشته باشن.
      دو تا فرگمنت (View) نمی‌تونن مستقیما با هم ارتباط داشته باشند و باید تا حد امکان تبادل اطلاعات از طریق مدل صورت بگیره.
      طبعا موارد زیادی رخ می‌ده که اگه قواعد MVP رو نقض کنیم (ظاهرا) کار خیلی راحت‌تر پیش می‌ره ولی با کمی دقت و تمرین و البته مطالعه برنامه‌های کدباز معروف و نوشتن و نوشتن و نوشتن کار براتون آسون میشه.
      پرسیدید که: «فرض بفرمایید پرزنتز لاگین، با پرزنتر رجیستر باید تبادل دیتا انجام بدهند…» در این حالت هر معرف کار خودش را انجام می‌دهد و اطلاعات را ذخیره می‌کند و با باز کردن نمایشگر و معرف بعدی کنترل کار را به آن‌ها می‌سپارد. معرف و نمایشگر بعدی اطلاعات خود را از مدل می‌گیرند و هیچ ارتباط افقی بین آن‌ها وجود ندارد و کاملا مستقل از هم هستند.
      ارسال اطلاعات از طریق اینتنت ایرادی نداره به شرطی که درست انجام بشه. فرض کنید نمایشگر شما یک فرگمنت است و می‌خواهد اطلاعاتی را به نمایشگر و پرزنتر بعدی بفرستد. راه درست این است که پرزنتر اطلاعات را از نمایشگر و مدل خودش بخواهد، آن‌ها را آماده کند و بعد از نمایشگر بخواهد که نمایشگر بعدی را با این اطلاعات صدا بزند. در فرگمنت دوم هم اطلاعات بعد از خوانده شدن از اینتنت بلافاصله به پرزنتر ارسال می‌شوند تا پرزنتر تصمیم بگیرد با این اطلاعات چه می‌خواهد بکند.
      در شبه کد زیر سعی کردم تا حدی موضوع رو توضیح بدم:

       presenter1:
      view1.startNextView(data)
      
      View1:
      void startNewxView(SomeData data) {
      	Intent i = new Intent(...)
      	i.putParcelable("DATA", data);
      	startNextActivity(i);
      }
      
      View2:
      void onCreate(Bundle extras) {
      	SomeData data = getIntent().getExtras().getParcelable("DATA");
      	presenter2.processData(data);
      }
      
      Presenter2:
      void processData(SomeData data) {
      	//...
      }
      
      پاسخ
      1. بهزاد شیرین دلی

        سلام.ممنون از وقتی که لحاظ کردین.
        سوال دیگه ای که ذهن منو درگیر کرده این هست که اغلب برنامه هایی که در اندروید می نویسیم با api خاصی به سرور متصل هستش. آیا نیاز هست که کدهای مربوط به دریافت دیتا را نیز درگیر تزریق وابستگی بکنیم؟
        برای مثال ما کلاسی داریم به اسم webService که تمامی فانکشن های مربوط به fetch کردن دیتا داخل آن قرار دارد. باطبع این کلاس نیاز به new کردن دارد و این یعنی وابستگی.آیا برای این کلاس ها هم باید از مفاهیم تزریق وابستگی استفاده کنیم. به نمونه کدهای گوگل مراجعه کردم.متاسفانه سمت سرور رو به صورت مجازی شبیه سازی کردن.برنامه ای todo رو عرض می کنم.
        ممنون از پاسختون.

        پاسخ
        1. علی بهزادیان نژاد نویسنده

          در سمت سرور هم معمولا از MVC استفاده می‌شود که بسیار نزدیک به MVP است. توصیه می‌کنم MVC را به همراه نام زبان برنامه نویسی سمت سرور جستجو کنید و نمونه‌ها را مطالعه کنید.
          اما اگر منظورتون کدهایی است که در اندروید می‌نویسید تا به سرور وصل بشید و اطلاعات را رد و بدل کنید قطعا باید از تزریق وابستگی برای ساخت این اشیا استفاده کنید.

          پاسخ
  2. مهسا

    سلام خسته نباشید..

    من برنامه ای نوشتم که برای اندروید های کمتر از ۴ وقتی توی Emulator اجرا میکنم حروف فارسی رو جدا ازهم نشون میده تا اندروید ۲٫۳ هم اینجوری هست ولی برای آندروید های بالا تر از ۴ این مشکل رو ندارم…نتونستم این مشکل رو حل کنم..

    ممنون میشم راه حلی بدید ..

    پاسخ
    1. علی بهزادیان نژاد نویسنده

      اندرویدهای ماقبل ۴ رو کنار بگذارید. طبق آمار گوگل تقریبا ۱۰۰ درصد کاربران از اندروید ۴.۱ و بالاتر استفاده می‌کنند.

      پاسخ
  3. احمد

    جناب بهزادیان سلام . من دارم یک کیبورد اندرویدی مینویسم میخام شکلک های کیبورد اندروید (Emoji) رو هم اضافه کنم .نمیدونم باید چکار کنم چون مثل حروف نیست که در حرف یم کد داشته باشه بلکه باید از یه سرس کلاس استفاده کرد . میشه یک آموزش بزارید ممنون

    پاسخ
    1. علی بهزادیان نژاد نویسنده

      هر ایموجی یک کاراکتره و تو فونت‌های مختلف به اشکال مختلف پیاده‌سازی شده

      پاسخ
  4. kourosh

    با عرض سلام و خسته نباشید …
    من به نوبه خودم از شما بابت زحماتی که برای ترجمه و انتشار مطالب کاربردی که بسیار روان و ساده توضیح داده شده اند،تشکر میکنم.
    ممنون میشوم اگر در مورد سیستم گزارش خطا مانند سیستم Acra ، مبحث ORM ها در اندروید و فایربیس هم در آینده مطلبی رو در سایت منتشر کنید.
    با تشکر

    پاسخ
  5. امیرحسین

    سلام و خسته نباشید.
    من در حال ساخت برنامه ای هستم که بتواند طول و عرض و ارتفاع را بگیرد و حجم یک مکعب را محاسبه کند.
    از انجایی که شاید اعداد اعشاری باشند از دستور double استفاده کردم.
    اگر از این دستور int استفاده میکردم برای خروجی گرفتن از این دستور استفاده میکردم :
    TextViewResult.setText.Integer.parseint(result));
    برای دستور double از چه دستوری استفاده کنم تا خروجی بگیرم ؟

    پاسخ
  6. mahdi

    سلام جناب بهزادیان نژاد
    دیگه سایت رو اپدیت نمی کنین؟
    واقعا مطالب خیلی خوبی رو منتشر کردین
    من خیلی استفاده کردم
    اگه امکانش هست باز هم سایتتون رو اپدیت کنید
    ممنون

    پاسخ
    1. علی بهزادیان نژاد نویسنده

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

      پاسخ
  7. Ehsan

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

    پاسخ

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

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