← Back to list

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…

Ahmed Ally · 2026-05-25 20:42 · 0 claps · 5.3 min read
#dagger-hilt #dagger-2 #dagger-multibinding #androiddev #jvmsuppresswildcards
Open on Medium ↗
Wiki topics: 📰 · Journalism & News

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 for Map<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:

  1. Multibinding lets you inject collections of related dependencies
  2. @IntoMap and @IntoSet tell Hilt to contribute to a collection
  3. @JvmSuppressWildcards fixes Kotlin/Java generic type mismatches
  4. Map multibinding is perfect for lookup-based scenarios
  5. 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