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

نویسنده : سید ایوب کوکبی ۱۶ مهر ۱۳۹۷

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

در بخش قبل تا حدود زیادی با RxJava و چارچوب کلی آن آشنا شدید. در این بخش سعی دارم توضیحاتی در رابطه با تزریق وابستگی (Dependency Injection) ارائه دهم. با من همراه باشید.

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

ابتدا به کلاس GitRepoRepository نگاهی بیندازیم:

class GitRepoRepository(private val netManager: NetManager) {

private val localDataSource = GitRepoLocalDataSource()
private val remoteDataSource = GitRepoRemoteDataSource()

fun getRepositories(): Observable<ArrayList<Repository>> {
…
}
}

 

کلاس GitRepoRepository به سه شی‌ء وابسته است: netManager, localDataSource و remoteDataSource. دیتاسورس‌ها در کلاس GitRepoRepository مقداردهی اولیه می‌شوند درحالی که netManager توسط سازنده تامین می‌شود. به عبارتی ما netManager را به داخل کلاس GitRepoRepository تزریق کرده‌ایم.

تزریق وابستگی مفهوم واقعاً ساده‌ای است: چیزی که لازم است را بگویید و یک نفر دیگر مسئول تامین آن است.

ببینیم GitRepoRepository کجا ساخته می‌شود (با دکمه‌های cmd+B در مک و Alt+B در ویندوز):

همانطور که می‌بینید، GitRepoRepository در MainViewModel ساخته می‌شود. همچنین، NetManager نیز همینجا ساخته می‌شود. آیا این کلاس‌ها را باید در ViewModel نیز تزریق کنیم؟ بله. نمونه‌ای از GitRepoRepository باید برای ViewModel مهیا شود چرا که GitRepoRepository می‌تواند در سایر ویومدل‌ها هم استفاده شود. از سوی دیگر، مطمئنیم که برای کل برنامه تنها به یک نمونه از NetManager نیاز داریم. بیایید این کار را با سازنده انجام دهیم. چیزی شبیه این:

class MainViewModel(private var gitRepoRepository: GitRepoRepository) : ViewModel() {
…
}

 

اگر به خاطر داشته باشید، ما MainViewModel را در MainActivity ایجاد نکردیم. بلکه از ViewModelProviders درخواست می‌کردیم:

class MainActivity : AppCompatActivity(), RepositoryRecyclerViewAdapter.OnItemClickListener {
…
override fun onCreate(savedInstanceState: Bundle?) {
…
val viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
…
}

…
}

 

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

نیازمند ساخت یک کلاس Factory ویژه برای MainViewModel هستیم چرا که از نسخه‌ی استاندارد نمی‌توانیم بهره‌ای ببریم:

class MainViewModelFactory(private val repository: GitRepoRepository)
: ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
return MainViewModel(repository) as T
}

throw IllegalArgumentException(“Unknown ViewModel Class”)
}

}

 

خب اکنون می‌توانیم در هنگام نیاز از پارامترها استفاده کنیم:

class MainActivity : AppCompatActivity(), RepositoryRecyclerViewAdapter.OnItemClickListener {
….

override fun onCreate(savedInstanceState: Bundle?) {
…

val repository = GitRepoRepository(NetManager(applicationContext))
val mainViewModelFactory = MainViewModelFactory(repository)
val viewModel = ViewModelProviders.of(this, mainViewModelFactory)
.get(MainViewModel::class.java)

…
}

…
}

 

اما صبر کنید، ما هنوز مشکل را حل نکرده‌ایم. آیا بایستی MainViewModelFactory را در اکتیویتی نمونه‌سازی کنیم؟ خیر. باید آنجا تزریقش کنیم. اجازه دهید اول کلاس Injection را ایجاد کنیم. این کلاس متدهایی دارد که نمونه‌های مورد نیاز ما را تأمین می‌کند:

object Injection {

fun provideMainViewModelFactory(context: Context) : MainViewModelFactory{
val netManager = NetManager(context)
val repository = GitRepoRepository(netManager)
return MainViewModelFactory(repository)
}
}

 

حالا می‌توانیم از داخل این کلاس به MainActivity.kt تزریق کنیم:

class MainActivity : AppCompatActivity(), RepositoryRecyclerViewAdapter.OnItemClickListener {

private lateinit var mainViewModelFactory: MainViewModelFactory

override fun onCreate(savedInstanceState: Bundle?) {
…

mainViewModelFactory = Injection.provideMainViewModelFactory(applicationContext)
val viewModel = ViewModelProviders.of(this, mainViewModelFactory)
.get(MainViewModel::class.java)

…

}
…
}

 

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

ما می‌توانیم مفهوم تزریق وابستگی را به صورت کامل‌تری در Injection.kt استفاده کنیم:

object Injection {

private fun provideNetManager(context: Context) : NetManager {
return NetManager(context)
}

private fun gitRepoRepository(netManager: NetManager) :GitRepoRepository {
return GitRepoRepository(netManager)
}

fun provideMainViewModelFactory(context: Context): MainViewModelFactory {
val netManager = provideNetManager(context)
val repository = gitRepoRepository(netManager)
return MainViewModelFactory(repository)
}

}

 

اکنون هر کلاسی متدهای خودش را برای فراهم کردن نمونه یا instanceها دارد. اگر بهتر نگاه کنید خواهید دید که هر یک از آن متدها با فرخوانی‌شان نمونه‌ی جدیدی برمی‌گردانند. آیا این کارِ درستی است؟ یعنی مثلاً هر وقت در یکی از کلاس‌های Repository به NetManager نیاز داشتیم باید نمونه‌ی جدیدی از آن ساخته شود؟ مسلماً خیر. ما فقط به یک نمونه از netManager که در سطح برنامه فقط یکی وجود دارد نیاز داریم. خب می‌توانیم NetManager را با الگوی Singleton پیاده‌سازی کنیم تا فقط یکبار قابل نمونه‌سازی باشد و در دفعات بعد نمونه‌ی قبلی که ایجاد شده برگردانده شود.

در مهندسی نرم‌افزار، الگوی singleton یک الگوی طراحی نرم‌افزار بوده که نمونه‌سازی از یک کلاس را تنها به یک شیء محدود می‌کند.

پیاده‌سازی‌اش می‌کنیم:

object Injection {

private var NET_MANAGER: NetManager? = null

private fun provideNetManager(context: Context): NetManager {
if (NET_MANAGER == null) {
NET_MANAGER = NetManager(context)
}
return NET_MANAGER!!
}

private fun gitRepoRepository(netManager: NetManager): GitRepoRepository {
return GitRepoRepository(netManager)
}

fun provideMainViewModelFactory(context: Context): MainViewModelFactory {
val netManager = provideNetManager(context)
val repository = gitRepoRepository(netManager)
return MainViewModelFactory(repository)
}
}

 

بخوانید  آموزش زبان کاتلین – درس 14 (توابع)

دقت داشته باشید که پیاده‌سازی Singleton با استفاده از آرگومان‌ها در کاتلین لزوماً بهترین روش نیست. توصیه می‌کنم برای کسب اطلاعات بیشتر این مقاله را مطالعه کنید.

با این روش مطمئن هستیم که برای کل برنامه فقط یک نمونه از NetManager ایجاد خواهد شد. به عبارتی NetManager اسکوپش (Scope یا میدان دید) در سطح برنامه خواهد بود.

این هم نمودار وابستگی کلاس‌ها:چرا باید از Dagger استفاده کنیم؟

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

خب اول dagger را ایمپورت کنیم:

…
dependencies {
…

implementation “com.google.dagger:dagger:2.14.1”
implementation “com.google.dagger:dagger-android:2.14.1”
implementation “com.google.dagger:dagger-android-support:2.14.1”
kapt “com.google.dagger:dagger-compiler:2.14.1”
kapt “com.google.dagger:dagger-android-processor:2.14.1”

…
}

 

برای استفاده از Dagger نیازمند یک کلاس application هستیم که کلاس DaggerApplication را اکستند کرده باشد:

class ModernApplication : DaggerApplication(){

override fun applicationInjector(): AndroidInjector<out DaggerApplication> {
TODO(“not implemented”)
}

}

 

از آنجایی که ()DaggerApplication را اکستند کرده‌ایم بایستی متد ()applicationInjector را پیاده‌سازی کنیم تا یک پیاده‌سازی از AndroidInjector را برگرداند. AndroidInjector را بعداً توضیح می‌دهم.

فراموش نکنید که ModernApplication را در مانیفست برنامه رجیستر کنید:

<?xml version=”۱٫۰″ encoding=”utf-8″?>
<manifest xmlns:android=”http://schemas.android.com/apk/res/android”
package=”me.mladenrakonjac.modernandroidapp”>

…

<application
android:name=”.ModernApplication”
…>
…
</application>

</manifest>

 

ابتدا ماژول AppModule را ایجاد کنید. ماژول‌ها کلاس‌هایی هستند که توابعی با انوتیشن Providers@ دارند. ما این متدها را در حکم فراهم‌کنندگان نمونه‌ها می‌بینیم. برای اینکه کلاسی را به ماژول تبدیل کنیم باید آن کلاس را با انوتیشن Module@ علامت بگذاریم. این انوتیشن به Dagger کمک می‌کند تا گراف وابستگی را ارزیابی کند. AppModule ما تنها یک متد برای تولید ApplicationContext دارد.

@Module
class AppModule{

@Provides
fun providesContext(application: ModernApplication): Context {
return application.applicationContext
}
}

 

خب حالا بیایید یک کامپوننت ایجاد کنیم:

@Singleton
@Component(
modules = [AndroidSupportInjectionModule::class,
AppModule::class])
interface AppComponent : AndroidInjector<ModernApplication> {

@Component.Builder
abstract class Builder : AndroidInjector.Builder<ModernApplication>()
}

 

کامپوننت، اینترفیسی است که مشخص می‌کند از کدام ماژول‌ها به کدام کلاس‌ها اینجکت یا تزریق انجام شود. در این مثال ما AppModule و AndroidSupportInjectionModule را مشخص کرده‌ایم.

AndroidSupportInjectionModule ماژولی است که به ما کمک می‌کند تا نمونه‌ها را به کلاس‌های مرتبط با اکوسیستم اندروید مثلاً اکتیویتی‌ها، فرگمنت‌ها، سرویس‌ها، BroadcastReceiverها و ContentProviderها تزریق کنیم.

از آنجایی که می‌خواهیم از کامپوننتِ خود برای تزریق آن کلاس‌ها استفاده کنیم AppComponent باید <AndroidInjector<T را اکستند کند. به جای T که نشان‌گر نوع مورد نظر می‌باشد کلاس ModernApplication را قرار می‌دهیم. اگر اینترفیس AndroidInjector را باز کنید، کد پایین را خواهید دید:

abstract class Builder<T> implements AndroidInjector.Factory<T> {
@Override
public final AndroidInjector<T> create(T instance) { … }
public abstract void seedInstance(T instance);
…
}
}

 

کلاس Builder دو متد دارد: اولی (create(T instance که برای ساخت AndroidInjector استفاده می‌شود و دومی (seedInsance(T instancاست که برای آماده کردن آن نمونه‌ها به کار می‌رود. در مثال ما AndroidInjector را ایجاد می‌کنیم که حاوی نمونه‌ای از ModernApplication است و این نمونه زمانی که به آن نیاز داشته باشیم تولید می‌شود.

بخوانید  آموزش زبان کاتلین – درس 15 (توابع infix)

برای مشخص کردن نوعِ instance، باید این کد را به AppComponent اضافه کنیم:

@Component.Builder
abstract class Builder : AndroidInjector.Builder<ModernApplication>()

به صورت خلاصه:

  • AppComponent را داریم که کامپوننت اصلی برنامه است و AndroidInjector را اکستند کرده است؛
  • زمانی که می‌خواهیم کامپوننت خود را بسازیم، نیازمند استفاده از نمونه‌ای از کلاس ModernApplication در قالب یک آرگومان خواهیم بود؛
  • نمونه‌ی ایجاد شده از ModernApplication برای تمام متدهایی از AppComonent که انوتیشن Providers@ را دارند فراهم می‌شود. برای مثال نمونه‌ای از ModernApplication برای متد
    (providesContext(application: ModernApplication واقع در AppModule تامین می‌شود؛

خب حالا باید پروژه را Make کنیم.

در پایان کار تعدادی کلاس جدید که به صورت خودکار توسط Dagger جنریت شده ایجاد می‌شوند. Dagger برای AppComponent کلاس DaggerAppComponent را ایجاد می‌کند.

به کلاس ModernApplication برمی‌گردیم و کامپوننت اصلی را ایجاد می‌کنیم. کامپوننت ایجاد شده باید توسط متد ()applicationInjector برگردانده شود.

class ModernApplication : DaggerApplication(){

override fun applicationInjector(): AndroidInjector<out DaggerApplication> {
return DaggerAppComponent.builder().create(this)
}

}

 

خب اکنون پیکربندی استاندراد Dagger به اتمام رسید. از آنجایی که می‌خواهیم نمونه‌های ساخته شده را به کلاس MainActivity تزریق کنیم باید کلاس MainActivityModule را ایجاد کنیم.

@Module
internal abstract class MainActivityModule {

@ContributesAndroidInjector()
internal abstract fun mainActivity(): MainActivity

}

 

انوتیشن ContributesAndroidInjector@ به Dagger کمک می‌کند تا چیزهایی که برای تزریق کردن نمونه‌ها در اکتیویتی خاصی لازم است را سامان‌دهی کند.

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

@Module
internal abstract class MainActivityModule {

@Module
companion object {

@JvmStatic
@Provides
internal fun providesMainViewModelFactory(gitRepoRepository: GitRepoRepository)
: MainViewModelFactory {
return MainViewModelFactory(gitRepoRepository)
}
}

@ContributesAndroidInjector()
internal abstract fun mainActivity(): MainActivity

}

 

اما چه چیزی GitRepoRepository را برای متد ()providesMainViewModelFactoty آماده می‌کند؟

دو انتخاب داریم: می‌توانیم متد تامین‌کننده‌ای برایش بنویسیم و آن را برگردانیم یا از سازنده‌ای با انوتیشن Injected@ استفاده کنیم.

به GitRepoRepository برمی‌گردیم و سازنده‌اش را با اتوتیشن Inject@ علامت می‌زنیم:

class GitRepoRepository @Inject constructor(var netManager: NetManager) {
…
}

 

از آنجایی که GitRepoRepository ما به نمونه‌ای از NetManager نیاز دارد اجازه دهید همین کار را برای NetManager هم انجام دهیم:

@Singleton
class NetManager @Inject constructor(var applicationContext: Context) {
…
}

 

از انوتیشن Singleton@ برای مشخص کردن میدان دید NetManager استفاده کرده‌ایم. همچنین NetManager به applicationContext نیاز دارد. متدی در AppModule برای تولید آن وجود دارد.

فراموش نکنید که MainActivityModule را به لیست ماژول‌های AppComponent.kt اضافه کرده باشید.

@Component(
modules = [AndroidSupportInjectionModule::class,
AppModule::class,
MainActivityModule::class])
interface AppComponent : AndroidInjector<ModernApplication> {

@Component.Builder
abstract class Builder : AndroidInjector.Builder<ModernApplication>()
}

 

نهایتاً باید تزریقش کنیم به کلاس MainActivity. برای اینکه Dagger بتواند آنجا کار کند، MainActivity باید کلاس daggerAppCompatActivity را اکستند کند.

class MainActivity : DaggerAppCompatActivity(), RepositoryRecyclerViewAdapter.OnItemClickListener {
…
@Inject lateinit var mainViewModelFactory: MainViewModelFactory

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

val viewModel = ViewModelProviders.of(this, mainViewModelFactory)
.get(MainViewModel::class.java)
…
}

…
}

 

برای ترریق نمونه‌‌ی MainViewModelFactory نیازمند انوتیشن Inject@ هستیم.

مهم: متغیر mainViewModelFactory باید public باشد.

دیگر نیازی به تزریق از کلاس Injection نیست:

mainViewModelFactory = Injection.provideMainViewModelFactory(applicationContext)

در واقع با وجود کتابخانه‌ی Dagger دیگر نیازی به کلاس Injection نداریم و با خیال راحت می‌توانیم حذفش کنیم.

قدم به قدم

  • می‌خواهیم MainViewModelFactory را به داخل MainActivity تزریق کنیم؛
  • برای اینکه Dagger بتواند در MainActivity کار کند کلاس MainActivity باید DaggerAppCompatActivity را اکستند کند؛
  • لازم است mainViewModelFactory را با انوتیشن Inject@ علامت بگذاریم؛
  • Dagger شروع می‌کند به جستجوی ماژولی که حاوی متدی با انوتیشن ContributesAndroidInjector@ و مقدار برگشتی MainActivity است؛
  • Dagger به دنبال تامین‌کننده‌ یا سازنده‌ای انوتیت شده با Inject@ میگردد که نمونه‌ای از MainViewModelFactory را برگرداند؛
  • ()provideMainViewModelFactory نمونه را برمی‌گرداند ولی برای ساختش به نمونه‌ای از کلاس GitRepoRepository نیاز دارد؛
  • Dagger به دنبال تامین‌کننده یا سازنده‌ای انوتیت شده با علامت Inject@ می‌گردد که نمونه‌ای از GitRepoRepository را برگرداند؛
  • کلاس GitRepoRepository سازنده‌ای دارد که با Inject@ انوتیت شده است. ولی این سازنده به نمونه‌ای از کلاس NetManager نیاز دارد؛
  • Dagger به دنبال تامین‌کننده یا سازنده‌ای با انوتیشن Inject@ که نمونه‌ای از NetManager را برگرداند می‌گردد؛
  • کلاس NetManager سازنده‌ای دارد که با Inject@ انوتیت شده است. اما سازنده‌اش به نمونه‌ای از Application Context نیاز دارد؛
  • Dagger به دنبال تامین‌کننده‌ای می‌گردد که نمونه‌ای از Application Context را برمی‌گرداند؛
  • AppModule متدی تامین‌کننده دارد که مقدار application context را برمی‌گرداند. اما سازنده‌اش به نمونه‌ای از ModernApplication نیاز دارد؛
  • AndroidInjector این تامین‌کننده را در اختیار دارد.
بخوانید  برنامه‌نویسی پیشرفته اندروید با زبان کاتلین - بخش اول

این بود مراحل کار.

روش خودکار و بهتری برای تامین ViewModelFactory وجود دارد

مسئله: برای هر ViewModel که حاوی آرگومان باشد نیازمند ساخت یک کلاس ViewModelFactory هستیم. در سورس‌کدِ اپلیکیشن Tivi از کریس بینز راه بسیار جالبی برای خودکار کردن این کار پیدا کردم.

ViewModelKey.kt را بسازید:

@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
@Retention(AnnotationRetention.RUNTIME)
@MapKey
annotation class ViewModelKey(val value: KClass<out ViewModel>)

سپس DaggerAwareViewModelFactory را بسازید:

class DaggerAwareViewModelFactory @Inject constructor(
private val creators: @JvmSuppressWildcards Map<Class<out ViewModel>, Provider<ViewModel>>
) : ViewModelProvider.Factory {

override fun <T : ViewModel> create(modelClass: Class<T>): T {
var creator: Provider<out ViewModel>? = creators[modelClass]
if (creator == null) {
for ((key, value) in creators) {
if (modelClass.isAssignableFrom(key)) {
creator = value
break
}
}
}
if (creator == null) {
throw IllegalArgumentException(“unknown model class ” + modelClass)
}
try {
@Suppress(“UNCHECKED_CAST”)
return creator.get() as T
} catch (e: Exception) {
throw RuntimeException(e)
}
}
}

 

ماژول ViewModelBuilder را ایجاد کنید:

@Module
internal abstract class ViewModelBuilder {

@Binds
internal abstract fun bindViewModelFactory(factory: DaggerAwareViewModelFactory):
ViewModelProvider.Factory
}

 

ViewModelBuilder را به AppComponent اضافه کنید:

@Singleton
@Component(
modules = [AndroidSupportInjectionModule::class,
AppModule::class,
ViewModelBuilder::class,
MainActivityModule::class])
interface AppComponent : AndroidInjector<ModernApplication> {

@Component.Builder
abstract class Builder : AndroidInjector.Builder<ModernApplication>()
}

 

Inject@ را به کلاس MainViewModel اضافه کنید:

class MainViewModel @Inject constructor(var gitRepoRepository: GitRepoRepository) : ViewModel() {
…
}

 

از حالا به بعد، فقط نیازمند این هستیم که MainViewModel را به ماژول MainActivity بایند کنیم:

@Module
internal abstract class MainActivityModule {

@ContributesAndroidInjector
internal abstract fun mainActivity(): MainActivity

@Binds
@IntoMap
@ViewModelKey(MainViewModel::class)
abstract fun bindMainViewModel(viewModel: MainViewModel): ViewModel
}

 

هیچ نیازی به تامین‌کننده‌ی MainViewModelFactory نیست. در واقع هیچ نیازی به MainViewModelFactory.kt نیست. می‌توانید حذفش کنید.

نهایتاً آن را در MainActivity.kt تغییر دهید طوری که به جای MainViewModelFactory از ViewModel.Factory استفاده کنیم:

class MainActivity : DaggerAppCompatActivity(), RepositoryRecyclerViewAdapter.OnItemClickListener {

@Inject lateinit var viewModelFactory: ViewModelProvider.Factory

override fun onCreate(savedInstanceState: Bundle?) {
…

val viewModel = ViewModelProviders.of(this, viewModelFactory)
.get(MainViewModel::class.java)
…
}
…
}

 

از کریس، بابت این روش جالب تشکر می‌کنم.

خب به پایان مقاله رسیدیم. امیدوارم مورد رضایت شما قرار گرفته باشد. در بخش بعدی با کمک کتابخانه‌ی Retrofit ریپوهای واقعی را از گیت‌هاب استخراج می‌کنیم سپس در بخش بعدی ذخیره‌ی آفلاین داده‌ها را به کمک کتابخانه‌ی Room یاد می‌گیرید. اگر وقت یاری کند قسمت‌های بیشتری هم به این سلسله مقالات اضافه می‌کنم. مثلاً روش تست لایه UI توسط کتابخانه Expresso یا تست لایه دیتا توسط آزمون واحد. چیزهای دیگری نیز مثل تحویل پیوسته (Continuous Delivery) با استفاده از Fastlane، Circle.ci و کتابخانه‌ی Fabric وجود دارد که اگر وقت باشد حتماً توضیحاتی خواهم داد.

ممنون از اینکه وقتتان را صرف خواندن این پست کردید.

سید ایوب کوکبی

نویسنده و مترجم...

0 دیدگاه

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