ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Kotlin MVVM(Dagger2, Room, Retrofit)
    Android 외 개발 2020. 3. 5. 16:09

    기존까지는 MVP 패턴이 프로젝트 구성에 적합하여 사용하였지만, 새롭게 만들 Demo Application 에는 DB 와 추후 서버 연결도 사용할수 있어서 MVVM 패턴으로 구성을 하게 되었습니다. 

    최근 많이 사용하고 있어 구글 검색으로 알게 되었으며, 틀린 부분이 있을 수 있으니 양해 부탁드립니다.

     

    Project 구성을 위해 build.gradle 파일에 dependencies 를 추가하였습니다.

    apply plugin: 'com.android.application'
    apply plugin: 'kotlin-android'
    apply plugin: 'kotlin-android-extensions'
    apply plugin: 'kotlin-kapt'
    
    android {
        ...생략
        
        compileOptions {
            sourceCompatibility JavaVersion.VERSION_1_8
            targetCompatibility JavaVersion.VERSION_1_8
        }
        dataBinding {
            enabled = true
        }
    }
    kapt {
        generateStubs = true
    }
    
    dependencies {
        implementation fileTree(dir: 'libs', include: ['*.jar'])
        implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
        implementation 'androidx.appcompat:appcompat:1.1.0'
        implementation 'androidx.cardview:cardview:1.0.0'
        implementation 'com.google.android.material:material:1.1.0'
        implementation 'androidx.legacy:legacy-support-v13:1.0.0'
        implementation 'androidx.recyclerview:recyclerview:1.1.0'
        implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
        implementation 'com.github.bumptech.glide:glide:4.10.0'
        implementation 'com.github.franmontiel:PersistentCookieJar:v1.0.1'
    
        // Android Architecture Components
        implementation "android.arch.lifecycle:extensions:1.1.1"
        implementation "android.arch.lifecycle:common-java8:1.1.1"
        implementation "android.arch.lifecycle:reactivestreams:1.1.1"
        //retrofit2
        implementation "com.squareup.retrofit2:retrofit:2.5.0"
        implementation "com.squareup.retrofit2:converter-gson:2.5.0"
        implementation "com.squareup.retrofit2:adapter-rxjava2:2.5.0"
        implementation "com.squareup.retrofit2:converter-moshi:2.3.0"
        //rxjava2
        implementation "io.reactivex.rxjava2:rxandroid:2.1.0"
        implementation "io.reactivex.rxjava2:rxjava:2.2.2"
        //okhttp
        implementation "com.squareup.okhttp3:logging-interceptor:3.8.1"
        implementation "com.squareup.okhttp3:okhttp-urlconnection:3.8.1"
        // Dagger 2
        implementation "com.google.dagger:dagger:2.24"
        implementation "com.google.dagger:dagger-android:2.16"
        implementation "com.google.dagger:dagger-android-support:2.16"
        kapt "com.google.dagger:dagger-compiler:2.15"
        kapt "com.google.dagger:dagger-android-processor:2.15"
    
        // Room SQLite for data persistence
        implementation "android.arch.persistence.room:runtime:1.1.1"
        implementation "android.arch.persistence.room:rxjava2:1.1.1"
        kapt "android.arch.persistence.room:compiler:1.1.1"
    
    }
    

     

    먼저 DI 는 Dependency Injection(의존성 주입) 으로 Class 에서 객체를 생성하는 방식이 아니라 외부에서 객체를 생성하여 Class 로 넘겨주어 사용하는 방식을 말합니다. 이렇게 객체의 제어가 외부에서 이루어지는 방식을 Inversion of Control(제어의 역전) 이라고 합니다. 이 방식을 사용하기 위한 프레임워크 라이브러리가 Dagger 입니다.

     

    ActivityBuilderModule- module - components  형태로 패키지를 구성하였습니다.

     

    1. ActivityBuilderModule

    @Module
    abstract class ActivityBuilderModule {
    
        @ContributesAndroidInjector
        abstract fun mainAcitivity(): MainActivity
    }

    @ContributesAndroidInjector 의 경우 dagger.Subcomponent 를 생성합니다.

     

    2. AppModule

    @Module(includes = [ViewModelModule::class])
    class AppModule {
    
        @Provides
        @Singleton
        internal fun provideContext(application: Application): Context {
            return application
        }
    
        @Provides
        @Singleton
        internal fun provideAccountDatabase(application: Application): PetIDDatabase {
            return Room.databaseBuilder<PetIDDatabase>(application, PetIDDatabase::class.java, ApiConstants.DB_NAME)
                    .build()
        }
    
        @Provides
        @Singleton
        internal fun provideArticleDao(petIDDatabase: PetIDDatabase): PetIDDao {
            return petIDDatabase.petIDDatabase()
        }
    
        @Provides
        @Singleton
        internal fun providePreference(preference: Preference): PreferencesHelper {
            return preference
        }
    
        @Provides
        @Singleton
        internal fun provideOkHttpClient(): OkHttpClient {
            val cookieManager = CookieManager()
            cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ALL)
            val logging = HttpLoggingInterceptor { message -> }
            logging.level = HttpLoggingInterceptor.Level.BODY
    
            val okHttpClient = OkHttpClient.Builder()
            okHttpClient.cookieJar(JavaNetCookieJar(cookieManager))
            okHttpClient.connectTimeout(ApiConstants.CONNECT_TIMEOUT, TimeUnit.MILLISECONDS)
            okHttpClient.readTimeout(ApiConstants.READ_TIMEOUT, TimeUnit.MILLISECONDS)
            okHttpClient.writeTimeout(ApiConstants.WRITE_TIMEOUT, TimeUnit.MILLISECONDS)
            okHttpClient.addInterceptor(logging)
            return okHttpClient.build()
        }
    
        @Provides
        @Singleton
        internal fun provideRetrofit(okHttpClient: OkHttpClient): ApiService {
            val retrofit = Retrofit.Builder()
                .baseUrl(ApiConstants.BASE_URL)
                .addConverterFactory(GsonConverterFactory.create())
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .client(okHttpClient)
                .build()
    
            return retrofit.create(ApiService::class.java)
        }
    }

    viewModel 에서 사용할 module 들을 정의하였습니다. room, okhttp, retrofit, preference 를 선언하였습니다.

     

    - includes 된 ViewModel::class 입니다.

    @Module
    abstract class ViewModelModule {
    
        @Binds
        @IntoMap
        @ViewModelKey(MainViewModel::class)
        internal abstract fun bindMainViewModel(mainViewModel: MainViewModel): ViewModel
    
        @Binds
        internal abstract fun bindsViewModelFactory(viewModelFactory: ViewModelFactory): ViewModelProvider.Factory
    }

    위에서 ViewModelKey 부분 입니다.

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

    3. AppComponent

    Module 정의가 끝났다면 Application과 연결할 Component를 구성하였습니다.

    @Singleton
    @Component(modules = [AppModule::class, AndroidInjectionModule::class, ActivityBuilderModule::class])
    interface AppComponent : AndroidInjector<App> {
    
        @Component.Builder
        interface Builder {
            @BindsInstance
            fun application(application: Application): Builder
    
            fun build(): AppComponent
        }
    
        override fun inject(app: App)
    }

    4. Application 과 Activity

     

    Component를 생성할 코드를 Application에 구성하였습니다.

    class App : Application(), HasActivityInjector {
    
        companion object{
            lateinit var context : Context
        }
    
        @Inject
        lateinit var activityDispatchingInjector: DispatchingAndroidInjector<Activity>
    
    
        override fun onCreate() {
            super.onCreate()
            context = this
        }
    
        private fun initializeComponent() {
            DaggerAppComponent.builder()
                .application(this)
                .build()
                .inject(this)
        }
    
        override fun activityInjector(): AndroidInjector<Activity>? {
            return activityDispatchingInjector
        }
    }

    Activity 부분은 BaseActivity 를 구성하고 extends 하여 사용하도록 구성하였습니다.

    abstract class BaseActivity<V : ViewModel, D : ViewDataBinding> : AppCompatActivity() {
    
        @Inject
        lateinit var viewModelFactory: ViewModelProvider.Factory
    
        protected lateinit var viewModel: V
        protected lateinit var dataBinding: D
    
        @get:LayoutRes
        protected abstract val layoutRes: Int
        protected lateinit var handler : WeakHandler
    
        protected abstract fun getViewModel(): Class<V>
    
        protected abstract fun initLiveData()
    
        override fun onCreate(savedInstanceState: Bundle?) {
            AndroidInjection.inject(this)
            super.onCreate(savedInstanceState)
            dataBinding = DataBindingUtil.setContentView(this, layoutRes)
            viewModel = ViewModelProviders.of(this, viewModelFactory).get(getViewModel())
    
            handler = WeakHandler(this)
        }
    
        inner class WeakHandler constructor(act: BaseActivity<*, *>) : Handler() {
            private val mWeakActivity: WeakReference<BaseActivity<*, *>> = WeakReference(act)
    
            override fun handleMessage(msg: Message) {
                super.handleMessage(msg)
                val activity = mWeakActivity.get()
                activity?.handleMessage(msg)
            }
        }
    
        open fun handleMessage(msg: Message) {
            when (msg.what) {
    
            }
        }
    }

    위의 BaseActivity 를 상속하여 MainActivity를 구성하였습니다. Fragment 도 유사한 형태로 구성이 가능합니다만, Demo App 에서 필요하지 않아서 예제로 만들지 않았습니다.

    class MainActivity : BaseActivity<MainViewModel, ActivityMainBinding>() {
        override val layoutRes: Int
            get() = R.layout.activity_main
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            Trace.d("onCreate")
            setToolbar()
            initLiveData()
        }
    
        override fun getViewModel(): Class<MainViewModel> {
            return MainViewModel::class.java
        }
    
        override fun initLiveData() {
            Trace.d("initLiveData")
        }
    
        private fun setToolbar() {
    
        }
    }

     

    현재 구성은 UI - ViewModel - Repository(Database, 서버통신부분) 으로 구성되었습니다.

    이제 다음으로 ViewModel을 구성하였습니다.

    class MainViewModel @Inject
    constructor(private val mainRepository: MainRepository) : ViewModel() {
        private val compositeDisposable: CompositeDisposable = CompositeDisposable()
    
        val currentPosition: String
            get() = mainRepository.psosition
    
        override fun onCleared() {
            super.onCleared()
            compositeDisposable.clear()
        }
    }

    ViewModelModule 에서 사용된 ViewModelFactory 입니다.

    @Singleton
    class ViewModelFactory @Inject constructor(private val creators: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>) : ViewModelProvider.Factory {
    
        override fun <T : ViewModel> create(modelClass: Class<T>): T {
            Trace.d("create ViewModelFactory")
            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 {
                return creator.get() as T
            } catch (e: Exception) {
                throw IllegalStateException(e)
            }
    
        }
    }

    마지막으로 Repository 부분입니다.

    class MainRepository @Inject
    constructor(private val apiService: ApiService, private val petIDDao: PetIDDao, private val preferencesHelper: PreferencesHelper) {
    
        val psosition: String
            get() = preferencesHelper.position
    
        fun loadAccoutResult(): Flowable<Account> {
            return petIDDao.loadAccountResult()
        }
    
        val position: Flowable<Int>
            get() = apiService.positions.doOnNext { }
    }

     

    Room , Preference , retrofilt 부분을 여기에서 제어 하도록 구성하였습니다.

     

    MVVM 패턴과 DI 부분의 구성이 끝났습니다.

    추가적으로 Data 부분의 코드들은 참고용으로 보시면 될것 같습니다.

    interface ApiService {
    
        @get:GET("positions")
        val positions: Flowable<Int>
    }

    Retrofit 을 이용한 서버 통신 interface 구현 부분입니다.

    interface PreferencesHelper {
        var prefKeyFirstSetep: Boolean
        var id: String
        var userID: String
        var password: String
        var position: String
        var group: String
        fun setStringPreferences(key: String, data: String)
        fun setIntPreferences(key: String, data: Int)
        fun setLongPreferences(key: String, data: Long?)
        fun setBooleanPreferences(key: String, data: Boolean)
        fun setLongPreferences(key: String, data: Float)
        fun getStringPreferences(key: String): String
        fun getIntPreferences(key: String): Int
        fun getLongPreferences(key: String): Long
        fun getBooleanPreferences(key: String): Boolean
        fun getFloatPreferences(key: String): Float
    }
    
    class Preference @Inject
    constructor(context: Context) : PreferencesHelper {
    
        private val mPreferences: SharedPreferences =
            context.getSharedPreferences(ApiConstants.PREF_NAME, Context.MODE_PRIVATE)
        private var  mEditor: SharedPreferences.Editor
    
           ... 생략
    
        override fun getFloatPreferences(key: String): Float {
            return mPreferences.getFloat(key, 0f)
        }
    }

    preference를 호출할 수 있도록 구현한 interface와 실제 구현 부분입니다.

     

    @Dao
    interface PetIDDao {
        @Insert(onConflict = OnConflictStrategy.REPLACE)
        fun saveAccountResult(user: Account)
    
        @Query("SELECT * FROM accountResult LIMIT 1")
        fun loadAccountResult(): Flowable<Account>
    }
    
    @Database(entities = [Account::class], version = 1)
    @TypeConverters(Converters::class)
    abstract class PetIDDatabase : RoomDatabase() {
        abstract fun petIDDatabase(): PetIDDao
    }

    Room 구성 부분 입니다.

     

    이제 대부분의 구조 설계는 끝난것 같습니다.

    아직 완전한 이해를 바탕으로 하지 않아서 설명이 많이 부족한것 같습니다.

     

    참고한 사이트가 너무 많아서 정리가 되지 않았습니다. 추후 추가하도록 하겠습니다.

    'Android 외 개발' 카테고리의 다른 글

    Android Notification example  (0) 2020.01.07
    [kotlin] SharedPreferences example  (0) 2020.01.03
    kotlin null check 할때 let 을 써야할까?  (0) 2019.12.10
    kotlin use example  (0) 2019.12.10
    Kotlin error  (0) 2019.12.04
Designed by Tistory.