На данный момент я работаю с весьма развесистыми проектами (один из них состоит из почти 120 градл модулей) и уже достаточно давно разные факторы подталкивали меня написать статью о том, как я организую свои проекты: стажеры и коллеги, чтение различных статей и книг. Понятное дело, что не существует серебряной пули, но я надеюсь, что эта статья поможет кому-то в понимании, как можно организовывать проекты. Добро пожаловать в комментарии для обмена опытом :)
Статью планируется разделить от большего к меньшему - от организации всего проекта до организации UI экранов. Все проекты я создаю с помощью gradle
и, соответственно, в рамках статьи слово "модуль" будет обозначать gradle
модуль.
Дисклеймер об оригинальности
В этой статье будет много очевидных (казалось бы) вещей - общие части приложения должны быть в общих модулях и прочее такое. Тем не менее, такие вещи важно проговаривать во избежание недопонимания подхода.
Дисклеймер о библиотеках
В своих проектах я, как правило, работаю со своим семейством библиотек:
MicroUtils для почти всего, от корутин до репозиториев
Krontab для отложенных и периодических задач
KSLog для логгирования
KTgBotAPI для работы с телеграм ботами
Navigation для собственно UI навигации
Это сделано потому, что популярные библиотеки имеют ряд недостатков, часто критичных на моих проектах. Например, если в библиотеке есть баг, который мешает, иногда можно годами ждать его исправления (особенно если он некритичен для большей части пользователей). Аналогично, если нужна какая-то фича в библиотеке - ситуация примерно как с багом, можно хоть PR запилить, он может проваляться в бэклоге неизвестно сколько.
Тем не менее, все эти инструменты базируются на таких понятных и надёжных решениях, как ktor, kotlinx serialization, koin и многих других.
Организация проекта
Проект обычно разделяется на три большие папки: features
, client
, server
. Бывают ситуации, когда нужно разделить типы клиентов и/или типы серверов, и тогда имеет смысл в папках client
/server
создавать соответствующие подмодули. Пример на диаграмме ниже:

Таким образом, клиенты и серверы никак не зависят друг от друга и могут иметь любой собственный код и способы запуска.
Фичи (модули в папке features) всегда зависят от common и тех фич, которые логически им нужны. В таком случае они не будут зависеть (даже косвенно) от тех частей приложения, которые им не нужны.
В итоге получается, что каждый модуль самостоятелен и располагается в логичном месте, не имея привязок к другим модулям, которые ему не нужны. Это же в итоге помогает IDE понимать, что именно в данном модуле может быть доступно.
Организация фичи
Фича состоит из простых модулей: common
, client
и server

common
модуль отвечает за общий код: модели (например, пользователь, токен авторизации и т.д.), базовые инструменты (математика работы с локациями, стандартные преобразования типов). То есть если что-то в рамках фичи будет использоваться и на сервере, и на клиенте, либо не зависит от расположения (та же математика) - оно идёт в common
.
client
и server
зависят от common
и, соответственно, отвечают за свои части: репозитории, алгоритмы, UI (в случае клиента), конфигурации запуска.
Поскольку я работаю в основном с KMP, каждый модуль может опционально иметь платформенные или общие сорссеты, контент которых составляется примерно по тем же принципам, что и данное разделение модулей.
Организация UI
В работе мы используем классический MVVM по нескольким причинам:
Он очень хорошо расширяется
С ним легко следовать общим принципам программирования (общее не зависит от частного и наоборот)
Каждый элемент легко объясним с точки зрения его присутствия в цепочке
Идеально ложится на большинство UI фреймворков и на сырую работу с UI (тем же html)
Принцип при этом очень простой - View
отрисовывает, ViewModel
отвечает за выдачу текущего состояния и логику UI части, Model
- источник данных.
С точки зрения UI, абсолютно не важно, что находится за Model
, поэтому переезды между библиотеками, смена логики какого-то кэширования и прочие не связанные с UI штуки не особо влияют на UI часть как таковую.
Организация DI
В своих проектах мы используем связку Koin + MicroUtils/Startup. Суть очень простая: каждый модуль имеет набор плагинов, каждый из которых может быть тем или иным образом подключен в клиентах и серверах. При этом, как правило, используется следующая диаграмма плагинов:

В данной диаграмме Platform
соответственно заменяется на, например, JVM
/JS
/etc., а Client
можно заменить на Server
для серверных модулей.
При старте сервера, в конфигурации указывается список модулей, которые мы включаем в сервер. Метод старомодный, но работает на 100%, плюс теоретически этот подход легко улучшается с помощью написания соответствующего KSP плагина. Таким же образом работает Client
В плагинах мы имеем две части:
Часть инициализации
Koin
модуля - здесь определяются репозитории,ViewModel
,Model
, фабрикиView
и т.д.Часть старта приложения - здесь запускаются серверные и клиентские сервисы, такие как сервисов автоматизации авторизации на клиентах и сборки мусора на сервере
Подведение итогов, или плюсы/минусы этого безобразия
В двух словах, получается следующая структура:
Есть фичи. В фичах есть общая фича и все остальные, от неё зависящие. Каждая фича делится на common, server и client
Есть клиенты. Модули конечных клиентов зависят от
client
модулей фичей, но каждый клиент зависит только от тех модулей, которые ему нужныЕсть серверы. Модули конечных серверов зависят от
server
модулей фичей, но каждый сервер зависит только от тех модулей, которые ему нужныДля DI используем Koin + MicroUtils/startup
В UI используем
MVVM
.ViewModel
иModel
регистрируются вCommonPlugin
common
модуля,View
регистрируется и встраивается в UI из модулей на платформах
А теперь пришла пора плюсов и минусов, и начнём мы с минусов:
Иногда, особенно в сложных проектах, получается крайне ветвистая структура модулей, что мешает быстро ориентироваться в проекте
Много бойлерплейта. Спасает плагин для идеи SegmentGenerator
Сложности конфигурирования из-за весьма топорных способов запуска проекта/модуля
А теперь к плюсам:
Я еще ни разу не встречал штуку, которую было бы сложно внедрять в проект на такой архитектуре
Всегда понятно, где и что нужно искать
Обычно не возникает вопросов, что куда нужно положить
Благодаря общему следованию принципу
частное зависит от общего
, крайне редко возникают какие-то циклические зависимости и прочие схожие проблемы
В следующих статьях я постараюсь рассмотреть всё описанное на примерах