일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 2022 플러터 설치
- jvm이란
- 객체
- 클래스
- Rxjava Observable
- 자바 다형성
- 안드로이드 레트로핏 사용법
- 큐 자바 코드
- 안드로이드 라이선스
- ANR이란
- 안드로이드 유닛 테스트 예시
- 안드로이드 os 구조
- 안드로이드 레트로핏 crud
- 플러터 설치 2022
- 멤버변수
- android ar 개발
- rxjava hot observable
- 서비스 쓰레드 차이
- rxjava cold observable
- 안드로이드 유닛 테스트
- 안드로이드 라이선스 종류
- rxjava disposable
- jvm 작동 원리
- 서비스 vs 쓰레드
- 스택 큐 차이
- 안드로이드 유닛테스트란
- android retrofit login
- 스택 자바 코드
- 2022 플러터 안드로이드 스튜디오
- ar vr 차이
- Today
- Total
나만을 위한 블로그
[Android] Koin이란? Koin 사용법 본문
Koin은 안드로이드에서 사용할 수 있는 코틀린 DI 라이브러리다.
아래는 공식 홈페이지에서 설명하는 Koin이다. 메인 화면과 공식문서에 적힌 내용이 조금 다르다.
도구가 아닌 앱에 집중할 수 있는 코틀린 주입 라이브러리
https://insert-koin.io/docs/reference/introduction/
Koin은 코틀린 개발자를 위한 실용적이고 가벼운 종속성 주입 프레임워크다
자바로 사용할 수 있는지 찾아보니 코틀린+자바 혼용 프로젝트에서는 사용할 수 있지만 미디엄의 어떤 글에선 순수 자바 프로젝트에선 사용할 수 없다고 한다. 애초에 공식문서에서 "코틀린 개발자를 위한 라이브러리"라고 못박아놨으니 코틀린으로 쓰자.
https://medium.com/android-news/koin-simple-android-di-a47827a707ce
...(중략) Koin은 코틀린의 DSL을 써서 런타임에 종속성 그래프를 느리게 해결하는 가벼운 종속성 주입 프레임워크다
나머지 글들도 비슷하게 설명하기 때문에 그만 찾아보고 DSL이 뭔지 확인해봤다.
https://en.wikipedia.org/wiki/Domain-specific_language
DSL(Domain specific language)은 특정 응용 프로그램 도메인에 특화된 컴퓨터 언어다. 도메인 전반에 걸쳐 적용할 수 있는 범용 언어(GPL)와 대조적이다. 웹 페이지용 HTML 같이 공통 도메인에 널리 쓰이는 언어에서 MUSH 소프트 코드와 같이 하나 또는 소수의 소프트웨어에서만 쓰이는 언어에 이르기까지 다양한 DSL이 있다
DSL은 언어 종류에 따라 더 세분화될 수 있으며, 도메인 별 마크업 언어, 도메인 별 모델링 언어(일반적으로 사양 언어), 도메인 별 프로그래밍 언어가 포함된다...(중략)...DSL은 특정 문제 영역, 특정 문제 표현 기법, 특정 솔루션 기법 또는 영역의 다른 측면에 특화될 수 있다
https://www.raywenderlich.com/2780058-domain-specific-languages-in-kotlin-getting-started
DSL은 앱의 특정 부분에 특화된 언어다. 코드 일부를 추출해서 재사용 가능하고 이해하기 쉽게 만드는 데 쓰인다. 함수나 메서드와 달리 DSL은 코드를 사용하고 작성하는 방식도 바꾼다. 일반적으로 DSL은 거의 구어체처럼 코드를 읽기 쉽게 만든다. 즉 코드 이면의 아키텍처를 이해하지 못하는 사람들도 코드의 의미를 이해할 수 있다
DSL을 만든단 것은 코드의 특정 부분의 구문을 바꾸는 걸 의미한다. 코틀린에선 람다와 확장 함수, 표현식을 써서 많은 상용구 코드를 제거하고 사용자에게 내부 구현을 숨김으로써 이를 달성한다. 가장 좋은 예시 중 하나는 안드로이드 개발의 중요한 부분인 gradle이다
DSL은 대충 이렇다고 한다. 그럼 Koin은 어떻게 쓰는지 확인해보자.
먼저 앱 수준 gradle에 Koin 관련 의존성들을 추가해줘야 한다. 모두 오늘 날짜인 22.04.29 기준으로 최신 버전들이다.
// Koin
implementation "io.insert-koin:koin-core:3.1.6"
// Koin main features for Android
implementation "io.insert-koin:koin-android:3.1.6"
// No more koin-android-viewmodel, koin-android-scope, koin-android-fragment
// Java Compatibility
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"
// Koin Unit Test
testImplementation "io.insert-koin:koin-test:3.1.6"
testImplementation "io.insert-koin:koin-test-junit4:3.1.6"
마지막 2줄은 주석에 써 있듯 유닛 테스트 관련된 의존성이다. 다음 포스팅에서 Koin을 사용한 유닛 테스트를 포스팅할 예정이라 미리 복붙해두면 좋다.
그리고 Application 클래스를 상속받는 클래스를 만들고 아래 코드를 복붙한다. 만들고 나서 매니페스트에 적어주는 걸 잊지 말자. 그리고 인터넷 권한도 꼭 설정한다.
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.core.context.startKoin
class BaseApplication: Application() {
override fun onCreate() {
super.onCreate()
/* startKoin {} : GlobalContext API를 쓰기 위해 KoinApplication 컨테이너를 구성하고 GlobalContext에 등록
* koinApplication {} : KoinApplication 컨테이너 구성 생성 */
startKoin {
androidContext(this@BaseApplication)
androidLogger()
// 위의 메서드 2개는 없어도 작동하긴 한다
/* modules() : 컨테이너에 로드할 Koin 모듈 목록 설정. listOf()로 appModule을 구성한 예제가 있었다 */
modules(appModule)
}
}
}
<application
android:name=".BaseApplication"
onCreate()를 여기서 재정의한 다음 startKoin {} 안에 필요한 내용들을 넣는다.
startKoin이 아니라 koinApplication을 써도 작동하는데 둘의 차이는 주석에 간단하게 써놨다. 궁금하다면 따로 찾아보자. 여기선 startKoin을 사용한다.
modules() 안에는 내가 사용할 Koin 모듈들을 넣는다. appModule은 AppModule.kt라는 파일 안에 만든 var 프로퍼티명이다.
import org.koin.dsl.module
val appModule = module {
// single instance of HelloRepository
/* single : 싱글톤 빈 정의를 제공. 즉 1번만 객체를 생성한다 */
single<ApiInterface>() { ApiInterfaceImpl() }
// Simple Presenter Factory
/* factory : 호출될 때마다 객체 생성 */
factory { ApiViewModel(get()) }
}
single, factory는 Koin과 관련된 키워드니 마찬가지로 궁금하다면 따로 검색해서 찾아보자.
ApiInterface는 레트로핏을 사용할 때 필요한 추상 메서드들을 정의해 놓은 인터페이스, ApiInterfaceImpl은 ApiInterface를 구현하고 이 안에 정의된 추상 메서드를 구현한 클래스다. ApiViewModel은 MVVM 패턴에 쓰이는 그 뷰모델이다.
아래에 각 파일 별 사용된 코드를 파일명 주석과 같이 쓴다. RxJava도 같이 썼는데 이에 관련된 의존성들은 생략한다.
ServerResponse는 서버에서 주는 응답을 프로퍼티 형태로 정의한 data class인데 안에는 boolean 프로퍼티만 존재한다. Koin 테스트를 먼저 하기 위해 프로퍼티 하나만 정의했다.
import com.google.gson.annotations.SerializedName
class ServerResponse(@SerializedName("data") val result: Boolean)
// ApiInterface.kt
import io.reactivex.Observable
import retrofit2.http.GET
import retrofit2.http.Query
interface ApiInterface {
@GET("api/user/email/{userEmail}")
fun isRegisteredUser(
@Query("userEmail") email: String
): Observable<ServerResponse>
}
// ApiInterfaceImpl.kt
import io.reactivex.Observable
class ApiInterfaceImpl: ApiInterface {
override fun isRegisteredUser(email: String): Observable<ServerResponse> =
ApiClient.getApiInterface().isRegisteredUser(email)
}
// ApiClient.kt
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.converter.scalars.ScalarsConverterFactory
object ApiClient {
private val gson: Gson = GsonBuilder()
.setLenient()
.create()
private val client: OkHttpClient = OkHttpClient.Builder()
.addInterceptor(setHttpLoggingInterceptor())
.build()
private val retrofit: Retrofit = Retrofit.Builder()
.baseUrl("SERVER_URL")
.client(client)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(ScalarsConverterFactory.create())
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
private fun setHttpLoggingInterceptor(): HttpLoggingInterceptor {
val interceptor = HttpLoggingInterceptor { message: String ->
Log.e(
"HttpLoggingInterceptor",
"message : $message"
)
}
return interceptor.setLevel(HttpLoggingInterceptor.Level.BODY)
}
fun getApiInterface(): ApiInterface = retrofit.create(ApiInterface::class.java)
}
// ApiViewModel.kt
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.example.kointest.BaseViewModel
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers.io
class ApiViewModel(private val repo: ApiInterface) : BaseViewModel() {
val TAG = this.javaClass.simpleName
private val _isRegisteredUserData = MutableLiveData<ServerResponse>()
val isRegisteredUserData: LiveData<ServerResponse>
get() = _isRegisteredUserData
fun test(email: String): MutableLiveData<ServerResponse> {
addDisposable(repo.isRegisteredUser(email)
.subscribeOn(io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
it.run {
if (result) {
Log.e(TAG, "test() 성공 : $result");
} else {
Log.e(TAG, "test() 실패 : $result");
}
_isRegisteredUserData.postValue(this)
}
}, {
Log.e(TAG, "error : ${it.printStackTrace()}");
})
)
return _isRegisteredUserData
}
}
ApiViewModel은 BaseViewModel을 상속받는 걸 볼 수 있다.
BaseViewModel은 뷰모델 사용 시 공통적으로 쓰이는 메서드나 프로퍼티들을 모아둔 클래스로, RxJava를 썼기 때문에 CompositeDisposable을 사용하는 메서드 2개를 BaseViewModel 안에 구현해뒀다.
import androidx.lifecycle.ViewModel
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable
open class BaseViewModel: ViewModel() {
private val compositeDisposable = CompositeDisposable()
fun addDisposable(disposable: Disposable) {
compositeDisposable.add(disposable)
}
override fun onCleared() {
compositeDisposable.clear()
super.onCleared()
}
}
이제 메인 액티비티에서 위에서 만든 뷰모델을 통해 RxJava 함수를 호출하면 된다.
import org.koin.android.ext.android.inject
class MainActivity : BaseActivity<ActivityMainBinding>() {
val TAG = this.javaClass.simpleName
override val layoutResourceId: Int
get() = R.layout.activity_main
private val testPresenter: ApiViewModel by inject()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
testPresenter.test("abcd@sdf.com").observe(this) {
if (it.result) {
Log.e(TAG, "액티비티에서 확인한 result 값이 true : ${it.result}");
} else {
Log.e(TAG, "액티비티에서 확인한 result 값이 false : ${it.result}");
}
}
}
}
액티비티도 BaseActivity를 만들어 사용했는데 이 안에서 제네릭으로 데이터 바인딩 관련 처리들을 좀 넣었다.
Koin 관련 설명 및 코드를 참고한 사이트는 밑에 링크로 걸어뒀으니 그걸 참고하면 된다. 원래는 제네릭 안에 데이터 바인딩, 뷰모델을 넣는데 난 데이터 바인딩만 넣어서 사용했다.
참고한 사이트)
https://deque.tistory.com/108?category=984011