🎯 Główna Idea

Inject to rewolucyjnie prosty system Dependency Injection, który łączy moc Koin i Hilt bez ich złożoności. Działa od razu - zero konfiguracji, zero adnotacji, zero generowania kodu.

💡 Dlaczego ADict.Inject? Koin wymaga startKoin(), Hilt wymaga adnotacji wszędzie + plugin Gradle. ADict.Inject? Po prostu rejestrujesz i używasz. Koniec.

📦 Podstawy - Singleton i Factory

🔹 Singleton - jedna instancja na całą aplikację

Używaj gdy obiekt jest "ciężki" do stworzenia lub powinien być współdzielony (baza danych, HTTP client, repository):

// PRZYKŁAD 1: Prosty singleton bez zależności
// OkHttpClient jest ciężki - chcemy jeden na całą aplikację
ADict.Inject.single {
    OkHttpClient.Builder()
        .connectTimeout(30, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS)
        .addInterceptor(HttpLoggingInterceptor())
        .build()
}

// PRZYKŁAD 2: Singleton z zależnością od innego singletona
// Retrofit potrzebuje OkHttpClient - użyj get() żeby go pobrać
ADict.Inject.single {
    Retrofit.Builder()
        .baseUrl("https://api.myapp.com/v1/")
        .client(get())  // ← get() pobiera OkHttpClient zarejestrowany wyżej!
        .addConverterFactory(GsonConverterFactory.create())
        .build()
        .create(ApiService::class.java)
}

// PRZYKŁAD 3: Singleton potrzebujący Context
// Room database potrzebuje Context - użyj context()
ADict.Inject.single {
    Room.databaseBuilder(
        context(),  // ← context() zwraca Context aplikacji
        AppDatabase::class.java,
        "myapp.db"
    )
    .addMigrations(MIGRATION_1_2)
    .build()
}

// PRZYKŁAD 4: Singleton z wieloma zależnościami
// UserRepository potrzebuje ApiService i Database
ADict.Inject.single {
    UserRepositoryImpl(
        api = get(),      // ← pobiera ApiService
        database = get()  // ← pobiera AppDatabase
    )
}

// UŻYCIE - zawsze ta sama instancja
val db1: AppDatabase = ADict.Inject.get()
val db2: AppDatabase = ADict.Inject.get()
println(db1 === db2)  // true - to ten sam obiekt!

🔹 Factory - nowa instancja za każdym razem

Używaj gdy obiekt jest lekki, ma stan, lub nie powinien być współdzielony:

// PRZYKŁAD 1: Formatter z wewnętrznym stanem
// SimpleDateFormat NIE jest thread-safe - każdy wątek potrzebuje swojego
ADict.Inject.factory {
    SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
}

// PRZYKŁAD 2: Builder do tworzenia requestów
// Każdy request potrzebuje świeżego buildera
ADict.Inject.factory {
    Request.Builder()
        .addHeader("X-App-Version", BuildConfig.VERSION_NAME)
}

// PRZYKŁAD 3: Mapper (zwykle bezstanowy, ale warto mieć świeży)
ADict.Inject.factory {
    UserMapper(gson = get())  // ← Gson jest singletonem, mapper jest factory
}

// UŻYCIE - zawsze nowa instancja
val formatter1: SimpleDateFormat = ADict.Inject.get()
val formatter2: SimpleDateFormat = ADict.Inject.get()
println(formatter1 === formatter2)  // false - to różne obiekty!

🔹 Użycie w Activity/Fragment - property delegate

class MainActivity : AppCompatActivity() {
    // "by inject()" to property delegate - zależność jest pobierana
    // DOPIERO przy pierwszym użyciu (lazy), nie w konstruktorze!

    val api: ApiService by inject()
    val userRepo: UserRepository by inject()
    val formatter: SimpleDateFormat by inject()  // każde użycie = nowa instancja

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Tutaj zależności są już dostępne
        lifecycleScope.launch {
            val users = userRepo.getUsers()
            val formattedDate = formatter.format(Date())
        }
    }
}

🏷️ Named Dependencies

Problem: Masz dwa różne URL-e (oba są typu String). Jak je rozróżnić?

Rozwiązanie: Named dependencies - nadajesz nazwy:

// REJESTRACJA - różne URL-e z nazwami
ADict.Inject.single(name = "apiUrl") { "https://api.myapp.com" }
ADict.Inject.single(name = "cdnUrl") { "https://cdn.myapp.com" }
ADict.Inject.single(name = "wsUrl") { "wss://ws.myapp.com" }

// Różne konfiguracje tego samego typu
ADict.Inject.single(name = "defaultClient") {
    OkHttpClient.Builder()
        .connectTimeout(30, TimeUnit.SECONDS)
        .build()
}
ADict.Inject.single(name = "longPollingClient") {
    OkHttpClient.Builder()
        .readTimeout(5, TimeUnit.MINUTES)  // długi timeout dla long polling
        .build()
}
ADict.Inject.single(name = "uploadClient") {
    OkHttpClient.Builder()
        .writeTimeout(10, TimeUnit.MINUTES)  // długi timeout dla uploadu
        .build()
}

// UŻYCIE - pobieranie z nazwą
class NetworkManager {
    // Bezpośrednie pobranie
    val apiUrl: String = ADict.Inject.get(name = "apiUrl")
    val cdnUrl: String = ADict.Inject.get(name = "cdnUrl")
    val uploadClient: OkHttpClient = ADict.Inject.get(name = "uploadClient")
}

// W Activity/Fragment z delegate
class MyActivity : AppCompatActivity() {
    val apiUrl: String by inject(name = "apiUrl")
    val cdnUrl: String by inject(name = "cdnUrl")
}
⚠️ Uwaga! Literówka w nazwie (np. "apiUrl" vs "apiURl") spowoduje crash w runtime. Rozważ użycie Type-Safe Qualifiers (poniżej).

🎯 Type-Safe Qualifiers

Problem: Named dependencies używają Stringów - literówka = crash w runtime.

Rozwiązanie: Qualifiers - obiekty zamiast stringów, błąd = błąd kompilacji!

// KROK 1: Zdefiniuj qualifiery (najlepiej w osobnym pliku Qualifiers.kt)
object Qualifiers {
    val ApiUrl = qualifier("api_url")
    val CdnUrl = qualifier("cdn_url")
    val WsUrl = qualifier("ws_url")

    val DefaultClient = qualifier("default_client")
    val LongPollingClient = qualifier("long_polling_client")
    val UploadClient = qualifier("upload_client")
}

// KROK 2: Rejestracja z qualifier (zamiast name = "...")
ADict.Inject.single(Qualifiers.ApiUrl) { "https://api.myapp.com" }
ADict.Inject.single(Qualifiers.CdnUrl) { "https://cdn.myapp.com" }
ADict.Inject.single(Qualifiers.WsUrl) { "wss://ws.myapp.com" }

ADict.Inject.single(Qualifiers.DefaultClient) {
    OkHttpClient.Builder().build()
}
ADict.Inject.single(Qualifiers.LongPollingClient) {
    OkHttpClient.Builder().readTimeout(5, TimeUnit.MINUTES).build()
}

// KROK 3: Użycie - IDE podpowiada dostępne qualifiery!
class MyActivity : AppCompatActivity() {
    // Jeśli zrobisz literówkę, dostaniesz błąd KOMPILACJI, nie runtime crash!
    val apiUrl: String by inject(Qualifiers.ApiUrl)
    val cdnUrl: String by inject(Qualifiers.CdnUrl)

    // IDE podpowie: Qualifiers.ApiUrl, Qualifiers.CdnUrl, Qualifiers.WsUrl
}

// Porównanie:
// ❌ inject(name = "apiURl")  // literówka - crash w runtime
// ✅ inject(Qualifiers.ApiURl)  // literówka - błąd kompilacji!

🔗 Bind - Interface → Implementation

Problem: Chcesz używać interfejsu w kodzie, ale wstrzykiwać implementację.

Rozwiązanie: bind - wiążesz interfejs z konkretną klasą:

// Masz interfejs i implementację
interface UserRepository {
    suspend fun getUser(id: String): User
    suspend fun saveUser(user: User)
    suspend fun deleteUser(id: String)
}

class UserRepositoryImpl(
    private val api: ApiService,
    private val db: AppDatabase,
    private val cache: UserCache
) : UserRepository {
    override suspend fun getUser(id: String): User {
        // Najpierw cache, potem DB, potem API
        return cache.get(id)
            ?: db.userDao().getById(id)
            ?: api.getUser(id).also {
                cache.put(it)
                db.userDao().insert(it)
            }
    }
    // ... reszta implementacji
}

// BEZ bind - musisz określić typ zwracany:
ADict.Inject.single<UserRepository> {
    UserRepositoryImpl(get(), get(), get())
}

// Z bind - bardziej czytelne i idiomatyczne:
ADict.Inject.bind<UserRepository, UserRepositoryImpl> {
    UserRepositoryImpl(get(), get(), get())
}
// Czytaj jako: "UserRepository jest implementowany przez UserRepositoryImpl"

// ═══════════════════════════════════════════════════════════
// UŻYCIE - WAŻNE! Gdzie używać by inject(), a gdzie nie?
// ═══════════════════════════════════════════════════════════

// ✅ W ACTIVITY/FRAGMENT - używasz by inject():
class UserActivity : AppCompatActivity() {
    val repo: UserRepository by inject()  // ← DZIAŁA!
}

// ❌ W VIEWMODEL - NIE możesz użyć by inject() w konstruktorze!
// Konstruktor przyjmuje PARAMETRY, nie property delegates
class UserViewModel(
    private val repo: UserRepository  // ← to jest PARAMETR konstruktora
) : ViewModel() {
    // repo jest przekazane przy tworzeniu ViewModel
}

// ✅ JAK TWORZYĆ VIEWMODEL Z ZALEŻNOŚCIAMI?
class UserActivity : AppCompatActivity() {
    // Użyj by viewModel { } i get() w środku:
    val viewModel: UserViewModel by viewModel {
        UserViewModel(
            repo = get()  // ← get() pobiera UserRepository
        )
    }
}

// ✅ ALTERNATYWNIE - zarejestruj ViewModel w DI:
// W module:
ADict.Inject.factory { UserViewModel(get()) }

// W Activity:
class UserActivity : AppCompatActivity() {
    val viewModel: UserViewModel by viewModel()  // bez { }
}

🎛️ Parametrized Injection

Problem: Niektóre zależności potrzebują parametrów znanych dopiero w runtime (np. userId po zalogowaniu).

Rozwiązanie: factoryWithParams - przekazujesz parametry przy pobieraniu:

// PRZYKŁAD 1: Sesja użytkownika (userId i token znasz po logowaniu)
data class UserSession(
    val userId: String,
    val token: String,
    val api: ApiService,  // to z DI
    val analytics: Analytics  // to z DI
)

// Rejestracja - params to tablica parametrów
ADict.Inject.factoryWithParams<UserSession> { params ->
    UserSession(
        userId = params[0],      // ← pierwszy parametr
        token = params[1],       // ← drugi parametr
        api = get(),             // ← z DI
        analytics = get()        // ← z DI
    )
}

// Użycie - po zalogowaniu
class LoginViewModel : ViewModel() {
    fun onLoginSuccess(response: LoginResponse) {
        // Tworzysz sesję z parametrami z response
        val session = ADict.Inject.get<UserSession>(
            parametersOf(response.userId, response.accessToken)
        )
        // session.userId = "user_123"
        // session.token = "eyJhbG..."
        // session.api = wstrzyknięty singleton ApiService
    }
}

// PRZYKŁAD 2: Ekran szczegółów (productId z Intent)
data class ProductDetailPresenter(
    val productId: String,
    val repo: ProductRepository,
    val analytics: Analytics
)

ADict.Inject.factoryWithParams<ProductDetailPresenter> { params ->
    ProductDetailPresenter(
        productId = params[0],
        repo = get(),
        analytics = get()
    )
}

class ProductDetailActivity : AppCompatActivity() {
    private lateinit var presenter: ProductDetailPresenter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val productId = intent.getStringExtra("PRODUCT_ID")
            ?: throw IllegalStateException("Missing PRODUCT_ID")

        presenter = ADict.Inject.get(parametersOf(productId))
    }
}

// PRZYKŁAD 3: Dialog z konfiguracją
data class ConfirmDialogConfig(
    val title: String,
    val message: String,
    val confirmText: String,
    val analytics: Analytics
)

ADict.Inject.factoryWithParams<ConfirmDialogConfig> { params ->
    ConfirmDialogConfig(
        title = params[0],
        message = params[1],
        confirmText = params[2],
        analytics = get()
    )
}

// Użycie
val config = ADict.Inject.get<ConfirmDialogConfig>(
    parametersOf("Usuń?", "Czy na pewno chcesz usunąć?", "Tak, usuń")
)

🔀 Conditional Injection

Problem: W Debug chcesz mock API, w Release prawdziwe. Albo różne implementacje dla różnych flavor.

Rozwiązanie: Conditional injection - rejestracja zależy od warunku:

// SPOSÓB 1: singleIf - zarejestruj tylko gdy warunek = true
// Debug
ADict.Inject.singleIf(BuildConfig.DEBUG) {
    MockApiService()  // Tylko w debug build
}
// Release
ADict.Inject.singleIf(!BuildConfig.DEBUG) {
    RealApiService(get())  // Tylko w release build
}

// SPOSÓB 2: singleWhen - wybierz jedną z dwóch opcji (czytelniejsze!)
ADict.Inject.singleWhen<ApiService>(
    condition = BuildConfig.DEBUG,
    ifTrue = { MockApiService() },     // gdy DEBUG = true
    ifFalse = { RealApiService(get()) }  // gdy DEBUG = false
)

// PRAKTYCZNY PRZYKŁAD: Logger
ADict.Inject.singleWhen<Logger>(
    condition = BuildConfig.DEBUG,
    ifTrue = {
        // W debug - loguj do konsoli + zapisuj do pliku
        CompositeLogger(
            ConsoleLogger(minLevel = Log.VERBOSE),
            FileLogger(context(), "debug.log")
        )
    },
    ifFalse = {
        // W release - tylko Crashlytics (bez wrażliwych danych!)
        CrashlyticsLogger(minLevel = Log.ERROR)
    }
)

// PRAKTYCZNY PRZYKŁAD: Feature flag z Remote Config
class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        ADict.init(this, BuildConfig.DEBUG)

        // Pobierz feature flag
        val useNewCheckout = Firebase.remoteConfig.getBoolean("new_checkout_v2")

        ADict.Inject.singleWhen<CheckoutProcessor>(
            condition = useNewCheckout,
            ifTrue = { NewCheckoutProcessor(get(), get()) },
            ifFalse = { LegacyCheckoutProcessor(get()) }
        )
    }
}

// PRAKTYCZNY PRZYKŁAD: Różne bazy dla flavor
val useSqlite = BuildConfig.FLAVOR == "lite"

ADict.Inject.singleWhen<Database>(
    condition = useSqlite,
    ifTrue = { SqliteDatabase(context()) },  // Lekka wersja
    ifFalse = { RoomDatabase(context()) }    // Pełna wersja
)

⚡ Async Initialization

Problem: Inicjalizacja bazy danych trwa 500ms+ i blokuje UI w onCreate!

Rozwiązanie: singleAsync - inicjalizacja w tle, bez blokowania Main thread:

// REJESTRACJA - ten kod wykona się w Dispatchers.Default (NIE na Main thread!)
ADict.Inject.singleAsync {
    // Ciężka operacja - nie blokuje UI!
    Room.databaseBuilder(context(), AppDatabase::class.java, "app.db")
        .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4)
        .build()
        .also { db ->
            // Dodatkowe ciężkie operacje
            db.runPendingMigrations()
            db.userDao().cleanupOldData()  // może trwać długo
        }
}

// Inny przykład - model ML
ADict.Inject.singleAsync {
    // Ładowanie modelu TensorFlow może trwać sekundy
    TensorFlowLite.Interpreter(
        loadModelFile(context(), "recommendation_model.tflite"),
        Interpreter.Options().apply {
            setNumThreads(4)
        }
    )
}

// UŻYCIE - getAsync() jest suspend function!
class HomeViewModel : ViewModel() {
    val users = MutableLiveData<List<User>>()

    init {
        viewModelScope.launch {
            // getAsync() czeka aż baza się zainicjalizuje
            // ALE nie blokuje Main thread - ViewModel jest responsywny!
            val db = ADict.Inject.getAsync<AppDatabase>()
            users.value = db.userDao().getAll()
        }
    }
}

// PRE-LOADING w Splash Screen
// Załaduj wszystkie async zależności ZANIM przejdziesz do głównej
class SplashActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_splash)

        lifecycleScope.launch {
            // Pokaż progress
            binding.progressBar.isVisible = true

            // Załaduj WSZYSTKIE async singletony
            ADict.Inject.preloadAsync()
            // Po tej linii: baza gotowa, ML model gotowy, wszystko gotowe!

            // Przejdź do głównej - wszystko już załadowane
            startActivity(Intent(this@SplashActivity, MainActivity::class.java))
            finish()
        }
    }
}

📦 Provider Pattern

Problem: Chcesz kontrolować KIEDY zależność jest tworzona, albo pobierać świeżą instancję wielokrotnie.

Rozwiązanie: Provider - "fabryka" którą wywołujesz gdy potrzebujesz:

// Bez providera - zależność wstrzyknięta od razu:
class OrderService(
    private val currentUser: CurrentUser  // Wstrzyknięty w konstruktorze
) {
    fun createOrder() {
        // Problem: currentUser może być nieaktualny po logout/login!
        val order = Order(userId = currentUser.id)
    }
}

// Z providerem - kontrolujesz kiedy pobrać:
class OrderService(
    private val currentUserProvider: Provider<CurrentUser>
) {
    fun createOrder() {
        // Za każdym razem pobierasz AKTUALNEGO użytkownika!
        val user = currentUserProvider.get()
        val order = Order(userId = user.id)
    }

    fun validateOrder(order: Order) {
        // Ponownie pobierasz - może być inny użytkownik!
        val user = currentUserProvider.get()
        if (order.userId != user.id) throw SecurityException()
    }
}

// REJESTRACJA
// CurrentUser jako factory - każde pobranie = aktualny stan
ADict.Inject.factory {
    CurrentUser.fromSharedPreferences(context())
}

// OrderService z providerem
ADict.Inject.single {
    OrderService(
        currentUserProvider = provider()  // ← tworzy Provider<CurrentUser>
    )
}

// PROVIDER vs LAZY PROVIDER
// provider() - każde .get() wywołuje factory (nowa instancja jeśli factory)
// lazyProvider() - pierwsze .get() tworzy, potem zwraca tę samą

class AnalyticsTracker(
    // Logger może być ciężki - nie twórz od razu
    private val loggerProvider: Provider<Logger>
) {
    fun track(event: String) {
        // Logger tworzony DOPIERO tutaj, przy pierwszym uzyciu
        // I potem używany ten sam (bo lazyProvider)
        loggerProvider.get().log("Event: $event")
    }
}

ADict.Inject.single {
    AnalyticsTracker(
        loggerProvider = lazyProvider()  // ← lazy - pierwszy get() tworzy, potem reużywa
    )
}

🎨 Multi-Binding (Set i Map)

Problem: Potrzebujesz WSZYSTKIE implementacje interfejsu (np. wszystkie interceptory HTTP).

Rozwiązanie: Multi-binding - rejestrujesz wiele elementów, pobierasz jako kolekcję:

Set Multi-Binding

// REJESTRACJA - dodajesz elementy do "setu"
ADict.Inject.intoSet<Interceptor> {
    HttpLoggingInterceptor().apply { level = Level.BODY }
}
ADict.Inject.intoSet<Interceptor> {
    AuthInterceptor(tokenProvider = get())
}
ADict.Inject.intoSet<Interceptor> {
    CacheInterceptor(cacheDir = context().cacheDir)
}
ADict.Inject.intoSet<Interceptor> {
    RetryInterceptor(maxRetries = 3)
}

// UŻYCIE - pobierasz wszystkie naraz
val allInterceptors: Set<Interceptor> = ADict.Inject.getAll()
// allInterceptors zawiera 4 interceptory!

// PRAKTYCZNE UŻYCIE - OkHttpClient ze wszystkimi interceptorami
ADict.Inject.single {
    OkHttpClient.Builder().apply {
        // Pobierz wszystkie zarejestrowane interceptory i dodaj
        ADict.Inject.getAll<Interceptor>().forEach { interceptor ->
            addInterceptor(interceptor)
        }
    }.build()
}

// PRAKTYCZNE UŻYCIE - System pluginów
interface AppPlugin {
    val name: String
    fun onAppStart()
    fun onAppStop()
}

ADict.Inject.intoSet<AppPlugin> { AnalyticsPlugin() }
ADict.Inject.intoSet<AppPlugin> { CrashReportingPlugin() }
ADict.Inject.intoSet<AppPlugin> { PerformancePlugin() }
ADict.Inject.intoSet<AppPlugin> { PushNotificationPlugin() }

class MyApp : Application() {
    private val plugins: Set<AppPlugin> by lazy { ADict.Inject.getAll() }

    override fun onCreate() {
        super.onCreate()
        ADict.init(this, BuildConfig.DEBUG)

        // Inicjalizuj wszystkie pluginy
        plugins.forEach { plugin ->
            Log.d("App", "Starting plugin: ${plugin.name}")
            plugin.onAppStart()
        }
    }

    override fun onTerminate() {
        plugins.forEach { it.onAppStop() }
        super.onTerminate()
    }
}

Map Multi-Binding

// REJESTRACJA - klucz → wartość
interface ScreenFactory {
    fun create(args: Bundle?): Fragment
}

ADict.Inject.intoMap<String, ScreenFactory>("home") {
    object : ScreenFactory {
        override fun create(args: Bundle?) = HomeFragment()
    }
}
ADict.Inject.intoMap<String, ScreenFactory>("profile") {
    object : ScreenFactory {
        override fun create(args: Bundle?) = ProfileFragment().apply {
            arguments = args
        }
    }
}
ADict.Inject.intoMap<String, ScreenFactory>("settings") {
    object : ScreenFactory {
        override fun create(args: Bundle?) = SettingsFragment()
    }
}

// UŻYCIE
class Navigator {
    private val factories: Map<String, ScreenFactory> = ADict.Inject.getAllMap()

    fun navigateTo(screenId: String, args: Bundle? = null): Fragment {
        val factory = factories[screenId]
            ?: throw IllegalArgumentException("Unknown screen: $screenId")
        return factory.create(args)
    }
}

// Użycie navigatora
val navigator: Navigator by inject()
val fragment = navigator.navigateTo("profile", bundleOf("userId" to "123"))

🔄 Lifecycle Cleanup

Problem: Baza danych, HTTP client powinny być zamknięte przy wyjściu z aplikacji.

Rozwiązanie: onDestroy callback - definiujesz co zrobić przy cleanup:

// REJESTRACJA z cleanup callback
ADict.Inject.single(
    onDestroy = onDestroy<AppDatabase> { db ->
        db.close()  // Zamknij połączenie z bazą
        Log.d("DI", "Database closed")
    }
) {
    Room.databaseBuilder(context(), AppDatabase::class.java, "app.db").build()
}

ADict.Inject.single(
    onDestroy = onDestroy<OkHttpClient> { client ->
        // Zamknij wszystkie połączenia
        client.dispatcher.executorService.shutdown()
        client.connectionPool.evictAll()
        client.cache?.close()
        Log.d("DI", "OkHttpClient cleaned up")
    }
) {
    OkHttpClient.Builder()
        .cache(Cache(context().cacheDir, 50 * 1024 * 1024))
        .build()
}

// WYWOŁANIE cleanup - np. w Application.onTerminate lub przy wylogowaniu
class MyApp : Application() {
    override fun onTerminate() {
        super.onTerminate()
        // Wywołuje wszystkie zarejestrowane onDestroy callbacks
        ADict.Inject.cleanup()
    }
}

// Lub przy wylogowaniu - wyczyść dane użytkownika
fun logout() {
    ADict.Inject.cleanup()  // Zamknij bazę, wyczyść cache
    ADict.Inject.reset()    // Wyczyść wszystkie zależności
    // Zarejestruj na nowo z czystym stanem
    reinitializeDependencies()
}

📊 Dependency Graph Debug

Wizualna reprezentacja wszystkich zarejestrowanych zależności:

// W debug mode - wyświetl graf
if (BuildConfig.DEBUG) {
    Log.d("DI", ADict.Inject.dependencyGraph())
}

// Wynik w Logcat:
// ╔════════════════════════════════════════╗
// ║      ADict.Inject Dependency Graph      ║
// ╠════════════════════════════════════════╣
// ║ Singletons (5 cached):                 ║
// ║   ✓ OkHttpClient          ← utworzony  ║
// ║   ✓ Retrofit              ← utworzony  ║
// ║   ✓ ApiService            ← utworzony  ║
// ║   ○ AppDatabase           ← niezaładowany║
// ║   ○ UserRepository        ← niezaładowany║
// ║ Factories:                             ║
// ║   ⚙ SimpleDateFormat                   ║
// ║   ⚙ UserMapper                         ║
// ║ Scoped:                                ║
// ║   ⊕ DetailPresenter                    ║
// ║ Active Scopes: 2                       ║
// ║   └ MainActivity@12345 (1 dep)         ║
// ║   └ ProfileFragment@67890 (2 deps)     ║
// ║ Multi-bindings:                        ║
// ║   Sets: 1 (Interceptor: 4 items)       ║
// ║   Maps: 1 (ScreenFactory: 3 items)     ║
// ║ Async: 2 (1 loaded)                    ║
// ╚════════════════════════════════════════╝

// Lista tekstowa
ADict.Inject.listDefinitions().forEach { Log.d("DI", it) }
// SINGLETON: OkHttpClient
// SINGLETON: ApiService
// FACTORY: SimpleDateFormat
// ...

🏗️ Moduły - Organizacja Kodu

Pogrupuj zależności w logiczne moduły zamiast rejestrować wszystko w Application:

// ============ NetworkModule.kt ============
val networkModule = module {
    // HTTP client
    single {
        OkHttpClient.Builder()
            .connectTimeout(30, TimeUnit.SECONDS)
            .addInterceptor(HttpLoggingInterceptor())
            .build()
    }

    // Retrofit
    single {
        Retrofit.Builder()
            .baseUrl(get<String>(Qualifiers.ApiUrl))
            .client(get())
            .addConverterFactory(GsonConverterFactory.create(get()))
            .build()
    }

    // API Service
    single { get<Retrofit>().create(ApiService::class.java) }
}

// ============ DatabaseModule.kt ============
val databaseModule = module {
    // Room database (async)
    singleAsync {
        Room.databaseBuilder(context(), AppDatabase::class.java, "app.db")
            .addMigrations(MIGRATION_1_2)
            .build()
    }

    // DAOs
    single { get<AppDatabase>().userDao() }
    single { get<AppDatabase>().productDao() }
    single { get<AppDatabase>().orderDao() }
}

// ============ RepositoryModule.kt ============
val repositoryModule = module {
    // Bind interfejsy do implementacji
    bind<UserRepository, UserRepositoryImpl> {
        UserRepositoryImpl(get(), get(), get())
    }
    bind<ProductRepository, ProductRepositoryImpl> {
        ProductRepositoryImpl(get(), get())
    }
    bind<OrderRepository, OrderRepositoryImpl> {
        OrderRepositoryImpl(get(), get())
    }
}

// ============ ViewModelModule.kt ============
val viewModelModule = module {
    // ViewModels jako factory (każda Activity/Fragment dostaje swój)
    factory { HomeViewModel(get(), get()) }
    factory { ProfileViewModel(get(), get()) }
    factory { ProductDetailViewModel(get(), get()) }
    factory { CheckoutViewModel(get(), get(), get()) }
}

// ============ Application.kt ============
class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()

        // Inicjalizacja ADict
        ADict.init(this, BuildConfig.DEBUG)

        // Załaduj wszystkie moduły
        ADict.Inject.modules(
            networkModule,
            databaseModule,
            repositoryModule,
            viewModelModule
        )

        // Debug - pokaż co zostało załadowane
        if (BuildConfig.DEBUG) {
            Log.d("DI", ADict.Inject.dependencyGraph())
        }
    }
}

🧪 Testowanie

class UserRepositoryTest {

    // Mock API
    private val mockApi = mockk<ApiService>()
    private val mockDb = mockk<AppDatabase>(relaxed = true)

    @Before
    fun setup() {
        // Nadpisz prawdziwe zależności mockami
        InjectTestUtils.override<ApiService> { mockApi }
        InjectTestUtils.override<AppDatabase> { mockDb }

        // Skonfiguruj mocki
        coEvery { mockApi.getUser(any()) } returns User("1", "Test User")
        coEvery { mockDb.userDao().getById(any()) } returns null
    }

    @After
    fun tearDown() {
        // WAŻNE: zawsze resetuj po teście!
        ADict.Inject.reset()
    }

    @Test
    fun `getUser fetches from API when not in cache`() = runTest {
        // Arrange
        val repo: UserRepository = ADict.Inject.get()

        // Act
        val user = repo.getUser("123")

        // Assert
        assertEquals("Test User", user.name)
        coVerify { mockApi.getUser("123") }
    }
}

// Test instrumentowany (Android)
class MainActivityTest {

    @Before
    fun setup() {
        // Użyj fake implementacji
        InjectTestUtils.override<UserRepository> { FakeUserRepository() }
        InjectTestUtils.override<Analytics> { FakeAnalytics() }
    }

    @After
    fun tearDown() {
        ADict.Inject.reset()
    }

    @Test
    fun homeScreen_displaysUserName() {
        // Activity automatycznie użyje FakeUserRepository
        ActivityScenario.launch(MainActivity::class.java).use { scenario ->
            onView(withId(R.id.userName))
                .check(matches(withText("Fake User")))
        }
    }
}

// Weryfikacja że wszystkie zależności mogą być rozwiązane
@Test
fun `all dependencies can be resolved`() {
    val result = InjectTestUtils.verifyAll()
    assertTrue(result.success, "Errors: ${result.errors}")
}

// Izolowany kontekst dla testów
@Test
fun `isolated test`() = InjectTestUtils.withIsolatedContext {
    single { FakeService() }
    // Test używa tylko FakeService, potem automatyczny reset
}

📊 Performance Stats

Monitoruj wydajność DI w aplikacji:

// Włącz zbieranie statystyk (najlepiej tylko w debug)
if (BuildConfig.DEBUG) {
    // Statystyki są zbierane automatycznie
}

// Pobierz statystyki
val stats = InjectionStats.getStats()
stats.forEach { (key, stat) ->
    Log.d("DI", "$key: ${stat.avgCreationTimeMs}ms avg, ${stat.cacheHits} hits")
}

// Top 5 najwolniejszych zależności
InjectionStats.getSlowest(5).forEach { stat ->
    Log.w("DI", "Slow: ${stat.key} - ${stat.avgCreationTimeMs}ms")
}

// Top 5 najczęściej używanych
InjectionStats.getMostUsed(5).forEach { stat ->
    Log.d("DI", "Popular: ${stat.key} - ${stat.cacheHits + stat.creationCount} uses")
}

// Wygeneruj pełny raport
Log.d("DI", InjectionStats.generateReport())
// Wynik:
// ╔════════════════════════════════════════════════════════╗
// ║           ADict.Inject Performance Report              ║
// ╠════════════════════════════════════════════════════════╣
// ║ Total creations: 15                                    ║
// ║ Total cache hits: 127                                  ║
// ║ Cache hit rate: 89.4%                                  ║
// ║ Total creation time: 423ms                             ║
// ╠════════════════════════════════════════════════════════╣
// ║ Slowest dependencies:                                  ║
// ║   AppDatabase: 312ms avg                               ║
// ║   Retrofit: 45ms avg                                   ║
// ╚════════════════════════════════════════════════════════╝

🔄 Coroutine Scoped Container

Zależności powiązane z CoroutineScope - automatyczny cleanup:

class MyViewModel : ViewModel() {
    // Kontener DI powiązany z viewModelScope
    // Gdy ViewModel jest cleared, zależności są automatycznie czyszczone
    private val diScope = ADict.Inject.scopedTo(viewModelScope)

    // Pobierz zależność z tego scope
    val presenter = diScope.get<MyPresenter>()

    // Lub stwórz inline
    val helper = diScope.getOrCreate { HelperClass(get()) }
}

// W zwykłej coroutine
class MyService {
    suspend fun doWork() = coroutineScope {
        val scopedDI = ADict.Inject.scopedTo(this)
        val worker = scopedDI.get<Worker>()
        // worker jest automatycznie "zniszczony" po zakończeniu coroutine
    }
}

🏷️ Tagi - Grupowanie Zależności

Grupuj zależności tagami dla łatwiejszego zarządzania:

// Rejestracja z tagami
ADict.Inject.single(tags = setOf("network", "api")) { ApiService(get()) }
ADict.Inject.single(tags = setOf("network")) { OkHttpClient() }
ADict.Inject.single(tags = setOf("database")) { AppDatabase() }
ADict.Inject.single(tags = setOf("database", "cache")) { UserCache() }

// Pobierz klucze z tagiem
val networkKeys = ADict.Inject.getKeysWithTag("network")
// ["ApiService", "OkHttpClient"]

// Wyczyść wszystkie zależności z tagiem (np. przy wylogowaniu)
fun onLogout() {
    ADict.Inject.clearTag("user_session")  // czyści tylko sesyjne zależności
}

// Przydatne do modularyzacji
val featureAModule = module {
    single(tags = setOf("featureA")) { FeatureAService() }
    single(tags = setOf("featureA")) { FeatureARepository() }
}

// Wyłączenie feature - wyczyść wszystkie jej zależności
fun disableFeatureA() {
    ADict.Inject.clearTag("featureA")
}

❓ Optional Injection

Opcjonalne zależności - null zamiast crash gdy brak definicji:

class MyActivity : AppCompatActivity() {
    // Zwykłe inject - crash jeśli nie zarejestrowane
    val api: ApiService by inject()

    // Optional inject - null jeśli nie zarejestrowane
    val analytics: AnalyticsService? by injectOptional()
    val crashReporter: CrashReporter? by injectOptional()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Używaj bezpiecznie
        analytics?.trackScreen("MainActivity")
        crashReporter?.log("Activity created")

        // Lub z elvis operator
        val tracker = analytics ?: NoOpAnalytics()
    }
}

// Przydatne dla opcjonalnych feature'ów
class PaymentService(
    private val fraudDetection: FraudDetectionService?  // może nie być w lite version
) {
    fun processPayment(amount: Double) {
        fraudDetection?.check(amount)  // tylko jeśli dostępne
        // ... process payment
    }
}

🔍 Circular Dependency Detection

Wykrywanie cyklicznych zależności (tylko w debug - kosztowne):

// Włącz wykrywanie (tylko debug!)
if (BuildConfig.DEBUG) {
    ADict.Inject.enableCircularDependencyCheck()
}

// Teraz jeśli masz cykliczną zależność:
// A wymaga B, B wymaga C, C wymaga A
// Dostaniesz jasny błąd:

// IllegalStateException: Circular dependency detected:
// ServiceA -> ServiceB -> ServiceC -> ServiceA

// Zamiast trudnego do debugowania StackOverflowError

// Wyłącz gdy nie potrzebujesz (performance)
ADict.Inject.disableCircularDependencyCheck()

⚡ Eager Initialization

Natychmiastowa inicjalizacja zamiast lazy:

// Normalny singleton - tworzony przy pierwszym użyciu (lazy)
ADict.Inject.single { SlowService() }

// Eager singleton - tworzony OD RAZU przy rejestracji
ADict.Inject.eagerSingle { CrashReporter(context()) }
// CrashReporter jest już gotowy!

// Użyteczne dla:
// - Crash reporterów (muszą być gotowe od startu)
// - Analytics (pierwsze eventy od razu)
// - Loggerów
// - Rzeczy które MUSZĄ działać natychmiast

// Tworzenie obiektu bez rejestracji
val presenter = ADict.Inject.create {
    MyPresenter(get(), get(), get())
}
// presenter jest utworzony z wstrzykniętymi zależnościami
// ale NIE jest zarejestrowany w kontenerze

📡 Injection Events

Nasłuchuj na eventy DI (debugging, monitoring):

// Dodaj listener
InjectionEvents.addListener { event ->
    Log.d("DI", """
        Injected: ${event.className}
        Type: ${event.type}
        Duration: ${event.durationMs}ms
        From cache: ${event.fromCache}
    """.trimIndent())
}

// Przykładowe użycie - slow injection warning
InjectionEvents.addListener { event ->
    if (event.durationMs > 100 && !event.fromCache) {
        Log.w("DI", "Slow injection: ${event.className} took ${event.durationMs}ms")
    }
}

// Wyczyść listenery
InjectionEvents.clearListeners()

🔄 Provider - wielokrotne pobieranie

Provider przydatny gdy potrzebujesz factory w klasie bez bezpośredniego dostępu do Inject:

// Provider zwraca świeżą instancję za każdym razem
class OrderService(
    private val userProvider: Provider<User>
) {
    fun processOrder() {
        val currentUser = userProvider.get()  // Lub userProvider()
        println("Processing order for ${currentUser.name}")
    }
}

// Rejestracja - użyj provider() w DefinitionContext
ADict.Inject.single {
    OrderService(provider())  // automatycznie Provider<User>
}

// Lub pobierz Provider bezpośrednio
val userProvider: Provider<User> = ADict.Inject.provider()
val namedProvider: Provider<String> = ADict.Inject.provider("apiUrl")

📦 Multi-binding - wiele implementacji

Idealne dla pluginów, interceptorów, event handlerów:

// Interfejs dla interceptorów
interface Interceptor {
    fun intercept(request: Request): Request
}

// Rejestruj wiele implementacji tego samego interfejsu
ADict.Inject.intoSet<Interceptor> { LoggingInterceptor() }
ADict.Inject.intoSet<Interceptor> { AuthInterceptor(get()) }
ADict.Inject.intoSet<Interceptor> { CacheInterceptor() }
ADict.Inject.intoSet<Interceptor> { RetryInterceptor() }

// Pobierz wszystkie naraz jako Set
val interceptors: Set<Interceptor> = ADict.Inject.getAll()

// Użycie w kliencie API
class ApiClient(interceptors: Set<Interceptor>) {
    fun execute(request: Request): Response {
        var req = request
        interceptors.forEach { req = it.intercept(req) }
        return doExecute(req)
    }
}

// Rejestracja z automatycznym wstrzyknięciem wszystkich
ADict.Inject.single { ApiClient(getAll()) }

🛡️ Conditional Registration

Rejestruj tylko jeśli nie istnieje - idealne dla domyślnych implementacji:

// singleOrNull rejestruje TYLKO jeśli nie ma jeszcze definicji
// Biblioteka rejestruje domyślną implementację:
ADict.Inject.singleOrNull<Logger> { ConsoleLogger() }

// Aplikacja może nadpisać PRZED (ta wygrywa):
ADict.Inject.single<Logger> { FirebaseLogger() }

// singleOrNull zwraca Boolean - czy zarejestrowało
if (ADict.Inject.singleOrNull<Cache> { MemoryCache() }) {
    println("Użyto domyślnego cache")
} else {
    println("Cache już był zarejestrowany")
}

// Przydatne dla modułów bibliotecznych
val coreModule = module {
    // Te zostaną użyte tylko jeśli aplikacja nie dostarczy swoich
    singleOrNull<HttpClient> { DefaultHttpClient() }
    singleOrNull<JsonParser> { GsonParser() }
}

📊 Statystyki kontenera

Monitoruj stan kontenera DI:

// Podstawowe liczniki
println("Zarejestrowane: ${ADict.Inject.count()}")
println("Zcache'owane: ${ADict.Inject.cachedCount()}")

// Pełne statystyki
val stats = ADict.Inject.stats()
println(stats)
// Output: Inject Stats: 15 definitions (10 singletons, 5 factories), 8 cached, 2 sets

// Szczegóły
println("Singletons: ${stats.singletons}")
println("Factories: ${stats.factories}")
println("Multi-binding sets: ${stats.multiBindingSets}")

🔔 Lifecycle Callbacks (onCreate/onDispose)

Wykonuj kod przy tworzeniu i usuwaniu singletonów:

// onCreate - wywoływane gdy singleton jest tworzony po raz pierwszy
// onDispose - wywoływane gdy singleton jest usuwany (dispose/reset)
ADict.Inject.single(
    onCreate = { db ->
        Log.d("DI", "Database created: $db")
        Analytics.track("db_initialized")
    },
    onDispose = { db ->
        db.close()
        Log.d("DI", "Database closed")
    }
) {
    Room.databaseBuilder(context(), AppDatabase::class.java, "app.db").build()
}

// Ręczne usunięcie singletona (wywołuje onDispose)
ADict.Inject.dispose<AppDatabase>()

// Usunięcie wszystkich singletonów (wywołuje wszystkie onDispose)
ADict.Inject.disposeAll()

// reset() też wywołuje onDispose dla wszystkich
ADict.Inject.reset()

🔗 Alias - jeden obiekt, wiele typów

Pozwala pobierać ten sam obiekt pod różnymi typami:

// Rejestrujesz Application
ADict.Inject.single<Application> {
    context().applicationContext as Application
}

// Tworzysz alias - Context będzie zwracał Application
ADict.Inject.alias<Context, Application>()

// Teraz możesz pobierać pod oboma typami!
val app: Application = ADict.Inject.get()
val ctx: Context = ADict.Inject.get()  // Też zwróci Application

// Praktyczne użycie - klasa wymaga Context
class MyRepository(private val context: Context) {
    // ...
}

// Działa mimo że zarejestrowaliśmy Application
ADict.Inject.single { MyRepository(get()) }  // get<Context>() zwróci Application

📥 Parametrized Injection

Przekazuj parametry runtime przy tworzeniu instancji:

// Rejestracja factory z parametrami
ADict.Inject.factoryWithParams<UserDetailViewModel> { params ->
    val userId: String = params.get()   // Pobiera kolejny parametr
    val mode: String = params[1]        // Lub po indeksie
    UserDetailViewModel(userId, mode)
}

// Użycie - przekaż parametry przy pobieraniu
val vm: UserDetailViewModel = ADict.Inject.getWithParams(
    parametersOf("user123", "edit")
)

// Praktyczny przykład - ekran szczegółów użytkownika
class UserDetailActivity : AppCompatActivity() {
    private val userId by lazy {
        intent.getStringExtra("USER_ID")!!
    }

    private val viewModel by lazy {
        ADict.Inject.getWithParams<UserDetailViewModel>(
            parametersOf(userId)
        )
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // viewModel jest już gotowy z właściwym userId
    }
}

⚡ Eager Initialization

Natychmiastowe tworzenie singletonów (nie lazy):

// Domyślnie singletony są lazy (tworzone przy pierwszym użyciu)
// Czasem chcesz utworzyć od razu przy starcie aplikacji

// Sposób 1: Rejestruj i od razu pobierz
ADict.Inject.single { CrashReporter(context()) }
ADict.Inject.eager<CrashReporter>()

// Sposób 2: Krócej - eagerSingle robi oba na raz
ADict.Inject.eagerSingle { AnalyticsService(context()) }

// Praktyczne użycie - w Application.onCreate
class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        ADict.init(this)

        // Te serwisy muszą działać od początku
        ADict.Inject.eagerSingle { CrashReporter(context()) }
        ADict.Inject.eagerSingle { PerformanceMonitor(context()) }
    }
}

🔄 Refresh Singleton

Odświeżanie instancji singletona gdy konfiguracja się zmieni:

// refreshSingleton - usuwa starą instancję i tworzy nową

// Po zmianie języka
fun onLocaleChanged(newLocale: Locale) {
    Locale.setDefault(newLocale)
    ADict.Inject.refreshSingleton<DateFormatter>()
    ADict.Inject.refreshSingleton<ResourceProvider>()
}

// Po wylogowaniu - odśwież serwisy z cache użytkownika
fun onLogout() {
    ADict.Inject.refreshSingleton<UserCache>()
    ADict.Inject.refreshSingleton<SessionManager>()
}

// refreshSingleton wywołuje onDispose dla starej instancji!
ADict.Inject.single(
    onDispose = { cache -> cache.clear() }
) { UserCache() }

// Wywoła cache.clear() przed utworzeniem nowej instancji
ADict.Inject.refreshSingleton<UserCache>()

🎯 Get Or Default

Pobierz zależność lub użyj domyślnej wartości (fallback):

// Pobierz zależność lub użyj domyślnej wartości
val logger: Logger = ADict.Inject.getOrDefault { ConsoleLogger() }

// Różnica vs getOrNull:
// - getOrNull<Logger>() -> Logger? (może być null)
// - getOrDefault { ConsoleLogger() } -> Logger (nigdy null)

// Praktyczne zastosowania:
val cache: Cache = ADict.Inject.getOrDefault { NoOpCache() }
val analytics: Analytics = ADict.Inject.getOrDefault { NoOpAnalytics() }

// Przydatne w bibliotekach - nie wymagaj rejestracji, ale pozwól nadpisać
class MyLibrary {
    private val logger = ADict.Inject.getOrDefault<Logger> {
        DefaultLibraryLogger()
    }
}

⚡ Porównanie z Konkurencją

Feature ADict.Inject Koin Hilt
Setup✅ 0 linii⚠️ ~10 linii❌ ~30+ linii + plugin
Adnotacje✅ Brak✅ Brak❌ Wymagane wszędzie
Code generation✅ Brak✅ Brak❌ Tak (kapt/ksp)
Czas kompilacji✅ Bez wpływu✅ Bez wpływu❌ Znaczący
Curva uczenia✅ 5 min⚠️ 30 min❌ 2+ godziny
Provider
Multi-binding
Parametrized⚠️ AssistedInject
Lifecycle Callbacks
Alias
Eager Init
Refresh Singleton
Conditional Reg
Debug Graph⚠️
Stats