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()
}
@@ -40,7 +40,6 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import androidx.core.app.ActivityCompat
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.domain.ui.model.AppTheme
import eu.kanade.presentation.manga.components.MangaCover
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 mihon.icons.materialsymbols.MaterialSymbols
import mihon.icons.materialsymbols.roundedfilled.CheckCircle
import tachiyomi.core.common.preference.InMemoryPreferenceStore
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.secondaryItemAlpha
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.fullType
@Composable
internal fun AppThemePreferenceWidget(
@@ -261,7 +257,6 @@ fun AppThemePreviewItem(
@Composable
private fun AppThemesListPreview() {
var appTheme by remember { mutableStateOf(AppTheme.DEFAULT) }
Injekt.addSingleton(fullType<UiPreferences>(), UiPreferences(InMemoryPreferenceStore()))
TachiyomiTheme(appTheme = appTheme) {
Surface {
AppThemesList(
+3 -11
View File
@@ -52,7 +52,7 @@ import logcat.AndroidLogcatLogger
import logcat.LogPriority
import logcat.LogcatLogger
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.migration.Migration
import mihon.core.migration.Migrator
@@ -66,7 +66,7 @@ import tachiyomi.domain.source.service.SourceManager
import tachiyomi.i18n.MR
import tachiyomi.presentation.widget.WidgetManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.addSingleton
import uy.kohesive.injekt.api.InjektScope
import java.security.Security
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 injektMetroInteropModule: MetroInteropModule
@Inject private lateinit var migrations: Set<Migration>
private val disableIncognitoReceiver = DisableIncognitoReceiver()
@@ -110,8 +108,8 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
if (packageName != process) WebView.setDataDirectorySuffix(process)
}
Injekt = InjektScope(MetroInjektRegistrar(application = this, graphProvider = this))
graph.inject(this)
setupInjekt()
TelemetryConfig.init(applicationContext)
@@ -185,12 +183,6 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
initializeMigrator()
}
private fun setupInjekt() {
Injekt.addSingleton<Application>(this)
Injekt.addSingleton<Context>(this)
Injekt.importModule(injektMetroInteropModule)
}
private fun initializeMigrator() {
val preference = preferenceStore.getInt(Preference.appStateKey("last_version_code"), 0)
logcat {
@@ -5,7 +5,7 @@ import android.net.Uri
import androidx.annotation.CallSuper
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.model.InstallStep
import uy.kohesive.injekt.injectLazy
import mihon.app.di.appGraph
import java.util.Collections
import java.util.concurrent.CopyOnWriteArraySet
import kotlin.concurrent.atomics.AtomicReference
@@ -17,7 +17,7 @@ import kotlin.concurrent.atomics.ExperimentalAtomicApi
@OptIn(ExperimentalAtomicApi::class)
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 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.restore.BackupRestoreJob
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.DownloadJob
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.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.util.ExtensionInstallActivity
import eu.kanade.tachiyomi.network.JavaScriptEngine
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.NetworkPreferences
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.util.CrashLogUtil
import kotlinx.serialization.json.Json
import kotlinx.serialization.protobuf.ProtoBuf
import mihon.core.metro.IsDebugBuild
import mihon.domain.extension.interactor.GetExtensionStoreCountAsFlow
import nl.adaptivity.xmlutil.serialization.XML
import tachiyomi.domain.backup.service.BackupPreferences
import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.category.interactor.ResetCategoryFlags
@@ -101,10 +105,14 @@ interface AppGraph : ViewModelGraph {
val trackerManager: TrackerManager
val extensionManager: ExtensionManager
val chapterCache: ChapterCache
val coverCache: CoverCache
val downloadCache: DownloadCache
val json: Json
val protoBuf: ProtoBuf
val xml: XML
val networkHelper: NetworkHelper
val javaScriptEngine: JavaScriptEngine
val getFavorites: GetFavorites
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)
}
}