-
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