Last active
February 22, 2021 07:04
-
-
Save chriscoomber/80a9e66dd4fba92e7480d29c925440e9 to your computer and use it in GitHub Desktop.
Singletons with constructors in Kotlin
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.example.app.utils | |
import org.jetbrains.annotations.TestOnly | |
/** | |
* Generic singleton implementation. | |
* | |
* # Usage | |
* | |
* First, defining your singleton: | |
* | |
* ``` | |
* import com.example.app.utils.SingletonBase | |
* | |
* class Example private constructor(args: Args) { | |
* data class Args(val x: Int, val y: Int) | |
* object Singleton : SingletonBase<Example, Args>(::Example) | |
* | |
* // Now define whatever your singleton does | |
* fun doAThing() {} | |
* } | |
* ``` | |
* | |
* Using your singleton: | |
* | |
* ``` | |
* val example: Example = Example.Singleton.getInstanceOrCreate(Example.Args(1, 2)) | |
* example.doAThing() | |
* ``` | |
* | |
* Test usage (using [mockk](https://github.com/mockk/mockk) as an example): | |
* | |
* ``` | |
* val mock: Example = mockk<Example>() | |
* Example.Singleton.injectMock { mock } | |
* // Run test, calls to getInstanceOrCreate will receive the mocked version. | |
* ``` | |
*/ | |
// FIXME: ideally T should be marked as covariant (out) and A should be marked as contravariant (in) | |
// for greater generality, however this causes problems with the `injectMock` function. I don't know | |
// the solution. | |
open class SingletonBase<T, A>(private val constructor: (A) -> T) { | |
@Volatile | |
private var instance: T? = null | |
private var mockConstructor: ((A) -> T)? = null | |
/** | |
* Create a new instance of the single with the provided arguments, replacing any that might | |
* have previously existed. | |
* @param args: Arguments used to construct instance. | |
*/ | |
fun createInstance(args: A): T { | |
val newInstance = mockConstructor?.invoke(args) ?: constructor(args) | |
instance = newInstance | |
return newInstance | |
} | |
/** | |
* Get instance of singleton if it exists, otherwise return null. | |
*/ | |
fun getInstanceOrNull(): T? = instance | |
/** | |
* Get instance of singleton if it exists, otherwise create it with the provided arguments. | |
* @param args: Arguments used to construct instance. | |
*/ | |
fun getInstanceOrCreate(args: A): T = instance ?: synchronized(this) { | |
instance ?: createInstance(args) | |
} | |
/** | |
* Inject a mock, so that future calls to [getInstanceOrCreate] or [createInstance] will use the | |
* mock constructor. | |
*/ | |
@TestOnly | |
protected fun injectMock(mockConstructor: (A) -> T) { | |
this.mockConstructor = mockConstructor | |
} | |
/** | |
* Reset injected mocks. | |
*/ | |
@TestOnly | |
protected fun resetMock() { | |
this.mockConstructor = null | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment