آموزش استفاده کردن از Koin (تزریق وابستگی) در اندروید

آموزش استفاده کردن از Koin (تزریق وابستگی) در اندروید

Koin یکی از کتابخونه ها و روش های موجود برای تزریق وابستگی در اندروید هستش.

اندروید روش ها و کتابخونه های مختلفی رو برای تزریق وابستگی ها داره، کتابخونه هایی مثل : DaggerHiltKoinKodein و …

هر کدوم از این روش ها یکسری مزایا و معایب هایی رو دارن.

از بین این کتابخونه ها به نظر من Hilt و Koin خیلی خوب عمل کردن و الان دیگه تقریبا 90درصد پروژه ها از این روش ها استفاده میکنند.

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

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

دونستن این 2 تا واقعا بهتون توی اسخدام و گرفتن پروژه های اندروید کمک زیادی میکنه.

برای تزریق وابستگی Koin یک دوره جامع و کامل هم آماده شده که میتونی از لینک زیر سرفصل های اون رو ببینی.

تزریق وابستگی چیه؟

قبل از اینکه بخوام راجب کوین توضیح بدم بهتره که اول از همه بدونی که اصلا تزریق وابستگی یعنی چی؟ چرا باید ازش استفاده کنیم؟

فرض کن که 2 تا کلاس داریم، کلاس User و کلاس UserInfo.

کلاس UserInfo وظیفه نمایش دادن اطلاعات کاربر رو برعهده داره و کلاس User هم اطلاعات کاربر رو ذخیره میکنه.

برای اینکه ما بتونیم اطلاعات کاربر رو به کمک کلاس UserInfo نشون بدیم، اول از همه باید اطلاعات رو از کلاس User دریافت کنیم.

به کلاس های زیر خوب توجه کن.

کلاس User :

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
class User {
val name = "Mohammad"
val family = "Nouri"
val site = "https://nouri.academy"
}
class User { val name = "Mohammad" val family = "Nouri" val site = "https://nouri.academy" }
class User {
    val name = "Mohammad"
    val family = "Nouri"
    val site = "https://nouri.academy"
}

کلاس UserInfo :

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
class UserInfo(private val user: User) {
fun userName() = "${user.name} ${user.family}"
fun siteAddress() = user.site
}
class UserInfo(private val user: User) { fun userName() = "${user.name} ${user.family}" fun siteAddress() = user.site }
class UserInfo(private val user: User) {
    fun userName() = "${user.name} ${user.family}"

    fun siteAddress() = user.site
}

اگه به کلاس userInfo توجه کنی میبینی که 2 تا fun داره که وظیفه نمایش نام کاربر و نام سایت رو برعهده دارن.

این اطلاعات از چه کلاسی تامین میشه؟ از کلاس User.

کلاس User در سازنده کلاس UserInfo قرار داده شده که بتونیم به اطلاعات کاربر دسترسی داشته باشیم.

این یعنی وابستگی! یعنی کلاس UserInfo برای اینکه بتونه اطلاعات کاربر رو نشون بده به کلاس User وابستگی داره.

این مثال یک وابستگی از نوع کلاس هستش.

ما انواع وابستگی ها رو داریم :

  • وابستگی به کلاس
  • وابستگی به فانکشن (متد)
  • وابستگی به متغیر

حالا که با مفهوم وابستگی آشنا شدی، بهتره که بریم ببینیم Koin چطور میتونه توی این زمینه به ما کمک منه.

اضافه کردن Koin به پروژه

برای اینکه بتونی از کوین استفاده کنی، باید اون رو به پروژه خودت اضافه کنی.

اول از همه کد زیر رو به فایل project/build.gradle اضافه کن

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
repositories {
mavenCentral()
}
repositories { mavenCentral() }
repositories {
    mavenCentral()
}

سپس برای اضافه کردن کوین با توجه به مواردی که توی پروژه بهشون نیاز داری باید از کدهای زیر توی module/build.gradle استفاده کنی.

برای Kotlin

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// هسته اصلی کوین
implementation "io.insert-koin:koin-core:3.1.6"
// کوین برای تست نویسی
testImplementation "io.insert-koin:koin-test:3.1.6"
// هسته اصلی کوین implementation "io.insert-koin:koin-core:3.1.6" // کوین برای تست نویسی testImplementation "io.insert-koin:koin-test:3.1.6"
// هسته اصلی کوین
implementation "io.insert-koin:koin-core:3.1.6"
// کوین برای تست نویسی
testImplementation "io.insert-koin:koin-test:3.1.6"

برای JUnit

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// کوین برای JUnit 4
testImplementation "io.insert-koin:koin-test-junit4:3.1.6"
// کوین برای JUnit 5
testImplementation "io.insert-koin:koin-test-junit5:3.1.6"
// کوین برای JUnit 4 testImplementation "io.insert-koin:koin-test-junit4:3.1.6" // کوین برای JUnit 5 testImplementation "io.insert-koin:koin-test-junit5:3.1.6"
// کوین برای JUnit 4
testImplementation "io.insert-koin:koin-test-junit4:3.1.6"
// کوین برای JUnit 5
testImplementation "io.insert-koin:koin-test-junit5:3.1.6"

برای Android

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// کوین برای اندروید
implementation "io.insert-koin:koin-android:3.1.6"
// کوین برای جاوا
implementation "io.insert-koin:koin-android-compat:3.1.6"
// برای Jetpack WorkManager
implementation "io.insert-koin:koin-androidx-workmanager:3.1.6"
// برای Navigation Graph
implementation "io.insert-koin:koin-androidx-navigation:3.1.6"
// برای Jetpack Compose
implementation "io.insert-koin:koin-androidx-compose:3.1.6"
// کوین برای اندروید implementation "io.insert-koin:koin-android:3.1.6" // کوین برای جاوا implementation "io.insert-koin:koin-android-compat:3.1.6" // برای Jetpack WorkManager implementation "io.insert-koin:koin-androidx-workmanager:3.1.6" // برای Navigation Graph implementation "io.insert-koin:koin-androidx-navigation:3.1.6" // برای Jetpack Compose implementation "io.insert-koin:koin-androidx-compose:3.1.6"
// کوین برای اندروید
implementation "io.insert-koin:koin-android:3.1.6"

// کوین برای جاوا
implementation "io.insert-koin:koin-android-compat:3.1.6"
// برای Jetpack WorkManager
implementation "io.insert-koin:koin-androidx-workmanager:3.1.6"
// برای Navigation Graph
implementation "io.insert-koin:koin-androidx-navigation:3.1.6"
// برای Jetpack Compose
implementation "io.insert-koin:koin-androidx-compose:3.1.6"
نکته
اصلا نیاز نیست که تمامی کدها رو به Gradle پروژه اضافه کنی.
در 95درصد مواقع اضافه کردن هسته اصلی کوین و کوین اندروید کافی هستش.

نحوه تزریق وابستگی ها به کمک کوین

طبق گفته های سازنده های کوین، این کتابخونه میتونه فقط توی 3 مرحله وابستگی های مختلف رو برای ما تزریق کنه (واقعا هم میتونه همین کار رو بکنه ? ، فقط توی 3 مرحله).

  1. اولین قدم اینه که فایل Module (ماژول) رو درست کنی و چیزهایی که میخوای تزریق کنی رو اونجا قرار بدی.
  2. سپس فایل ماژول رو باید توی کلاس Application معرفی کنی.
  3. درنهایت هم کلاس ها رو به کمک دستور inject توی صفحه ای که میخوای ازشون استفاده کنی تزریق کنی.

بعد از خوندن این 3 مرحله شاید برات سوال باشه که Module و inject چیه؟ اصلا چرا باید استفاده کنیم؟

Module چیست؟

ماژول درواقع وابستگی هایی که ما بهشون نیاز داریم رو برای ما تامین میکنه.

وابستگی های مختلف، اعم از کلاس، فانکشن و متغیر رو داخل ماژول تعریف میکنیم و ماژول هم وظیفه تامین کردن اونارو بر عهده داره.

inject چیست؟

وابستگی هایی که توسط ماژول تامین شدن رو به کمک inject (اینجکت) میتونیم توی Activity / Fragment استفاده کنیم.

آموزش استفاده کردن از Koin

همانطور که توی مثال بالا که مربوط به اطلاعات کاربر بود بهتون توضیح دادم، برای اینکه UserInfo بتونه اطلاعات رو نشون بده به کلاس User وابستگی داره.

حالا بریم ببینیم که چطور میتونیم وابستگی کلاس های بالا رو به کمک کوین تامین کنیم.

هر دو کلاس بالا یعنی User و UserInfo رو مانند مثال بالا تعریف میکنیم و کاری بهشون نداریم.

الان میخوایم با 3 مرحله ای که کوین گفته وابستگی ها رو تامین کنیم.

مرحله اول

اولین مرحله ساختن ماژول بود.

برای اینکار میتونیم یک فایل جدید درست کنیم و کلاس ها رو داخلش قرار بدیم.

من یک فایل جدید با نام UserModule درست کردم و کدهای زیر رو هم داخل قرار دادم.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
val userModule = module {
single { User() }
single { UserInfo(get()) }
}
val userModule = module { single { User() } single { UserInfo(get()) } }
val userModule = module {
    single { User() }
    single { UserInfo(get()) }
}

برای اینکه بتونی از ماژول کوین استفاده کنی باید اون رو داخل یک متغیری بریزی، برای همین هستش که من داخل فایل UserModule یک متغیر با نام userModule درست کردم و اون رو از نوع module قرار دادم.

نکته
نیازی نیست که اسم های فایل و متغیر هم نام باشن.
میتونی از هر اسمی که میخوای براشون قرار بدی.
مثلا :
نام فایل : MyModule
نام متغیر : userModule

انواع روش های تامین وابستگی در ماژول

توی کوین ما 2 روش برای تامین کردن وابستگی ها داریم :

  • single : به صورت سینگلتون تامین میکنه (یکبار در روند کل اجرای اپلیکیشن)
  • factory : در هربار صدا کردن یک آبجکت جدید ازش میسازه.

توی فایل بالا یعنی UserModule من هردو کلاس رو توسط single تامین کردم.

کلاس User چون وابستگی به چیزی نداشت برای همین در ورودی این کلاس چیزی رو قرار ندادم.

ولی کلاس UserInfo برای ساخته شدن به کلاس User وابستگی داشت.

چون وابستگی کلاس User رو تامین کردیم، پس خیلی راحت به کمک کلمه کلیدی get (جز کلمات کلیدی کوین هستش) میتونی وابستگی کلاس UserInfo رو تامین کنیم.

درواقع با نوشتن کلمه get ، کوین به صورت خودکار وابستگی مورد نیاز رو (در صورتی که قبلا تامین شده باشه) تامین میکنه.

بعد از اینکه وابستگی ها رو توی ماژول فراهم کردیم، نوبت این رسیده که اون ماژول رو توی کلاس Application تعریف کنیم.

مرحله دوم

برای اینکار باید کلاس اپلیکیشن رو درست کنی و داخل متد onCreate اون کد زیر رو قرار بدی.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
//Koin
startKoin {
modules(userModule)
}
}
}
class MyApp : Application() { override fun onCreate() { super.onCreate() //Koin startKoin { modules(userModule) } } }
class MyApp : Application() {

    override fun onCreate() {
        super.onCreate()
        //Koin
        startKoin {
            modules(userModule)
        }
    }
}

 

برای اینکه بتونی ماژول رو توی کلاس Application فراخوانی کنی، باید توی onCreate متد startKoin رو بنویسی.

با نوشتن startKoin درواقع به کوین میگیم که ماژول هایی که توی این متد نوشته میشن رو به اپلیکیشن تزریق کنه.

نکته
هر چندتا ماژولی که داریم رو باید داخل modules بنویسیم، فقط کافیه که با , از هم جداشون کنیم.
مثال : modules(userModule, networkModule, databaseModule)

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

فقط کافیه که داخل startKoin از کد androidContext استفاده کنی.

مثالش رو میتونی توی کد زیر ببینی :

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
//Context
androidContext(this@MyApp)
//User
modules(userModule)
}
}
class MyApp : Application() { override fun onCreate() { super.onCreate() startKoin { //Context androidContext(this@MyApp) //User modules(userModule) } }
class MyApp : Application() {

    override fun onCreate() {
        super.onCreate()
        
        startKoin {
            //Context
            androidContext(this@MyApp)
            //User
            modules(userModule)
     }
}
یادت نره که کلاس اپلیکیشن رو توی فایل Manifest پروژه تعریف کنی.

مرحله سوم

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

برای استفاده کردن از مواردی که وابستگی هاشون رو تامین کردیم، باید وارد Activity یا Fragment بشیم و به کمک inject اون کلاس/متد/متغیر رو تزریق کنیم.

برای اینکار میتونم از کد زیر استفاده کنیم :

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
private val userInfo: UserInfo by inject()
private val userInfo: UserInfo by inject()
private val userInfo: UserInfo by inject()

به همین راحتی دوستان. ما اومدیم کلاس UserInfo که وظیفه نمایش دادن اطلاعات رو برعهده داره رو توی Activity تزریق کردیم.

این کلاس به طور خودکار با کلاس User ارتباط برقرار میکنه و اطلاعات رو دریافت میکنه. بدون اینکه نیاز باشه خودمون به صورت دستی بخوایم کلاس User رو تعریف کنیم و اون رو به UserInfo بدیم.

کد کامل کلاس Activity :

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
class MainActivity : AppCompatActivity() {
//Binding
private lateinit var binding: ActivityMainBinding
//Inject
private val userInfo: UserInfo by inject()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.infoTxt.text = userInfo.siteAddress()
}
}
class MainActivity : AppCompatActivity() { //Binding private lateinit var binding: ActivityMainBinding //Inject private val userInfo: UserInfo by inject() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) binding.infoTxt.text = userInfo.siteAddress() } }
class MainActivity : AppCompatActivity() {
    //Binding
    private lateinit var binding: ActivityMainBinding
    //Inject
    private val userInfo: UserInfo by inject()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.infoTxt.text = userInfo.siteAddress()
    }
}

به همین راحتی دوستان. کار دیگه تمومه!

خیلی راحت ما تونستیم وابستگی که بین User و UserInfo بود رو تامین کنیم و ازش توی صفحه مورد نظر استفاده کنیم.

تزریق کردن کلاس همراه با اینترفیس (Interface)

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

برای تزریق این نوع کلاس ها فقط کافیه که دستورات داخل ماژول رو به شکل زیر تغییر بدی:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
val bmwModule = module {
factory { CarImpl() } bind Car::class
}
val bmwModule = module { factory { CarImpl() } bind Car::class }
val bmwModule = module {
    factory { CarImpl() } bind Car::class
}

بقیه موارد دقیقا مثل روش قبل هستن.

درواقع با نوشتن bind ما اون اینترقیس رو هم معرفی میکنیم.

اعمال محدوده اجرایی (Scope) در موارد تزریق شده

کوین به 4 حالت مختلف میتونه Scope موارد تزریق شده رو برای ما فراهم کنه.

  • activityScope : به چرخه حیات Activity آگاه است.
  • activityRetainedScope : به چرخه حیات ViewModel مربوط به Activity آگاه است.
  • fragmentScope : به چرخه حیات Fragment آگاه است.
  • serviceScope : به چرخه حیات Service آگاه است.

زمانی که میخوای از Scope استفاده کنی، روش تعریف کردن و دسترسی داشتن به موارد تزریق شده یکم متفاوت میشه.

به کدهای فایل ماژولی که پایین مینویسم خوب توجه کن :

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
val personModule = module {
scope<ScopesActivity> {
scoped<PersonInfo>()
}
}
val personModule = module { scope<ScopesActivity> { scoped<PersonInfo>() } }
val personModule = module {
    scope<ScopesActivity> {
        scoped<PersonInfo>()
    }
}

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

اینجا دیگه از single یا factory استفاده نمی‌کنیم و بجای اون باید از scope  و scoped استفاده کنیم.

scope درواقع اون Activity یا Fragment رو مشخص میکنه که میخوایم فایل تزریق شدمون با توجه به چرخه حیات اون صفحه آگاه باشه.

برای همین من اسم اکتیویتی خودم یعنی ScopesActivity رو داخل scope نوشتم.

بعد از اون به کمک scoped که داخل scope نوشته میشه، کلاس/متد/متغیر مورد نظرمون رو بهش معرفی میکنیم.

به توضیحات و موارد استفاده scope و scoped خوب توجه کنی.، اینا باهم یکی نیستن.

برای استفاده کردنش داخل Activity یا Fragment هم مجددا با روش قبلی یکم متفاوت هستش.

باید اول صفحه مورد نظرت رو از KoinScopeComponent ارث بری کنی.

بعد از ارث بری کردن یک متغیر با عنوان override val scope : Scope به Activity / Fragment اضافه میشه.

با توجه به 4 حالت بالا میتونی سطح دسترسی scope رو مشخص کنی، مانند کد زیر :

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
override val scope: Scope by activityScope()
override val scope: Scope by activityScope()
override val scope: Scope by activityScope()

در نهایت برای تزریق کردن کلاس هم بجای اینکه از inject استفاده کنی، باید از scope.inject استفاده کنی. مثل کد زیر :

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
class ScopesActivity : AppCompatActivity(), KoinScopeComponent {
//Binding
private lateinit var binding: ActivityScopesBinding
//Inject
override val scope: Scope by activityScope()
private val person: PersonInfo by scope.inject()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityScopesBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.infoTxt.text = person.showInfo()
}
}
class ScopesActivity : AppCompatActivity(), KoinScopeComponent { //Binding private lateinit var binding: ActivityScopesBinding //Inject override val scope: Scope by activityScope() private val person: PersonInfo by scope.inject() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityScopesBinding.inflate(layoutInflater) setContentView(binding.root) binding.infoTxt.text = person.showInfo() } }
class ScopesActivity : AppCompatActivity(), KoinScopeComponent {
    //Binding
    private lateinit var binding: ActivityScopesBinding

    //Inject
    override val scope: Scope by activityScope()
    private val person: PersonInfo by scope.inject()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityScopesBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.infoTxt.text = person.showInfo()
    }
}

 

سعی کردم که تزریق وابستگی به کمک Koin رو خیلی ساده و کاربردی یاد بدم.

ولی اگر میخوای به صورت کامل و عملی همراه با پروژه های Room  و Retrofit نحوه کار کردن Koin رو یاد بگیری، پیشنهاد میکنم حتما یکسری به لینک زیر بزنی : ???

دیدگاه‌ها ۰
ارسال دیدگاه جدید