Wstęp
Wstrzykiwanie zależności
(Dependency Injection
) niezależnie od sposobu realizacji jest nieodłącznym elementem procesu budowania większości aplikacji. Wykorzystanie dostawcy zależności realizuje paradygmat Odwrócenia sterowania
(Inversion of Control
), który przenosi odpowiedzialność za inicjalizację niektórych obiektów na rzecz kontenera. Pozwala zmniejszyć ilość często powtarzającego się nadmiarowego kodu (boilerplate
) dzięki czemu tworzony kod staje się krótszy, czytelniejszy, łatwiejszy w utrzymaniu i testowaniu. Koin
jest biblioteką wstrzykiwania zależności o lekkiej strukturze przeznaczoną przede wszystkim dla programistów Kotlin
. W przeciwieństwie do innych popularnych rozwiązań (np. Dagger
) nie generuje dodatkowego kodu oraz nie wykorzystuje proxy czy refleksji, a pragmatyczne API sprawia, że jego użycie i konfiguracja jest łatwa i zwięzła.
Moduł
Konfiguracja i inicjalizacja obiektów dla kontenera zależności odbywa się w instancji modułu (module
) dzięki czemu nie jest wymagane tworzenie osobnej klasy dla każdego modułu. Aby utworzyć moduł należy przypisać do zmiennej rezultat funkcji module w której zwracane są instancje danych typów - funkcje single
dla pojedynczej instancji w obrębie zakresu i funkcja factory
dla zwyczajnej instancji.
//create main module for whole app
class App : Application() {
//create Koin module
val appModule = module {
//single instance of something
single { PreferenceManager.getDefaultSharedPreferences(get()) }
//provide names to recognize instances of the same type
single(name = "red") { SimpleDependency("red", "oval") }
single(name = "blue") { SimpleDependency("blue", "triangle") }
//get provided NetworkManager into ComplexDependency factory
factory { ComplexDependency(get()) }
single { NetworkManager("androidcode.pl", 80) }
//single instance of ParentDependency provided by ChildDependency
single<ParentDependency> { ChildDependency("text", 100) }
//for providing single instance matching multiple types use code below:
//single { ChildDependency("text", 100) } bind ParentDependency::class
}
}
Rejestracja
Tworzenie i rejestracja komponentu kontenera KoinApplication
zachodzi przy wywołaniu funkcji startKoin
w której dokonuje się konfiguracji i przekazania referencji do modułów. Tworzenie kontenera może odbywać się w dowolnej klasie Android jednakże warto zarejestrować go w głównej klasie aplikacji dzięki czemu stanie się dostępny globalnie.
class App : Application() {
//create Koin module
val appModule = module {
//definition
}
override fun onCreate() {
super.onCreate()
//create and register KoinApplication instance
startKoin {
androidLogger()
androidContext(this@App)
modules(appModule)
}
}
}
Wstrzykiwanie
Wstrzykiwanie zależności może zostać zrealizowane przez leniwą inicjalizację członków klasy przy pomocy funkcji inject
lub poprzez bezpośrednie wstrzyknięcie do instancji funkcją get
.
class MainActivity : AppCompatActivity() {
//lazy inject
val simpleDependencyRed by inject<SimpleDependency>("red")
val simpleDependencyBlue: SimpleDependency by inject("blue")
val complexDependency: ComplexDependency by inject()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//inject by directly getting instance
val pref: SharedPreferences = get()
}
}
Zakres
Zakres pozwala na wykorzystanie wybranych zależności tylko w obrębie danej klasy, a jego tworzenie realizowane jest przy pomocy funkcji scope
i oznaczania zależności definicją scoped
. Wstrzyknięcie zależności odbywa się z poziomu obiektu ScopeInstance
otrzymywanego z funkcji getActivityScope
(getFragmentScope
dla Fragment
), którego należy powiązać z cyklem życia komponentu za pomocą bindScope
.
class App : Application() {
val appModule = module {
//definitions
}
val scopeModule = module {
scope<ScopeActivity> {
scoped("scoped") { SimpleDependency("black", "square") }
}
}
override fun onCreate() {
super.onCreate()
startKoin {
androidLogger()
androidContext(this@App)
modules(appModule, scopeModule)
}
}
}
class ScopeActivity : AppCompatActivity() {
//inject instance from current scope
val simpleDependencyBlack: SimpleDependency by getActivityScope().inject()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_scope)
//bind scope session to lifecycle
bindScope(getActivityScope())
}
}
ViewModel
Koin oferuje także integracje z klasą ViewModel
co znacząco ułatwia pracę w przypadku realizacji aplikacji z wykorzystaniem mechanizmu data binding
dla widoków. Aby dostarczyć obiekt typu ViewModel
należy go oznaczyć w module jako viewModel
oraz pobrać w miejscu docelowym za pomocą funkcji viewModel
dla leniwej inicjalizacji lub bezpośrednio zainicjalizować funkcją getViewModel
.
class App : Application() {
val appModule = module {
//definitions
}
val viewModelModule = module {
single { SimpleDependency("white", "rectangle") }
viewModel {
SomeViewModel(get())
}
}
override fun onCreate() {
super.onCreate()
startKoin {
androidLogger()
androidContext(this@App)
modules(appModule, viewModelModule)
}
}
}
class ViewModelActivity : AppCompatActivity() {
//lazy inject ViewModel
val viewModel: SomeViewModel by viewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_viewmodel)
//inject by directly getting ViewModel instance
val viewModel: SomeViewModel = getViewModel()
}
}
Testowanie
Wstrzykiwanie zależności może zostać także wykorzystywane w procesie testowania z JUnit
dokładnie w taki sam sposób jak w klasach kodu źródłowego. Klasa testowa powinna rozszerzać KoinTest
. Dodatkowo Koin umożliwia również tworzenie atrap (mock
) obiektów dzięki czemu możliwa jest współpraca z bibliotekami naiwnych implementacji (np. Mockito
).
class SimpleDependencyTest: KoinTest {
//define some test module dependencies
val testModule = module {
single { SimpleDependency("red", "oval") }
}
//inject instance into member or get inside method test
val dependency: SimpleDependency by inject()
//init test dependencies before each test
@Before
fun before() {
startKoin {
modules(testModule)
}
}
//stop test dependencies after each test
@After
fun after() {
stopKoin()
}
//test dependency
@Test
fun testChangeColorWithInjection() {
assertEquals("SimpleDependency is red oval", dependency.getInfo())
dependency.changeColor("orange")
assertEquals("SimpleDependency is orange oval", dependency.getInfo())
}
//test mock
@Test
fun testCheckMock() {
//declare and get mock
declareMock<SimpleDependency>(name = "mock")
val mock: SimpleDependency = get(name = "mock")
//asserts, rules, verify
assertNotNull(mock)
Mockito.`when`(mock.getInfo()).thenReturn("SimpleDependency is green oval")
assertEquals("SimpleDependency is green oval", mock.getInfo())
Mockito.verify(mock, times(1)).getInfo()
}
}