خوندن کد های code lab اندروید | Dagger 2

نوشته که بدون عکس نمیشه!
نوشته که بدون عکس نمیشه!


من قبلا با کتاب خونه koin کار کردم اما بنا به دلایلی متوجه شدم که کتاب خونه Dagger بهتر و پر‌استفاده تر هست در شرکت های بزرگ پس گفتم بیام و درکنار یادگرفتن dagger بیام و این متن رو هم بنویسم.

تا اون جایی که تونستم کلمات رو مهم رو ترجمه نکردم، یا اگر هم ترجمه کردم لینک دیکشنری و این ها رو گذاشتم. سعی کردم خیلی خودمونی و ساده بنویسم، اولش میخواستم بیشتر خلاصه باشه تا ترجمه، ولی فصل ها طوری شدند که بیشتر شبیه به ترجمه شده.

یه نکته قبل از شروع: بعضی جا ها از کلمه "جنس" یا "نوع" استفاده کردم. این همون DataType ها هستن. مثلا int یا هر چی.

یادتون نره از خوندن و یادگیری لذت ببرید، متن یک کم طولانی هست، میدونم، فصل فصل جداشون کردم تا راحت باشید.

بریم تو کار

لینک مقاله اصلی:

https://developer.android.com/codelabs/android-dagger#0
چون دو فصل اول مفاهیم و مقدمات بود اون هارو خلاصه کردم.

شما هم میتونید با من پروژه رو باز کنید و انجام بدید

دانلود پروژه اولیه (این پروژه خام هست و شما توش dagger رو پیاده سازی میکنید)

یا میتونیداز گیت هاب clone کنید:

$   git clone -b solution https://github.com/android/codelab-android-dagger

خوب من دوست داشتم به صورت پروژه محور و بسیار خلاصه مفاهیم رو متوجه بشم، همیشه دوست دارم چیز های سخت رو آسون متوجه بشم. پس رفتم سراغ سایت اصلی!!

راستی این سایت تحریم هست، و با انواع روش های تحریم شکن برین توش، پیشنهاد: سایت شکن و سایت ۴۰۳

همون طور که توی لینکی که گذاشتم میتونید ببینید، ۱۶ تا فصل داره، که اولین مقدمه هست، و دومیش هم برای clone کردن از Github هست پس من از فصل ۳ میخوام شروع کنم.

فصل ۳ | اجرا کردن برنامه

خوب، میگه برنامه ۴ تا Activity داره:

  • احراز هویت
  • ورود
  • خانه
  • تنظیمات

این برنامه از معماری MVVM استفاده میکنه.

شمای کلی از پروژه ای که میخوایم روش کار کنیم
شمای کلی از پروژه ای که میخوایم روش کار کنیم


توجه: اگه با معماری MVVM آشنایی ندارید، اول اون رو مطالعه کنید: نمونه ای با زبان کاتلین، نمونه ای با زبان جاوا ، داکیومنت اصلی

در عکس بالا پیکان ها بیانگر وابستگی‌ها هستند.

گراف برنامه (Application Graph): تمامی کلاس هایی بین اون ها وابستگی (Dependency) وجود داره

ما بجای این که خودمون به صورت دستی بیایم و وابستگی ها رو ایجاد کنیم باید این کار ها رو به Dagger محول کنیم. (تقریبا تمام ایده پشت Dagger همینه)

چرا Dagger؟

[این قسمت رو خودم میگم] ببینید، کلا مفهوم تزریق وابستگی واسه این هست که ما توی پروژه داریم کار میکنیم و همین جوری پیش میریم، اما بعد مدتی بنا به هر دلیلی دیگه نمیخوایم از اون وابستگی ها استفاده کنیم. مثال بزنم (انیشتین میگه: چیزی رو نمیشه فهمید جز با مثال): مثلا شما برای احراز هویت کاربرانتون در حال حاضر از ایمیل استفاده میکنید اما بعد مدتی تصمیم میگیرید بجای این کار از احراز هویت گوگل و SMS استفاده کنید. مشکل کجاست؟ این که باید توی کل برنامه بچرخید و سیستم جایگزین رو عوض کنید. یا مثلا شما برای ارسال request از Retrofit استفاده میکنید و به هر دلیلی میخواید از یه کتاب خونه دیگه استفاده کنید. به اصطلاح به این کد ها میگن boilerplate code. کتابخونه Dagger به ما کمک میکنه تا این داستان ها رو رفع کنیم. برای مطالعه بیشتر

فصل ۴ | اضافه کردن Dagger به پروژه

خوب، بسیار ساده هست، برید توی فایل app/build.gradle و این تنظیمات رو اضافه کنید:

plugins {
   id 'com.android.application'
   id 'kotlin-android'
   id 'kotlin-android-extensions'
   id 'kotlin-kapt'
}

...

dependencies {
    ...
    def dagger_version = &quot2.40&quot
    implementation &quotcom.google.dagger:dagger:$dagger_version&quot
    kapt &quotcom.google.dagger:dagger-compiler:$dagger_version&quot
}

شما همچنیم میتونید آخرین نسخه Dagger رو توی این لینک پیدا کنید.

توجه: دلیل استفاده Dagger از kapt این هست، چون Dagger برای اجرا نیاز به حاشیه نویسی داره (annotation) و این حاشیه نویسی ها توی زمان کامپایل اتفاق میوفته. اطلاعات بیشتر

فصل ۵ | حاشیه نویسی Inject@

مثالی که میزنه این هست: میگه برید توی RegistrationViewModel.kt، و بجای این که توی پارامتر constructor متغیر userManager رو پاس بدید، بگذارید که Dagger اون رو برای شما فراهم کنه. به این صورت:

// @Inject tells Dagger how to provide instances of this type
// Dagger also knows that UserManager is a dependency
class RegistrationViewModel @Inject constructor(val userManager: UserManager) {
    ...
}

وقتی شما از Inject@ استفاده میکنید، به Dagger میگویید که او مسوول این فراهم کردن userManager هست و البته نباید فراموش کنید که کلمه constructor رو حتما بعدش بنویسید.

شاید براتون جالب باشه: معنی کلمه inject: دیکشنری Longman ، دیکشنری Oxford
راستی یه نکته مهم دیگه: پروژه رو کنارتون باز کنید و کار ها رو انجام بدید. چون توی پروژه گیت هاب که clone کردید این تغییرات اعمال نشده، شما میخواهید به پروژه ای که دارید Dagger اضافه کنید.

[توضیحات من] به عکس بالا که فایل های پروژه رو نشون میداد برگردید. همون طور که میتونید ببینید با اضافه کردن قطعه کد بالا اون فلش (پیکان) از RegistrationViewModel به UserManager متصل میشه.

با اضافه کردن Inject@ کتاب خونه Dagger متوجه میشه که :

  1. چجوری یک نمونه (instance) از جنس RegistrationViewModel بسازه
  2. و متوجه میشه که خود RegistrationViewModel وابستگی هایی داره و باید برای ساختن این کلاس قبلش userManager رو بسازه.
خوب حالا Dagger میدونه که چجوری باید فراهم کنه نمونه هایی از جنش RegistrationViewModel و userManager رو.

از اون جا که userManager هم وابستگی داره که وابستگی اون به interface‌ی هست به نامه Storage ما باید به Dagger بگیم که چجوری باید این نمونه ها (instance) ها رو بسازه. که به این مساله بعدا میپردازیم.

توجه: یادتون نره که همین کار رو برای کلاس UserManager هم انجام بدید

لایه View نیاز داره به شی (Object)ی از این گراف

class RegistrationActivity : AppCompatActivity() {

    // @Inject annotated fields will be provided by Dagger
    @Inject
    lateinit var registrationViewModel: RegistrationViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        // Remove following line
        registrationViewModel = RegistrationViewModel((application as MyApplication).userManager)
    }
}

همون طور که میبینید در RegistrationActivity - که لایه view برای ویو مدل بالا هست -

در بیرون بخش onCreate ، ما متغیری رو تعریف کردیم به صورت lateinit و حاشیه گذاری Inject@ براش اعمال میکنیم. توی onCreate اومدیم و اون خطوط رو حذف میکنیم. [خطوطی که برای ساختن نمونه ای از viewModel هست درواقع]. اما ما باید به Dagger بگیم که باید خودش یه instance از همین view model بهمون بده، (در‌واقع خودش همه چیز هایی که باید inject کنه رو inject کنه) به این صورت:

 override fun onCreate(savedInstanceState: Bundle?) {
     (application as MyApplication).appComponent.inject(this) // add this line
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_registration)
        supportFragmentManager.beginTransaction()
            .add(R.id.fragment_holder, EnterDetailsFragment())
            .commit()
    }
نکته خیلی مهم:‌ یادتون باشه که حتما باید این فراخوانی (همون خطی که اضافه کردیم) قبل از super.onCreate باشه

فصل ۶ | حاشیه گذاری Component@

ما میخوایم که Dagger بتونه برای ما گراف برنامه رو بسازه و اون ها رو برامون مدیریت کنه. برای این کار باید یه interface بسازیم و اون رو با نماد Component@ حاشیه گذاری کنیم.

برای این کار یه package جدید میسازیم به اسم di (اگه گفتی مخفف چیه؟) و بعدش از نوع Kotlin interface یک فایل میسازیم و اسم اون رو AppComponent میذاریم.

package com.example.android.dagger.di

import com.example.android.dagger.registration.RegistrationActivity
import dagger.Component

// Definition of a Dagger component
@Component
interface AppComponent {
    // Classes that can be injected by this Component
    fun inject(activity: RegistrationActivity)
}

توی این interface متد injectی که تعریف کردیم توش کلاسی که اجازه استفاده کردن از این Component رو دارند رو اضافه میکنیم.

توجه: بعد از این داستان ها حتما باید یه build از پروژمون بگیریم.

آماده یه ارور میشیم دوستان!
این ارور (Error) رو میبینیم، (اروری هست که احتمالا زیاد ببینید):

dagger/app/build/tmp/kapt3/stubs/debug/com/example/android/dagger/di/AppComponent.java:7: error: [Dagger/MissingBinding] com.example.android.dagger.storage.Storage cannot be provided without an @Provides-annotated method

چون ما به Dagger نگفتیم که چطور Storage رو برای ما فراهم کنه از توی userManager این مشکل رو دریافت کردیم. بریم فیکسش کنیم ؟؟؟

فصل ۷ | Module, @Bind, @BindInstance@

راه حل استفاده Dagger از Storage متفاوته چون یک interface هست و نمیشه به صورت مستقیم از آن نمونه ساخت، ما باید کاری کنیم که Dagger بتونه خودش یه نمونه ازش بسازه و از اون استفاده کنه، برای انجام این کار ما از Dagger Module استفاده میکنیم. و کلاس مون رو با Module@ حاشیه نویسی میکنیم.

همانند کامپوننت ها، Module ها به Dagger میگن که چجوری باید از این نوع نمونه ای بسازه. وابستگی ها تعریف میشن با Provide@ و ‌Bind@

توی پوشه di میایم و کلاس جدیدی به اسم StorageModule میسازیم:

package com.example.android.dagger.di

import dagger.Module

// Tells Dagger this is a Dagger module
@Module
class StorageModule {

}
نکته ۱ : وقتی از Bind@ استفاده میکنیم که بخوایم یه interface رو به Dagger معرفی کنیم
نکته ۲ : برای اضافه کردن Bind@ باید حتما کلاس مون abstract باشه.

خوب، بعدش میبینیم که چه کسی (چه کلاسی) داره این interface رو داره implementation میکنه؟ (در‌ واقع میشه این طوری پرسید که توابعی که داریم کجا داره بدنه شون تامین میشه؟) که جوابش این هست:SharedPreferencesStorage .

حالا میایم و با حاشیه گذاری Bind@ یه abstract fun میسازیم که قراره هست برای ما Storage رو فراهم کنه:

// Tells Dagger this is a Dagger module
// Because of @Binds, StorageModule needs to be an abstract class
@Module
abstract class StorageModule {

    // Makes Dagger provide SharedPreferencesStorage when a Storage type is requested
    @Binds
    abstract fun provideStorage(storage: SharedPreferencesStorage): Storage
}

حالا طبق کاری که قبلا هم کردیم میایم توی SharedPreferencesStorageو :

// @Inject tells Dagger how to provide instances of this type
class SharedPreferencesStorage @Inject constructor(context: Context) : Storage { ... }

مثلا کاری که قبلا هم کردیم Inject@ و constructor رو اضافه میکنیم تا به کلاس هایی که Dagger به اون ها دسترسی داره اضافه بشه.

حاشیه گذاری BindInstance@

خوب حالا همین SharedPreferencesStorage داره به عنوان ورودی constructor داره context رو دریافت میکنه. حالا چجوری ما میتونیم این context رو به Dagger بفهمونیم؟؟ context رو که ما نمیسازیم، خود سیستم اندروید میسازه. چجوری باید این سیستم رو پیاده سازی کنیم؟؟

میریم توی AppComponent و این کار ها رو میکنیم:

@Component(modules = [StorageModule::class])
interface AppComponent {

    // Factory to create instances of the AppComponent
    @Component.Factory
    interface Factory {
        // With @BindsInstance, the Context passed in will be available in the graph
        fun create(@BindsInstance context: Context): AppComponent
    }

    fun inject(activity: RegistrationActivity)
}

یه interface توش میسازیم و حاشیه گذاری میکنیم با Component.Factory@، و یه متد توش میسازیم که AppComponent رو برامون برمیگردونه (return میکنه) و توی پارامتر ورودی هم با حاشیه BindInstance@ میایم و context رو برمیداریم.(به عنوان پارامتر ورودی متد)

حالا وقت آزمون هست: یه بار Build اش کنید ببینید کار میکنه یا نه، این دفعه باید بدون مشکل اجرا بشه.

توجه توجه: همون طور که گفتم من هم دارم همراه این مقاله همون طور که این رو مینویسم دارم کد ها رو هم مینویسم (هم دارم خلاصه ترجمه رو مینویسم هم یاد میگیرم و هم کد میزنم)
برای اجرا کردن به یه مشکلی خورده بودم که دیدم تو Github یکی براش راه حل گذاشته، چون برای من کار کرد این جا هم براتون میذارمش : بزن رو لینک

یه مرور بکنیم؟

تا حالا ما این کار ها رو انجام دادیم
تا حالا ما این کار ها رو انجام دادیم
حال ندارم عکس رو توضیح بدم!! خودتون یک کم دقت کنید دستتون میاد :)



فصل ۸ | اینجکت (Inject) کردن گراف توی Activity

شما معمولا میخواهید که گراف برنامه شما تا وقتی که برنامه در‌حال اجرا هست ازش استفاده کنید و توی حافظه دستگاه تون باشه. برای همین اون رو توی کلاس Application باید تعریف کنیم. توی پروژه ما، ما نیاز به context برنامه هم داریم که باید بهش دسترسی داشته باشیم.

خوب پس بریم توی فایل MyApplication.kt:

open class MyApplication : Application() {

    // Instance of the AppComponent that will be used by all the Activities in the project
    val appComponent: AppComponent by lazy {
        // Creates an instance of AppComponent using its Factory constructor
        // We pass the applicationContext that will be used as Context in the graph
        DaggerAppComponent.factory().create(applicationContext)
    }

    open val userManager by lazy {
        UserManager(SharedPreferencesStorage(this))
    }
}

همون طور که توی فصل قبل اشاره کردیم (شایدم اشاره نکردیم، یادم نیست :) Dagger برای ما کلاس هایی میسازه که یکی از اون ها DaggerAppComponent هست که شامل implementation از کلاس AppComponent خودمون هست. البته زمانی که build بگیریم. (همون دکمه چکش رو میگم)

همون طور که مشاهده میکنید برای ساختن appComponent ما با استفاده از Component.Factory@ که قبلا توی فصل قبل ساختیم استفاده میکنیم تا context رو به گرافمون بدیم. که توی مورد ما context همون applicationContext هست،

کلمه lazy چیه ؟ و داستانش چیه؟ بیشتر دربارش بخونید
دقت و توجه کافی :
عزایزان دقت کنید که dagger میتونه برای شما کد بسازه (generate کنه) پس اگه دیدید که DaggerAppComponent رو برنامه شما نمیشناسه باید حتما build اش کنید.

خوب همه کار ها رو کردیم و به نظر میرسه هیچ مشکلی وجود نداره، و میریم که یه بار برنامه مون رو اجرا کنیم. انتظار میره که برنامه ما یه باگ داشته باشه، چرا؟

چون صفحه Main باید بعد از جریانات احراز هویت بیاد بالا و جریانات Main هنوز گراف Dagger از اون ها اطلاعی نداره. در واقع MainActivity هنوز داره از UserData استفاده میکنه اما الان UserData تحت کنترل Dagger هست و Dagger باید نمونه ای رو MainActivity بده.

استفاده از Dagger در در Main Flow

اول از همه باید بریم سراغ AppComponent.kt و به Dagger بگیم که میتونه چیز ها رو تو MainActivity اینجکت کنه. اینجوری:

@Component(modules = [StorageModule::class])
interface AppComponent {
    ...

    // Classes that can be injected by this Component
    fun inject(activity: RegistrationActivity)
    fun inject(activity: MainActivity)
}

بعد باید بریم توی MainActivity و چیز هایی که میخوایم inject کنیم رو بیاریم (که viewModel و userManager هست) :

class MainActivity : AppCompatActivity() {

    // @Inject annotated fields will be provided by Dagger
    @Inject
    private lateinit var userManager: UserManager

    @Inject
    private lateinit var mainViewModel: MainViewModel

    ...
}

اون قسمت هایی که داره نمونه میسازه رو حذف میکنیم، چون Dagger داره برامون میسازه (فراهم میکنه):

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        // Remove this line
        userManager = (application as MyApplication).userManager
        if (!userManager.isUserLoggedIn()) {
           ...
        } else {
           ...
           // Remove this line too
            mainViewModel = MainViewModel(userManager.userDataRepository!!)
           ...
        }
    }
    ...
}

و بجاش:

class MainActivity : AppCompatActivity() {
    ...
    override fun onCreate(savedInstanceState: Bundle?) {

        (application as MyApplication).appComponent.inject(this)

        super.onCreate(savedInstanceState)
        ...
    }
}

حالا view model و repository هم به صورتی که قبلا کار کرده بودیم به Dagger میفهمونیم.

view model:

class MainViewModel @Inject constructor(private val userDataRepository: UserDataRepository) { ... }

و همینطور user data repository :

class UserDataRepository @Inject constructor(private val userManager: UserManager) { ... }

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

گراف برنامه ما همین الان یهویی
گراف برنامه ما همین الان یهویی

خوب اما این داستان تموم نشده!!! باز هم ارور داریم (ای بابا) باید اون private ها رو برداریم از توی MainActivity ، اینجوری:

class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var userManager: UserManager

    @Inject
    lateinit var mainViewModel: MainViewModel

    ...
}

حالا باید پروژه ما به درستی build بشه، و به درستی هم اجرا شه. اگه شما احراز هویت شده باشید قبلا شما به صفحه login screen میرید. و میتونید دکمه "Unregister" رو بزنید و دوباره مراحل رو از اول ثبت نام از سر بگیرید. اما وقتی شما احراز هویت کردید شما رو به صفحه اصلی منتقل نمیکنه بلکه شما رو دوباره به Login Activity هدایت میکنه. دوباره مثل این که باگ داریم!!

اما چرا؟‌ main و جرایانات registration هر دو از گراف برنامه userManager رو دریافت میکنند.

مشکل اینجاست که Dagger به صورت پیشفرض همیشه نمونه جدیدی از از همون نوع (توی پروژه ما userData) تعریف میکنه، اما چگونه میتونیم کاری کنیم که فقط یه نمونه برای هر دفعه که میخوایم از اون استفاده کنیم داشته باشیم و هر دفعه اون رو مورد باز‌استفاده قرار بدیم؟؟ با Scoping

معنی کلمه Scoping: دیکشنری لانگمن، دیکشنری آکسفورد

فصل ۹ | استفاده از Scope ها

بعضی وقت ها شما میخواید یک نمونه از وابستگی رو در یک کامپوننت داشته باشید. بنابر دلایلی مثل:

  1. میخواید همه یک نمونه رو داشته باشند چون میخواید یه سری داده ها رو با اون ها به اشتراگ بگذارید (مثل پروژه ما)
  2. زمانی درست کردن یک شی(object) جدید بسیار پر‌هزینه باشه. و شما نمیخواید همینجوری نمونه های زیادی بسازید (مثلا مفسر (parser) های json)

با استفاده از Scope ها شما یک نمونه یکتا برای اون نوع از (Data type) برای کامپوننت تون دارید. در واقع اسکوپ ها در چرخه زمانی کامپوننت ها وجود دارند.

برای AppComponent ما میتونیم از Singleton@ که یک حاشیه گذاری اسکوپ هست استفاده کنیم. (از پکیج javax.inject)

خوب برای این که ما هم بتونیم توی برنامه خودمون از این استفاده کنیم باید چیکار کنیم؟؟

AppComponent:

@Singleton
@Component(modules = [StorageModule::class])
interface AppComponent { ... } 

حال کلاس هایی که با Singleton@ حاشیه گذاری بشن اسکوپ میشن در AppComponent، خوب حالا بریم و UserManager رو کاری کنیم که نمونه های یکتا بسازه:

@Singleton
class UserManager @Inject constructor(private val storage: Storage) {
    ...
}

حالا ما کاری کردیم که کلاس UserManager فقط یه نمونه داشته باشه و این نمونه رو بین بقیه درخواست ها به اشتراک بگذاره. به همین سادگی و به همین خوشمزگی. بریم پروژه رو اجرا کنیم ببینیم چی میشه.

ایول درست شد

گراف برنامه:

یه مرور کنیم ببینیم تا حالا چیکار کردیم؟
یه مرور کنیم ببینیم تا حالا چیکار کردیم؟

فصل۱۰ | Subcomponent ها

بگذارید همینطور ادامه بدهیم، فرگمنت های احراز هویت هنوز از Dagger استفاده نمیکنند.

از اون جا که ما میخوایم EnterDetailsFragment و TermsAndConditionsFragment توسط Dagger اینجکت بشن باید اجازه این کار رو به AppComponent بدیم:

@Singleton
@Component(modules = [StorageModule::class])
interface AppComponent {
    ...
    fun inject(activity: RegistrationActivity)
    fun inject(fragment: EnterDetailsFragment)
    fun inject(fragment: TermsAndConditionsFragment)
    fun inject(activity: MainActivity)
}

و همینطوری ادامه میدهیم (کار هایی هست که قبلا انجام دادیم):

EnterDetailsFragment.kt

class EnterDetailsFragment : Fragment() {

    @Inject
    lateinit var registrationViewModel: RegistrationViewModel
 
    @Inject
    lateinit var enterDetailsViewModel: EnterDetailsViewModel

    ...
}

در ادامه خطوط زیر رو هم باید حذف کنید:

class EnterDetailsFragment : Fragment() {

    override fun onCreateView(...): View? {
        ...
        // Remove following lines
        registrationViewModel = (activity as RegistrationActivity).registrationViewModel
        enterDetailsViewModel = EnterDetailsViewModel()

        ...
    }
}

حالا همون طور که قبلا هم انجام داده بودیم توی onAttach :

class EnterDetailsFragment : Fragment() {

    override fun onAttach(context: Context) {
        super.onAttach(context)

        (requireActivity().application as MyApplication).appComponent.inject(this)
    }
}
توجه - Best practices:
اکتیویتی Dagger رو توی تابع onCreate قبل از super صدا میزنه
توی فرگمنت ها توی onAttach بعد از super

بعدش هم نوبت ویو مدل هست:

EnterDetailsViewModel.kt

class EnterDetailsViewModel @Inject constructor() { ... }

الان EnterDetailsFragmentآماده شده، حالا نوبت TermsAndConditionsFragmentهست

  1. حاشیه گذاری Inject@ در ترم اند کاندیشن و حذف کردن private
  2. حذف کردن نمونه گیری معمولی registrationViewModel
  3. اینجکت کردن توی onAttach

TermsAndConditionsFragment.kt

class TermsAndConditionsFragment : Fragment() {

    @Inject
    lateinit var registrationViewModel: RegistrationViewModel

    override fun onAttach(context: Context) {
        super.onAttach(context)

        (requireActivity().application as MyApplication).appComponent.inject(this)
    }

    override fun onCreateView(...): View? {
         ...
         // Remove following line
         registrationViewModel = (activity as RegistrationActivity).registrationViewModel
         ...
    }
}

حالا برنامه رو اجرا میکنیم. چی اتقاقی افتاد؟ آیا برنامه بعد از احراز هویت کرش کرد؟؟ مشکل اینجاست که نمونه های متفاوتی از RegistrationViewModel اینجکت شده: در RegistrationActivity و EnterDetailsFragmentو TermsAndConditionsFragment. درحالی که این چیزی نیست که ما میخوایم که یک نمونه اینجکت بشه اکتیویتی و فرگمنت ها.

چی میش در RegistrationViewModelاز Singleton@ استفاده کنیم؟ این مشکل ما رو برای الان حل میکنه اما در آینده ممکن هست مشکلاتی رو بوجود بیاره:

  • ما نمیخوایم که نمونه ای از RegistrationViewModel در حافظه باشه زمانی که پروسه احراز هویت کارش تموم شده.
  • ما میخوایم نمونه های متفاوتی از RegistrationViewModel وجود داشته باشه وقتی که جریان متفاوتی از پروسه های احراز هویت در جریان باشه، وقتی که کاربر داره ورود یا خروج میکنه.به قولی ما نمیخوایم داده های احراز هویت قبلی رو برای احراز هویت جدید داشته باشیم.

ما میخوایم که فرگمنت های احراز هویت ویو مدلی که از اکتیویتی میاد رو مورد باز استفاده قرار بدن (از یه ویو مدل استفاده کنند) اما اگه اکتیویتی مورد تغییر قرار گرفت، ما نمونه جدیدی لازم داریم .ما نیاز داریم که RegistrationViewModel اسکوپ کنیم برای RegistrationActivity. برای این کار ما میتونیم کامپوننت دیگه ای برای فرایند احراز هویت و اسکوپ کردن برای ویو مدل بسازیم. برای این که بتونیم این کار رو عملی کنیم از Dagger subcomponent ها استفاده میکنیم.

ساب‌کامپوننت (زیر‌کامپوننت) های Dagger

ساب‌کامپوننت ها، کامپوننت هایی هستند که اشیاء گراف کامپوننت پدر(مادر) رو به ارث میبرند. همچنین همه اشیاء فراهم شده توسط کامپوننت پدر فراهم خواهند شد در ساب‌کامپوننت. در این روش objectی که از ساب‌کامپوننت هست میتونه به objectی از کامپوننت پدر وابسته باشه.

خوب برای این کار یه فایل میسازیم به اسم RegistrationComponent.kt توی پکیج registration٫. حالا میتونیم interfaceی بسازیم به اسم RegistrationComponent. و اون با نماد Subcomponent@ حاشیه گذاری کنیم:

registration/RegistrationComponent.kt

package com.example.android.dagger.registration

import dagger.Subcomponent

// Definition of a Dagger subcomponent
@Subcomponent
interface RegistrationComponent {

}

این کامپوننت نیاز داره که شامل اطلاعات احراز هویت بشه. برای این کار:

  1. اضافه کردن متد های inject از AppComponent که به طور خاص به احراز هویت مربوط هستند (RegistrationActivityوEnterDetailsFragmentوTermsAndConditionsFragment)
  2. ساختن ساب‌کامپوننت factory که باهاش بتونیم نمونه ای از این ساب‌کامپوننت بسازیم

RegistrationComponent.kt

// Definition of a Dagger subcomponent
@Subcomponent
interface RegistrationComponent {

    // Factory to create instances of RegistrationComponent
    @Subcomponent.Factory
    interface Factory {
        fun create(): RegistrationComponent
    }

    // Classes that can be injected by this Component
    fun inject(activity: RegistrationActivity)
    fun inject(fragment: EnterDetailsFragment)
    fun inject(fragment: TermsAndConditionsFragment)
}

توی AppComponent، ما باید همه inject هایی که به ساب‌کامپوننت بردیم رو پاک کنیم. چون این ها دیگه قرار نیست استفاده بشه. بجای اون ها RegistrationActivity برای ساختن نمونه هایی از RegistrationComponent، ما نیاز داریم که factory رو در معرض AppComponent قرار بدیم:

AppComponent.kt

@Singleton
@Component(modules = [StorageModule::class])
interface AppComponent {

    @Component.Factory
    interface Factory {
        fun create(@BindsInstance context: Context): AppComponent
    }

    // Expose RegistrationComponent factory from the graph
    fun registrationComponent(): RegistrationComponent.Factory

    fun inject(activity: MainActivity)
}

ما با استفاده از تابعی با خروجی RegistrationComponent.Factory ساب‌کامپوننت مون رو در معرض AppComponent گذاشتیم.

دو راه برای تعامل با گراف Dagger وجود داره:
۱. درست کردن متدی با خروجی Unit و دادن کلاس به عنوان پارامتر ورودی
۲. درست کردن متد با خروجی یه typeی که بازیابی میکنه اون رو از گراف(مثل همینی که الان ساختیم)

خوب حالا باید یه جوری به AppComponent بفهمونیم که این RegistrationComponent یه ساب‌کامپوننت هست و میتونه برای ما کد بسازه. و باید برای این یه ماجول Dagger بسازیم.

میریم توی di و میسازیمش:

app/src/main/java/com/example/android/dagger/di/AppSubcomponents.kt

// This module tells AppComponent which are its subcomponents
@Module(subcomponents = [RegistrationComponent::class])
class AppSubcomponents

حالا باید این رو توی AppComponent هم اضافه کنیم:

@Singleton
@Component(modules = [StorageModule::class, AppSubcomponents::class])
interface AppComponent { ... }

برنامه مون چجوری شده؟؟

اینجوری شده
اینجوری شده
دوستان برید یه آبی بخوید، دستی به آب بزنید، از وسط های این نوشته هی به خودم میگفتم مهدی این که کاری هست داری میکنی! :)

فصل ۱۱ | اسکوپ کردن ساب‌کامپوننت ها

ما ساب‌کامپوننت رو احتیاج داشتیم تا بتونیم یک نمونه از RegistrationViewModel بین اکتیویتی و فرگمنت به اشتراک بگذاریم. همون طوری که قبلا هم انجام دادیم، اگه در حاشیه گذاری کامپوننت و کلاس مورد نظر از اسکوپ استفاده کنیم، میتونیم نمونه یکتا براش درست کنیم. اما نمیتونیم از Singleton٬@ استفاده کنیم چون هم اکنون در‌حال استفاده توسط AppComponent هست. پس ما یه نوع دیگه ای نیاز داریم.

در این مورد ما میتونیم اسکوپ RegistrationScope@ رو صدا بزنیم. اما این برای تمرین خوب نیست. چون نام حاشیه گذاری اسکوپ باید کاملا دقیق باشه تا بتونه منظور رو برسونه. اون باید نامیده بشه بسته به مدت زمانی که این حاشیه گذاری قابل باز‌استفاده هست توسط کامپوننت های هم‌رده (کامپوننت های برادر) که (برای این مثال: LoginComponent ،SettingsComponent و ...) برای همین منظور بجای استفاده از RegistrationScope@ از ActivityScope@ استفاده میکنیم.

قوانین اسکوپ:
۱. وقتی نوعی نشانه گذاری میشه با حاشیه گذاری اسکوپ، اون فقط میتونه توسط کامپوننت هایی که حاشیه گذاری با همون اسکوپ شدند استفاده بشه.
۲. زمانی که کامپوننت با حاشیه‌گذاری اسکوپ نشانه گذاری شده، اون فقط میتونه نوع هایی رو فراهم کنه که همون حاشیه گذاری رو دارند یا نوع هایی که حاشیه گذاری نشده‌ اند.
۳. یک ساب‌کامپوننت نمیتونه از حاشیه گذاری اسکوپی استفاده کنه که قبلا توسط کامپوننت پدر استفاده شده

کامپوننت ها همچنین میتونن ساب‌کامپوننت ها رو با همون context درگیر کنن.

حالا بیاید و ActivityScope.kt رو در پکیچ di ایجاد کنیم:

app/src/main/java/com/example/android/dagger/di/ActivityScope.kt

@Scope
@MustBeDocumented
@Retention(value = AnnotationRetention.RUNTIME)
annotation class ActivityScope

حالا برای این که RegistrationViewModel رو بتونیم در RegistrationComponent اسکوپ کنیم. باید کلاس و اینترفیس مون از حاشیه گذاری ActivityScope@ بشن:

app/src/main/java/com/example/android/dagger/di/ActivityScope.kt

// Scopes this ViewModel to components that use @ActivityScope
@ActivityScope
class RegistrationViewModel @Inject constructor(val userManager: UserManager) {
    ...
}

RegistrationComponent.kt

// Classes annotated with @ActivityScope will have a unique instance in this Component
@ActivityScope
@Subcomponent
interface RegistrationComponent { ... }

حالا RegistrationComponent برای ما همیشه همون نمونه از RegistrationViewModel رو فراهم میکنه.

چرخه حیات (lifecycle) ساب‌کامپوننت

اول از همه AppComponent میاد توی چرخه حیات برنامه، چون ما میخوایم یه گراف برنامه تا زمانی که برنامه توی حافظه هست داشته باشیم.

حالا چرخه حیات RegistrationComponent: یکی از دلایل این که چرا ما نیاز داریم این هست که ما میخوایم یک نمونه از RegistrationViewModel رو بین اکتیویتی و فرگمنت داشته باشیم. اما همچنین میخوایم نمونه جدیدی ایجاد کنیم اگه جریانات جدیدی برای احراز هویت رخ بده.

اکتیویتی RegistrationActivity دقیقا چرخه حیات RegistrationComponent رو داره: برای هر اکتیویتی جدید ما یک RegistrationComponent جدید خواهیم ساخت و فرگمنت ها هم از همون نمونه از RegistrationComponent استفاده کنند.

تا زمانی که RegistrationComponent متصل شده در چرخه حیات RegistrationActivity، ما باید نگهداریم منبعی (reference) از این کامپوننت رو در اکتیویتی، به همون صورت که kept منبعی از appComponent رو در کلاس Application نگه میداره. با این روش فرگمنت ها در دسترس قرار میگیرند.

دوستان اگه میتونید این قسمت رو بهتر ترجمه کنید ممنون میشم! لینک (توان من همین بود :)

RegistrationActivity.kt

class RegistrationActivity : AppCompatActivity() {

    // Stores an instance of RegistrationComponent so that its Fragments can access it
    lateinit var registrationComponent: RegistrationComponent
    ...
}

کار های زیر رو انجام میدیم:

RegistrationActivity.kt

class RegistrationActivity : AppCompatActivity() {
    ...

    override fun onCreate(savedInstanceState: Bundle?) {

        // Remove lines 
        (application as MyApplication).appComponent.inject(this)

        // Add these lines

        // Creates an instance of Registration component by grabbing the factory from the app graph
        registrationComponent = (application as MyApplication).appComponent.registrationComponent().create() 
        // Injects this activity to the just created registration component
        registrationComponent.inject(this)

        super.onCreate(savedInstanceState)
        ...
    }
    ...
}
همون طور که مشاهده میکنید registrationComponent نیازی نداره به Inject@ چون نمیخوایم Dagger اون رو برای ما بسازه.

حالا registrationComponent در RegistrationActivity موجود هست و میتونیم همون نمونه رو برای فرگمنت احراز هویت اینجکت کنیم. توی onAttach تغییرات رو انجام میدیدم:

EnterDetailsFragment.kt

class EnterDetailsFragment : Fragment() {
    ...
    override fun onAttach(context: Context) {
        super.onAttach(context)

        (activity as RegistrationActivity).registrationComponent.inject(this)
    }
    ...
}

و همین کار رو حالا برای TermsAndConditions این هم انجام میدیم:

class TermsAndConditionsFragment : Fragment() {
    ...
    override fun onAttach(context: Context) {
        super.onAttach(context)

        (activity as RegistrationActivity).registrationComponent.inject(this)
    }
}
دوستان بهتون پیشنهاد میکنم برای هر فصلی که میخونید و اجرا میگیرید و درست کار میکنه یه git commit بزنید. برای اجرای این فصل یه اشتباه خیلی ساده که توی فصل قبلی انجام داده بودم ۲ ساعت و نیم وقتم رو گرفت!!
حالا اشتباه چی بود؟ یادم رفته بود توی AppComponent بیام توی تابع registrationComponent خروجی رو با
Factory. بزنم!

بریم ببینیم الان پروژمون چه شکلی شده:

اینجوری
اینجوری

تفاوت دیاگرام قبلی با این دیاگرام این هست که RegistrationViewModel اسکوپ شده به RegistrationComponent که اون رو با نقطه نارنجی نشون دادیم.

فصل ۱۲ | ریفکتو کردن جریانات Login (ورود)

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

ساختار سازی برنامه شما با ساختن زیر‌گراف های متفاوت در Dagger بسته به جریانات برنامه شما کمک میکنه که بتونید برنامتون رو توسعه پذیر کنید اون هم در زمان حافظه و در لحظه شروع. از ساختن کامپوننت های خیلی بزرگ که تغییر دادن اون ها سخت هست امتناع کنید. کامپوننت هایی که تعداد زیادی از اشیاء برنامه شما رو فراهم میکنند. این کار باعث میشه کامپوننت های Dagger خوانایی کم و قطعه قطعه (modularize) شدن بشه.

حالا بریم Login رو درست کنیم، فایل زیر رو ایجاد میکنیم:

login/LoginComponent.kt

// Scope annotation that the LoginComponent uses
// Classes annotated with @ActivityScope will have a unique instance in this Component
@ActivityScope
// Definition of a Dagger subcomponent
@Subcomponent
interface LoginComponent {

    // Factory to create instances of LoginComponent
    @Subcomponent.Factory
    interface Factory {
        fun create(): LoginComponent
    }

    // Classes that can be injected by this Component
    fun inject(activity: LoginActivity)
}

بعدش توی view model :

LoginViewModel.kt

class LoginViewModel @Inject constructor(private val userManager: UserManager) {
    ...
}

در این مورد LoginViewModel نیازی نیست که مورد باز‌استفاده توسط بقیه کلاس ها قرار بگیره، برای همین نیازی به استفاده ActivityScope@ نیست.

همچنین ما باید این ساب‌کامپوننت مون رو به لیست ساب‌کامپوننت های AppComponent اضافه کنیم در ماجول AppSubcomponents :

AppSubcomponents.kt

@Module(subcomponents = [RegistrationComponent::class, LoginComponent::class])
class AppSubcomponents

برای LoginActivity هم که بتونه فکتوریLoginComponent رو در دسترس داشته باشه باید در اینترفیس AppComponent :

AppComponent.kt

@Singleton
@Component(modules = [StorageModule::class, AppSubcomponents::class])
interface AppComponent {
    ...
    // Types that can be retrieved from the graph
    fun registrationComponent(): RegistrationComponent.Factory
    fun loginComponent(): LoginComponent.Factory

    // Classes that can be injected by this Component
    fun inject(activity: MainActivity)
}

حالا باید بریم و توی LoginActivity و همون سه تغییر همیشگی رو انجام بدیم:

LoginActivity.kt

class LoginActivity : AppCompatActivity() {

    // 1) LoginViewModel is provided by Dagger
    @Inject
    lateinit var loginViewModel: LoginViewModel

    ...

    override fun onCreate(savedInstanceState: Bundle?) {

        // 2) Creates an instance of Login component by grabbing the factory from the app graph
        // and injects this activity to that Component
        (application as MyApplication).appComponent.loginComponent().create().inject(this)

        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login)

        // 3) Remove instantiation
        loginViewModel = LoginViewModel((application as MyApplication).userManager)
        ...
    }
}

حالا اگه شما برنامتون رو اجرا کنید باید به درستی اجرا بشه.

برناممون چه شکلی شده؟

اینجوری!
اینجوری!

فصل ۱۳ | چند اکتیویتی با یه اسکوپ

روی دکمه تنظیمات کلیک کنید شما خواهید دید که برنامه شما کرش (crash) میکنه. بیاید این مشکل رو با Dagger حل کنیم.

وقتی ما میخوایمSettingsActivity توسط Dagger اینجکت بشه:

  1. باید به Dagger بگیم که چجوری باید نمونه ای ازSettingsActivity و وابستگی هاش بسازه (SettingsViewModel) :

SettingsViewModel.kt

class SettingsViewModel @Inject constructor(
    private val userDataRepository: UserDataRepository,
    private val userManager: UserManager
) { ... }

۲. بهSettingsActivity اجازه بدیم تا توسط Dagger اینجکت بشه با اضافه کردن متدی کهSettingsActivity رو در اینترفیسAppComponent دریافت میکنه:

AppComponent.kt

@Singleton
@Component(modules = [StorageModule::class, AppSubcomponents::class])
interface AppComponent {
    ...
    fun inject(activity: SettingsActivity)
}

۳. درSettingsActivity،باید کار های همیشگی رو انجام بدیم:

SettingsActivity.kt

class SettingsActivity : AppCompatActivity() {

    // 1) SettingsViewModel is provided by Dagger
    @Inject
    lateinit var settingsViewModel: SettingsViewModel

    override fun onCreate(savedInstanceState: Bundle?) {

        // 2) Injects appComponent
        (application as MyApplication).appComponent.inject(this)

        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_settings)

        // 3) Remove following lines
        val userManager = (application as MyApplication).userManager
        settingsViewModel = SettingsViewModel(userManager.userDataRepository!!, userManager)
        ...
    }
}

اگه برنامه رو اجرا کنید میبیند که فیچر(feature) بروز رسانی نوتیفیکیشن ها در تنظیمات کار نمیکنه. این بخاطر این هست که ما از یک نمونه ازUserDataRepository بینMainActivity وSettingsActivity استفاده نمیکنیم!

آیا ما میتونیمUserDataRepository رو درAppComponent اسکوپ کنیم با حاشیه گذاری Singleton@؟ بنا به دلایلی قبلا بحث شده ما نمیخوایم این کار رو انجام بدیم، چون اگه کاربر خروج انجام بده یا (unregisters) کنه ما نمیخوایم نمونه ای ازUserDataRepository در حافظه داشته باشیم چون این داده ها برای هر کاربر ورود(login) شده بخصوص هست.

پس ما میخوایم کامپوننتی بسازیم که تا وقتی کار کنه که کاربر در حالت ورود هست. همه اکتیویتی هایی باید بعد از ورود کاربر در دسترس قرار بگیرند در اون کامپوننت قرار بگیرند. ( MainActivity و SettingsActivity)

پس اجازه دهید تا یک ساب‌کامپوننت جدیدی بسازیم و اسمش رو همUserComponent بگذاریم و همون طور که تویLoginComponent وRegistrationComponent این کار رو انجام دادیم:

  1. ساختن فایل کاتلین به نامUserComponent.kt در پوشهuser.
  2. ساختن اینترفیسی به نامUserComponent و اون رو باSubcomponent@ حاشیه گذاری کنیم که میتونه کلاس هایی که باید بعد از ورود کاربر به حساب کاربری اتفاق میوفتند رو داشته باشه و یک factory

app/src/main/java/com/example/android/dagger/user/UserComponent.kt

// Definition of a Dagger subcomponent
@Subcomponent
interface UserComponent {

    // Factory to create instances of UserComponent
    @Subcomponent.Factory
    interface Factory {
        fun create(): UserComponent
    }

    // Classes that can be injected by this Component
    fun inject(activity: MainActivity)
    fun inject(activity: SettingsActivity)
}

۳. اضافه کردن این ساب‌کامپوننت جدید به لیست کامپوننت هایAppComponent :

AppSubcomponents.kt

@Module(subcomponents = [RegistrationComponent::class, LoginComponent::class, UserComponent::class])
class AppSubcomponents

چه چیزی چرخه حیاتUserComponent رو شارژ میکنه؟LoginComponent و RegistrationComponent توسط اکتویتی ها مدیریت میشوند اماUserComponent میتونه بیش از یک اکتیویتی اینجکت بشه و تعداد اکتیویتی ها پتانسیل بیشتر شدن رو داره.

ما باید چیزی به چرخه حیات این کامپوننت اضافه کنیم که بفهمه کاربر کی ورود یا خروج کرده. در مورد ماUserManager هست.

که احراز هویت رو بر عهده داره. و ورود و خروج به نظر میرسه که مربوط به وضایف این کلاس هست. پس این جا باید نمونه ای ازUserComponent ایجاد بشه.

اگهUserManager نیاز داره که نمونه ای ازUserComponent رو داشته باشه، باید به UserComponent دسترسی داشته باشه، اگه ما به عنوان پارامتر سازنده(constructor) بدیم، Dagger خودش نمونه ای از UserManager رو برای ما فراهم میکنه.

UserManager.kt

@Singleton
class UserManager @Inject constructor(
    private val storage: Storage,
    // Since UserManager will be in charge of managing the UserComponent lifecycle,
    // it needs to know how to create instances of it
    private val userComponentFactory: UserComponent.Factory
) {
    ...
}

در تزریق وابستگی دستی، ما باید نشستی(session) از داده کاربر درUserManager نگهداری میکردیم. که تصمیم میگیره کاربر ورود یا ورود کنه. ما میتوینم همین رو درUserComponent درست کنیم.

ما میتونیم نمونه ای ازUserComponent درUserManager داشته باشیم تا چرخه یات اون رو مدیریت کنیم. کاربر زمانی ورود میکنه کهUserComponent خالی (null) نباشه. وقتی هم که کاربر خروج میکنه ما میتونیم اون نمونه ازUserComponent رو حذف کنیم. با این روش وقتی این کامپوننت نابود میکنیم، همه داده ها همراهش در حافظه حذف میشه.

تغییر دادن UserManager برای استفاده از نمونه‌ای ازUserComponent بجای UserDataRepository:

UserManager.kt

@Singleton
class UserManager @Inject constructor(...) {
    //Remove line
    var userDataRepository: UserDataRepository? = null

    // Add or edit the following lines
    var userComponent: UserComponent? = null
          private set

    fun isUserLoggedIn() = userComponent != null

    fun logout() {
        userComponent = null
    }

    private fun userJustLoggedIn() {
        userComponent = userComponentFactory.create()
    }
}

همون طوری که در قطعه کد بالا میبینید، ما زمانی نمونه ایuserComponent ساختیم که کاربر ورود میکنه با استفاده از factory. و وقتی هم که خروج میکنه اون رو نابود میکنیم.

ما میخوایمUserDataRepository اسکوپ بشه در UserComponent تا MainActivity و SettingsActivity هر دو بتونن یک نمونه ازش داشته باشند. وقتی که از حاشیه گذاری اسکوپ ActivityScope@ برای حاشیه گذاری کردن کامپوننت هایی که مدیریت زمان اکتیویتی رو برعهده دارند استفاده میکنم، ما نیاز به اسکوپی داریم که بتوینم چند اکتیویتی رو تحت پوشش قرار بدیم اما همه برنامه رو نه، ما هنوز همچین چیزی رو پیاده سازی نکردیم. پس باید اسکوپ جدیدی بسازیم.

زمانی که اسکوپ کاور میکنه زمان حیات رو اگه کاربر ورود انجام بده، ما میتونیم LoggedUserScope رو صدا بزنیم.

ساختن یه فایل کاتلین به اسم LoggedUserScope.kt در پکیج user و تعریف کردن حاشیه گذاری اسکوپ LoggedUserScope :

app/src/main/java/com/example/android/dagger/user/LoggedUserScope.kt

@Scope
@MustBeDocumented
@Retention(value = AnnotationRetention.RUNTIME)
annotation class LoggedUserScope

ما میتونیم UserComponent و UserDataRepository با این حاشیه گذاری کنیم تا UserComponent بتونه همیشه یک نمونه از UserDataRepository رو فراهم کنه.

UserComponent.kt

// Scope annotation that the UserComponent uses
// Classes annotated with @LoggedUserScope will have a unique instance in this Component
@LoggedUserScope
@Subcomponent
interface UserComponent { ... }

UserDataRepository.kt

// This object will have a unique instance in a Component that 
// is annotated with @LoggedUserScope (i.e. only UserComponent in this case).
@LoggedUserScope
class UserDataRepository @Inject constructor(private val userManager: UserManager) {
    ...
}

توی کلاس MyApplication ما نمونه ای از userManager نگهداری میکنیم که احتیاج داره به پیاده سازی دستی تزریق وابستگی، از اون جا که کل برنامه توسط Dagger تغییر کرده (refactored شده) ما دیگه اون رو نمیخوایم. پس MyApplication به این شکل درمیاد:

MyApplication.kt

open class MyApplication : Application() {

    val appComponent: AppComponent by lazy {
        DaggerAppComponent.factory().create(applicationContext)
    }
}

ما باید AppComponent رو هم تغییر بدیم:

@Singleton
@Component(modules = [StorageModule::class, AppSubcomponents::class])
interface AppComponent {
    ... 
    // 2) Expose UserManager so that MainActivity and SettingsActivity
    // can access a particular instance of UserComponent
    fun userManager(): UserManager

    // 1) Remove following lines
    fun inject(activity: MainActivity)
    fun inject(activity: SettingsActivity)
}

در SettingsActivity ویو مدل رو با Inject@ حاشیه گذاری (وقتی که ما میخوایم توسط Dagger اینجکت بشه) و private هم حذف میکنیم. برای گرفتن نمونه UserComponent که باید مقدار اولیه داده بشه بهش، چون کاربر ورود انجام داده، ما میتونیم متد ()userManager رو در appComponent قرار بدیم. حالا میتونیم به userComponent در اکتیویتی userComponent داشته باشیم.

SettingsActivity.kt

class SettingsActivity : AppCompatActivity() {
    // @Inject annotated fields will be provided by Dagger
    @Inject
    lateinit var settingsViewModel: SettingsViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        // Gets the userManager from the application graph to obtain the instance
        // of UserComponent and gets this Activity injected
        val userManager = (application as MyApplication).appComponent.userManager()
        userManager.userComponent!!.inject(this)

        super.onCreate(savedInstanceState)
        ...
    }
    ...
}

MainActivity.kt

class MainActivity : AppCompatActivity() {

    // 1) Remove userManager field
    @Inject
    lateinit var userManager: UserManager

    @Inject
    lateinit var mainViewModel: MainViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_settings)

        // 2) Grab userManager from appComponent to check if the user is logged in or not
        val userManager = (application as MyApplication).appComponent.userManager()
        if (!userManager.isUserLoggedIn()) { ... }
        else {
            setContentView(R.layout.activity_main)
            // 3) If the MainActivity needs to be displayed, we get the UserComponent
            // from the application graph and gets this Activity injected
            userManager.userComponent!!.inject(this)
            setupViews()
        }
    }
    ...
}
نکته مهم:
انجام دادن اینجکت کردن شرطی (همین کاری که ما توی MainActivity.kt انجام دادیم برای چک کردن ورود بودن کاربر) خیلی خطرناک هست. برنامه نویس نباید ریسک گرفتن NullPointerException وقتی که داره با فیلد های اینجکت شده ارتباط میگیره و باید از این کار اجتناب کنه.
برای اجتناب از این داستان، میتونیم کار هایی انجام تا این کار غیر مستقیم باشه مثل ساختن صفحه splash.

حالا برنامه رو اجرا میکنیم. برناممون چجوری شده حالا؟!؟!

اینجوری، عزیزکانم :)))
اینجوری، عزیزکانم :)))

خبر خوب، الان کل برنامه داره از dagger استفاده میکنه.

فصل ۱۴ | تست نویسی برای Dagger

اما آیا کار ما برنامه تموم شده؟؟ نه معلومه که نه! دیگه نبینم برنامه هاتون وقتی درست کار میکنه به امون خدا بسپورینش ها!!!! آفرین حتما براش تست بنویسید، باریکلا، ثواب داره :)
-- مادر عروس :))

یکی از مهم ترین مزایای استفاده از تزریق وابستگی و Dagger این هست که تست نویسی برای برنامه رو بسیار ساده میکنه.

یونیت تست (Unit tests)

نیازی نیست کد هایی که به Dagger مربوط هست برای یونیت تست بنویسید. وقتی شما از کلاسی استفاده میکنید که به صورت سازنده (constructor) اینجکت شده شما نیازی ندارید به Dagger بگید تا برای شما نمونه از کلاس ایجاد کنه. شما میتونید به صورت مستقیم سازنده رو صدا بزنید و بهش داده های فیک(fake) یا mock شده رو بهش پاس بدید. انگار که حاشیه گذاری ها وجود ندارند.

برای مثال اگه به LoginViewModelTest.kt نگاه کنید، ما فقط UserManager رو mock میکنیم و بهش پاس میدیم. مثل این که اصلا Dagger وجود نداره:

LoginViewModelTest.kt

class LoginViewModelTest {
    ...
    private lateinit var viewModel: LoginViewModel
    private lateinit var userManager: UserManager

    @Before
    fun setup() {
        userManager = mock(UserManager::class.java)
        viewModel = LoginViewModel(userManager)
    }

    @Test
    fun `Get username`() {
        whenever(userManager.username).thenReturn(&quotUsername&quot)

        val username = viewModel.getUsername()

        assertEquals(&quotUsername&quot, username)
    }
    ...
}

همه یونیت تست های ما مثل قبلا باقی میمونه بجز یکی، وقتی ما UserComponent.Factory رو به UserManager اضافه کردیم، یونیت تست مون رو خراب کردیم. ما باید چیزی که Dagger میخواد برگردونه رو mock کنیم. زمانی که ()create صدا زده میشه در factory.

فایل UserManagerTest.kt رو باز کنید و mock رو پیکربندی کنید برای فکتوریUserComponent:

UserManagerTest.kt

class UserManagerTest {
    ...

    @Before
    fun setup() {
        // Return mock userComponent when calling the factory
        val userComponentFactory = Mockito.mock(UserComponent.Factory::class.java)
        val userComponent = Mockito.mock(UserComponent::class.java)
        `when`(userComponentFactory.create()).thenReturn(userComponent)

        storage = FakeStorage()
        userManager = UserManager(storage, userComponentFactory)
    }

    ...
}

حالا باید همه تست ها درست کار کنند.

تست های End-to-end

ما integration test (ترجمه) ها مون رو بدون dagger انجام دادیم. وقتی برناممون با dagger رو آشنا شد، و پیاده سازی MyApplication رو تغییر داد، ما اون ها رو خراب کردیم.

ساختن Application شخصی سازی شده در تست های instrumentation (ترجمه)

قبل این داستان، تست های end-to-end های ما از اپلیکیشن شخصی شده ای به نام MyTestApplication استفاده میکنند. برای این که از یه اپلیکیشن دیگه استفاده کنیم، ما باید یک TestRunner بسازیم. این کد در برنامه وجود داره و نیاز نیست اون رو دوباره بسازید:

app/src/androidTest/java/com/example/android/dagger/MyCustomTestRunner.kt

class MyCustomTestRunner : AndroidJUnitRunner() {

    override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application {
        return super.newApplication(cl, MyTestApplication::class.java.name, context)
    }
}

این پروژه TestRunner رو میشناسه و نیاز داره تا استفاده بشه وقتی که instrumentation تست ها اجرا میشن. چون این در فایل app/build.gradle مشخص شده:

app/build.gradle

...
android {
    ...
    defaultConfig {
        ...
        testInstrumentationRunner &quotcom.example.android.dagger.MyCustomTestRunner&quot
    }
    ...
}
...

استفاده از تست های instrumentation در Dagger

ما باید MyTestApplication رو پیکربندی کنیم تا از Dagger استفاده کنه. برای تست های integration، ساختن TestApplicationComponent تمرین خوبی هست تا منظور رو برسونه. ساختن (production) و تست کردن از پیکرندی کامپوننت های متفاوتی استفاده میکنند.

تفاوت بین پیکربندی تست و پیکربندی ساختن (production) ما چیه؟ بجای استفاده از SharedPreferencesStorage در UserManager، ما میخوایم از FakeStorage استفاده کنیم. چه چیزی برای ما SharedPreferencesStorage? StorageModule رو ایجاد میکنه.

ما باید StorageModule رو با کلاس دیگه ای جایگزین کنیم که از FakeStorage استفاده میکنه. تا زمانی که این فقط در تست های instrumentation نیاز هست، ما کلاس دیگه‌ای در پوشه androidTest میسازیم. بعد پکیج جدیدی به نام di ایجاد میکنیم درون app/src/androidTest/java/com/example/android/dagger/.


app/src/androidTest/java/com/example/android/dagger/di/TestStorageModule.kt

// Overrides StorageModule in android tests
@Module
abstract class TestStorageModule {

    // Makes Dagger provide FakeStorage when a Storage type is requested
    @Binds
    abstract fun provideStorage(storage: FakeStorage): Storage
}

به دلیل چگونگی کارکرد Binds@ ، بجای ساختن متد در SharedPreferencesStorage به عنوان پامتر، برای TestStorageModule،ما FakeStorage به عنوان پارامتر پاس میدیم. که اون TestAppComponent رو میسازه که پیاده سازی Storage بعدی رو انجام میده.

کتابخونه Dagger نمیدونه چجوری میتونه نمونه ای از FakeStorage ایجاد کنه، مثل همیشه:

FakeStorage.kt

class FakeStorage @Inject constructor(): Storage { ... }

حالا وقتی Dagger نوع Storage رو درخواست میده ما نمونه ای از FakeStorage رو فراهم میکنیم. زمانی که هم روی پروداکشن و هم روی تست از پیکربندی کامپوننت متفاوتی استفاده میکنیم، ما باید کامپوننت دیگه ای بسازیم که مانند AppComponent رفتار کنه. ما اسمش رو میذاریم TestAppComponent.

فایل رو در این مسیر میسازیم:

app/src/androidTest/java/com/example/android/dagger/di/TestAppComponent.kt

@Singleton
@Component(modules = [TestStorageModule::class, AppSubcomponents::class])
interface TestAppComponent : AppComponent

همچنین ما نیاز داریم همه ماژول (module) ها رو براش مشخص کنیم.دور ازTestStorageModule، ما همچنین باید ماژول AppSubcomponents را شامل (include) کنیم که اطلاعاتی درباره ساب‌کامپوننت ها اضافه میکنه. از اون جا که به Context احتیاج نداریم برای برای تست گراف مون (تنها وابستگی که به کانتکست احتیاج داره همون SharedPreferencesStorage بود) نیازی به ساختن factory برای TestAppComponent نیست.

اگه شما پروژه رو build کنید (علامت چکش)، MyTestApplication خطای کامپایل (compilation error) رخ میده. چون شما باید یک نمونه ای از userManager کلاس حذف کنید. همچنین شما خواهید دید که dagger ـ implementation رو برای TestAppComponent نمیسازه، بلکه کلاس DaggerTestAppComponent با گراف تست میسازه. بخاطر این که kapt در پوشه androidTest کار نمیکنه. شما باید آرتیفکت (ترجمه) حاشیه گذاری های پردازنده Dagger مانند قطعه کد زیر به androidTest اضافه کنید.

app/build.gradle

...
dependencies {
    ...
    kaptAndroidTest &quotcom.google.dagger:dagger-compiler:$dagger_version&quot
}

حالا اگه پروژه تون رو sink کنید و بعد build کنید، DaggerTestAppComponent در دسترس خواهد بود، اگه هنوز در دسترس نیست دلیلش اینه که هنوز کاری با androidTest نداره. سعی کنید تا تست های instrumentation رو انجام بدید، با کلیک راست کردن روی درون پوشه androidTest و کلیک بر روی Run 'All Tests' (البته دستی هم تک تک میتونید دکمه تست رو بزنیم !!)

بعدش باید چند تا تغییر درMyApplication انجام بدیم تا به MyTestApplication اجازه بدیم تا کامپوننت Dagger خودش رو بسازه.

MyApplication.kt

open class MyApplication : Application() {

    val appComponent: AppComponent by lazy {
        initializeComponent()
    }

    open fun initializeComponent(): AppComponent {
        return DaggerAppComponent.factory().create(applicationContext)
    }
}

حالا ما میتونیم در MyTestApplication که ارث‌بری میکنه از MyApplication از TestAppComponent استفاده کنیم:

MyTestApplication.kt

class MyTestApplication : MyApplication() {

    override fun initializeComponent(): AppComponent {
        // Creates a new TestAppComponent that injects fakes types
        return DaggerTestAppComponent.create()
    }
}
توجه:
اگه نمیتونید DaggerTestAppComponent رو import کنید، تلاش کنید تا تست های instrumentation رو اجرا کنید تا چیز هایی که درونش هست رو مشخص کنه. وقتی که پروژه نمیتونه تست های android رو پیکربندی کنه، کلاسی هم نمیسازه.

خوب دوستان توجه:

توجه:
من برای پیاده سازی این قسمت (تست نویسی) چیزی بیش از ۱۰ ساعت وقت گذاشتم!! چون فایل های پروژه نیازی به تغییرات بود که من نمیدونستم چجوری باید اون ها رو برطرف کنم. و خوندن و انجام دادن این لینک و این لینک کمک کرد تا بتونم این قسمت رو هم پیاده سازی کنم (چون متن نوشته ام خیلی طولانی شده به اون ها نمی پردازم) اما در این جد بگم سیستم ساختن sharedTest که توی پروژه وجود داره کاربردی نیست و میتونید بجاش از یک ماژول (ماژول های اندروید) جدید استفاده کنید. همون دو تا رو بخونید کاملا توضیح داده و کد ها هم هست، اون دو تا لینک مربوی به pull request های همین پروژه گیت هاب هست.


درس ۱۵ | حاشیه گذاری Provides@ و Qualifier ها

ترجمه Qualifier

یکی دیگه از حاشیه گذاری های پروژه که میتونه پر‌کاربرد باشه توی پروژه provides هست:

حاشیه گذاری Provides@

بجز Inject@ و Binds@ ، میتونید از Provides@ استفاده کنید تا به dagger بگید چجوری نمونه جدیدی بگیره از کلاس های درون ماجول های dagger.

نوع خروجی توابع Provides@ (مهم نیست که چجوی صدا زده میشه) به dagger میگه که چه نوعی به گراف اضافه شده. پارامتر های تابع، وابستگی هایی هست که dagger به اون ها نیاز داره تا بتونه اون ها رو برای این تابع فراهم کنه.

در مثال ما میتونیم پیاده سازی provide رو هم برای Storage داشته باشیم:

StorageModule.kt

@Module
class StorageModule {

    // @Provides tell Dagger how to create instances of the type that this function 
    // returns (i.e. Storage).
    // Function parameters are the dependencies of this type (i.e. Context).
    @Provides
    fun provideStorage(context: Context): Storage {
        // Whenever Dagger needs to provide an instance of type Storage,
        // this code (the one inside the @Provides method) will be run.
        return SharedPreferencesStorage(context)
    }
}

شما میتونید از حاشیه گذاری Provides@ در dagger استفاده کنید تا به dagger بگید چجوری فراهم کنه:

  • پیاده سازی interdace (گرچه Binds@ پیشنهاد میشه، چون کد کمتری میسازه و بهره‌وری بیشتری داره)
  • کلاس هایی که برای خود پروژه شما نیست (مثلا Retrofit)

واجد شرایط ها Qualifiers

ما نباید از واجد شرایط ها توی برناممون استفاده کنیم، تازمانی که برنامه مون به همین سادگی که هست، هست. واجد شرایط ها موقعی استفده میشن که ما میخوایم استفاده دیگه ای از یک نوع(data type) توی پروژمون داشته باشیم توی گراف برناممون. برای مثال ما میخوایم Storage دیگه ای توی برناممون داشته باشیم که میخوایم فراهم بشه، میتونیم با گذاشتن کیفیت ساز اون ها رو از هم تمییز بدیم.

برای مثال اگه بخوایم توی SharedPreferencesStorage یک نام رو به عنوان ورودی بگیریم:

SharedPreferencesStorage.kt

class SharedPreferencesStorage @Inject constructor(name: String, context: Context) : Storage {

    private val sharedPreferences = context.getSharedPreferences(name, Context.MODE_PRIVATE)

    ...
}

میتونیم پیاده سازی های متفاوتی با Provides@ در StorageModule داشته باشیم. ما میتونیم از واجد شرایط استفاده کنیم تا نوع پیاده سازی رو مشخص کنیم:

StorageModule.kt
@Retention(AnnotationRetention.BINARY)
@Qualifier
annotation class RegistrationStorage

@Retention(AnnotationRetention.BINARY)
@Qualifier
annotation class LoginStorage

@Module
class StorageModule {

    @RegistrationStorage
    @Provides
    fun provideRegistrationStorage(context: Context): Storage {
        return SharedPreferencesStorage(&quotregistration&quot, context)
    }

    @LoginStorage
    @Provides
    fun provideLoginStorage(context: Context): Storage {
        return SharedPreferencesStorage(&quotlogin&quot, context)
    }
}

در این مثال ما محافظت کردیم از دو واجد شرایط: RegistrationStorage و LoginStorage که میتونیم از حاشیه گذاری Provides@ استفاده کنیم. ما دو نوع از Storage در گراف برنامه داریم. RegistrationStorage و LoginStorage . که هر دو متد خروجیش Storage هست، که پارامتر ها (وابستگی های) یکسانی دارند. اما نام های متفاوتی دارند. چون نام در توابع Provides@ هیچ خاصیتی نداره. و ما باید اون ها رو از گراف برنامه بازیابی کنیم.

استفاده از واجد شرایط به صورت زیر است:

چجوری واجد شرایط رو از بازیابی کنیم به عنوان وابستگی

// In a method
class ClassDependingOnStorage(@RegistrationStorage private val storage: Storage) { ... } 

// As an injected field
class ClassDependingOnStorage {

    @Inject
    @field:RegistrationStorage lateinit var storage: Storage
}

شما میتونید چند خاصیت مشابه هم به عنوان واجد شرایط با استفاده از @Named annotation بسازید. گرچه واجد شرایط ها پیشنهاد میشن چون:

  • میتونید اون ها رو در Proguard یا R8 استفاده کنید
  • شما نیاز ندارید تا نگهدارید ثابت (constant)ی برای مچ کردن نام ها
  • اون ها میتونن مستند (document) بشن


دو فصل آخر | ۱۶و۱۷

فصل ۱۶ میگه تلاش کنید کد هایی به پروژه اضافه کنید (این هم فکر کنم توی pull request ها بود) اگه دوست دارید ببینید.

فصل ۱۷ هم داره میگه ایول به شما که تونستید تموم کنید این بخش رو :))

پس ایول به شما


سخن پایانی |

کلا تو وب فارسی ما محتوای تخصصی خوبی نداریم!! همه جا پر شده از این که چجوری پول دار بشیم، کلا کسی حوصله نمیکنه محتوای عمیق بسازه و از این حرف ها، پیشنهاد میکنم که بیکار هستید برای افزایش علم جمعی شما هم بنویسید.
اگر هم وبلاگ دارید و یا دوستانی دارید که مینویسن این حتما کامنت بذارید تا بخونیم و یاد بگیریم :))

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

امیدوارم از این نوشته لذت بره باشید. اگه کم و کسری داشت ببخشید :)))