Inject
Ultra-prosty Dependency Injection dla Androida
🎯 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.
📦 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")
}
"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
}
}
❓ 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 | ✅ | ❌ | ❌ |