Замечали некий 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-методы.


Какое решение пришло в голову первым? Может, написать отдельно 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-модулях. Очень надеюсь, что вы тоже поняли, и теперь будете писать код, зная, как он работает и для чего вы его пишете.
