Tutoriel pour comprendre l'injection de dépendances Android avec Koin et le langage Kotlin

Koin est un framework d'injection de dépendances pour Kotlin & Android, qui se veut pragmatique. Il permet de décrire les composants à assembler via un DSL intuitif. Le conteneur léger de Koin permet de résoudre définitions et instances et de les récupérer par une API très simple et prête pour Android.

Cet article est une introduction à l'injection de dépendances sous Android. Nous verrons ensemble comment nous pouvons déclarer nos composants, utiliser l'injection de dépendances par constructeur et récupérer nos instances depuis Android.

Le projet Koin sur internet :

Pour réagir au contenu de ce tutoriel, un espace de dialogue vous est proposé sur le forum Commentez Donner une note  l'article (5).

Article lu   fois.

L'auteur

Profil Pro

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Code source

Le code source de cet article est disponible sur Github : https://github.com/InsertKoinIO/getting-started-koin-android

Koin est maintenant en version 1.0.0, vous pouvez utiliser ce numéro pour les variables $koin_version Gradle utilisées ci-dessous. Vous pouvez également vérifier la dernière version disponible du projet sur le site officiel ou Github.

II. Premiers pas

Dans un premier temps, nous allons déclarer quelques composants simples.

II-A. Gradle - koin-android

Pour intégrer Koin à votre projet, il suffit d'ajouter la dépendance Gradle ci-dessous :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
// Add Jcenter to your repositories if needed
repositories {
    jcenter()    
}
dependencies {
    // Koin for Android
    implementation 'org.koin:koin-android:$koin_version'
}

II-B. Premiers composants

Nous allons partir sur un cas très simple, inspiré d'une architecture MVP : récupérer une donnée (notre message "Hello Koin") via un Repository et l'intégrer à une vue Android via un Presenter.

Nous allons mettre en œuvre deux composants :

  • HelloRepository qui va nous fournir un message via la fonction giveHello(). Nous utiliserons son implémentation HelloRepositoryImpl ;
  • MySimplePresenter, un composant qui va nous aider à récupérer notre message et qui sera utilisé par une vue Android.
 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
interface HelloRepository {
    fun giveHello(): String
}

class HelloRepositoryImpl() : HelloRepository {
    override fun giveHello() = "Hello Koin"
}
 
Sélectionnez
1.
2.
3.
4.
class MySimplePresenter(val repo: HelloRepository) {

    fun sayHello() = "${repo.giveHello()} from $this"
}

À noter que nous utilisons le constructeur de MySimplePresenter afin d'utiliser le composant HelloRepository

II-C. Déclaration de composants avec Koin

Nous allons utiliser ici le DSL Kotlin de Koin afin de déclarer nos composants décrits ci-dessus.

Dans Koin nous déclarons nos composants via des définitions ou dans des modules. Une définition peut être de type single ou factory :

  • single permet de déclarer un singleton, une instance unique dans le conteneur Koin ;
  • factory permet de créer une nouvelle instance à chaque appel de la définition.

Chaque définition est déclarée avec une expression qui indique comment créer l'instance en question. Pour déclarer un singleton de HelloRepositoryImpl, je vais utiliser la fonction single :

 
Sélectionnez
1.
single { HelloRepositoryImpl() }

Koin aura donc une définition correspondant au type HelloRepositoryImpl en singleton. Par contre, si je veux résoudre cette définition par le type HelloRepository, il va falloir que je le précise via le DSL (Koin ne fait pas d'introspection pour deviner l’héritage d'un type) :

 
Sélectionnez
1.
single<HelloRepository> { HelloRepositoryImpl() }

Pour notre « presenter » MySimplePresenter, je vais devoir déclarer une factory afin d'avoir une nouvelle instance à chaque fois que ma vue sera créée. Nous utilisons ici la fonction factory

 
Sélectionnez
1.
factory { MySimplePresenter() }

La classe possède aussi un constructeur, dans lequel je vais devoir récupérer mon instance de HelloRepository. À chaque fois que j'ai besoin d'injecter une instance dans un constructeur, je vais utiliser la fonction get() :

 
Sélectionnez
1.
factory { MySimplePresenter(get()) }

Pour finir, nous pourrons déclarer nos définitions dans un module avec la fonction module :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
val appModule = module {

    // single instance of HelloRepository
    single<HelloRepository> { HelloRepositoryImpl() }

    // Simple Presenter Factory
    factory { MySimplePresenter(get()) }
}

II-D. Démarrons avec Koin !

Voilà, nos déclarations sont prêtes. Nous allons pouvoir les exécuter dans le conteneur Koin. Pour lancer Koin avec notre module, il suffit d'appeler la fonction startKoin() de la manière suivante (ici depuis une classe Application Android) :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
class MyApplication : Application(){
    override fun onCreate() {
        super.onCreate()
        // Start Koin
        startKoin(this, listOf(appModule))
    }
}

II-E. Intégration avec Android

Koin met à disposition une API simple pour permettre d'aller chercher les instances déclarées dans Koin.

Pour récupérer une instance de Koin dans un composant Android :

  • by inject(), un « lazy delegate » pour récupérer l'instance uniquement quand elle est utilisée ;
  • get() récupère directement l'instance.

Tous les composants Android (Activity, Fragment, Service…) sont munis des extensions Koin pour pouvoir utiliser l'API Koin ci-dessus.

Écrivons l'Activity MySimpleActivity afin de récupérer une instance de MySimplePresenter :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
class MySimpleActivity : AppCompatActivity() {

    // Lazy injected MySimplePresenter
    val firstPresenter: MySimplePresenter by inject()

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

Nous déclarons notre Presenter via la propriété firstPresenter :

 
Sélectionnez
1.
val firstPresenter: MySimplePresenter by inject()

C'est tout ! :)

III. Tester facilement avec Koin

Koin permet également d'écrire des tests qui utilisent les instances du conteneur Koin.

III-A. Gradle - koin-test

Il suffit d'ajouter la dépendance Gradle ci-dessous :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
// Add Jcenter to your repositories if needed
repositories {
    jcenter()    
}
dependencies {
    // Koin for Android
    testImplementation 'org.koin:koin-test:$koin_version'
}

III-B. Tests unitaires avec Junit

Le projet koin-test apporte quelques fonctionnalités intéressantes pour les tests unitaires, basés sur JUnit. Dans un premier temps, il faut utiliser l'interface de marquage KoinTest afin de débloquer les fonctions suivantes :

  • get() & by inject() pour récupérer les instances depuis Koin ;
  • checkModules() pour vérifier les modules Koin (vérifie que chaque instance peut être résolue) ;
  • declareMock() pour déclarer un mock Mockito à la volée.

Nous pouvons écrire les tests suivants :

 
Sélectionnez
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.
31.
32.
class MySimplePresenterTest : KoinTest {

    val presenter: MySimplePresenter by inject()

    @Before
    fun before() {
        startKoin(listOf(appModule))
    }

    @After
    fun after() {
        stopKoin()
    }

    @Test
    fun `say hello`() {
        // reuse the lazy injected presenter
        assertTrue(presenter.sayHello().contains("Hello Koin"))
    }

    @Test
    fun `say hello with mock`() {
        declareMock<HelloRepository>()
        // retrieve the HelloRepository mock
        val mock = get<HelloRepository>()
        // retrieve actual presenter (injected with mock)
        val presenter = get<MySimplePresenter>()
        presenter.sayHello()

        verify(mock, times(1)).giveHello()
    }
}

III-C. Vérification des modules

La résolution des dépendances dans Koin est dynamique. Par conséquent, le graphe des dépendances est découvert à la volée. Pour résoudre cette problématique et éviter de découvrir des problèmes de dépendances au lancement de l'application, il est possible de vérifier l'intégralité des dépendances avec la fonction checkModules() :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
class CheckModulesTest : KoinTest {

    @Test
    fun checkAllModules() {
        checkModules(listOf(appModule))
    }

    @After
    fun close() {
        stopKoin()
    }
}

IV. Koin, prêt pour les ViewModel

Dernier point pour clore cette longue introduction à l'injection de dépendances Android avec Koin : la possibilité d'utiliser les ViewModel (Architecture Components) avec Koin.

IV-A. Gradle - koin-android-viewmodel

Ci-dessous la dépendance Gradle nécessaire :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
// Add Jcenter to your repositories if needed
repositories {
    jcenter()
}
dependencies {
    // Koin for Android - Scope feature
    // include koin-android-scope & koin-android
    implementation 'org.koin:koin-android-viewmodel:$koin_version'
}

IV-B. Notre classe ViewModel - MyViewModel

Écrivons une classe ViewModel qui va utiliser notre composant HelloRepository. Il suffit d'utiliser l'injection par constructeur pour récupérer notre dépendance depuis Koin :

 
Sélectionnez
1.
2.
3.
4.
class MyViewModel(val repo : HelloRepository) : ViewModel() {

    fun sayHello() = "${repo.giveHello()} from $this"
}

IV-C. Déclaration dans Koin

Le projet koin-android-viewmodel met à disposition un nouveau mot clé dans le DSL : viewModel.

Celui-ci permet de déclarer une instance de ViewModel, qui sera câblée avec le lifecycle du composant qui l'appelle.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
val appModule = module {

    // single instance of HelloRepository
    single<HelloRepository> { HelloRepositoryImpl() }

    // MyViewModel ViewModel
    viewModel { MyViewModel(get()) }
}

IV-D. Intégration avec Android

En activant le projet koin-android-viewmodel, nous débloquons les extensions suivantes :

  • by viewModel() ou getViewModel() pour injecter une instance de type ViewModel ;
  • by sharedViewModel() ou getSharedViewModel() pour utiliser/partager l'instance de ViewModel de l'activité parente.

Ci-dessous, la récupération de notre MyViewModel en une ligne :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
class MyViewModelActivity : AppCompatActivity() {
    // Lazy Inject ViewModel
    val myViewModel: MyViewModel by viewModel()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_simple)

        //...
    }
}

V. Conclusion

Si le projet Koin vous plaît, n'hésitez pas à aller voir la documentation sur https://insert-koin.io.

Vous pouvez suivre les dernières infos du projet sur Twitter @insert-koin_io ou sur Medium.

VI. Remerciements

Cet article a été publié avec l'aimable autorisation de Arnaud Giuliani.

Nous tenons à remercier escartefigue et f-leb pour sa relecture orthographique attentive de cet article et Mickael Baron pour la mise au gabarit.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2018 Arnaud Giuliani. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.