Hilt Multibinding in Android: A Complete Guide
Dependency injection is crucial for building scalable Android applications, and Hilt makes it remarkably straightforward. But when you need…
Hilt Multibinding in Android: A Complete Guide

Dependency injection is crucial for building scalable Android applications, and Hilt makes it remarkably straightforward. But when you need to inject collections of related dependencies, that’s where multibinding comes in. In this article, we’ll explore how to use Hilt’s multibinding feature to manage multiple implementations elegantly, and we’ll tackle the notorious @JvmSuppressWildcards puzzle that trips up many Kotlin developers.
What is Multibinding?
Multibinding allows you to inject a collection (Set or Map) of objects that share a common interface or base type. Instead of manually creating and managing these collections, Hilt assembles them automatically at compile time.
Why is this useful?
Imagine you’re building a payment processing system that supports multiple payment methods: PayPal, Stripe, and Google Pay. Instead of hardcoding each processor or using complex factory patterns, multibinding lets you:
- Add new payment processors without modifying existing code
- Maintain clean separation of concerns
- Test each processor independently
- Enable/disable processors through modules
Types of Multibinding
Hilt supports two types of multibinding:
1. Set Multibinding (@IntoSet)
Injects a Set<T> where order doesn't matter and duplicates are automatically removed.
2. Map Multibinding (@IntoMap)
Injects a Map<K, V> where each binding is associated with a unique key, perfect for lookup-based scenarios.
Real-World Example: Payment Processing System
Let’s build a complete payment processing system using Map multibinding. This is the approach we’ll use when different payment methods need to be accessed by name.
Step 1: Define the Common Interface
interface PaymentProcessor {
fun processPayment(amount: Double)
}
Step 2: Create Concrete Implementations
class PayPalProcessor : PaymentProcessor {
override fun processPayment(amount: Double) {
Log.d(TAG_MAP, "Processing $$amount via PayPal")
// PayPal-specific logic here
}
}
class StripeProcessor : PaymentProcessor {
override fun processPayment(amount: Double) {
Log.d(TAG_MAP, "Processing $$amount via Stripe")
// Stripe-specific logic here
}
}
class GooglePayProcessor : PaymentProcessor {
override fun processPayment(amount: Double) {
Log.d(TAG_MAP, "Processing $$amount via Google Pay")
// Google Pay-specific logic here
}
}
Step 3: Define the Map Keys
For Map multibinding, we need to define keys. Hilt provides several key annotations, but the most common is @StringKey:
@Module
@InstallIn(SingletonComponent::class)
object PaymentModule {
@Provides
@IntoMap
@StringKey("payPal")
fun providePayPalProcessor(): PaymentProcessor {
return PayPalProcessor()
}
@Provides
@IntoMap
@StringKey("stripe")
fun provideStripeProcessor(): PaymentProcessor {
return StripeProcessor()
}
@Provides
@IntoMap
@StringKey("googlePay")
fun provideGooglePayProcessor(): PaymentProcessor {
return GooglePayProcessor()
}
}
Step 4: Inject and Use the Map
Here’s where the magic happens. Hilt automatically assembles all @IntoMap bindings into a single Map<String, PaymentProcessor>:
package com.emon.multibinding
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
const val TAG_MAP = "Multi-Binding-Hilt-map"
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var paymentProcessors: Map<String, @JvmSuppressWildcards PaymentProcessor>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_main)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
// Access payment processors by their keys
paymentProcessors["payPal"]?.processPayment(100.0)
paymentProcessors["stripe"]?.processPayment(10.0)
paymentProcessors["googlePay"]?.processPayment(22.0)
}
}
Notice that you can access any processor by its key and process payments without knowing anything about the specific implementation. This is the power of the Strategy pattern combined with dependency injection.
The @JvmSuppressWildcards Mystery Solved
You probably noticed the strange @JvmSuppressWildcards annotation in our injection code. This is one of the most confusing aspects of using Hilt multibinding in Kotlin. Let's demystify it.
The Problem
When you try to inject multibindings in Kotlin without @JvmSuppressWildcards, you might encounter this error:
error: [Dagger/MissingBinding] Map<String, ? extends PaymentProcessor> cannot be provided
Why Does This Happen?
The issue stems from how Kotlin and Java handle generics differently:
What you define in your module:
@Provides
@IntoMap
@StringKey("payPal")
fun providePayPalProcessor(): PaymentProcessor = PayPalProcessor()
Hilt sees this as: Map<String, PaymentProcessor>
What Kotlin shows to Hilt when you inject:
@Inject
lateinit var paymentProcessors: Map<String, PaymentProcessor>
Because Kotlin talks to Java under the hood, Kotlin may secretly represent this to Hilt as:
Map<String, ? extends PaymentProcessor>
The ? extends is a Java wildcard that means "any type that extends PaymentProcessor."
The conflict:
Hilt provides: Map<String, PaymentProcessor>
Kotlin requests: Map<String, ? extends PaymentProcessor>
Hilt’s type system is very strict. It says:
“I have
Map<String, PaymentProcessor>, but you're asking forMap<String, ? extends PaymentProcessor>. These are NOT exactly the same type, so I can't provide it."
The Solution
Use @JvmSuppressWildcards to tell Kotlin: "Don't add wildcards, use the exact type":
@Inject
lateinit var paymentProcessors: Map<String, @JvmSuppressWildcards PaymentProcessor>
Now Hilt sees exactly what you intended: Map<String, PaymentProcessor> - no wildcards, no mismatch, no error.
Simple Rule to Remember
When injecting Hilt multibindings in Kotlin, use @JvmSuppressWildcards in these scenarios:
For Set multibinding:
class AnimalShelter @Inject constructor(
private val animals: Set<@JvmSuppressWildcards Animal>
)
For Map multibinding:
class ProcessorFactory @Inject constructor(
private val processors: Map<String, @JvmSuppressWildcards Processor>
)
You don’t need to deeply understand the Java/Kotlin interop details. Just remember:
@JvmSuppressWildcards fixes Kotlin/Java generic type mismatch for Hilt multibinding.
Set Multibinding Example
While we focused on Map multibinding, here’s a quick example of Set multibinding:
@Module
@InstallIn(SingletonComponent::class)
object AnalyticsModule {
@Provides
@IntoSet
fun provideFirebaseAnalytics(): AnalyticsTracker {
return FirebaseAnalyticsTracker()
}
@Provides
@IntoSet
fun provideMixpanelAnalytics(): AnalyticsTracker {
return MixpanelAnalyticsTracker()
}
}
@AndroidEntryPoint
class AnalyticsManager @Inject constructor(
private val trackers: Set<@JvmSuppressWildcards AnalyticsTracker>
) {
fun trackEvent(event: String) {
// Send event to all trackers
trackers.forEach { it.track(event) }
}
}
Custom Map Keys
Beyond @StringKey, Hilt supports other key types:
@IntKey
@Provides
@IntoMap
@IntKey(1)
fun provideLevel1Boss(): Enemy = Goblin()
@ClassKey
@Provides
@IntoMap
@ClassKey(MainActivity::class)
fun provideMainPresenter(): Presenter = MainPresenter()
Custom Keys
You can even create your own key annotations:
@MapKey
annotation class PaymentMethodKey(val value: PaymentMethod)
enum class PaymentMethod {
CREDIT_CARD, DEBIT_CARD, PAYPAL
}
@Provides
@IntoMap
@PaymentMethodKey(PaymentMethod.PAYPAL)
fun providePayPalProcessor(): PaymentProcessor = PayPalProcessor()
Best Practices
1. Use Meaningful Keys
Choose keys that clearly identify what each binding represents:
// Good
@StringKey("creditCardProcessor")
// Bad
@StringKey("cc")
2. Consider Using Enums for Keys
Instead of strings, enums provide type safety:
enum class PaymentType { PAYPAL, STRIPE, GOOGLE_PAY }
@MapKey
annotation class PaymentTypeKey(val value: PaymentType)
3. Handle Missing Keys Gracefully
Always use safe calls or provide defaults:
val processor = paymentProcessors["unknown"]
?: DefaultPaymentProcessor()
4. Document Your Multibindings
Add comments to your modules explaining what each binding contributes:
@Module
@InstallIn(SingletonComponent::class)
object PaymentModule {
/**
* Provides PayPal payment processing capability.
* Supports international transactions.
*/
@Provides
@IntoMap
@StringKey("payPal")
fun providePayPalProcessor(): PaymentProcessor = PayPalProcessor()
}
Common Pitfalls and Solutions
1. Forgetting @JvmSuppressWildcards
Problem: Missing binding error Solution: Add @JvmSuppressWildcards to your injection site
2. Duplicate Keys
Problem: Only one binding survives Solution: Ensure all keys in a Map are unique
3. Wrong Component Scope
Problem: Bindings not found Solution: Ensure your module is installed in the correct component (usually SingletonComponent)
4. Null Safety Issues
Problem: map["key"] returns null Solution: Use safe calls map["key"]?.method() or Elvis operator map["key"] ?: default
Testing Multibindings
Testing classes that depend on multibindings is straightforward:
@HiltAndroidTest
class PaymentTest {
@get:Rule
var hiltRule = HiltAndroidRule(this)
@Inject
lateinit var paymentProcessors: Map<String, @JvmSuppressWildcards PaymentProcessor>
@Before
fun setup() {
hiltRule.inject()
}
@Test
fun testAllProcessorsAreAvailable() {
assertTrue(paymentProcessors.containsKey("payPal"))
assertTrue(paymentProcessors.containsKey("stripe"))
assertTrue(paymentProcessors.containsKey("googlePay"))
assertEquals(3, paymentProcessors.size)
}
@Test
fun testPayPalProcessing() {
val processor = paymentProcessors["payPal"]
assertNotNull(processor)
// Test processor behavior
}
}
When to Use Multibinding
Multibinding shines in these scenarios:
✅ Plugin architectures — Add features without modifying core code ✅ Strategy patterns — Multiple algorithms for the same task ✅ Observer patterns — Multiple listeners or callbacks ✅ Feature flags — Enable/disable features through modules ✅ Multi-tenant apps — Different configurations per client ✅ Analytics — Multiple tracking services ✅ Validators — Chain of validation rules
Conclusion
Hilt multibinding is a powerful feature that enables clean, extensible architecture. By understanding Map and Set multibinding, and mastering the @JvmSuppressWildcards annotation, you can build systems that are easy to maintain and extend.
Key takeaways:
- Multibinding lets you inject collections of related dependencies
- @IntoMap and @IntoSet tell Hilt to contribute to a collection
- @JvmSuppressWildcards fixes Kotlin/Java generic type mismatches
- Map multibinding is perfect for lookup-based scenarios
- Set multibinding works great when you need to process all items
The next time you find yourself managing multiple related implementations, remember: multibinding is your friend. It transforms rigid, hard-to-extend code into flexible, plugin-ready architecture.
Have you used multibinding in your projects? What creative use cases have you discovered? Share your experiences in the comments below!
메타데이터
- post_id
- 2c72bd60e567
- slug
- hilt-multibinding-in-android-a-complete-guide-2c72bd60e567
- url
- https://medium.com/@ahmed.ally2/hilt-multibinding-in-android-a-complete-guide-2c72bd60e567
- canonical_url
- https://medium.com/@ahmed.ally2/hilt-multibinding-in-android-a-complete-guide-2c72bd60e567
- author_url
- https://medium.com/@ahmed.ally2
- status
- ok
- fetched_at
- 2026-06-09 15:37:30