在Kotlin中具有参数的Singleton


75

我正在尝试将Android应用程序从Java转换为Kotlin。应用中有一些单例。对于没有构造函数参数的单例,我使用了一个伴随对象。还有另一个采用构造函数参数的单例。

Java代码:

public class TasksLocalDataSource implements TasksDataSource {

    private static TasksLocalDataSource INSTANCE;

    private TasksDbHelper mDbHelper;

    // Prevent direct instantiation.
    private TasksLocalDataSource(@NonNull Context context) {
        checkNotNull(context);
        mDbHelper = new TasksDbHelper(context);
    }

    public static TasksLocalDataSource getInstance(@NonNull Context context) {
        if (INSTANCE == null) {
            INSTANCE = new TasksLocalDataSource(context);
        }
        return INSTANCE;
    }
}

我在Kotlin中的解决方案:

class TasksLocalDataSource private constructor(context: Context) : TasksDataSource {

    private val mDbHelper: TasksDbHelper

    init {
        checkNotNull(context)
        mDbHelper = TasksDbHelper(context)
    }

    companion object {
        lateinit var INSTANCE: TasksLocalDataSource
        private val initialized = AtomicBoolean()

        fun getInstance(context: Context) : TasksLocalDataSource {
            if(initialized.getAndSet(true)) {
                INSTANCE = TasksLocalDataSource(context)
            }
            return INSTANCE
        }
    }
}

我有什么想念的吗?线程安全?懒惰?

有一些类似的问题,但我不喜欢答案:)


INSTANCE物业公开曝光于公众
面前

@miensol还有其他选项可以将参数(上下文)传递给伴随对象吗?
LordRaydenMK '16

1
将Context实例存储在全局单例对象(无论是Java还是Kotlin)中都会导致内存泄漏:stackoverflow.com/a/11908685/147024
yole 2016年

5
@yole如果是应用程序上下文是单例,则不是内存泄漏。
LordRaydenMK

1
@LordRaydenMK如果您在Android中使用Kotlin,我认为您的代码有4个改进。我做了一些解释。看看:gist.github.com/gaplo917/f186d5c541fbc0d6f77f9b720ec4694c
Gary LO

Answers:


123

这是使用该函数的Google体系结构组件示例代码的一种很好的替代方案also

class UsersDatabase : RoomDatabase() {

    companion object {

        @Volatile private var INSTANCE: UsersDatabase? = null

        fun getInstance(context: Context): UsersDatabase =
            INSTANCE ?: synchronized(this) {
                INSTANCE ?: buildDatabase(context).also { INSTANCE = it }
            }

        private fun buildDatabase(context: Context) =
            Room.databaseBuilder(context.applicationContext,
                    UsersDatabase::class.java, "Sample.db")
                    .build()
    }
}

1
我不确定为什么在同步块内有INSTANCE?:?因为仅当INSTANCE为null时才调用该块,所以为什么要检查INSTANCE为null还是不在那里?有人可以解释吗?
Sandip Fichadiya

8
@SandipSoni阅读有关双重检查锁定:stackoverflow.com/questions/18093735/...
kuhnroyal

2
我看到此代码有两个问题。1.您始终必须传递上下文(或定义的任何其他var),在您要检索Singleton时可能不可用。2.假设您在第二次使用时传递了不同的上下文。那是什么意思?您将仅获得旧的单例。对我来说似乎是一种反模式。
A1m

1
@ A1m关于2):第二次传递不同的上下文无关紧要,因为应用程序上下文用于构造数据库。Context您传递的对象仅用于检索应用程序上下文。
mfb

1
@MathiasBrandt在这种情况下可能是正确的,它Context本身应该是一个单例。但是总的来说,问题是关于创建此单例的模式。我觉得这种行为是不确定的并且具有误导性。
A1m

37

Thread-Safe SolutionWrite Once; Use Many;

创建一个实现单例逻辑的类是一个很好的解决方案,该类也包含单例实例,如下所示。

它使用同步块中的Double-Check Locking实例化实例,以消除在多线程环境中出现竞争状况的可能性。

SingletonHolder.kt

open class SingletonHolder<out T, in A>(private val constructor: (A) -> T) {

    @Volatile
    private var instance: T? = null

    fun getInstance(arg: A): T {
        return when {
            instance != null -> instance!!
            else -> synchronized(this) {
                if (instance == null) instance = constructor(arg)
                instance!!
            }
        }
    }
}

Usage

现在,在要成为单例的每个类中,编写一个companion object扩展上述类的类。SingletonHolder是一个通用类,它接受目标类的类型及其要求的参数作为通用参数。它还需要对用于实例化实例的目标类的构造函数的引用:

class MyManager private constructor(context: Context) {

    fun doSomething() {
        ...
    }

    companion object : SingletonHolder<MyManager, Context>(::MyManager)
}

最后:

MyManager.getInstance(context).doSomething()

2
假设MyManger需要多个构造函数参数,如何使用SingletonHolder处理它
Abhi Muktheeswarar

@AbhiMuktheeswarar:与SingletonHolder通用类一样,不幸的是,其通用类型无法在运行时动态定义,可能对于每个输入参数的计数,我们都应定义一个这样的类:hastebin.com/avunuwiviv.m
Aminography

如何为此类创建测试用例?
Jimit Patel

@AbhiMuktheeswarar为A使用对。然后:伴侣对象:SingletonHolder <MyManager,Pair <A,B >> {MyManager(it.first,it.second)}
Dan Brough

21

我不完全确定您为什么需要这样的代码,但是这是我的最佳选择:

class TasksLocalDataSource private constructor(context: Context) : TasksDataSource {
    private val mDbHelper = TasksDbHelper(context)

    companion object {
        private var instance : TasksLocalDataSource? = null

        fun  getInstance(context: Context): TasksLocalDataSource {
            if (instance == null)  // NOT thread safe!
                instance = TasksLocalDataSource(context)

            return instance!!
        }
    }
}

这类似于您编写的内容,并且具有相同的API。

一些注意事项:

  • 不要lateinit在这里使用。它有不同的用途,在这里,可为空的变量是理想的。

  • 怎么checkNotNull(context)办?context在这里永远不会为空,这是Kotlin保证的。编译器已经实现了所有检查和断言。

更新:

如果您需要的TasksLocalDataSource只是class的延迟初始化实例,则只需使用一堆惰性属性(在对象内部或在程序包级别):

val context = ....

val dataSource by lazy {
    TasksLocalDataSource(context)
}

正是我要找的东西...关于此代码的用法...我试图将github.com/googlesamples/android-architecture转换为Kotlin,以使我对Kotlin有所了解并比较代码。这就是为什么我不想包含Dagger或github.com/SalomonBrys/Kodein
LordRaydenMK

1
@LordRaydenMK恕我直言,不要通过进行Java转换来开始学习Kotlin。一次阅读官方文档,尝试从头开始做一个完全等效的Kotlin实现,但不要进行Java转换。“不要用Java方式编码Kotlin”的概念非常重要,因为在大多数情况下,您不需要Java(著名的编码模式)(因为它是为Java设计的)。
加里LO

1
@GaryLO我在try.kotlinglang.org上做了koans。正如Hadi Hariri(twitter.com/hhariri)在某会议的演讲中说的(现在找不到链接)... Kotlin比起没有Kotlin更好。我在这里写的原因是尝试以Kotlin方式进行操作。谢谢。
LordRaydenMK '16

2
请注意,此解决方案不是线程安全的。如果多个线程尝试同时访问它,则可能存在两个单例实例并进行初始化。
BladeCoder

1
@musooffTasksLocalDataSource(context)是构造函数,因此它会返回一个对象或引发异常。
voddan

4

您可以声明Kotlin对象,重载“ invoke”运算符

object TasksLocalDataSource: TasksDataSource {
    private lateinit var mDbHelper: TasksDbHelper

    operator fun invoke(context: Context): TasksLocalDataSource {
        this.mDbHelper = TasksDbHelper(context)
        return this
    }
}

无论如何,我认为您应该将TasksDbHelper注入TasksLocalDataSource而不是注入上下文


此方法是最简单的方法,并且invoke作为将转换为函数的方法,对于函数来说是一个很好的用例。我在Coinverse应用程序的存储库中使用了这种模式。
亚当·赫维兹

但这是线程安全的吗?我看到很多人对此表示赞赏,这实际上是从这本2yo流行的中等帖子中借用的,正如作者所说,该帖子也是从Kotlin借用“懒惰”来源本身借来的。这个答案看起来更简洁(我讨厌添加样板工具文件,更不用说类了),但是我在多线程场景中使用这个单例很烦恼。
leRobot

4

如果您想以更简单的方式将参数传递给单例,我认为这是更好和更短的方法

object SingletonConfig {

private var retrofit: Retrofit? = null
private const val URL_BASE = "https://jsonplaceholder.typicode.com/"

fun Service(context: Context): Retrofit? {
    if (retrofit == null) {
        retrofit = Retrofit.Builder().baseUrl(URL_BASE)
                .addConverterFactory(GsonConverterFactory.create())
                .build()
    }
    return retrofit
}

}

你以这种简单的方式来称呼它

val api = SingletonConfig.Service(this)?.create(Api::class.java)

您的方法正在填充改造对象,但其返回的可为空值。这是令人困惑和错误的
Saman Sattari

3

该方法synchronized()在通用标准库中被标记为已弃用,因此可以选择以下方法:

class MySingleton private constructor(private val param: String) {

    companion object {
        @Volatile
        private var INSTANCE: MySingleton? = null

        @Synchronized
        fun getInstance(param: String): MySingleton = INSTANCE ?: MySingleton(param).also { INSTANCE = it }
    }
}

为此的导入是kotlin.jvm.Synchronized。所以我想它不适用于多平台吗?
tonisives

1
class CarsRepository(private val iDummyCarsDataSource: IDummyCarsDataSource) {

    companion object {
        private var INSTANCE: CarsRepository? = null
        fun getInstance(iDummyCarsDataSource: IDummyCarsDataSource): CarsRepository {
            if (INSTANCE == null) {
                INSTANCE = CarsRepository(
                    iDummyCarsDataSource = iDummyCarsDataSource)
            }
            return INSTANCE as CarsRepository
        }
    }

}


1

如果要查找具有多个自变量的基本SingletonHolder类。我已经创建了SingletonHolder泛型类,该类仅支持创建具有一个参数,两个参数和三个参数的Singleton类的一个实例。

在这里链接基类的Github

非参数(Kotlin的默认值):

object AppRepository 

一个参数(来自上面链接中的示例代码):

class AppRepository private constructor(private val db: Database) {
    companion object : SingleArgSingletonHolder<AppRepository, Database>(::AppRepository)
}
// Use
val appRepository =  AppRepository.getInstance(db)

两个参数:

class AppRepository private constructor(private val db: Database, private val apiService: ApiService) {
    companion object : PairArgsSingletonHolder<AppRepository, Database, ApiService>(::AppRepository)
}
// Use
val appRepository =  AppRepository.getInstance(db, apiService)

三个论点:

class AppRepository private constructor(
   private val db: Database,
   private val apiService: ApiService,
   private val storage : Storage
) {
   companion object : TripleArgsSingletonHolder<AppRepository, Database, ApiService, Storage>(::AppRepository)
}
// Use
val appRepository =  AppRepository.getInstance(db, apiService, storage)

超过3个参数:

为了实现这种情况,我建议创建一个配置对象以传递给单例构造函数。


0

懒惰的解决方案

class LateInitLazy<T>(private var initializer: (() -> T)? = null) {

    val lazy = lazy { checkNotNull(initializer) { "lazy not initialized" }() }

    fun initOnce(factory: () -> T) {
        initializer = factory
        lazy.value
        initializer = null
    }
}

val myProxy = LateInitLazy<String>()
val myValue by myProxy.lazy

println(myValue) // error: java.lang.IllegalStateException: lazy not inited

myProxy.initOnce { "Hello World" }
println(myValue) // OK: output Hello World

myProxy.initOnce { "Never changed" } // no effect
println(myValue) // OK: output Hello World

0

我看到了所有答案。我知道这是一个重复的答案,但是如果我们在方法声明中使用synced关键字,它将把整个方法同步到对象或类。并且尚未弃用同步块。

您可以使用以下实用程序类来获取单例行为。

open class SingletonWithContextCreator<out T : Any>(val creator: (Context) -> T) {
    @Volatile
    private var instance: T? = null

    fun with(context: Context): T = instance ?: synchronized(this) {
        instance ?: creator(context).apply { instance = this }
    }
}

您可以扩展上述类,使其成为单例。

在您的情况下,以下是使TasksLocalDataSource类单例的代码。

companion object : SingletonWithContextCreator<TasksDataSource>(::TasksLocalDataSource)

-10
Singletons

单例经常被使用,以创建它们的简单方法。Kotlin使用对象表示法,而不是通常的静态实例,getInstance()方法和私有构造函数。为了保持一致性,对象符号还用于定义静态方法。

 object CommonApiConfig {
private var commonApiConfig: CommonApiConfig? = null
fun getInstance(): CommonApiConfig {
    if (null == commonApiConfig) {
        commonApiConfig = CommonApiConfig
       }
    return CommonApiConfig.commonApiConfig!!
   }
}

3
不回答问题。问题是当必须在构造函数中传递参数时如何创建单例。
LordRaydenMK '17
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.