Repository navigation
Conversation
…VM-desktop) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJeGoThgxNDUkMDUpUmqho
Stage 0 (infrastructure):
- Add flow.kmp.library convention plugin targeting Android, JVM (desktop),
iOS and macOS. Under AGP 9.0+ the Kotlin Multiplatform plugin is no longer
compatible with com.android.library, so it uses the dedicated
com.android.kotlin.multiplatform.library plugin (kotlin { androidLibrary {} }).
- Make flow.kotlin.serialization KMP-aware (commonMainImplementation vs
implementation depending on whether the multiplatform plugin is applied).
- Add the KMP Android library plugin marker to the buildSrc classpath.
Stage 1 (leaf modules -> KMP commonMain):
- core:models, core:common, core:network:api, core:auth:api, core:work:api
moved from src/main/kotlin to src/commonMain/kotlin and switched to
flow.kmp.library. These modules are pure Kotlin (kotlinx only), so they
compile unchanged in commonMain.
Apple targets are declared but only built on Apple hosts; the Android CI
tasks (lintDebug, testDebugUnitTest, assembleDebug/Release) never trigger
their compile/test tasks. Android consumers resolve the Android variant and
the JVM consumer (core:network:rutracker / proxy) resolves the JVM variant.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DJeGoThgxNDUkMDUpUmqho
- Convert core:network:rutracker to flow.kmp.library.
- Replace jsoup (JVM-only) with Ksoup (com.fleeksoft.ksoup, KMP port) across
all parser use cases.
- Make the parsers commonMain-safe by removing JVM-only APIs:
- java.util.regex.Pattern -> kotlin.text.Regex (LoginUseCase,
WithFormTokenUseCase, Utils.getTags)
- java.net.URI query parsing -> pure-Kotlin substring/split (Utils.parseUrl)
- String.format("%.1f") -> manual rounding (Utils.formatSize)
- java.util.Locale -> Char.titlecase() (ParsePostUseCase)
- Element.tag().name -> Element.tagName()
- Keep the Windows-1251 form encoding (java.net.URLEncoder) and the Ktor HTTP
client impl in a nonNativeMain source set shared by Android + JVM only;
RuTrackerInnerApiImpl and RuTrackerApiFactory live there. Native engines /
charset handling are deferred to the networking stage.
commonMain is verified free of java.*/javax.*; the Android CI build compiles
commonMain (exercising the Ksoup API) and nonNativeMain via the Android target.
JVM consumers (proxy) resolve the JVM variant, which includes nonNativeMain.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DJeGoThgxNDUkMDUpUmqho
…perty Ksoup exposes getWholeText() as a function, not a 'wholeText' property like jsoup. Fixes the commonMain compilation error in ParsePostUseCase. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJeGoThgxNDUkMDUpUmqho
Pilot for the DI migration on a single, self-contained module. The DI graph cannot be partially migrated, so this uses the strangler pattern: - core:dispatchers no longer depends on Hilt: removed @Inject/@BINDS, dropped the flow.android.hilt plugin. DispatchersImpl is now a plain class with a framework-agnostic createDispatchers() factory. - core:dispatchers exposes a Koin module (dispatchersModule) — the target DI wiring for the multiplatform graph (not yet bootstrapped at runtime). - The Android app keeps working via a thin Hilt bridge (:app .../di/ DispatchersModule) that @provides Dispatchers through the same factory. Existing Hilt consumers are unchanged. - Declare kotlinx-coroutines-core explicitly (was pulled in transitively via hilt-android, which is now gone). DispatchersModule/DispatchersImpl are referenced only within core:dispatchers and tests use TestDispatchers directly, so removing the Hilt binding is safe. Verified by the Android CI build, which assembles the full Hilt graph. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJeGoThgxNDUkMDUpUmqho
Apply the strangler pattern from the dispatchers pilot to core:logger: - Remove Hilt from core:logger (drop flow.android.hilt, remove @Inject/@BINDS). LoggerFactoryImpl (debug + release variants) become plain classes; a framework-agnostic createLoggerFactory() resolves the variant-specific impl. - Expose a Koin module (loggerModule) as the target multiplatform wiring. - Bridge LoggerFactory into the Android Hilt graph from :app (.../di/ LoggerModule). Existing Hilt consumers (feature ViewModels) are unchanged. LoggerModule/LoggerFactoryImpl are referenced only within core:logger; tests use TestLoggerFactory directly, and there are no @TestInstallIn replacements, so removing the Hilt binding is safe. Verified by the Android CI build. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJeGoThgxNDUkMDUpUmqho
Continue the strangler DI migration with a module that has constructor dependencies and a dual binding: - core:auth:impl no longer depends on Hilt (drop flow.android.hilt, remove @Inject/@singleton). AuthServiceImpl is a plain class; createAuthService() builds it from NetworkApi + PreferencesStorage. - One stateful instance backs both AuthService and TokenProvider. The Koin module and the Hilt bridge both expose TokenProvider derived from the same singleton AuthService instance, preserving the previous @singleton semantics. - Bridge in :app (.../di/AuthModule) injects NetworkApi + PreferencesStorage from the existing Hilt graph and provides both interfaces. - Declare kotlinx-coroutines-core explicitly (was transitive via hilt-android). AuthServiceImpl/AuthModule are internal and referenced only within the module; no @TestInstallIn replacements exist. Verified by the Android CI build. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJeGoThgxNDUkMDUpUmqho
First Context-dependent infra module via the strangler pattern: - core:preferences no longer depends on Hilt (drop flow.android.hilt, remove @Inject/@Singleton/@ApplicationContext). PreferencesStorageImpl and SharedPreferencesFactoryImpl are plain classes. - createPreferencesStorage(context, dispatchers) wires the internal encrypted SharedPreferences factory, so the internal SharedPreferencesFactory needs no DI binding. - Bridge in :app (.../di/PreferencesModule) provides PreferencesStorage, injecting @ApplicationContext Context + Dispatchers from the Hilt graph. - Declare kotlinx-coroutines-core explicitly (was transitive via hilt-android). No Koin module yet: this module is Android-specific (EncryptedSharedPreferences) and will get a multiplatform impl (DataStore/Keychain) + Koin module when converted to KMP. SecureStorageModule/PreferencesStorageImpl were internal and unreferenced elsewhere. Verified by the Android CI build. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJeGoThgxNDUkMDUpUmqho
Two more Context-only infra modules via the strangler pattern: - Drop flow.android.hilt; remove @Inject/@Singleton/@ApplicationContext. NotificationServiceImpl and DownloadServiceImpl become plain classes. - createNotificationService(context) / createDownloadService(context) factories. - Bridges in :app (.../di/NotificationsModule, DownloadsModule) provide the services with @ApplicationContext Context from the Hilt graph. Both are Android-specific (platform notifications / DownloadManager) and will get multiplatform abstractions when converted to KMP, hence no Koin module yet. The impls/modules are internal and unreferenced elsewhere. Verified by the Android CI build. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJeGoThgxNDUkMDUpUmqho
Removing flow.android.hilt also removed androidx.core, which was pulled in transitively via hilt-android. core:downloads (getSystemService ktx) and core:notifications (NotificationCompat) use androidx.core.* directly, so add an explicit androidx.core.ktx dependency to both. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJeGoThgxNDUkMDUpUmqho
Phased plan (Ф0-Ф7) to bootstrap Koin in :app and migrate the remaining, interconnected modules (data, network:impl, domain, features, work:impl) off Hilt, with a Koin-owns-instances + inverse-bridge coexistence strategy, ViewModel (koinViewModel) and @HiltWorker (KoinWorkerFactory) migration details, and checkModules/verify in CI to validate the graph at test time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJeGoThgxNDUkMDUpUmqho
- Record the agreed decisions in docs/kmp-di-bigbang-plan.md (use Koin Annotations; data/domain as separate PRs; work:impl as its own step; no intermediate dual-container period in prod) plus the dependency-ordered migration sequence. - Add koin-android to :app and start Koin (empty graph) before super.onCreate, so Koin and Hilt coexist and later inverse-bridges can resolve Koin-owned instances. No ownership changes yet (zero runtime risk). - Add a Koin smoke test (app unit test) asserting the dependency-free infra modules (dispatchers, logger) resolve; it will grow into a verify()/checkModules graph check once Koin Annotations land. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJeGoThgxNDUkMDUpUmqho
Move the 5 dependency-light infra modules to Koin ownership: - Add Koin modules for preferences/notifications/downloads (Context + Dispatchers resolved from the Koin graph via androidContext()/dispatchersModule); dispatchers/ logger already had Koin modules. - Register all 5 modules in startKoin (FlowApplication). - Convert the 5 Hilt bridges in :app to inverse-bridges: Koin owns the single instance, Hilt consumers read it via GlobalContext.get().get(). This guarantees a single instance regardless of Hilt/Koin consumer mix (important for stateful ones like the downloads cache). auth:impl stays on a direct Hilt bridge for now (its NetworkApi dependency is not in Koin yet); it migrates after network:impl. Koin is started before super.onCreate so inverse-bridges resolve. The Koin smoke test covers dispatchers/logger; the Context modules are compile-validated (instantiation needs Android, out of scope for unit tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJeGoThgxNDUkMDUpUmqho
Bottom of the data stack (depends only on Context): - Drop flow.android.hilt from core:database (keep KSP + Room). Replace the Hilt DatabaseModule/DaosModule with a Koin databaseModule providing AppDatabase (Context via androidContext()) and the 8 DAOs. - Register databaseModule in startKoin. - Inverse-bridge in :app exposes the 8 DAOs to core:data (still on Hilt) so a single Room instance is owned by Koin. These bridges are removed when core:data migrates to Koin (next step). Coroutines stay available via room-ktx (not via hilt-android). core:database has no @Inject classes; the modules were the only Hilt usage. Verified by the Android CI build. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJeGoThgxNDUkMDUpUmqho
Migrate the 9 data repositories to Koin (services stay on Hilt for now since they depend on NetworkApi/AuthService, which migrate with network:impl): - De-Hilt the 9 repository impls (remove @Inject); they depend only on DAOs, PreferencesStorage and LoggerFactory, all already in Koin. - repositoryModule wires them with singleOf(::Impl) bind Interface. - DataModule keeps only the 7 service @BINDS. - Replace the 8 DAO inverse-bridges in :app with 9 repository inverse-bridges (DAOs are now consumed directly by the Koin repositories; repositories are read from Koin by the remaining Hilt consumers — data services, domain, network:impl ProxyController -> SettingsRepository). - Add RepositoryModuleTest.verify(): statically validates the repository graph at test time in CI (the runtime DI safety net), declaring DAOs/PreferencesStorage/ LoggerFactory as extra types. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJeGoThgxNDUkMDUpUmqho
Record completed phases (Ф0, Ф1, Ф2a, Ф2b) and clarify that data services + network:impl + auth:impl migrate together as a cluster (mutual NetworkApi <-> SettingsRepository dependency), plus the CI verification limitation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJeGoThgxNDUkMDUpUmqho
Migrate the two Context-only data services (ConnectionService, StoreService) to Koin, leaving the network-backed services (Favorites/Forum/Search/Topic/Torrent) on Hilt until network:impl + auth migrate together: - De-Hilt ConnectionServiceImpl/StoreServiceImpl (remove @Inject/@ApplicationContext). - serviceModule wires them with singleOf; ServiceModuleTest.verify() validates the graph (Context/Dispatchers/LoggerFactory as extra types). - DataModule keeps the 5 network-backed @BINDS. - Inverse-bridge in :app exposes both services to the remaining Hilt consumers (domain use cases). network:impl (DI cycle Provider<OkHttpClient>, multibinding, OkHttp/Coil) is deferred per the agreed approach. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJeGoThgxNDUkMDUpUmqho
Migrate the network/auth cluster to Koin: - core:network:impl off Hilt: de-Hilt all impls; networkModule wires them with singleOf/single. The OkHttpClient <-> ProxyController cycle is broken by changing ProxyControllerImpl to take a lazy () -> OkHttpClient (was javax Provider). Interceptors are collected via getAll<Interceptor>(); the debug Chucker interceptor is contributed by networkDebugModules() (debug/release source sets, empty in release), replacing the Hilt @Multibinds + @IntoSet. - core:auth:impl: register the existing authModule; the :app bridge becomes an inverse-bridge (Koin owns the single AuthService/TokenProvider instance). - Inverse-bridges in :app for NetworkApi, ImageLoader, ProxyController, NetworkApiRepository (consumed by the still-Hilt data services, domain and FlowApplication). - NetworkModuleTest.verify() validates the network graph in CI. - Declare kotlinx-coroutines-core explicitly (was transitive via hilt-android). The data network-backed services stay on Hilt and now resolve NetworkApi/Auth from Koin via the bridges; they migrate with domain. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJeGoThgxNDUkMDUpUmqho
NetworkModuleTest.verify() statically reflects ProxyControllerImpl's constructor and flagged its () -> OkHttpClient (Function0) param as a missing definition. That parameter is supplied directly in the module definition to break the OkHttpClient <-> ProxyController cycle, so declare it via injectedParameters. Verified locally: testDebugUnitTest + :app:lintDebug + :app:assembleDebug all pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJeGoThgxNDUkMDUpUmqho
Complete the big-bang DI migration from Hilt to Koin in preparation for Kotlin Multiplatform. Hilt is now fully removed from the codebase. - buildSrc: delete AndroidHiltConventionPlugin; AndroidFeatureConventionPlugin now wires koin-android instead of Hilt - core:data / core:domain: replace generated Hilt modules with hand-written Koin modules (singleOf/factoryOf with interface binds), de-Hilt all 57 use cases and the 7 data services; add koin verify() tests - core:work:impl: drop @HiltWorker/DelegatingWorker/HiltWorkerFactory entry point, schedule workers directly, provide them via workerOf in a Koin module - core:navigation: viewModel() now resolves through koinViewModel - app: FlowApplication starts Koin with all modules and uses KoinWorkerFactory as the WorkManager factory (default WorkManagerInitializer removed from the manifest); MainActivity/TvActivity drop @androidentrypoint; delete the nine app-level Hilt bridge modules - features: de-Hilt all ViewModels (assisted VMs converted to plain constructors) and add a per-feature Koin module with viewModelOf Validated locally: :app:lintDebug, testDebugUnitTest and :app:assembleDebug all pass; :app:assembleRelease (R8) also succeeds. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJeGoThgxNDUkMDUpUmqho
Add an app-level test that statically verifies the complete dependency graph — the same modules FlowApplication assembles at startup — by merging them via includes() and calling Koin's verify(). This catches missing bindings that only surface across module boundaries, which the existing per-module verify tests cannot see. Wire it as a dedicated "Verify Koin DI graph" CI step so the graph is checked as a fast, clearly-named gate (the full testDebugUnitTest run also executes it along with the per-module verify tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJeGoThgxNDUkMDUpUmqho
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8ce8002c10
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| workerOf(::SyncBookmarksWorker) | ||
| workerOf(::SyncFavoritesWorker) |
There was a problem hiding this comment.
Preserve existing delegated WorkManager jobs
When upgrading users who already enabled favorites/bookmarks sync, their WorkManager database still contains periodic specs whose worker class is flow.work.workers.DelegatingWorker because the old scheduler enqueued DelegatingWorker for SyncFavoritesWork/SyncBookmarksWork. This module now only registers the new concrete workers and the commit deletes DelegatingWorker, and there is no startup path that re-enqueues those unique periodic jobs, so WorkManager cannot instantiate the persisted specs and automatic sync stays broken until the user manually changes the setting. Keep a compatibility worker/registration for one release or cancel/recreate the old unique work during migration.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Good catch — confirmed: the only schedulers are the two Set*SyncPeriodUseCase (menu actions), with no startup re-enqueue path, so persisted periodic specs pointing at the deleted DelegatingWorker would break auto-sync after update.
Fixed in 6ed6f87 by keeping DelegatingWorker as a thin compatibility shim (same FQN + persisted WorkerDelegateClassName key) that now resolves the delegate via KoinWorkerFactory instead of Hilt, registered in workModule. The current scheduler enqueues concrete workers directly, so nothing new is delegated through it — marked to remove one release after the migration ships.
Generated by Claude Code
Older builds scheduled the unique periodic jobs SyncFavoritesWork / SyncBookmarksWork through DelegatingWorker, storing the real worker class name in the input data. Those specs are still persisted in the WorkManager database after an update, so deleting the class left WorkManager unable to instantiate them — automatic favorites/bookmarks sync silently broke until the user re-picked the sync period. Re-introduce DelegatingWorker (same FQN and persisted data key) as a thin compatibility shim that resolves the delegate via KoinWorkerFactory instead of Hilt, and register it in workModule so persisted specs remain runnable. The current scheduler enqueues concrete workers directly, so nothing new is delegated through it; safe to remove one release after the migration ships. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJeGoThgxNDUkMDUpUmqho
Что это
Документ
docs/kmp-migration-plan.md— поэтапный план перехода Flow на Kotlin Multiplatform с таргетами Android, iOS, macOS, JVM-desktop. UI шарится через Compose Multiplatform.План основан на инвентаризации текущей кодовой базы (21 core-модуль + 15 feature-модулей, Hilt/Ktor/Room/Coil/Orbit/Navigation3).
Ключевые выводы
BundledSQLiteDriver).Context-зависимые сервисы / secure storage / WorkManager / notifications / downloads черезexpect/actual, и перевод UI на Compose Multiplatform.Структура плана
Это документ для обсуждения — реальные изменения кода последуют отдельными PR по этапам.
🤖 Generated with Claude Code
Generated by Claude Code