Предисловие
Недавно портировал довольно большой проект с 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:
- не нужно создавать интерфейс для лисенера
- не нужно создавать под него переменную
- не нужен метод сеттер для лисенера
Более сложные примеры, подробно о деталях реализации, читайте в следующей статье.
(Продолжение следует)
ссылка на оригинал статьи http://habrahabr.ru/post/157363/
Добавить комментарий