Мы решаем пойти в сторону single-Activity архитектуры. У нас в модуле app есть MainActivity, которое будет отображать фрагменты. На замену TasksActivity будем создавать TasksFragment. Причем не в app, а в отдельном модуле. 

 

 

 

Создаем новый модуль task. В нем создаем фрагмент TasksFragment:

TasksFragment (:task)

class TasksFragment : Fragment() {

   override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
       return inflater.inflate(R.layout.fragment_tasks, container, false)
   }
  
}

А TasksActivity можно удалять.

 

В этом фрагменте мы планируем использовать Database. Значит надо, чтобы модуль task знал про модуль data

Добавляем зависимость:

build.gradle (:task)

implementation project(path: ':data')

 Фрагменту нужен будет только Database, а значит только модуль data. Про объект FileManager фрагмент ничего знать не будет, поэтому core тут не нужен.


Теперь можно добавлять Database во фрагмент:

TasksFragment (:task)

class TasksFragment : Fragment() {

   @Inject
   lateinit var database: Database

   override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
       return inflater.inflate(R.layout.fragment_tasks, container, false)
   }
}

Пока оставим фрагмент таким. Работу с компонентом добавим чуть позже.

 

Этот фрагмент нам надо показать в MainActivity. Но пока что мы не можем этого сделать. MainActivity из модуля app не знает про TasksFragment из модуля task, потому что мы не добавили зависимость.

Добавляем

build.gradle (:app)

implementation project(path: ':task')

 

Добавим TasksFragment в MainActivity самым простым способом:

activity_main.xml (:app)

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

   <androidx.fragment.app.FragmentContainerView
       android:id="@+id/container"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:name="ru.startandroid.task.TasksFragment"/>

</FrameLayout>

 

Давайте посмотрим на схему модулей:
Было так:

Стало так:

 

Модуль core я подвинул вправо от модуля data для удобства.

Добавился модуль task, в котором создан фрагмент. Этот модуль взаимодействует с двумя модулями: app и data. 

Модулю app необходимо знать про task, чтобы фрагмент TasksFragment можно было использовать в MainActivity. 

А модуль task должен знать про модуль data, чтобы использовать Database во фрагменте.

 

 

Компонент

Осталось определиться с компонентом, который будет инджектить Database в TasksFragment.

У нас фрагмент находится в новом отдельном модуле task. Ранее мы уже говорили о том, что компонент, который будет инджектить объекты во фрагмент, рекомендуется создавать в том же модуле, где и фрагмент. Мы обязательно так и сделаем, но чуть позже. А пока попробуем использовать App компонент, который находится в модуле app.

 

Добавляем в компонент метод инджекта объектов во фрагмент вместо Activity

AppComponent (:app)

@Component(modules = [DataModule::class, CoreModule::class])
interface AppComponent {

   fun injectTasksFragment(tasksFragment: TasksFragment)

}

Т.к. модуль app знает про task, то компонент знает про класс TasksFragment.

Dagger теперь создаст App компонент, который умеет инджектить во фрагмент.

 

Поменяем и мы свою реализацию компонента:

MyAppComponent (:app)

class MyAppComponent(private val context: Context): AppComponent {

   private val dataModule = DataModule()
   private val coreModule = CoreModule()

   override fun injectTasksFragment(tasksFragment: TasksFragment) {
       tasksFragment.database = dataModule.provideDatabase(context, coreModule.provideFileManager())
   }

}

Минимум изменений. Вместо TasksActivity инджектим в TasksFragment.

Компонент готов, теперь надо его использовать во фрагменте.

 

Давайте еще раз посмотрим, как выглядела схема с TasksActivity:

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

 

Схема с фрагментом в модуле task могла бы выглядеть так:

Объект App я убрал, чтобы разгрузить схему. И добавил MainActivity, в котором отображается TasksFragment.

Компонент все также собирает Database. И фрагмент хотел бы использовать этот компонент для получения готового Database. Но есть проблема. Фрагмент ничего не знает про компонент, потому что модуль task не знает про модуль app.

 

Давайте попробуем во фрагменте написать код получения компонента и инджекта:

TasksFragment (:task)

override fun onAttach(context: Context) {
   super.onAttach(context)
   (context.applicationContext as App)
       .appComponent
       .injectTasksFragment(this)
}

Получаем Application объект, приводим его к App, берем из него AppComponent и выполняем инджект.

Это работало в TasksActivity, потому что оно было в модуле app. Но фрагмент находится в модуле task, и ничего не знает ни про App, ни про AppComponent. Поэтому такой код невозможен в этом фрагменте.

 

Давайте рассмотрим эту проблему подробнее:

Когда мы получаем объект Application, нам надо привести его к классу App, чтобы получить компонент. Но если мы по каким то причинам не можем выполнить это приведение, то и до компонента мы не доберемся. Потому что Application ничего не знает про компонент и не может нам его предоставить.

Именно это и происходит во фрагменте. Он знает про Application (т.к. это системный класс), но не знает про App (т.к. он в модуле app)

Когда мы во фрагменте вызываем код context.applicationContext, мы получаем Application объект, который мы не можем привести к App. А значит, не можем получить от него компонент.

Можно ли сделать так, чтобы фрагмент знал про App и AppComponent?

Обычно мы это решали добавлением зависимости. Но сейчас так сделать не получится, потому что модули не могут зависеть друг от друга. Модуль app уже зависит от task. Поэтому модуль task не сможет зависеть от app. Зависимость возможна только в одну сторону.

Но есть другой выход.

 

 

Интерфейсы

Нам при получении компонента надо вместо App и AppComponent (которые видны только в модуле app), использовать другие классы/интерфейсы, которые видны и в app и в task. Тогда фрагмент сможет с ними работать.

Для этого мы в модуле task создадим 2 интерфейса.

Первый интерфейс - для компонента:

TaskComponent (:task)

interface TaskComponent {
   fun injectTasksFragment(tasksFragment: TasksFragment)
}

от него нам нужно только, чтобы он умел инджектить в наш фрагмент

Второй интерфейс для App:

TaskComponentProvider (:task)

interface TaskComponentProvider {
   fun getTaskComponent(): TaskComponent
}

от него нужно, чтобы он умел предоставлять TaskComponent (т.е. реализацию первого интерфейса)

 

Вешаем эти интерфейсы на AppComponent и App. Мы можем это сделать, они видны в app модуле, т.к. app знает про task.

Интерфейс TaskComponent - на интерфейс компонента:

AppComponent (:app)

@Component(modules = [DataModule::class, CoreModule::class])
interface AppComponent: TaskComponent {
   override fun injectTasksFragment(tasksFragment: TasksFragment)
}

Метод не меняется, только добавляется override.

 

Интерфейс TaskComponentProvider - на класс App:

App (:app)

class App: Application(), TaskComponentProvider {

   lateinit var appComponent: AppComponent

   override fun onCreate() {
       super.onCreate()
       appComponent = MyAppComponent(this)
   }

   override fun getTaskComponent(): TaskComponent {
       return appComponent
   }
}

Он теперь предоставляет appComponent в виде TaskComponent

 

Так это выглядит на схеме:

Application класс приложения теперь можно трактовать не только как App, но и как TaskComponentProvider. А AppComponent - как TaskComponent. В модуле app от этого толку никакого. А вот в модуле task фрагмент теперь может получить компонент и использовать его для инджекта:

TasksFragment (:task)

override fun onAttach(context: Context) {
   super.onAttach(context)
   (context.applicationContext as TaskComponentProvider)
       .getTaskComponent()
       .injectTasksFragment(this)
}

Мы все также получаем Application объект через context.applicationContext. Но теперь мы этот класс приводим не к App (из app), а к TaskComponentProvider (из task). А от него уже получаем компонент, но не как AppComponent (из app), а как TaskComponent (из task). И далее выполняем инджект.

 

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

Любое Activity или любой фрагмент из любого модуля могут получить Application класс через Context. Это не проблема. Вопрос в том, в виде какого интерфейса или класса они могут его получить. Потому что чистый Application класс не может предоставить нам компоненты. У него просто нет таких методов. Мы их добавляем в своих классах и интерфейсах.

 

Итоговая схема:

Уберем из нее активити, т.к. оно не играет никакой роли:

Компонент создает Database. Фрагмент использует компонент для инджекта.

 

 

 

Результат

Мы вынесли фрагмент в новый модуль, и решили пока не создавать новый компонент, а использовать старый AppComponent. Это привело к тому, что фрагмент (из модуля task) не мог получить компонент (из модуля app), потому что task не знает про app и не может работать с App и AppComponent.

На помощь пришли интерфейсы. В модуле task мы создали два интерфейса и обернули в них класс App и интерфейс AppComponent. Это позволило фрагменту получить компонент и выполнить инджект.

Также из интересного можно отметить модуль core. Он нужен в app, но не нужен в task. Потому что компонент создает Database, и для этого ему нужен FileManager. А фрагменту не надо знать FileManager, чтобы использовать Database.

 


Присоединяйтесь к нам в Telegram:

- в канале StartAndroid публикуются ссылки на новые статьи с сайта startandroid.ru и интересные материалы с хабра, medium.com и т.п.

- в чатах решаем возникающие вопросы и проблемы по различным темам: Android, Compose, Kotlin, RxJava, Dagger, Тестирование, Performance 

- ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня




Комментарии   

# ОшибкаЕвгений 25.09.2022 04:35
appComponent = MyAppComponent(DataModule(this))

   }

У нас же вроде наша реализация компонента должна контекст принимать
# RE: ОшибкаDmitry Vinogradov 30.09.2022 15:25
Верно, спасибо!
# As приведениеЕвгений 25.09.2022 04:44
(context.applicationContext as TaskComponentProvider)
Вот application могу представить как TaskComponentProvider, но applicationContext хоть убей никак. Может в котлине какое-то отличие есть, тк ооп учил на Java, объясните плиз
# RE: As приведениеDmitry Vinogradov 30.09.2022 15:30
Так applicationContext возвращает именно Application объект, так что все в порядке )
# api зависимость от coreВладислав 28.08.2023 00:03
"Также из интересного можно отметить модуль core. Он нужен в app, но не нужен в task. Потому что компонент создает Database, и для этого ему нужен FileManager. А фрагменту не надо знать FileManager, чтобы использовать Database." - но task все еще знает про core, потому что data принимает core как api зависимость. Как быть?
# RE: api зависимость от coreDmitry Vinogradov 06.09.2023 16:05
В Уроке 5 я упомянул, что использую в проекте только implementation, не api
# АндрейАндрей 01.12.2023 18:39
В итоге получается зависимость нижележащего уровня (task) от вышележащего по иерархии (app). Это нарушает принцип инверсии зависимостей, да и на практике неудобно: теряется инкапсуляция модуля.

Language

Автор сайта

Дмитрий Виноградов

Подробнее можно посмотреть или почитать.

Никакие другие люди не имеют к этому сайту никакого отношения и просто занимаются плагиатом.

Социальные сети

 

В канале я публикую ссылки на интересные и полезные статьи по Android

В чате можно обсудить вопросы и проблемы, возникающие при разработке



Группа ВКонтакте



Поддержка проекта

Яндекс
410011180491924

WebMoney
R248743991365
Z551306702056

Paypal