Привет, меня зовут Саша и я Android-разработчик :-)
(Это моя первая статья здесь и я буду рад вашей поддержке)
Разрабатывая мобильные приложения я обратил внимание что все операции которые делает приложение - это по сути запросы данных от разных источников.
Часто источниками данных являются: человек, сервер и устройство (User, Server, Platform).
Приложение принимает данные от какого-то источника, преобразует их и выдает преобразованные данные другому источнику и так пока не дойдет до корня дерева запросов.
Подобно тому как вызовы функций вкладываются друг в друга:
// псевдокод
fun main() {
val result = abs(min(x1, x2) + max(y1, sum(y2 + 42)))
}
Можно написать вот такой код запрос-ответов:
// псевдокод
fun main() {
ToUser.GetChoice(MainMenu) { choice ->
when (choice) {
MainMenu.AddNote -> {
editNote()
}
MainMenu.ReadNotes -> {
ToPlatform.GetNotes() { notes ->
readNotes(notes)
}
}
MainMenu.SearchNotes -> {
searchNotes()
}
MainMenu.Trash -> {
ToPlatform.GetNotes() { removedNotes ->
openTrash(removedNotes)
}
}
}
}
}
Я решил сделать экспериментальное приложение по моей задумке и смотри что из этого получилось :)
Для начала я определился со стеком и целями:
✅ Стек:
Android, Kotlin, Compose, Coroutines, Room
✅ Цели:
Сделать приложение на Compose
Попрактиковаться в создании приложений на Compose
Написать приложение в стиле запрос-ответов (реализовать идею)
Написать приложение максимально просто
Это должно быть полноценное, функциональное приложение
Использовать код в качестве демонстрации, как домашнее тестовое задание
Затем написал реализацию:
Это варианты действий главного экрана, корзины и списка заметок:
enum class MainMenu(override val text: String) : IMenuItem {
AddNote("Написать заметку"),
ReadNotes("Читать заметки"),
SearchNotes("Искать заметки"),
Trash("Корзина"),
}
enum class TrashMenu(override val text: String) : IMenuItem {
ReadNotes("Читать заметки"),
SearchNotes("Искать заметки"),
EmptyTrash("Очистить корзину"),
}
sealed class NotesChoice() : IChoice {
data class Remove(val note: Note) : NotesChoice()
data class Select(val note: Note) : NotesChoice()
}
Вся логика приложения сводится к такому дерево-графу запросов (и к нему можно прикрутить любую UI реализацию, навигацию, платформу, сервер-клиента):
Точка входа: main()
fun main() {
ToUser.GetChoice(
title = "Блокнот",
items = MainMenu.entries,
canBack = false
).request { choice ->
when (choice) {
MainMenu.AddNote -> {
editNote()
}
MainMenu.ReadNotes -> {
ToPlatform.GetNotes().request { notes ->
readNotes(notes)
}
}
MainMenu.SearchNotes -> {
searchNotes()
}
MainMenu.Trash -> {
ToPlatform.GetNotes(removed = true).request { removedNotes ->
openTrash(removedNotes)
}
}
}
}
}
fun openTrash(removedNotes: List<Note>) {
// ...
}
fun searchNotes(removed: Boolean = false) {
ToUser.GetString(
title = "Поиск заметок",
label = "Искать строку",
actionName = "Найти",
).request { response ->
when (response) {
is Back -> {
User.requestPrevious()
}
is String -> {
ToPlatform.GetNotes(query = response, removed = removed).request { notes ->
readNotes(notes, "Заметки по запросу \n\n\"$response\"")
}
}
}
}
}
fun editNote(initial: Note? = null) {
// ...
}
fun readNotes(notes: List<Note>, title: String = "Заметки") {
ToUser.GetChoice(
title = title,
items = notes.toMutableStateList(),
).request { response ->
debug {
println("choice: $response")
}
when (response) {
is Back -> User.requestPrevious()
is NotesChoice.Remove -> {
response.note.removed = true
ToPlatform.UpdateNote(response.note)
.request {
User.request.pop()
ToPlatform.GetNotes().request { notes ->
readNotes(notes, title)
}
}
}
is NotesChoice.Select -> editNote(response.note)
}
}
}
Чтобы было понятнее добавил вводные:
✅ Вводные для читателя кода проекта на github:
handleRequestsToPlatform() - обработка запросов к платформе
handleRequestsToUser() - обработка запросов к пользователю
Клиенты - делаем к ним запросы и получаем от них ответы:
User - пользователь как клиент
Platform - платформа/OS как клиент
Backend - бэк как клиент
ToUser - запросы к пользователю:
PostMessage - передать сообщение пользователю
GetChoice - получить выбор из вариантов
GetString - получить текст
Вся работа корутин производится в едином скоупе приложения.
✅ Дополнительно мне удалось:
отказаться от использования suspend функций (они используются только там где действительно нужны, а в бизнес-логике их нет)
использовать минимум фреймворков (без фреймворков DI, Flow, доп. навигации, вьюмоделей)
В результате получился легковесный и полнофункциональный Блокнот с простой и наглядной бизнес-логикой.
Залил код проекта на GitHub
https://github.com/e16din/LikeNotesApp
Если чего-то не достает для ясности, задавайте мне вопросы и я с удовольствием на них отвечу :)
Ну и "велкам ту зе клаб" - трогайте, пробуйте на вкус, ставьте звезды проекту, делитесь с друзьями и пишите в комментариях свои мысли.
По секрету: я в открытом поиске :)
P.S. Пока писал статью пересмотрел код и могу сказать: мне нравится то что получилось :)