Use app scoped CoroutineScope (#3403)

This commit is contained in:
AntsyLich
2026-06-12 17:14:29 +06:00
committed by GitHub
parent 63a71fa447
commit 509eee5dfb
17 changed files with 91 additions and 80 deletions
@@ -19,6 +19,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
@@ -53,6 +54,7 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
/**
@@ -63,17 +65,16 @@ import kotlin.time.Duration.Companion.seconds
*/
class DownloadCache(
private val context: Context,
private val scope: CoroutineScope,
private val provider: DownloadProvider = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(),
private val extensionManager: ExtensionManager = Injekt.get(),
private val storageManager: StorageManager = Injekt.get(),
) {
private val scope = CoroutineScope(Dispatchers.IO)
private val _changes: Channel<Unit> = Channel(Channel.UNLIMITED)
val changes = _changes.receiveAsFlow()
.onStart { emit(Unit) }
.flowOn(Dispatchers.IO)
.shareIn(scope, SharingStarted.Lazily, 1)
/**
@@ -101,7 +102,7 @@ class DownloadCache(
init {
// Attempt to read cache file
scope.launch {
scope.launchIO {
rootDownloadsDirMutex.withLock {
try {
if (diskCacheFile.exists()) {
@@ -4,6 +4,7 @@ import android.content.Context
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.Page
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.drop
@@ -35,6 +36,7 @@ import uy.kohesive.injekt.api.get
*/
class DownloadManager(
private val context: Context,
private val scope: CoroutineScope,
private val provider: DownloadProvider = Injekt.get(),
private val cache: DownloadCache = Injekt.get(),
private val getCategories: GetCategories = Injekt.get(),
@@ -45,7 +47,7 @@ class DownloadManager(
/**
* Downloader whose only task is to download chapters.
*/
private val downloader = Downloader(context, provider, cache)
private val downloader = Downloader(context, provider, cache, scope)
val isRunning: Boolean
get() = downloader.isRunning
@@ -221,7 +223,7 @@ class DownloadManager(
* @param source the source of the chapters.
*/
fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source) {
launchIO {
scope.launchIO {
val filteredChapters = getChaptersToDelete(chapters, manga)
if (filteredChapters.isEmpty()) {
return@launchIO
@@ -248,7 +250,7 @@ class DownloadManager(
* @param removeQueued whether to also remove queued downloads.
*/
fun deleteManga(manga: Manga, source: Source, removeQueued: Boolean = true) {
launchIO {
scope.launchIO {
if (removeQueued) {
downloader.removeFromQueue(manga)
}
@@ -44,7 +44,6 @@ import okhttp3.Response
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.storage.extension
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.launchNow
import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.core.common.util.system.ImageUtil
import tachiyomi.core.common.util.system.logcat
@@ -71,6 +70,7 @@ class Downloader(
private val context: Context,
private val provider: DownloadProvider,
private val cache: DownloadCache,
private val scope: CoroutineScope,
private val sourceManager: SourceManager = Injekt.get(),
private val chapterCache: ChapterCache = Injekt.get(),
private val downloadPreferences: DownloadPreferences = Injekt.get(),
@@ -95,7 +95,6 @@ class Downloader(
*/
private val notifier by lazy { DownloadNotifier(context) }
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private var downloaderJob: Job? = null
/**
@@ -111,7 +110,7 @@ class Downloader(
var isPaused: Boolean = false
init {
launchNow {
scope.launch {
val chapters = async { store.restore() }
addAllToQueue(chapters.await())
}
@@ -190,7 +189,7 @@ class Downloader(
private fun launchDownloaderJob() {
if (isRunning) return
downloaderJob = scope.launch {
downloaderJob = scope.launchIO {
val activeDownloadsFlow = combine(
queueState,
downloadPreferences.parallelSourceLimit.changes(),
@@ -28,6 +28,8 @@ import eu.kanade.tachiyomi.util.system.cancelNotification
import eu.kanade.tachiyomi.util.system.getBitmapOrNull
import eu.kanade.tachiyomi.util.system.notificationBuilder
import eu.kanade.tachiyomi.util.system.notify
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import tachiyomi.core.common.Constants
import tachiyomi.core.common.i18n.pluralStringResource
import tachiyomi.core.common.i18n.stringResource
@@ -44,7 +46,7 @@ import java.text.NumberFormat
class LibraryUpdateNotifier(
private val context: Context,
private val scope: CoroutineScope = Injekt.get(),
private val securityPreferences: SecurityPreferences = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(),
) {
@@ -209,7 +211,7 @@ class LibraryUpdateNotifier(
// Per-manga notification
if (!securityPreferences.hideNotificationContent.get()) {
launchUI {
scope.launch {
context.notify(
updates.map { (manga, chapters) ->
NotificationManagerCompat.NotificationWithIdAndTag(
@@ -6,6 +6,8 @@ import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.core.net.toUri
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.lifecycleScope
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
@@ -17,6 +19,10 @@ import eu.kanade.tachiyomi.util.system.getParcelableExtraCompat
import eu.kanade.tachiyomi.util.system.notificationManager
import eu.kanade.tachiyomi.util.system.toShareIntent
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import tachiyomi.core.common.Constants
import tachiyomi.core.common.util.lang.launchIO
@@ -32,6 +38,8 @@ import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
/**
@@ -45,6 +53,7 @@ class NotificationReceiver : BroadcastReceiver() {
private val getChapter: GetChapter by injectLazy()
private val updateChapter: UpdateChapter by injectLazy()
private val downloadManager: DownloadManager by injectLazy()
private val scope: CoroutineScope by injectLazy()
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
@@ -197,7 +206,7 @@ class NotificationReceiver : BroadcastReceiver() {
val downloadPreferences: DownloadPreferences = Injekt.get()
val sourceManager: SourceManager = Injekt.get()
launchIO {
async(scope, Dispatchers.IO) {
val toUpdate = chapterUrls.mapNotNull { getChapter.await(it, mangaId) }
.map {
val chapter = it.copy(read = true)
@@ -223,13 +232,27 @@ class NotificationReceiver : BroadcastReceiver() {
* @param mangaId id of manga
*/
private fun downloadChapters(chapterUrls: Array<String>, mangaId: Long) {
launchIO {
val manga = getManga.await(mangaId) ?: return@launchIO
async(scope, Dispatchers.IO) {
val manga = getManga.await(mangaId) ?: return@async
val chapters = chapterUrls.mapNotNull { getChapter.await(it, mangaId) }
downloadManager.downloadChapters(manga, chapters)
}
}
private fun BroadcastReceiver.async(
scope: CoroutineScope,
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit,
) {
val result = goAsync()
try {
scope.launch(context, start, block)
} finally {
result.finish()
}
}
companion object {
private const val NAME = "NotificationReceiver"
@@ -2,6 +2,8 @@ package eu.kanade.tachiyomi.di
import android.app.Application
import androidx.core.content.ContextCompat
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
import app.cash.sqldelight.db.SqlDriver
import com.eygraber.sqldelight.androidx.driver.AndroidxSqliteConfiguration
@@ -20,6 +22,9 @@ import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.network.JavaScriptEngine
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.AndroidSourceManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.plus
import kotlinx.serialization.json.Json
import kotlinx.serialization.protobuf.ProtoBuf
import nl.adaptivity.xmlutil.XmlDeclMode
@@ -101,18 +106,20 @@ class AppModule(val app: Application) : InjektModule {
ProtoBuf
}
addSingletonFactory<CoroutineScope> { ProcessLifecycleOwner.get().lifecycleScope + SupervisorJob() }
addSingletonFactory { ChapterCache(app, get()) }
addSingletonFactory { CoverCache(app) }
addSingletonFactory { NetworkHelper(app, get()) }
addSingletonFactory { NetworkHelper(app, get(), get()) }
addSingletonFactory { JavaScriptEngine(app) }
addSingletonFactory<SourceManager> { AndroidSourceManager(app, get(), get()) }
addSingletonFactory { ExtensionManager(app) }
addSingletonFactory<SourceManager> { AndroidSourceManager(app, get(), get(), get()) }
addSingletonFactory { ExtensionManager(app, get()) }
addSingletonFactory { DownloadProvider(app) }
addSingletonFactory { DownloadManager(app) }
addSingletonFactory { DownloadCache(app) }
addSingletonFactory { DownloadManager(app, get()) }
addSingletonFactory { DownloadCache(app, get()) }
addSingletonFactory { TrackerManager() }
addSingletonFactory { DelayedTrackingStore(app) }
@@ -122,7 +129,7 @@ class AppModule(val app: Application) : InjektModule {
addSingletonFactory { AndroidStorageFolderProvider(app) }
addSingletonFactory { LocalSourceFileSystem(get()) }
addSingletonFactory { LocalCoverManager(app, get()) }
addSingletonFactory { StorageManager(app, get()) }
addSingletonFactory { StorageManager(app, get(), get()) }
// Asynchronously init expensive components for a faster cold start
ContextCompat.getMainExecutor(app).execute {
@@ -41,12 +41,10 @@ import java.util.Locale
*/
class ExtensionManager(
private val context: Context,
private val scope: CoroutineScope,
private val preferences: SourcePreferences = Injekt.get(),
private val trustExtension: TrustExtension = Injekt.get(),
) {
val scope = CoroutineScope(SupervisorJob())
private val _isInitialized = MutableStateFlow(false)
val isInitialized: StateFlow<Boolean> = _isInitialized.asStateFlow()
@@ -58,7 +56,7 @@ class ExtensionManager(
/**
* The installer which installs, updates and uninstalls the extensions.
*/
private val installer by lazy { ExtensionInstaller(context) }
private val installer by lazy { ExtensionInstaller(context, scope) }
private val iconMap = mutableMapOf<String, Drawable>()
@@ -22,6 +22,7 @@ import kotlinx.coroutines.launch
import logcat.LogPriority
import okhttp3.OkHttpClient
import okhttp3.Request
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.system.logcat
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@@ -34,9 +35,8 @@ import java.io.File
*/
internal class ExtensionInstaller(
private val context: Context,
private val scope: CoroutineScope,
) {
private val scope = CoroutineScope(Dispatchers.IO)
private val activeJobs = mutableMapOf<String, Job>()
private val activeSteps = mutableMapOf<Long, MutableStateFlow<InstallStep>>()
private val extensionInstaller = Injekt.get<BasePreferences>().extensionInstaller
@@ -57,7 +57,7 @@ internal class ExtensionInstaller(
val step = MutableStateFlow(InstallStep.Pending)
activeSteps[downloadId] = step
val job = scope.launch {
val job = scope.launchIO {
val tmpFile = File(context.cacheDir, "extension_${extension.pkgName}.apk")
try {
step.value = InstallStep.Downloading
@@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.domain.source.model.StubSource
import tachiyomi.domain.source.repository.StubSourceRepository
import tachiyomi.domain.source.service.SourceManager
@@ -26,6 +27,7 @@ import java.util.concurrent.ConcurrentHashMap
class AndroidSourceManager(
private val context: Context,
private val scope: CoroutineScope,
private val extensionManager: ExtensionManager,
private val sourceRepository: StubSourceRepository,
) : SourceManager {
@@ -35,8 +37,6 @@ class AndroidSourceManager(
private val downloadManager: DownloadManager by injectLazy()
private val scope = CoroutineScope(Job() + Dispatchers.IO)
private val sourcesMapFlow = MutableStateFlow(ConcurrentHashMap<Long, Source>())
private val stubSourcesMap = ConcurrentHashMap<Long, StubSource>()
@@ -46,7 +46,7 @@ class AndroidSourceManager(
}
init {
scope.launch {
scope.launchIO {
extensionManager.installedExtensionsFlow
.collectLatest { extensions ->
val mutableMap = ConcurrentHashMap<Long, Source>(
@@ -69,7 +69,7 @@ class AndroidSourceManager(
}
}
scope.launch {
scope.launchIO {
sourceRepository.subscribeAll()
.collectLatest { sources ->
val mutableMap = stubSourcesMap.toMutableMap()
@@ -100,9 +100,9 @@ class AndroidSourceManager(
}
private fun registerStubSource(source: StubSource) {
scope.launch {
scope.launchIO {
val dbSource = sourceRepository.getStubSource(source.id)
if (dbSource == source) return@launch
if (dbSource == source) return@launchIO
sourceRepository.upsertStubSource(source.id, source.lang, source.name)
if (dbSource != null) {
downloadManager.renameSource(dbSource, source)
@@ -286,7 +286,7 @@ class ReaderViewModel @JvmOverloads constructor(
val context = Injekt.get<Application>()
val source = sourceManager.getOrStub(manga.source)
loader = ChapterLoader(context, downloadManager, downloadProvider, manga, source)
loader = ChapterLoader(context, viewModelScope, downloadManager, downloadProvider, manga, source)
loadChapter(loader!!, chapterList.first { chapterId == it.chapter.id })
Result.success(true)
@@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.data.download.DownloadProvider
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import kotlinx.coroutines.CoroutineScope
import mihon.core.archive.archiveReader
import mihon.core.archive.epubReader
import tachiyomi.core.common.i18n.stringResource
@@ -22,6 +23,7 @@ import tachiyomi.source.local.io.Format
*/
class ChapterLoader(
private val context: Context,
private val scope: CoroutineScope,
private val downloadManager: DownloadManager,
private val downloadProvider: DownloadProvider,
private val manga: Manga,
@@ -100,7 +102,7 @@ class ChapterLoader(
is Format.Epub -> EpubPageLoader(format.file.epubReader(context))
}
}
source is HttpSource -> HttpPageLoader(chapter, source)
source is HttpSource -> HttpPageLoader(chapter, source, scope)
source is StubSource -> error(context.stringResource(MR.strings.source_not_installed, source.toString()))
else -> error(context.stringResource(MR.strings.loader_not_implemented_error))
}
@@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.ui.reader.loader
import android.provider.Settings
import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.database.models.toDomainChapter
import eu.kanade.tachiyomi.source.model.Page
@@ -8,7 +9,9 @@ import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.filter
@@ -31,11 +34,10 @@ import kotlin.math.min
internal class HttpPageLoader(
private val chapter: ReaderChapter,
private val source: HttpSource,
scope: CoroutineScope,
private val chapterCache: ChapterCache = Injekt.get(),
) : PageLoader() {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
/**
* A queue used to manage requests one by one while allowing priorities.
*/
@@ -124,14 +126,14 @@ internal class HttpPageLoader(
queue.offer(PriorityPage(page, PriorityPage.RETRY))
}
@OptIn(DelicateCoroutinesApi::class)
override fun recycle() {
super.recycle()
scope.cancel()
queue.clear()
// Cache current page list progress for online chapters to allow a faster reopen
chapter.pages?.let { pages ->
launchIO {
GlobalScope.launchIO {
try {
// Convert to pages without reader information
val pagesToSave = pages.map { Page(it.index, it.url, it.imageUrl) }
@@ -5,6 +5,7 @@ import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor
import eu.kanade.tachiyomi.network.interceptor.IgnoreGzipInterceptor
import eu.kanade.tachiyomi.network.interceptor.UncaughtExceptionInterceptor
import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor
import kotlinx.coroutines.CoroutineScope
import okhttp3.Cache
import okhttp3.OkHttpClient
import okhttp3.brotli.BrotliInterceptor
@@ -15,6 +16,7 @@ import java.util.concurrent.TimeUnit
class NetworkHelper(
private val context: Context,
private val preferences: NetworkPreferences,
scope: CoroutineScope,
) {
val cookieJar = AndroidCookieJar()
@@ -64,7 +66,7 @@ class NetworkHelper(
val client = clientBuilder
.addInterceptor(
CloudflareInterceptor(context, cookieJar, ::defaultUserAgentProvider),
CloudflareInterceptor(context, cookieJar, scope, ::defaultUserAgentProvider),
)
.build()
@@ -11,6 +11,7 @@ import androidx.core.content.ContextCompat
import eu.kanade.tachiyomi.network.AndroidCookieJar
import eu.kanade.tachiyomi.util.system.isOutdated
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.CoroutineScope
import okhttp3.Cookie
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
@@ -25,8 +26,9 @@ import java.util.concurrent.CountDownLatch
class CloudflareInterceptor(
private val context: Context,
private val cookieManager: AndroidCookieJar,
scope: CoroutineScope,
defaultUserAgentProvider: () -> String,
) : WebViewInterceptor(context, defaultUserAgentProvider) {
) : WebViewInterceptor(context, scope, defaultUserAgentProvider) {
private val executor = ContextCompat.getMainExecutor(context)
@@ -9,6 +9,8 @@ import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.WebViewUtil
import eu.kanade.tachiyomi.util.system.setDefaultSettings
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import okhttp3.Headers
import okhttp3.Interceptor
import okhttp3.Request
@@ -21,6 +23,7 @@ import java.util.concurrent.TimeUnit
abstract class WebViewInterceptor(
private val context: Context,
private val scope: CoroutineScope,
private val defaultUserAgentProvider: () -> String,
) : Interceptor {
@@ -56,7 +59,7 @@ abstract class WebViewInterceptor(
}
if (!WebViewUtil.supportsWebView(context)) {
launchUI {
scope.launch {
context.toast(MR.strings.information_webview_required, Toast.LENGTH_LONG)
}
return response
@@ -10,39 +10,6 @@ import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/**
* Think twice before using this. This is a delicate API. It is easy to accidentally create resource or memory leaks when GlobalScope is used.
*
* **Possible replacements**
* - suspend function
* - custom scope like view or presenter scope
*/
@DelicateCoroutinesApi
fun launchUI(block: suspend CoroutineScope.() -> Unit): Job =
GlobalScope.launch(Dispatchers.Main, CoroutineStart.DEFAULT, block)
/**
* Think twice before using this. This is a delicate API. It is easy to accidentally create resource or memory leaks when GlobalScope is used.
*
* **Possible replacements**
* - suspend function
* - custom scope like view or presenter scope
*/
@DelicateCoroutinesApi
fun launchIO(block: suspend CoroutineScope.() -> Unit): Job =
GlobalScope.launch(Dispatchers.IO, CoroutineStart.DEFAULT, block)
/**
* Think twice before using this. This is a delicate API. It is easy to accidentally create resource or memory leaks when GlobalScope is used.
*
* **Possible replacements**
* - suspend function
* - custom scope like view or presenter scope
*/
@DelicateCoroutinesApi
fun launchNow(block: suspend CoroutineScope.() -> Unit): Job =
GlobalScope.launch(Dispatchers.Main, CoroutineStart.UNDISPATCHED, block)
fun CoroutineScope.launchUI(block: suspend CoroutineScope.() -> Unit): Job =
launch(Dispatchers.Main, block = block)
@@ -10,6 +10,7 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow
@@ -17,15 +18,14 @@ import kotlinx.coroutines.flow.shareIn
class StorageManager(
private val context: Context,
scope: CoroutineScope,
storagePreferences: StoragePreferences,
) {
private val scope = CoroutineScope(Dispatchers.IO)
private var baseDir: UniFile? = getBaseDir(storagePreferences.baseStorageDirectory.get())
private val _changes: Channel<Unit> = Channel(Channel.UNLIMITED)
val changes = _changes.receiveAsFlow()
.flowOn(Dispatchers.IO)
.shareIn(scope, SharingStarted.Lazily, 1)
init {
@@ -43,6 +43,7 @@ class StorageManager(
}
_changes.send(Unit)
}
.flowOn(Dispatchers.IO)
.launchIn(scope)
}