Skip to content

Instantly share code, notes, and snippets.

@chriscoomber
Last active February 22, 2021 07:04
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save chriscoomber/80a9e66dd4fba92e7480d29c925440e9 to your computer and use it in GitHub Desktop.
Save chriscoomber/80a9e66dd4fba92e7480d29c925440e9 to your computer and use it in GitHub Desktop.
Singletons with constructors in Kotlin
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