|
1 | 1 | # Разработка новых плагинов |
2 | 2 |
|
3 | | -**[!]Важно.** |
4 | | -1. Библиотека находится в статусе разработки и миграции на более актуальные решения. |
5 | | -Актуальность данного документа стоит уточнить у холдера библиотеки. В данный момент это **r.choryev@redmadrobot.com** |
6 | | -2. В библиотеке млогут быть спорные решения, но она открыта для предложений. |
7 | | -3. В текущих плагинах, для работы со списками используется [Groupie](https://github.com/lisawray/groupie). |
8 | | -Т.к. эта библиотека требует использование `jcenter` и уже не кажется таким уж подходящим решением, поэтому она будет удаляться из библиотеки. |
9 | | - Поэтому это стоит учесть при разработке ваших новых плагинов и использовать какое-то другое решение. |
| 3 | +## Общая структура |
10 | 4 |
|
| 5 | +Debug Panel построена на подходе с использованием плагинов — каждая функциональность реализуется в виде отдельного модуля-плагина. |
11 | 6 |
|
12 | | -## Общая структура |
13 | | -Debug panel разрабатывается опираясь на подход разработки функционала отдельными плагинами. |
14 | | -В данный момент есть несколько модулей на которых основана работа самой панели и разработка и работа плагинов. |
| 7 | +Базовые модули, на которых основана работа панели: |
15 | 8 |
|
16 | | -* **debug-panel-core** - Реализация самой панели и базовых классов для поддержки системы плагинов. |
17 | | -* **debug-panel-common** - Модуль содержащий общие библиотеки, классы и ресурсы переиспользуемые в плагинах. |
18 | | - Библиотеки пробрасываются как сквозные зависимости при помощи типа зависимости `api`. |
19 | | - Список предоставляемых библиотек можно посмотреть в файле [build.gradle](../debug-panel-common/build.gradle.kts) |
| 9 | +* **panel-core** — реализация панели, базовые классы системы плагинов и событийная модель. |
| 10 | +* **panel-no-op** — пустые реализации публичных API для релизных сборок (исключает отладочный код из продакшена). |
20 | 11 |
|
21 | 12 | ## Создание нового плагина |
22 | | -Для добавления нового плагина необходимо сделать несколько шагов: |
23 | | -1. Создать в дирректории **plugins** новый модуль для реализации своего плагина. |
24 | | - |
25 | | -``` |
26 | | -plugins |
27 | | -| your-plugin |
28 | | -``` |
29 | | -2. Объявить новый модуль в файле **settings.gradle** по примеру уже существующих плагинов. |
30 | | - |
31 | | -``` |
32 | | -include ':your-plugin' |
33 | 13 |
|
34 | | -project(':your-plugin').projectDir = new File(rootDir, 'plugins/your-plugin') |
| 14 | +### 1. Создать модуль |
| 15 | + |
| 16 | +Создайте новый модуль в директории `plugins/`: |
| 17 | + |
| 18 | +``` |
| 19 | +plugins/ |
| 20 | +└── plugin-your-feature/ |
35 | 21 | ``` |
36 | | -3. Добавить в **build.gradle** файл вашего модуля следующие настройки: |
37 | 22 |
|
38 | | -```groovy |
39 | | -android { |
40 | | - /*.......*/ |
41 | | - |
42 | | - compileSdkVersion build_versions.compile_sdk |
| 23 | +### 2. Зарегистрировать модуль в settings.gradle.kts |
43 | 24 |
|
44 | | - defaultConfig { |
45 | | - minSdkVersion build_versions.min_sdk |
46 | | - targetSdkVersion build_versions.target_sdk |
| 25 | +Добавьте модуль по аналогии с существующими плагинами: |
47 | 26 |
|
48 | | - versionCode getVersionCodeFromProperties() |
49 | | - versionName getVersionNameFromProperties() |
50 | | - } |
| 27 | +```kotlin |
| 28 | +// Plugins |
| 29 | +include( |
| 30 | + ":plugins:plugin-your-feature", |
| 31 | +) |
| 32 | +``` |
51 | 33 |
|
52 | | - /*.......*/ |
| 34 | +### 3. Настроить build.gradle.kts |
53 | 35 |
|
| 36 | +Примените convention-плагин, который содержит всю необходимую конфигурацию (compileSdk, minSdk, `explicitApi()`, зависимость на `panel-core` и Compose): |
54 | 37 |
|
55 | | - kotlinOptions { |
56 | | - freeCompilerArgs += "-Xexplicit-api=strict" |
57 | | - } |
| 38 | +```kotlin |
| 39 | +plugins { |
| 40 | + id("convention.debug.panel.plugin") |
58 | 41 | } |
59 | 42 |
|
60 | | -dependencies { |
61 | | - implementation( |
62 | | - project(path: ':debug-panel-core'), |
63 | | - project(path: ':debug-panel-common'), |
| 43 | +description = "Plugin description" |
64 | 44 |
|
65 | | - deps.kotlin.stdlib |
66 | | - ) |
| 45 | +android { |
| 46 | + namespace = "com.redmadrobot.debug.plugin.yourfeature" |
67 | 47 | } |
68 | 48 |
|
| 49 | +dependencies { |
| 50 | + // Только специфичные для плагина зависимости |
| 51 | +} |
69 | 52 | ``` |
70 | | -**[!]Важно. Конфигурация будет меняться при дальнейшей миграции с Groovy на Kotlin** |
71 | 53 |
|
72 | | -4. Создать в своем модуле класс-плагин который и будет отвечать за взаимодействие с DebugPanel. |
73 | | - Для этого класс должен унаследоваться от класса `Plugin()` и реализовать необходимые методы. |
74 | | - В качестве аргументов класса можно передать необходимые для инициализации плагина данные.\ |
75 | | - **(О методе `getPluginContainer()` и `PluginDependencyContainer()` можно будет почитать ниже)** |
76 | | - |
| 54 | +### 4. Создать класс плагина |
| 55 | + |
| 56 | +Класс плагина — точка входа, отвечающая за взаимодействие с DebugPanel. |
| 57 | +Унаследуйтесь от `Plugin()` и реализуйте обязательные методы. |
| 58 | +Подробнее о `getPluginContainer()` и `PluginDependencyContainer` — в разделе ниже. |
| 59 | + |
77 | 60 | ```kotlin |
78 | 61 | public class YourPlugin( |
79 | | - /*some arguments*/ |
| 62 | + /* аргументы для инициализации */ |
80 | 63 | ) : Plugin() { |
81 | 64 |
|
82 | | - internal companion object { |
83 | | - const val NAME = "AWESOME PLUGIN" |
84 | | - } |
85 | | - |
86 | | - override fun getName(): String = NAME |
| 65 | + override fun getName(): String = "YOUR PLUGIN" |
87 | 66 |
|
88 | | - /*Plugin dependency container initializing*/ |
89 | 67 | override fun getPluginContainer(commonContainer: CommonContainer): PluginDependencyContainer { |
90 | | - return YourPluginContainer(sharedPreferences) |
| 68 | + return YourPluginContainer(commonContainer) |
91 | 69 | } |
92 | 70 |
|
93 | | - /*Plugin Fragment initializing*/ |
94 | | - override fun getFragment(): Fragment? { |
95 | | - return YourPluginFragment() |
| 71 | + @Composable |
| 72 | + override fun content() { |
| 73 | + YourScreen() |
| 74 | + } |
| 75 | +} |
| 76 | +``` |
| 77 | + |
| 78 | +Если плагин поддерживает редактирование через экран настроек панели, реализуйте интерфейс `EditablePlugin`: |
| 79 | + |
| 80 | +```kotlin |
| 81 | +public class YourPlugin : Plugin(), EditablePlugin { |
| 82 | + // ... |
| 83 | + |
| 84 | + @Composable |
| 85 | + override fun content() { |
| 86 | + YourScreen(isEditMode = false) |
96 | 87 | } |
97 | 88 |
|
98 | | - /*Plugin Setting Fragment initializing.*/ |
99 | | - override fun getSettingFragment(): Fragment { //Нужно только если есть отдельный экран для настройки плагина. |
100 | | - return YourPluginSettingFragment() |
| 89 | + @Composable |
| 90 | + override fun settingsContent() { |
| 91 | + YourScreen(isEditMode = true) |
101 | 92 | } |
102 | 93 | } |
103 | 94 | ``` |
104 | | -5. Создать **Fragment** экрана плагина(если он нужен) и унаследовать его от `PluginFragment()`. |
105 | | -К фрагменту создать **ViewModel** и унаследовать от `PluginViewModel()`. |
106 | | - В этой связке (Fragment+ViewModel), реализовывать пользовательское взаимодействие пользователя с плагином. |
107 | | - |
108 | | -## PluginDependencyContainer |
109 | 95 |
|
110 | | -В библиотеке не используются библиотеки для реализации DI, т.к.: |
111 | | -1. Не хочется тащить их зависимости в библиотеку. |
112 | | -2. Библиотека не такая большая чтобы реализовывать полноценный DI. |
| 96 | +### 5. Создать UI на Jetpack Compose |
113 | 97 |
|
114 | | -Вместо этого, в библиотеке используется подход с **Service Locator**. |
115 | | -Для этого необходимо создать свой класс-контейнер, унаследовать его от **PluginDependencyContainer** и внутри него инициировать необходимые зависимости. |
116 | | -Если вам для этого понадобится **Context**, его можно получить из **CommonContainer** который прилетает в качестве аргумента в методе `getPluginContainer()` при инициализации плагина. |
117 | | -Пример реализации можно [посмотреть тут](../plugins/accounts-plugin/src/main/kotlin/com/redmadrobot/account/plugin/AccountsPluginContainer.kt) |
| 98 | +UI плагина реализуется с помощью Composable-функций. Для инъекции ViewModel используется хелпер `provideViewModel`: |
118 | 99 |
|
119 | | -## Работа с классом плагина |
| 100 | +```kotlin |
| 101 | +@Composable |
| 102 | +internal fun YourScreen( |
| 103 | + viewModel: YourViewModel = provideViewModel { |
| 104 | + getPlugin<YourPlugin>() |
| 105 | + .getContainer<YourPluginContainer>() |
| 106 | + .createYourViewModel() |
| 107 | + }, |
| 108 | +) { |
| 109 | + val state by viewModel.state.collectAsState() |
| 110 | + // UI |
| 111 | +} |
| 112 | +``` |
| 113 | + |
| 114 | +## PluginDependencyContainer |
| 115 | + |
| 116 | +В библиотеке не используются DI-фреймворки, чтобы не добавлять лишних зависимостей. Вместо этого применяется подход **Service Locator**. |
120 | 117 |
|
121 | | -Класс-плагин, описание которого было в пункте **4**, является точкой инициализации вашего плагина. |
122 | | -Поэтому доступ к данным и различным экземплярам классов нужно реализовывать через него. |
123 | | -Чтобу получить доступ к самому плагину, нужно использовать метод `getPlugin<YourPlugin>()`. |
124 | | -Например для получения контейнера зависимостей плагина, нужно вызвать: |
| 118 | +Для этого создайте класс-контейнер, реализующий интерфейс `PluginDependencyContainer`, и инициализируйте в нём необходимые зависимости. |
| 119 | +`Context` доступен через `CommonContainer`, который передаётся в метод `getPluginContainer()` при инициализации плагина. |
125 | 120 |
|
126 | 121 | ```kotlin |
127 | | -getPlugin<YourPlugin>() |
128 | | - .getContainer<YourPluginContainer>() |
| 122 | +internal class YourPluginContainer( |
| 123 | + private val container: CommonContainer, |
| 124 | +) : PluginDependencyContainer { |
| 125 | + |
| 126 | + private val dataStore by lazy { YourDataStore(container.context) } |
| 127 | + |
| 128 | + val repository by lazy { YourRepository(dataStore) } |
| 129 | + |
| 130 | + fun createYourViewModel(): YourViewModel { |
| 131 | + return YourViewModel(repository) |
| 132 | + } |
| 133 | +} |
129 | 134 | ``` |
130 | 135 |
|
131 | | -**Пример использования плагина для получения ViewModel во Fragment:** |
| 136 | +Пример реализации: [ServersPluginContainer](../plugins/plugin-servers/src/main/kotlin/com/redmadrobot/debug/plugin/servers/ServersPluginContainer.kt) |
| 137 | + |
| 138 | +## Работа с классом плагина |
| 139 | + |
| 140 | +Класс плагина является точкой доступа к данным и зависимостям. |
| 141 | +Для получения экземпляра плагина используйте `getPlugin<YourPlugin>()`: |
132 | 142 |
|
133 | 143 | ```kotlin |
134 | | - private val viewModel by lazy { |
135 | | - obtainShareViewModel { |
136 | | - getPlugin<ServersPlugin>() |
137 | | - .getContainer<ServersPluginContainer>() |
138 | | - .createServersViewModel() |
139 | | - } |
140 | | - } |
| 144 | +getPlugin<YourPlugin>() |
| 145 | + .getContainer<YourPluginContainer>() |
141 | 146 | ``` |
142 | 147 |
|
143 | 148 | ## Области видимости |
144 | 149 |
|
145 | | -Все внутренние классы используемые только для работы плагина и не требующиеся для работы клиентского приложения, должны иметь область видимости **inner** |
| 150 | +Все модули используют `explicitApi()` — модификаторы видимости обязательны для всех объявлений. |
| 151 | +Внутренние классы, не предназначенные для использования в клиентском приложении, должны иметь модификатор `internal`. |
146 | 152 |
|
147 | 153 | ## Тестирование |
148 | 154 |
|
149 | | -Для тестирования плагина, необходимо: |
150 | | -1. Подключить его как зависимость в модуль `sample`. |
| 155 | +Для тестирования плагина: |
| 156 | + |
| 157 | +1. Подключите его как зависимость в модуль `sample`: |
151 | 158 |
|
152 | | -```groovy |
153 | | - debugImplementation(project(path: ':your-plugin')) |
| 159 | +```kotlin |
| 160 | +debugImplementation(project(":plugins:plugin-your-feature")) |
154 | 161 | ``` |
155 | 162 |
|
156 | | -2. Инициировать плагин в **App** классе **sample** приложения. |
| 163 | +2. Инициализируйте плагин в классе `App` sample-приложения: |
157 | 164 |
|
158 | 165 | ```kotlin |
159 | | - DebugPanel.initialize( |
160 | | - application = this, |
161 | | - plugins = listOf(YourPluggin()) |
| 166 | +DebugPanel.initialize( |
| 167 | + application = this, |
| 168 | + plugins = listOf( |
| 169 | + YourPlugin(/* ... */) |
| 170 | + ) |
162 | 171 | ) |
163 | 172 | ``` |
164 | 173 |
|
165 | | -3. Запустить **sample** проект |
| 174 | +3. Запустите sample-проект. |
166 | 175 |
|
167 | | -## No-op зависимости |
| 176 | +## No-op реализации |
168 | 177 |
|
169 | | -Для того чтобы в релизную сборку не попадали реализации публичных классов вашего модуля, необходимо добавить их в модуль no-op зависимостей. |
170 | | -([Подробнее в статье](https://medium.com/@orhanobut/no-op-versions-for-dev-tools-b0a865934398)). \ |
171 | | -Для этого создайте пакет с именем вашего плагина в модуле **debug-panel-no-op** и скопируйте ваши публичные классы доступные пользователю в этот пакет. |
| 178 | +Чтобы отладочный код не попадал в релизную сборку, для каждого плагина необходимо создать no-op реализацию в модуле **panel-no-op**. |
172 | 179 |
|
173 | | -[!]Важно. Поле **package** должно остаться оригинальным. |
174 | | -Таким, каким оно было в вашем модуле. |
| 180 | +Создайте пакет с публичными классами плагина, доступными пользователю, и предоставьте пустые реализации. |
175 | 181 |
|
176 | | -## Публикация |
| 182 | +Важно: **package** должен совпадать с оригинальным пакетом вашего модуля. |
| 183 | + |
| 184 | +В sample-приложении подключение выглядит так: |
177 | 185 |
|
178 | | -Публикация новых плагнинов в основном репозитории должна проходить через создание **Merge Request** в ветку **develop**. |
| 186 | +```kotlin |
| 187 | +debugImplementation(project(":plugins:plugin-your-feature")) |
| 188 | +releaseImplementation(project(":panel-no-op")) |
| 189 | +``` |
| 190 | + |
| 191 | +Подробнее о подходе: [No-op versions for dev tools](https://medium.com/@orhanobut/no-op-versions-for-dev-tools-b0a865934398) |
| 192 | + |
| 193 | +## Публикация |
179 | 194 |
|
180 | | -Публикация на внутренний Maven пока делается вручную. |
181 | | -За публикацией обращаться к r.choryev@redmadrobot.com. |
182 | | -В ближайшее время есть планы пересмотреть этот подход. |
| 195 | +Публикация новых плагинов проходит через создание **Pull Request** в ветку **main**. |
0 commit comments