Resolve Injekt dependencies directly from the Metro graph (#3965)

The previous setup had a circular dependency between Injekt
registration and extension loading.

In regard to #3931, I've moved the culprit ExtensionManager out of
Injekt altogether, alongside CoverCache, PreferenceStore and
TrackPreferences, none of which a source needs.
This commit is contained in:
AntsyLich
2026-09-15 15:57:48 +06:00
committed by GitHub
parent b2cda03e3a
commit 1c43addeca
7 changed files with 114 additions and 68 deletions
@@ -72,7 +72,7 @@ fun Manga.copyFrom(other: SManga): Manga {
) )
} }
fun Manga.hasCustomCover(coverCache: CoverCache = Injekt.get()): Boolean { fun Manga.hasCustomCover(coverCache: CoverCache = Injekt.get<Context>().appGraph.coverCache): Boolean {
return coverCache.getCustomCoverFile(id).exists() return coverCache.getCustomCoverFile(id).exists()
} }
@@ -40,7 +40,6 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.domain.ui.model.AppTheme import eu.kanade.domain.ui.model.AppTheme
import eu.kanade.presentation.manga.components.MangaCover import eu.kanade.presentation.manga.components.MangaCover
import eu.kanade.presentation.theme.TachiyomiTheme import eu.kanade.presentation.theme.TachiyomiTheme
@@ -48,13 +47,10 @@ import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.isDynamicColorAvailable import eu.kanade.tachiyomi.util.system.isDynamicColorAvailable
import mihon.icons.materialsymbols.MaterialSymbols import mihon.icons.materialsymbols.MaterialSymbols
import mihon.icons.materialsymbols.roundedfilled.CheckCircle import mihon.icons.materialsymbols.roundedfilled.CheckCircle
import tachiyomi.core.common.preference.InMemoryPreferenceStore
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.secondaryItemAlpha import tachiyomi.presentation.core.util.secondaryItemAlpha
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.fullType
@Composable @Composable
internal fun AppThemePreferenceWidget( internal fun AppThemePreferenceWidget(
@@ -261,7 +257,6 @@ fun AppThemePreviewItem(
@Composable @Composable
private fun AppThemesListPreview() { private fun AppThemesListPreview() {
var appTheme by remember { mutableStateOf(AppTheme.DEFAULT) } var appTheme by remember { mutableStateOf(AppTheme.DEFAULT) }
Injekt.addSingleton(fullType<UiPreferences>(), UiPreferences(InMemoryPreferenceStore()))
TachiyomiTheme(appTheme = appTheme) { TachiyomiTheme(appTheme = appTheme) {
Surface { Surface {
AppThemesList( AppThemesList(
+3 -11
View File
@@ -52,7 +52,7 @@ import logcat.AndroidLogcatLogger
import logcat.LogPriority import logcat.LogPriority
import logcat.LogcatLogger import logcat.LogcatLogger
import mihon.app.di.AppGraph import mihon.app.di.AppGraph
import mihon.app.di.injekt.MetroInteropModule import mihon.app.di.injekt.MetroInjektRegistrar
import mihon.core.metro.GraphProvider import mihon.core.metro.GraphProvider
import mihon.core.migration.Migration import mihon.core.migration.Migration
import mihon.core.migration.Migrator import mihon.core.migration.Migrator
@@ -66,7 +66,7 @@ import tachiyomi.domain.source.service.SourceManager
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.widget.WidgetManager import tachiyomi.presentation.widget.WidgetManager
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.addSingleton import uy.kohesive.injekt.api.InjektScope
import java.security.Security import java.security.Security
class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factory, GraphProvider<AppGraph> { class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factory, GraphProvider<AppGraph> {
@@ -93,8 +93,6 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
@Inject private lateinit var widgetManager: WidgetManager @Inject private lateinit var widgetManager: WidgetManager
@Inject private lateinit var injektMetroInteropModule: MetroInteropModule
@Inject private lateinit var migrations: Set<Migration> @Inject private lateinit var migrations: Set<Migration>
private val disableIncognitoReceiver = DisableIncognitoReceiver() private val disableIncognitoReceiver = DisableIncognitoReceiver()
@@ -110,8 +108,8 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
if (packageName != process) WebView.setDataDirectorySuffix(process) if (packageName != process) WebView.setDataDirectorySuffix(process)
} }
Injekt = InjektScope(MetroInjektRegistrar(application = this, graphProvider = this))
graph.inject(this) graph.inject(this)
setupInjekt()
TelemetryConfig.init(applicationContext) TelemetryConfig.init(applicationContext)
@@ -185,12 +183,6 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
initializeMigrator() initializeMigrator()
} }
private fun setupInjekt() {
Injekt.addSingleton<Application>(this)
Injekt.addSingleton<Context>(this)
Injekt.importModule(injektMetroInteropModule)
}
private fun initializeMigrator() { private fun initializeMigrator() {
val preference = preferenceStore.getInt(Preference.appStateKey("last_version_code"), 0) val preference = preferenceStore.getInt(Preference.appStateKey("last_version_code"), 0)
logcat { logcat {
@@ -5,7 +5,7 @@ import android.net.Uri
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.model.InstallStep import eu.kanade.tachiyomi.extension.model.InstallStep
import uy.kohesive.injekt.injectLazy import mihon.app.di.appGraph
import java.util.Collections import java.util.Collections
import java.util.concurrent.CopyOnWriteArraySet import java.util.concurrent.CopyOnWriteArraySet
import kotlin.concurrent.atomics.AtomicReference import kotlin.concurrent.atomics.AtomicReference
@@ -17,7 +17,7 @@ import kotlin.concurrent.atomics.ExperimentalAtomicApi
@OptIn(ExperimentalAtomicApi::class) @OptIn(ExperimentalAtomicApi::class)
abstract class Installer(private val service: Service) { abstract class Installer(private val service: Service) {
private val extensionManager: ExtensionManager by injectLazy() private val extensionManager: ExtensionManager by lazy { service.appGraph.extensionManager }
private var waitingInstall = AtomicReference<Entry?>(null) private var waitingInstall = AtomicReference<Entry?>(null)
private val queue = Collections.synchronizedList(mutableListOf<Entry>()) private val queue = Collections.synchronizedList(mutableListOf<Entry>())
@@ -19,6 +19,7 @@ import eu.kanade.tachiyomi.core.security.SecurityPreferences
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
import eu.kanade.tachiyomi.data.cache.ChapterCache import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.download.DownloadCache import eu.kanade.tachiyomi.data.download.DownloadCache
import eu.kanade.tachiyomi.data.download.DownloadJob import eu.kanade.tachiyomi.data.download.DownloadJob
import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager
@@ -29,6 +30,7 @@ import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.data.updater.AppUpdateChecker import eu.kanade.tachiyomi.data.updater.AppUpdateChecker
import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.util.ExtensionInstallActivity import eu.kanade.tachiyomi.extension.util.ExtensionInstallActivity
import eu.kanade.tachiyomi.network.JavaScriptEngine
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.NetworkPreferences import eu.kanade.tachiyomi.network.NetworkPreferences
import eu.kanade.tachiyomi.ui.base.delegate.SecureActivityDelegateImpl import eu.kanade.tachiyomi.ui.base.delegate.SecureActivityDelegateImpl
@@ -39,8 +41,10 @@ import eu.kanade.tachiyomi.ui.setting.track.BaseOAuthLoginActivity
import eu.kanade.tachiyomi.ui.webview.WebViewActivity import eu.kanade.tachiyomi.ui.webview.WebViewActivity
import eu.kanade.tachiyomi.util.CrashLogUtil import eu.kanade.tachiyomi.util.CrashLogUtil
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.protobuf.ProtoBuf
import mihon.core.metro.IsDebugBuild import mihon.core.metro.IsDebugBuild
import mihon.domain.extension.interactor.GetExtensionStoreCountAsFlow import mihon.domain.extension.interactor.GetExtensionStoreCountAsFlow
import nl.adaptivity.xmlutil.serialization.XML
import tachiyomi.domain.backup.service.BackupPreferences import tachiyomi.domain.backup.service.BackupPreferences
import tachiyomi.domain.category.interactor.GetCategories import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.category.interactor.ResetCategoryFlags import tachiyomi.domain.category.interactor.ResetCategoryFlags
@@ -101,10 +105,14 @@ interface AppGraph : ViewModelGraph {
val trackerManager: TrackerManager val trackerManager: TrackerManager
val extensionManager: ExtensionManager val extensionManager: ExtensionManager
val chapterCache: ChapterCache val chapterCache: ChapterCache
val coverCache: CoverCache
val downloadCache: DownloadCache val downloadCache: DownloadCache
val json: Json val json: Json
val protoBuf: ProtoBuf
val xml: XML
val networkHelper: NetworkHelper val networkHelper: NetworkHelper
val javaScriptEngine: JavaScriptEngine
val getFavorites: GetFavorites val getFavorites: GetFavorites
val getCategories: GetCategories val getCategories: GetCategories
@@ -0,0 +1,100 @@
package mihon.app.di.injekt
import android.app.Application
import android.content.Context
import eu.kanade.tachiyomi.network.JavaScriptEngine
import eu.kanade.tachiyomi.network.NetworkHelper
import kotlinx.serialization.json.Json
import kotlinx.serialization.protobuf.ProtoBuf
import mihon.app.di.AppGraph
import mihon.core.metro.GraphProvider
import nl.adaptivity.xmlutil.serialization.XML
import uy.kohesive.injekt.api.InjektModule
import uy.kohesive.injekt.api.InjektRegistrar
import uy.kohesive.injekt.api.InjektionException
import uy.kohesive.injekt.api.TypeReference
import java.lang.reflect.Type
class MetroInjektRegistrar(
private val application: Application,
private val graphProvider: GraphProvider<AppGraph>,
) : InjektRegistrar {
private val graph: AppGraph inline get() = graphProvider.graph
private val bindings = mapOf<Type, () -> Any>(
Application::class.java to { application },
Context::class.java to { application },
Json::class.java to { graph.json },
ProtoBuf::class.java to { graph.protoBuf },
XML::class.java to { graph.xml },
NetworkHelper::class.java to { graph.networkHelper },
JavaScriptEngine::class.java to { graph.javaScriptEngine },
)
override fun <R : Any> getInstance(forType: Type): R = getInstanceOrNull(forType)
?: throw InjektionException("$forType is not exposed to Injekt, add it to ${this::class.simpleName}")
override fun <R : Any> getInstanceOrElse(forType: Type, default: R): R = getInstanceOrNull(forType) ?: default
override fun <R : Any> getInstanceOrElse(forType: Type, default: () -> R): R =
getInstanceOrNull(forType) ?: default()
@Suppress("UNCHECKED_CAST")
override fun <R : Any> getInstanceOrNull(forType: Type): R? = bindings[forType]?.invoke() as R?
override fun <R : Any, K : Any> getKeyedInstance(forType: Type, key: K): R = unsupported("Keyed injection")
override fun <R : Any, K : Any> getKeyedInstanceOrElse(forType: Type, key: K, default: R): R =
unsupported("Keyed injection")
override fun <R : Any, K : Any> getKeyedInstanceOrElse(forType: Type, key: K, default: () -> R): R =
unsupported("Keyed injection")
@Suppress("RedundantNullableReturnType")
override fun <R : Any, K : Any> getKeyedInstanceOrNull(forType: Type, key: K): R? = unsupported("Keyed injection")
override fun <R : Any> getLogger(expectedLoggerType: Type, byName: String): R = unsupported("Logger injection")
override fun <R : Any, T : Any> getLogger(expectedLoggerType: Type, forClass: Class<T>): R =
unsupported("Logger injection")
override fun <T : Any> addSingleton(forType: TypeReference<T>, singleInstance: T) = readOnly()
override fun <R : Any> addSingletonFactory(forType: TypeReference<R>, factoryCalledOnce: () -> R) = readOnly()
override fun <R : Any> addFactory(forType: TypeReference<R>, factoryCalledEveryTime: () -> R) = readOnly()
override fun <R : Any> addPerThreadFactory(forType: TypeReference<R>, factoryCalledOncePerThread: () -> R) =
readOnly()
override fun <R : Any, K : Any> addPerKeyFactory(forType: TypeReference<R>, factoryCalledPerKey: (K) -> R) =
readOnly()
override fun <R : Any, K : Any> addPerThreadPerKeyFactory(
forType: TypeReference<R>,
factoryCalledPerKeyPerThread: (K) -> R,
) = readOnly()
override fun <R : Any> addLoggerFactory(
forLoggerType: TypeReference<R>,
factoryByName: (String) -> R,
factoryByClass: (Class<Any>) -> R,
) = readOnly()
override fun <O : Any, T : O> addAlias(
existingRegisteredType: TypeReference<T>,
otherAncestorOrInterface: TypeReference<O>,
) = readOnly()
override fun <T : Any> hasFactory(forType: TypeReference<T>): Boolean = forType.type in bindings
override fun importModule(submodule: InjektModule) = readOnly()
private fun readOnly(): Nothing = throw UnsupportedOperationException("Mihon's Injekt instance is read-only")
private fun unsupported(feature: String): Nothing =
throw UnsupportedOperationException("$feature is not supported by Mihon's Injekt instance")
}
@@ -1,49 +0,0 @@
package mihon.app.di.injekt
import dev.zacsweers.metro.Inject
import eu.kanade.domain.track.service.TrackPreferences
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.network.JavaScriptEngine
import eu.kanade.tachiyomi.network.NetworkHelper
import kotlinx.serialization.json.Json
import kotlinx.serialization.protobuf.ProtoBuf
import nl.adaptivity.xmlutil.serialization.XML
import tachiyomi.core.common.preference.PreferenceStore
import uy.kohesive.injekt.api.InjektModule
import uy.kohesive.injekt.api.InjektRegistrar
import uy.kohesive.injekt.api.addSingleton
@Inject
class MetroInteropModule(
private val json: Json,
private val protoBuf: ProtoBuf,
private val xml: XML,
private val networkHelper: NetworkHelper,
private val javaScriptEngine: JavaScriptEngine,
private val preferenceStore: PreferenceStore,
private val trackPreferences: TrackPreferences,
private val extensionManager: ExtensionManager,
private val coverCache: CoverCache,
) : InjektModule {
override fun InjektRegistrar.registerInjectables() {
addSingleton(json)
addSingleton(protoBuf)
addSingleton(xml)
addSingleton(networkHelper)
addSingleton(javaScriptEngine)
addSingleton(preferenceStore)
addSingleton(trackPreferences)
addSingleton(extensionManager)
addSingleton(coverCache)
}
}