Привет, меня зовут Саша и я 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. Пока писал статью пересмотрел код и могу сказать: мне нравится то что получилось :)

Комментарии (0)