- Мультимодульное приложение proof-of-concept для Android на Jetpack Compose
- Используется технология блокчейна для реализации цифровой валюты
- Происходит передача банкнот между устройствами пользователей (защищенными кошельками) по произвольному каналу связи (здесь — Bluetooth Classic) с применением 2-х собственных протоколов передачи данных (см. пункт Протоколы Передачи ниже)
- Реализовано взаимодействие по HTTP с выделенным банковским сервером, который отвечает за регистрацию и авторизацию кошелька (пользователя), эмиссию банкнот, их выдачу/обмен на физические купюры.
Каждая банкнота имеет свой блокчейн, блоками в которой являются транзакции (операции передачи банкнот между кошельками), таким образом сохраняется полная история движения банкноты. Эту историю может видеть и анализировать, например, банк, выпускающий банкноты.
Note
Это почти полностью переработанная, обновленная и дополненная версия старого приложения, которое можно найти здесь (в том числе произведена миграция с Fragments на Compose с новым дизайном, описанном в Figma)
- Android Architecture Components (Lifecycle, Navigation, Room), Jetpack Compose, Dagger/Hilt, Kotlin, Coroutines, Flows, Datastore, Keystore, Retrofit, Bluetooth, Serialization, Gradle (Multi-module, Included Build, Convention Plugins, Version Catalogs, Build Variants), Git ;
- Testing: Junit5
Home | Settings | History | Wallet details | |
---|---|---|---|---|
Light | ||||
Dark |
Демонстрация работы приложения (запись экрана), в том числе 2-х splash-screen (системного android и внутреннего в приложении), анимаций.
Note
Во время передачи банкнот устройство взаимодействует по Bluetooth Classic с находящимся рядом другим смартфоном с такой же версией приложения OpenDigitalCash. Прием и отправка выглядят одинаково на обоих устройствах.
ODC_p2p_demo.compressed.mp4
Показать принципиальную возможность реализации цифрового аналога фиатной валюты (см. Электронные Деньги):
- с оффлайн передачей
- с централизованным управлением Банком (выделенный банковский сервер),
- который отвечает за
- эмиссию банкнот и их уничтожение
- регистрацию пользователей и кошельков
- анализ и проверку происходящих транзакций (в онлайн режиме)
- обмен фиатной валюты на цифровую и обратно (через банкомат)
- анализ истории движения конкретной банкноты (представленной блокчейном) и т. д.
Multi-module, Clean Architecture, SOLID, DI, MVI, MVVM, Unit-Testing (Junit5), Material Design, Material 3.
- Многомодульный проект (20+ модулей: app, 14 core, 6 feature), с отдельным модулем included build (build-logic), используются разные buildTypes (debug / release)
- Написаны собственные Gradle плагины (Convention Plugins) для конфигурации сборки Gradle (вынесена общая конфигурация и зависимости для модулей для избегания дублирования кода, удобства изменения и расширения сборки)
- Использованы каталоги версий (Version Catalogs).
- Между модулями четко определены границы и область применения, например
- Есть чистый Kotlin-модуль :core:model , где хранятся модели (вроде UserInfo), который сам не зависит от других, но от которого зависят другие модули, в том числе UI
- Другие модули, например :core:database (Room), имеют аналогичные внутренние модели с дополнительными свойствами (например, @Entity) и конвертеры из общих моделей :core:model в локальные
- На границе модулей происходит преобразование между внутренними и общими моделями
- Есть чистый Kotlin-модуль :core:model , где хранятся модели (вроде UserInfo), который сам не зависит от других, но от которого зависят другие модули, в том числе UI
Примечание: скорее всего, возможно ещё улучшить независимость модулей, если найти способ преобразовать модели в :core:wallet в локальные и вынести аналогичные общие в модуль :core:model. Таким образом будет убраны лишние связи других модулей с :core:wallet, которые возникают только из-за общих моделей.
:app: | :build-logic: | :core: | :feature: |
---|---|---|---|
convention | common-android | atm | |
common-jvm | history | ||
connectivity | home | ||
database | p2p | ||
datastore | settings | ||
design-system | wallet-details | ||
domain | |||
model | |||
network | |||
testing | |||
transaction-logic | |||
ui | |||
wallet | |||
wallet-repository |
Note
В этой версии модуль, реализующий кошелек, является открытым, а банкноты хранятся в базе данных. В Минимально Жизнеспособном Продукте кошелек должен находиться в отдельном защищенном разделе смартфона (например, на специальной симкарте OpenDigitalCash; потеря симкарты равна потере кошелька).
-
Использованы Kotlin Coroutines и Flows (с соблюдением структурированного паралеллизма) для асинхронного выполнения и возможности отмены всех ресурсоемких операций приложения.
-
Для UI использован Jetpack Compose со стандартной библиотекой навигации и 2 NavHost (вложенная навигация), паттерн MVI. Всего 7 основных экранов, из которых 3 верхнего уровня.
-
UI работает в режиме edgeToEdge() , адаптируясь и к системной навигации 3-мя клавишами и жестами, и к открытию IME (экранной клавиатуры). Состояние приложения полностью сохраняется и адаптируется при смене конфигурации смартфона.
-
Используются кастомные анимации.
Схема нового алгоритма передачи (схему старого можно найти в документации ODC к прошлой версии приложения):
В этой версии передача между кошельками происходит по защищенному каналу Bluetooth, используя два собственных протокола передачи данных:
-
Для разбиения больших пакетов (в виде массивов байт) на фрагменты малого размера и сборки целого пакета из его фрагментов, поскольку соединение (например, Bluetooth) имеет ограничение по размеру одного пакета. Для передачи большого пакета по частям сначала передается размер будущего пакета в байтах в виде лидирующего пакета 4 байта, после все части пакета по очереди.
-
Получатель ожидает лидирующий массив байт с размером, далее последовательно собирает большой пакет из фрагментов, после чего большой пакет (массив байт) обрабатывается по протоколу 2.
-
Для двойной сериализации и десериализации многоуровневых DTO в массивы байт и обратно:
- сначала сериализации DTO в массив байт
- далее обёртка этого массива в другой DTO (DataPacket) вместе с одним из заранее определенных типов, который соответствует этому пакету
- повторная сериализация в массив байт
- и отправка получателю по протоколу 1
-
Получателю известно, что пришедший массив байт всегда является сериализованным DataPacket, поэтому после получения по протоколу 1 всего пакета, он
- десериализует массив байт в DataPacket DTO,
- откуда извлекает тип передаваемого вложенного DTO
- и, в зависимости от типа, десериализует вложенный массив байт в DTO, который уже используется для транзакций.
Оба этих подхода в совокупности позволяют легко расширять DTO и добавлять их новые типы независимо от алгоритма передачи данных, при этом протокол 1 и протокол 2 никак не зависят друг от друга.
Для обоих протоколов написаны Unit-тесты (Junit5).
Обычный Bluetooth не позволяет отфильтровать пользователей по UUID сервиса, поэтому реализована фильтрация по имени устройства.
Начинается поиск устройств Bluetooth, но фильтруются и остаются только те, у которых в начале имени есть определенный префикс.
- Перед началом вещания оригинальное имя устройства Bluetooth сохраняется в Android Datastore (для того, чтобы откатить его обратно, в том числе даже при падении и восстановлении работы приложения)
- Далее имя пользователя, указанное в разделе настроек ODC, загружается из DataStore, к нему добавляется префикс
- Bluetooth-имя устройства Android меняется на новое.
- После отмены или успешного подключения Bluetooth-имя устройства Android сразу же меняется на старое, сохраненное ранее в DataStore.
Таким образом, личные настройки смартфона пользователя сохраняются нетронутыми.
Пользователь запрашивает сумму из своего кошелька; эта же ситуация может возникнуть и с обычным банкоматом.
Задача: можно ли из имеющихся банкнот (неделимых, как и бумажных) собрать точную сумму, и если да, то из каких именно?
Является задачей о сумме подмножеств, NP-Complete или NP-hard (в зависимости от задачи). Решена здесь при помощи динамического программирования с использованием Kotlin Coroutines, написаны Unit-тесты на Junit5.
КИБ, OpenDigitalCash