Замечали некий companion object в интерфейсах Hilt-модулей? Что он делает, как он работает под капотом, почему так популярен в Hilt-модулях, и почему нельзя обойтись обычными классами? Сегодня я развею эту магию!

Разбираться будем на этом примере:

@Module
@InstallIn(SingletonComponent::class)
interface DataModule {
    @Binds
    @Singleton
    fun bindUserRepository(
        impl: UserRepositoryImpl
    ): UserRepository

    companion object {
        @Provides
        @Singleton
        fun provideRetrofit(): Retrofit {
            return Retrofit.Builder()
               .baseUrl("https://api.example.com")
               .build()
        }
    }
}

Так как Hilt — библиотека с большой кодогенерацией, многое здесь происходит под капотом, и нам остаётся лишь вешать нужные аннотации. Для тех, кто забыл, что значат аннотации в Hilt, просмотрите документацию.

Давайте сделаем акцент на аннотациях @Provides и @Binds. Для Hilt это два противоположных понятия:

  • @Binds — аннотация, указывающая Hilt, что данный метод связывает интерфейс с его реализацией. Этот метод должен быть без реализации, так как Hilt сам напишет реализацию метода, возвращая интерфейс и передавая ему при необходимости зависимости!
    Ну и так как в методах с аннотацией @Binds мы не должны писать тело, данные методы должны быть абстрактными, то есть без реализации. Такие методы могут быть либо в interface, либо в abstract class.

@Module
@InstallIn(SingletonComponent::class)
interface DataModuleFirst {
    @Binds
    @Singleton
    fun bindUserRepository(
        impl: UserRepositoryImpl
    ): UserRepository
}

// или

@Module
@InstallIn(SingletonComponent::class)
abstract class DataModuleSecond {
    @Binds
    @Singleton
    abstract fun bindUserRepository(
        impl: UserRepositoryImpl
    ): UserRepository
}
  • @Provides — аннотация, указывающая Hilt, что данный метод создаёт и возвращает некий объект (зависимость). В методе с аннотацией @Provides мы должны сами сконструировать объект и вернуть его!
    Так как в методах с аннотацией @Provides мы должны писать тело, в котором создаём объект, данные методы должны быть не абстрактными, то есть с обязательной реализацией. Такие методы могут быть либо в object, либо в class.

@Module
@InstallIn(SingletonComponent::class)
object DataModuleFirst {
    @Provides
    @Singleton
    fun provideRetrofit(): Retrofit {
        return Retrofit.Builder()
            .baseUrl("https://api.example.com")
            .build()
    }
}

// или

@Module
@InstallIn(SingletonComponent::class)
class DataModuleSecond {
    @Provides
    @Singleton
    fun provideRetrofit(): Retrofit {
        return Retrofit.Builder()
            .baseUrl("https://api.example.com")
            .build()
    }
}

Данные аннотации требуют различного поведения методов! Вы не сможете запихнуть @Provides-метод в interface, в котором находятся @Binds-методы (не прибегая к помощи companion object), и не сможете написать @Binds-метод в object, в котором находятся @Provides-методы.

Пробуем добавить @Provides-метод в интерфейс и получаем незамысловатую ошибку.
Пробуем добавить @Provides-метод в интерфейс и получаем незамысловатую ошибку.
Пробуем добавить @Binds-метод в object и получаем соответствующую ошибку.
Пробуем добавить @Binds-метод в object и получаем соответствующую ошибку.

Какое решение пришло в голову первым? Может, написать отдельно interface со своими @Binds-методами, и отдельно object со своими @Provides-методами? Получится неудобно:

@Module
@InstallIn(SingletonComponent::class)
interface BindsDataModule {
    @Binds
    @Singleton
    fun bindUserRepository(
        impl: UserRepositoryImpl
    ): UserRepository
} 

@Module
@InstallIn(SingletonComponent::class)
object ProvidesDataModule {
    @Provides
    @Singleton
    fun provideRetrofit(): Retrofit {
        return Retrofit.Builder()
           .baseUrl("https://api.example.com")
           .build()
    }
} 

Если вспомнить изначальный пример и посмотреть на этот, первый кажется более привлекательным из-за своей компактности.

А может, писать только @Provides-методы? Но будет много лишнего кода, который за нас может генерировать Hilt.

Наша задача — комбинировать два типа методов DI в одном объекте. companion object — идеальное решение.

Что такое companion object?

сompanion object — это объект-компаньон внутри объекта в Kotlin. То есть, он позволяет создать внутри объекта вспомогательный класс, который является статическим, и методы в нем — тоже статические.

Иногда (в зависимости от версии Kotlin) в Hilt-модулях методам с @Provides-аннотациями, лежащим в companion object, нужно дополнительно навешивать аннотацию @JvmStatic, чтобы гарантировать статику метода в Java-коде.

Hilt требует, чтобы @Provides-методы в интерфейсах были статическими, потому что они не могут быть иными, ведь создать экземпляр класса-интерфейса не получится. Это и помогает сделать компаньон, создающий в интерфейсе класс со статическими методами.

Ошибка создания экземпляра класса-интерфейса
Ошибка создания экземпляра класса-интерфейса

Давайте посмотрим, что происходит (в общем плане) с Kotlin-интерфейсом с companion object при компиляции в Java-код:

public interface DataModule {
    public static final class Companion {
        public Retrofit provideRetrofit() {
            return new Retrofit.Builder().baseUrl("https://api.example.com").build();
        }
    }
}

Создаётся класс внутри интерфейса, в котором лежат созданные нами методы.

Теперь, думаю, вы понимаете, как нам сможет помочь companion object при создании Hilt-модуля, в котором нам нужно комбинировать методы с реализацией и без. Мы можем в интерфейс, где нужно создавать абстрактные методы без реализации, добавить companion object, в котором, как в обычном классе, создавать методы с реализацией, тем самым собирая разного типа методы в одном интерфейсе!

Итак, вот такое рассуждение должно помочь вам понять, что такое companion object, как он работает и почему мы его используем в Hilt-модулях. Очень надеюсь, что вы тоже поняли, и теперь будете писать код, зная, как он работает и для чего вы его пишете.

Комментарии (3)


  1. laptev_am
    05.03.2026 22:01

    Хочется и по содержанию статьи проехаться и по личности... походу я так и сдохну с отрицательной кармой :)

    Автор, ты - стажер, и тебе рано еще писать технические статьи, наберись опыта. Формально ты прав, и формально так можно делать. Но этот подход НЕ полулярный, и так НЕ делают. Потому что помимо одного репозитория и экземпляра Retrofit, будут еще репозитории (внезапно), API, которые ты получаешь из Retrofit, другие сущности, совершенно не связанные с сетевой активностью. Поддержка файла с таким подходом превратится в ад при наличии сколь-нибудь чуть более сложной логики, чем единственный репозиторий. Принцип Single Responsibility придуман не просто так. Дели по смыслу ответственности даже модули в DI, не стоит усложнять себе жизнь на ровном месте.

    Я как-то писал практический гайд по Hilt для новичков


    1. MarkDoroshko Автор
      05.03.2026 22:01

      Добрый день! Спасибо за ваш комментарий и уделённое время моей статье.

      Однако не могу согласиться с вами по ряду пунктов.

      Во-первых, вы критикуете пример за то, что в одном модуле смешаны репозиторий и Retrofit и говорите о принципе единственной ответственности. Но суть статьи была в другом - объяснить понятие и роль companion object и разницу между @Provides и @Binds в Hilt. Пример в статье был спроектирован намеренно минималистичным, чтобы сфокусировать внимание читателя на синтаксисе (interface + companion object), а не на архитектуре. Утверждать, что автор предлагает так проектировать Hilt-модули на основе одного простого примера - некорректно. Это то же самое, что обвинять учебник математики за наличие примеров 2+2, а не интегралов.

      Во-вторых, фразы «ты - стажер», «тебе рано еще писать статьи» - это не аргументы, а тем более в профессиональном сообществе.
      Я прекрасно понимаю, что в реальном проекте стоит делить модули по фичам или слоям (NetworkModule, DatabaseModule, UseCaseModule и т.д.). Но если бы я в статье приводит десятки примеров с 30+ зависимостями, читатель потерял бы суть.


      1. laptev_am
        05.03.2026 22:01

        Задело? Окей, я попробую объяснить, как я это воспринимаю.

        Читаю статью и вижу:

        Может, написать отдельно interface со своими @Binds-методами, и отдельно object со своими @Provides-методами? Получится неудобно

        Первая моя мысль: "Автор - сраный диверсант. Сейчас джуны начитаются, а потом будут такую хуиту на ревью приносить." Потом я захожу в профиль и вижу, что написано "стажер" и "Люблю промышленную разработку! Изучаю Android-разработку на Kotlin...". Моя вторая мысль: "Стажер открыл магию статики в Kotlin".

        Комментарий я все-таки решил написать, но писать "КГ/АМ" как-то не сильно конструктивно, поэтому я немного раскрыл мысль, почему этот "креатив - говно". Тем более ты же буквально сейчас сам написал:

        Я прекрасно понимаю, что в реальном проекте стоит делить модули по фичам или слоям

        Если в реальном проекте твой подход со статикой используется чуть более чем никогда (возможно в очень-очень редких случаях и с полным пониманием зачем и почему, и то вряд ли так будут делать), то зачем ты вообще это все написал?

        Однако, я прошу прощения за мой резкий тон. Тут я не прав, признаю.