Предисловие
Недавно портировал довольно большой проект с Qt (C++) на Android (Java), в процессе работы часто приходилось применять динамическое связывание объектов. Беда состояла в том что связывание (binding) в отличие от привычных сигналов и слотов в Qt в Java реализовано через лисенеры (listeners), и сколько я не пытался себя убедить что способ этот равноценен и тоже имеет место быть такого же удобства как при использовании сигналов и слотов достичь не удавалось.
Например, нам нужно связать бегунок (QSlider в Qt или SeekBar в Android) с каким либо действием, хотя бы привязать другой бегунок который будет послушно перемещаться следом за первым. В Qt подобная операция выглядит следующим образом:
Пример 1
// Создаём бегунки
QSlider *primary = new QSlider(this);
QSlider *secondary = new QSlider(this);
// Размещаем в слое, инициализируем
// ...
// Связываем перемещение первого со вторым
connect(primary, SIGNAL(valueChanged(int)), secondary, SLOT(setValue(int)));
В результате получаем связь сигнала valueChanged() бегунка primary со слотом setValue() бегунка secondary. То же самое на Android:
Пример 2
// Создаём бегунки
SeekBar firstBar = (SeekBar)findViewById(R.id.firstSeekBar);
SeekBar secondBar = (SeekBar)findViewById(R.id.secondSeekBar);
// Инициализируем
// ...
// Связываем перемещение первого со вторым
firstBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
@Override
// ........
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
secondBar.setProgress(progress);
}
});
И в результате получаем тоже самое, то есть связываем перемещение firstBar и secondBar.
Давайте разберём что здесь происходит. Где-то в недрах firstBar есть переменная типа OnSeekBarChangeListener которая при наступлении перемещения проверится на null и если вдруг окажется ненулевой, а так оно и случится, будет вызван её метод onProgressChanged() с соответствующими параметрами который в свою очередь вызовет secondBar.setProgress(progress) и установит значение второго бегунка.
Всё предельно ясно и понятно, хотя и несколько громоздко. Qt в данном случае более лаконичен, хотя для реализации динамического связывания выходит за рамки C++ догенеривая код в процессе сборки проекта с помощью MOC (Meta Object Compiler). За лаконичность приходится расплачиваться, и это становится очевидным когда в процессе отладки попадаешь в догенереный код. Но к счастью, если следовать простейшим правилам делать это приходится крайне редко.
Но вернёмся к Android. Все классы Android API имеют достаточный набор лисенеров чтобы обеспечить удобство их использования, но что делать если в наличии большой массив кода оперирующий сигналами и слотами? Погуглив, я нашёл несколько реализаций сигналов и слотов на Java, самая достойная из которых, что не удивительно, в составе библиотеки Qt Jambi, несправедливо забытой реализации Qt на Java. Отличная реализация однако не устроила меня по нескольким причинам, самая веская из которых, несоответствие синтаксиса оригиналу, странно что одна и та же технология в составе библиотек под C++ и Java реализована столь различно.
В результате появилась идея реализовать сигналы и слоты под Android на Java самостоятельно.
Задача
Реализовать на Java механизм сигналов и слотов максимально приближенный к синтаксису Qt C++ используя Android API.
Реализация
Что получилось
После нескольких попыток был написан Java класс Connector менее чем о 600 строках имеющий несколько статических методов и статическую карту (Map) сигналов и слотов, по сути являющийся синглтоном (singleton). Весь функционал заключён в четырёх статических методах:
- boolean connect(Object sender, String signal, Object receiver, String slot, ConnectionType type)
- void disconnect(Object sender, String signal, Object receiver, String slot)
- void emit(Object sender, String signalName, Object ...params)
- Object sender()
- connect() позволяет связать сигнал и слот, type в данном контексте экземпляр перечисления который может принимать значения DirectConnection и QueuedConnection по аналогии с Qt. DirectConnection, значение по умолчанию, работает также как лисенер в примере 1. QueuedConnection использует Handler из Android API для реализации асинхронного вызова слота. Остальные виды коннектов остались не реализованными, т.к. перечисленные 2 покрывали 100% случаев встречающихся в портируемом проекте.
- disconnect() операция обратная коннекту. Разрывет связь сигнала со слотом. Имеет варианты с 4мя, 3мя, 2мя, и 1м параметром. Которые соответственно разрывают связь сигнала с конкретным слотом, связь сигнала со всеми слотами ресивера (receiver), связь сигнала со всеми слотами, и связь сэндера (sender) со всеми слотами.
- emit() позволяет из произвольного места программы послать сигнал. Первый параметр содержит ссылку на объект (sender) который данный сигнал посылает, обычно this. Объявления сигнала, как метода или переменной, в отличие от других реализаций в данном случае не требуется, сигнал просто произвольная строка которая непременно должна совпадать со строкой переданной в методе connect(). Далее через запятую может следовать произвольное количество параметров произвольных типов, все из которых или хотя бы первые из них должны совпадать с параметрами слота
- sender() очень полезный метод, также аналог из Qt, вызываемый из тела слота и возвращающий указатель (в Java ссылку типа Object) на объект пославший сигнал.
В примере с бегунками коннект выглядел бы так:
Пример 3
// К сожалению стандартные элементы Android API требуют реализации лиснера
private static class SeekBarChangeListener implements SeekBar.OnSeekBarChangeListener {
private Object mSender = null;
public SeekBarChangeListener(Object sender) {
mSender = sender;
}
@Override
// ........
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
Connector.emit(mSender, "progressChanged", progress);
}
}
// Создаём бегунки
SeekBar firstBar = (SeekBar)findViewById(R.id.firstSeekBar);
SeekBar secondBar = (SeekBar)findViewById(R.id.secondSeekBar);
// Инициализируем
// ...
// Учим firsBar посылать сигналы
firstBar.setOnSeekBarChangeListener(new SeekBarChangeListener(firstBar));
// Связываем перемещение первого со вторым
Connector.connect(firstBar, "SIGNAL(progressChanged(int))"
, secondBar, "SLOT(setProgress(int))");
С первого взгляда может показаться что реализация с помощью Connector'а более громоздка чем с помощью лисенера, но не стоит забывать что SeekBar класс заточенный под использование лисенера, как и все другие стандартные классы Android API, потому приходится использовать врапперы (wrappers). Гораздо большую выгоду можно получить используя коннектор при разработке своих классов, или портируя Qt проекты на Android:
- не нужно создавать интерфейс для лисенера
- не нужно создавать под него переменную
- не нужен метод сеттер для лисенера
Более сложные примеры, подробно о деталях реализации, читайте в следующей статье.
(Продолжение следует)
Автор: AlexanderOfitserov