Кодеин против Кинжала - Не могу заставить Кинжал работать с несколькими модулями - PullRequest
1 голос
/ 08 июля 2019

( x-post от / r / androiddev )

Я просто хотел бы предвосхитить это, сказав, что это не пост "что лучше"; это строго вопрос о том, как я могу построить что-то, используя Dagger (и как я это сделал в Kodein, чтобы проиллюстрировать проблему).

Я уже несколько лет использую Kodein в нескольких рабочих проектах, и мне было так легко с ним работать, что я больше никогда не смотрю на Dagger. Я начал новый личный проект, и я подумал, что дам Dagger еще один шанс.

Для простоты у меня есть 3 модуля (это обычное настольное приложение, а не Android);

  1. Приложение
  2. общий
  3. Google

app содержит один класс App:

class App(
  private val api: GoogleApi,
  private val argParser: ArgParser
) {
  fun run() {
    while(true) {
      api.login(argParser.username, argParser.password);
    }
  }

}

common содержит один класс ArgParser (реализация не важна)

google содержит несколько классов:

class GoogleApi(  
  driveProvider: () -> Drive
) {

  private val drive by lazy {
    driveProvider()
  }

  fun login(username: String, password: String) {
    drive.login() // not real call
  }
}

internal class CredentialRetriever(
  private val transport: NetHttpTransport,
  private val jsonFactory: JacksonFactory
) {

  fun retrieveCredentials() = ...

}

Зависимости для google:

dependencies {

  implementation "com.google.api-client:google-api-client:$googleApiVersion"

  implementation "com.google.oauth-client:google-oauth-client-jetty:$googleApiVersion"

  implementation "com.google.apis:google-api-services-drive:v3-rev110-$googleApiVersion"

}

Я специально использую implementation, потому что я не хочу, чтобы кто-либо напрямую использовал базовые библиотеки Google.

Чтобы заставить это работать в Кодейне, я делаю следующее в main:

fun main(args: Array<String>) {

  val kodein = Kodein {
    import(commonModule(args = args))
    import(googleModule)
    import(appModule)

    bind<App>() with singleton {
      App(
        api = instance(),
        argParser = instance()
      )
    }
  }

  kodein.direct.instance<App>().run()
}

затем в google:

val googleModule = Kodein.Module("Google") {

  bind<CredentialRetriever>() with provider {
    CredentialRetriever(jsonFactory = instance(), transport = instance())
  }

  bind<Drive>() with provider {
    Drive.Builder(
      instance(),
      instance(),
      instance<CredentialRetriever>().retrieveCredentials()
    ).setApplicationName("Worker").build()
  }

  bind<GoogleApi>() with singleton {
    GoogleApi(drive = provider())
  }

  bind<JacksonFactory>() with provider {
    JacksonFactory.getDefaultInstance()
  }

  bind<NetHttpTransport>() with provider{
    GoogleNetHttpTransport.newTrustedTransport()
  }
}

и, наконец, common:

fun commonModule(args: Array<String>) = Kodein.Module("Common") {
  bind<ArgParser>() with singleton { ArgParser(args = args) }
}

Я попытался реализовать это в Dagger, но не смог заставить его работать. Моей первой попыткой было получить Component in app, в котором использовались модули из common и google. Это не сработало, потому что сгенерированный код ссылался на классы, которые не были представлены из google (например, Drive). Я мог бы исправить это, сделав их api зависимостями, но я не хочу показывать их:

// CredentialRetriever and GoogleApi were updated to have @Inject constructors

// GoogleApi also got an @Singleton

@Module
object GoogleModule {

  @Provides
  internal fun drive(
    transport: NetHttpTransport,
    jsonFactory: JacksonFactory,
    credentialRetriever: CredentialRetreiver
  ): Drive =
    Drive.Builder(
      transport,
      jsonFactory,
      credentialRetriever.retrieveCredentials()
    ).setApplicationName("Worker").build()

  @Provides
  internal fun jsonFactory(): JacksonFactory =
    JacksonFactory.getDefaultInstance()

  @Provides
  internal fun netHttpTransport(): NetHttpTransport = 
    GoogleNetHttpTransport.newTrustedTransport()
}

Далее я попытался создать компонент для каждого модуля (то есть модуля gradle):

// in google module

@Singleton
@Component(modules = [GoogleModule::class])
interface GoogleComponent {
  fun googleApi(): GoogleApi
}

// in common module

@Singleton
@Component(modules = [CommonModule::class])
interface CommonComponent {
  fun argParser(): ArgParser
}

Затем в app началось веселье:

// results in "AppComponent (unscoped) cannot depend on scoped components:"

@Component(dependencies = [CommonComponent::class, GoogleComponent::class])
interface AppComponent {
  fun app(): App
}

ОК, так что давайте сделаем так:

// results in "This @Singleton component cannot depend on scoped components:"

@Singleton
@Component(dependencies = [CommonComponent::class ,GoogleComponent::class])
interface AppComponent {
  fun app(): App
}

РЕДАКТИРОВАТЬ : попытался заставить AppComponent использовать пользовательскую область:

// results in "AppComponent depends on more than one scoped component:"

@AppScope
@Component(dependencies = [CommonComponent::class ,GoogleComponent::class])
interface AppComponent {
  fun app(): App
}

Как я могу добиться этого в Dagger? Я прочитал документы, думаю, что я их немного понимаю, но понятия не имею, что делать дальше.

1 Ответ

1 голос
/ 08 июля 2019

Я позволил себе немного изменить пример: а) удалить ненужные детали и б) упростить настройку.

Имеется 3 модуля со следующими классами:

// ----->> app <<-----
class App @Inject constructor(
        private val api: AbstractApi,
        private val argParser: ArgParser
)

// ----->> google <<-----
// expose a public interface
interface AbstractApi

// have our internal implementation
internal class GoogleApi @Inject constructor(
        private val argParser: ArgParser
) : AbstractApi

// ----->> common <<-----

// expose some common class
interface ArgParser

Итакнам нужно связать реализацию для ArgParser как google, так и app.Я использовал ArgParser в качестве примера, как мы можем передавать аргументы нашему API.GoogleApi полностью internal, чтобы убедиться, что ничего не протекает.Мы выставляем только интерфейс AbstractApi.

Я сделал GoogleApi внутренним, чтобы устранить сложность Gradle с помощью реализации / api.Поведение такое же, может быть, даже более строгое: в нашем модуле есть класс, который мы не можем выставить.Таким образом, у нас также есть проверка компилятора.

Мы можем скрыть все детали нашей реализации за компонентом, который мы добавляем в google, чтобы создать нашу GoogleApi реализацию для интерфейса.

// ----->> google
@Component(modules = [ApiModules::class])
interface ApiComponent {
    // has a provision method for our API
    fun api(): AbstractApi

    @Component.Factory
    interface Factory {
        // factory method to bind additional args that we need to supply
        fun create(@BindsInstance parser: ArgParser): ApiComponent
    }
}

@Module
internal interface ApiModules {
    @Binds
    fun bindApi(googleApi: GoogleApi): AbstractApi

}

Мы не используем здесь область, потому что область должна обрабатываться везде, где используется этот компонент.ArgParser - это пример аргумента, который нам может потребоваться для создания объекта.Мы могли бы также использовать @Component.Builder вместо фабрики.

Dagger сгенерирует компонент в том же модуле (google), поэтому не будет никаких проблем с ссылочным кодом.Все, что нам нужно сделать, это получить API в нашем модуле app:

// ----->> app
@Component(modules = [AppModule::class])
interface AppComponent {
    fun app(): App
}

@Module
class AppModule {

    @Provides
    fun provideParser(): ArgParser = object : ArgParser {} // just bind a dummy implementation

    @Provides
    fun provideApi(argParser: ArgParser): AbstractApi {
        return DaggerApiComponent.factory().create(argParser).api()
    }
}

Теперь мы можем использовать фабрику компонентов для создания экземпляра из нашего модуля.Если нам нужна область действия, мы можем добавить ее как обычно для метода @Provides.

Эта настройка должна полностью скрывать любые детали из модуля app за открытым интерфейсом.Сгенерированный код находится в том же модуле.


Почему бы не выставить @Module?A @Subcomponent?

Как сообщалось, добавление модуля к компоненту также сгенерирует заводской код в этом компоненте, который попытается использовать классы, на которые нет ссылок.То же самое относится и к подкомпоненту.

Почему бы не использовать компонентную зависимость?

Поскольку у компонента нет области действия, мы могли бы также добавить его как компонентную зависимость, но мы бы не сталитогда я не смогу добавить область.Кроме того, нам было бы сложнее передавать аргументы, так как мы должны были бы предоставить их при создании компонента.

...