Гипотеза, которую я бы хотел проверить, это полностью декларативный способ создания GUI приложений на Java, с полной совместимостью с другими JVM языками и поддержкой GraalVM, в коде верстки\макете, нет никакой логики, и является очень гибким, верстка возможна как и кодом, так и yaml, xml, rbml (Rubber Markup Language).
Изначально, гипотезу по подобному продукту, я хочу проверить на Desktop платформе, а позже, если это действительно может приглянуться сообществу, реализуем так же привязки к Android платформе. Технически, планируется использовать только Java, Skia.
Так же, rubber будет поддерживать только MVVM, так как я считаю его наиболее успешной идеей среди других, ну и просто, я его хорошо знаю%).
Основная цель фреймворка, не производительность (скорее всего не в этом дело будет), а именно качество кода продукта, и его поддержка.
Вы можете не соглашаться с моими идеями по поводу этой гипотезы\фреймворка, любые комментарии можете оставить в issue, я буду рад обсудить какие-либо вопросы.
⚠️ Дизайн примеров и сам код, может поменяться, или уже поменялся.
Некоторые примеры могут быть на двух языках (Java, Kotlin), но технически, поддерживается любой JVM язык (или даже с GraalVM с FFI, еще больший спектр языков)
-
Приложение, которое на главном мониторе отрисует окно, которое будет выровнено по центру экрана, и которое будет иметь автоматически вычисленный размер, которое так же будет иметь заголовок "Hello World", так же которое будет иметь корневой компонент, который будет создан из home.yaml файла разметки, со стилями из home.css, и с вью моделью HomeViewModel. В это же приложение, мы передаем аргументы из main функции. И просто, запускаем эту всю шарманку
go()
. Ссылка на пример - Java • Ссылка на пример - Kotlin -
Все тоже самое, что и в первом примере, но создание UI происходит именно в коде. Теперь контент (Content), внутри себя так же принимает остальные UI элементы, FlexBox, с двумя дочерними элементами, TextBlock, и Button, которые имеют StyleId (идентификатор стиля), так как стилизация может происходить не только в .css, а в любом другом еще формате (yaml к примеру). Так же устанавливаем Text для этих компонентов, внутри себя он может принимать не только String. А так же привязки, что свойственно для MVVM, к примеру, у TextBlock, есть FormattingBinding, в который мы передаем, с чем мы хотим отформатировать, соответственно, текст (Count: {}), а именно
{}
будет замен на первый аргумент. С кнопкой аналогично, но привязка идет именно по команде, то есть ее действие, в данном примере, мы будем просто увеличивать счетчик во viewmodel. Ссылка на пример - Java • Ссылка на пример - Kotlin -
Пример на RML (Rubber Markup Language), в котором создание UI элементов, происходит в RML, все тоже самое что во втором примере, но в RML. Важное примечание
<>
оператор, является оператором привязки (binding).<
оператор, является оператором односторонней привязки. (Соответственно>
является односторонней, но в обратную сторону). Ссылка на пример
Потому что он гибкий, как резина, по крайней мере, планируется таковым существовать и быть. Так как есть возможность, писать на разных JVM языках, использовать с FFI с GraalVM, в т.ч и просто компилировать в native приложение, верстать и стилизовать из yaml, xml, rml и кода. Расширять компоненты и тестировать их, при помощи декорирования.
Очень примитивный пример на kotlin, (пример)
fun main(args: Array<String>) {
Application(
Renderer(
PrimaryMonitor(
Monitors(),
Window(
WindowPos.CENTER,
WindowSize.AUTO,
WindowTitle("Hello World!"),
Root(
Component(
YamlContent("home.yaml"),
CssStylesheet("home.css"),
HomeViewModel()
)
)
)
)
),
StartupArgs(args)
).go() // А если вы творческая личность, .go!! :)
}
⚠️ Примеры, концепции, цели и философия, является лишь предметом, обсуждения дизайна фреймворка, большинство из вещей описанных ниже, не реализованы, дизайн может поменяться. Но что точно не поменяется, это подход и философия.
Прежде всего, начальная задумка, состоит в том, чтобы быть независимым, от технологии, при выборе языка программирования, безусловно в рамках JVM
.
Если хочется groovy
, scala
, java
, kotlin
, ceylon
или Xtend
, то почему бы их и не использовать?
Никакой зависимости на API компилятора, на язык.
Только чистая Java, и JDK (кстати 21) и несколько библиотек (Skija
, JWM
, Cactoos
, ph-css
, SnakeYAML
).
Таким образом, мы так же упрощаем потенциальную возможность, скомпилировать приложение в нативный образ, собрать его с меньшим выходным размером.
Компиляция в нативный образ, является моей главной целью, которую я бы хотел достичь, с этим, на данный момент, прототипом.
Но к сожалению, это сейчас не представляется технически возможным, в силу того, что с JWM
, придется знатно поколупаться, чтобы добавить поддержку GraalVM
, вероятно я этим в ближайшее время начну заниматься.
Я считаю, это одна из главных проблем сейчас, UI сложно тестировать. Для этого изначально, стараюсь закладываться на "полностью мокабельный UI", чтобы сильно упростить и тестирование фреймворка, и UI.
MVVM
служит предметом больших дискуссий, особенно в android направлении, где он, мягко говоря, абсолютно испорчен как понятие, и привито абсолютно неправильное понимание столь важного паттерна, я считаю.
В этом продукте, коммуникация, происходит по принципам MVVM
, включая в себя такие вещи как, свойство привязки, команды, вью модели.
Вся реализация идеи MVVM
, будет выпущена отдельным продуктом mvvmlight
, о которой возможно, некоторые мои читатели, уже слышали.
Казалось бы зачем, но я нахожу это хорошей практикой, отделять UI от кода, даже если и в коде, он не менее декларативный и не содержит логики. Моя задумка в простом, избавленном от многих пунктуаторов, языке разметки, минимум вербозности.
Пример простого экрана, с использованием FlexBox (все параметры по умолчанию), текстового поля и кнопки.
@declare
vm ~ com.mairwunnx.home.HomeViewModel
FlexBox
TextBox
styleId < "text"
text <> "Count: {}" vm::count
Button
styleId < "increment"
text < "Increment"
command > vm::increment
<
, оператор односторонней привязки влево (в сторону левого свойства привязки)
>
, оператор односторонней привязки вправо (в сторону правого свойства привязки)
<>
, оператор двусторонней привязки (к примеру для текстового поля, а именно для свойства text)
::
, ссылка на свойство привязки\команду
~
, оператор "алиаса" для деклараций в блоке @declare
Для команд всегда используется >
, так как привязка команды может быть только в одну сторону и в правую.
Если требуется кастомизация на более низком уровне, к примеру, создание не стандартного нативного окна (к примеру с кастомизацией области рисования),
или другая логика обработка сообщений системы, или заменить бэкэнд рендерера, то для этого будет класс LowLevelApplication
, (я реально, лучше названия даже не придумал).
Пример использования:
class T {
public static void main(String[] args) {
new LowLevelApplication(
new GraphicsPipeline(new Backend(new OpenGL()), Scale.DEPENDS_ON_SCREEN),
new SystemMessageHandler(new DefaultHandler())
/*new Renderer()*/
).go();
}
}
На данный момент не ясно, насколько гибко получится предоставить, конфигурацию низкоуровневого взаимодействия приложения с системой. Так как на данный момент, в качестве оконного менеджера использую JWM (большая благодарность за титанический труд), но в перспективах, хотелось бы иметь решения на случай "ну оооочень надо".
Мы можем настроить графический конвейер, передать реализацию бэкэнда, а так же задекларировать скейлинг (DEPENDS_ON_SCREEN
это не константа), на данный момент это:
public interface Scale {
Scale NO_SCALE = (Window w) -> 1;
Scale DEPENDS_ON_SCREEN = (Window w) -> w.getScreen().getScale();
float scale(Window w);
}
А так же можем передать кастомную реализацию обработчика сообщения системы (он же, на данный момент имеет некоторые нарушения по SRP, он должен создавать канвас скии),
DefaultHandler
тут чисто для примера.
Я нахожу это очень полезной частью Desktop GUI фреймворка, иметь щепотку контроля и определить декларативным образом, что должно происходить при определенных случаях, с приложением.
Политики передаются непосредственно в Application
класс, ниже представлен комплексный пример конфигурации политик для приложения.
В политиках, декларируется поведение при необработанном исключении, зависании, или каких-либо IO работ на главном потоке приложения (который обрабатывает сообщения системы).
class T {
public static void main(String[] args) {
new Application(
/*new Renderer()*/,
new Policies(
new ExceptionPolicy(
new CrashOn(ClassNotFoundException.class),
new InteruptChain(new Swallow(IllegalArgumentException.class)),
new Report(RuntimeException.class)
),
new StuckPolicy(
new WarnOnExceed(Duration.ofSeconds(1).dividedBy(360) /* 2777777ns == 2.777777ms (360fps) */),
new CrashOnExceed(Duration.ofSeconds(4))
),
new IOPolicy(new Warn(), new Crash()) // Warn and crash in case IO operation is executed on the main thread
)
).go();
}
}
ExceptionPolicy
, декларирует поведение, которое будет выполнено при каком-либо необработанном исключении.
Работает это по принципу цепочки, (сигнатура ExceptionPolicy
допускает использование ListOf
из Cactoos, либо обычный массив (варарг)).
Мы декларируем то, как у нас будет обрабатываться исключения в приложении, к примеру, приложение вылетит, если будет брошен ClassNotFoundException
,
или мы подавим IllegalArgumentException
, и прервем эту цепочку. Или зарепортим куда-нибудь исключение и его трассировку стека.
Report
несуществующий класс, сделан для примера, на его месте может быть репорт куда-угодно.
StuckPolicy
, декларирует то, что будет сделано в приложении, если главный поток "зависнет", аналогично, задекларированные действия выполняются последовательно,
сначала будет предупреждение в System.out
, если цикл завершен более чем за 2.7ms
, (для 360Гц монитора), и краш, если обработчик системных сообщений завис на 4 секунды.
IOPolicy
, декларирует поведение, если на главном потоке приложения, выполняются IO работы (поход в БД, сеть, файловую систему), по аналогичной схеме, что и выше, предупреждает в System.out
и убивает приложение.
На данный момент реализовано очень примитивно, поэтому есть проблемы с производительностью (выделяем отдельный поток на анализ трассировки стека с StackWalker главного потока).
Я лучше решения, не придумал.
Так же, еще в процессе реализации две политики, WarnOnInsufficient
, является полным аналогом Warn<OnInsufficient>
.
Так мы декларируем дополнительные политики, для приложения, предупреждаем если свободной памяти хипа, меньше 512МБ, и предупреждаем, если свободной памяти ПЗУ <=
1GB, убиваем приложение, если свободной памяти в ПЗУ, осталось <=
100MB.
new MemoryPolicy(new WarnOnInsufficient(new MB(512))),
new StoragePolicy(
new WarnOnInsuffient(new GB(1)),
new CrashOnInsuffient(new MB(100))
),
Подобное я видел в JavaFx, идея мне понравилась, и я решил что-то типа сервиса, но под другим "соусом", попробовать реализовать (на данный момент в работе), и в Rubber.
Суть простая, есть какие-то "джобы", которые можем запускать, один раз, периодически, периодические по крону, это может быть полезно для приложений в трее.
Ниже приведен простой пример, с тремя видами задач, которые можно задекларировать. Во все задачи, передается контекст приложения, по которому если потребуется, можно взаимодействовать с UI, или с чем-нибудь еще. Запускаются они все последовательно.
class T {
public static void main(String[] args) {
new Application(
/*new Renderer()*/,
new Jobs(
new PeriodicJob(Duration.ofSeconds(5), new JobAction()),
new PeriodicCronJob(new TextOf("5 * * * *"), new JobAction()),
new SingleShotJob(Lifecycle.START, new JobAction())
)
).go();
}
}
PeriodicJob
, периодическая задача, будет вызываться каждые 5 секунд.
JobAction это интерфейс (чуть сложнее JDK'шного Runnable
), пускай new
не смущает, это просто для примера, что туда должны передать реализующего JobAction.
PeriodicCronJob
, то же самое что и выше, только по CRON паттерну.
SingleShotJob
, задача выполняемая только один раз при X состоянии приложения, сейчас это START
, CLOSE
, MINIMIZE
, (не являются константами по аналогии с Scale
).
Не менее важный компонент любого desktop приложения, это возможность его открыть по диплинку\ссылке, к сожалению, регистрацией диплинка, фреймворк заниматься не может, обычно, это делается на уровне установщика. Но обрабатывать, открыть приложение, обработать URI мы можем.
class T {
public static void main(String[] args) {
new Application(
/*new Renderer()*/,
new Deeplinks(
new Deeplink(new TextOf("myapp://text"), params -> new DeeplinkAction(params)),
new Deeplink(new TextOf("myapp://authenticated"), params -> new DeeplinkAction(params))
)
).go();
}
}
DeeplinkAction
, служит простым примером, это так же является обычным интерфейсом, но в его наследника, мы должны будем передать params и работать непосредственно уже с params (от туда можно и вытащить контекст приложения).
Первый аргумент в конструкторе Deeplink
, наш URI, точнее схема и базовый URL, который мы и будем обрабатывать (и его параметры).
Для работы с ресурсами, мы можем задекларировать их используя Resources
, в котором мы декларируем каждый наш ресурс, который будет использоваться в приложении.
Есть несколько типов ресурсов (которые я выделил), Sounds
, Bundles
, Other
.
new Resources(
new Sounds(
new Sound(new Id(new TextOf("click")), new ResourceOf("click.wav"))
),
new Bundles(
new ResourceBundle(Locale.GERMAN, new ResourceOf("bundles/bundle-de.res")),
new ResourceBundle(Locale.ENGLISH, new ResourceOf("bundles/bundle-en.res"))
),
new Other(
new Other(new Id(new TextOf("conf")), new ResourceOf("conf.xml"))
)
),
Все задекларированные ресурсы, влияют на время запуска приложения, загружаются параллельно, и не ленивые (загрузка происходит на самой ранней стадии).
Так же, можно завернуть любой из ресурсов в LazyResource
, в таком случае, он будет загружен при первом его запросе.
Декларативным образом, мы так же можем задекларировать, на уровне приложения, IPC сообщения, которые мы можем обработать в приложении.
Под капотом, это очень примитивно реализовано, на данный момент, общение все реализуется через ServerSocketChannel
по семейству протокола UNIX
(Unix domain (Local) interprocess communication).
Сообщение декларируется с обобщенным типом (сигнатура обобщенного типа IPCMessage
имеет ограничение на java.io.Serializable
), с которым мы сможем работать, так же декларируется для сообщения "ключ", само название сообщения, для идентификации. Ну и само сообщение NotificationMessage
, к примеру, которое реализует Message
, с методом process
, и конструктор которого принимает аргумент NotificationMessageArg
.
new IPC(
new IPCMessage<SomeMessageArg>("somemessage", SomeMessage::new), // SomeMessageArg implements serializable
new IPCMessage<NotificationMessageArg>("notification", NotificationMessage::new)
),
Важный аспект любого приложения, его конфигурация, как и для пользователей, так и для инженеров, важно иметь возможность настроить что-то в продукте, который он использует. А для инженера, иметь простой API для работы с конфигурацией.
Ниже представлен пример, определения конфигурации для приложения, декларативным образом, с применением Cactoos библиотеки.
IsEnvironmentVariable
это, скаляр (реализует Scalar
).
В зависимости от окружения переменной release
, мы возьмем соответствующие properties
.
Configuration
так же имеет перегрузку для конструктора, чтобы передать напрямую PropertiesConfiguration
.
new Configurations(
new Configuration(
new Id(new TextOf("base")),
new Ternary<>(
new IsEnvironmentVariable("release"),
new PropertiesConfiguration("release.properties"),
new PropertiesConfiguration("debug.properties")
)
)
),
Так же есть другие реализации, RegeditConfiguration
(который работает только на ОС Windows), ну и пока все :).
Раз это уж декларативный фреймворк, почему бы не использовать существующие форматы описания данных, которые полностью по своей сути, декларативны.
Возьмем за пример yaml. (application.yaml
в resources
)
application:
name: "Rubber demo" # Application name, used with logger and some other components.
introspect: # Enable introspection with default settings.
port: 3001 # Local network port for introspection (with another app).
renderer: # Setup renderer!
window: # Setup window!
color: "#ffffff" # Setup default window color
title: "Rubber DWM Demo" # Specify window title (otherwise .jar name used (ProtectedDomain#codeSource->location))
icon: "like.png" # Specify window icon (otherwise default windows icon)
size: "800x600" # Specify window size (otherwise 640x420)
monitor: 0 # Specify monitor (othersise 0 (primary))
root: # Specify rendering root
- content: "home.yaml" # home.yaml will rendered
stylesheet: "home.css" # home.css will used for styling home.yaml
viewmodel: com.mairwunnx.dwm.HomeViewModel # viewmodel which will used for home.yaml
Я оставил комментарии для того, чтобы было легче понять что к чему. Таким образом в пользовательском коде, у нас остается
new Application().go();
Мы можем задекларировать все что нам понадобиться, политики, ресурсы, возможно даже джобы (пока точно не ясно). Все что мы не можем задекларировать по какой-то причине, или хотим отдать откуп конфигурации, то мы просто не декларируем это в коде.
Так же возможна конфигурация с properties (applicaiton.properties
в resources
)
application.name=Rubber demo
application.render.window.clear=#ffffff
application.render.window.title=Rubber DWM Demo
application.render.window.icon=like.png
application.render.window.size=800x600
application.render.monitor=0
Соответственно, чисто технически, мы можем обойтись полностью без кода, используя yaml, properties, для декларирования графического интерфейса и настройки приложения, безусловно viewmodel уже задекларировать подобным образом, не выйдет.
Если есть желание и возможность помочь этому проекту, поспособствовать его разработке, то можете написать на [email protected]
почту, все детали, реквизиты, отправлю ответным письмом.