Декларативное программирование клиент-серверных приложений на андроид. Часть 2

  • Tutorial
В предыдущей статье кратко были показаны преимущества декларативного программирования клиент-серверных приложений на андроид в сравнении с императивным.

Сейчас напишем небольшой, но достаточный, чтобы оценить эффективность библиотеки DePro, проект. Он представляет собой часть одного из учебных примеров библиотеки. Дизайн всех экранов, которые мы опишем приведен на следующих рисунках:

image image image
Экран DRAWER           Экран CATALOG          Экран PRODUCT_LIST

image image image
Экран CATALOG_а          Экран DESCRIPT         Экран CHARACTERISTIC

image image image
Экран FITNESS          Экран FITNESS_а         Экран Выхода


Из этих экранов в целом понятен их функционал. Боковое меню содержит два пункта «Каталог» и «Фитнес» при выборе какого нибудь пункта показывается соответствующий экран. Экран CATALOG содержит горизонтальный список «Новинки». При клике на любую «новинку» показывается экран с описанием этого товара. Описание состоит из двух вкладок: «Описание» и «Характеристики». Также на экране CATALOG имеется раскрывающийся список «Каталог». При клике на стрелке вниз каталог раскрывается (закрывается). При клике на всю строку показывается экран PRODUCT_LIST со списком продукции по выбранному пункту в каталоге.

При клике на пункт «Фитнес» показывается экран FITNESS со списком услуг. Список зависит от выбранного в спинере клуба. При попытке выйти из приложения показывается диалог — предупреждение.

Сервер передает в приложение данные в json формате. Структура данных для каждого экрана описана ниже. В приложениях написанных на DePro из API используется только URL, структура же данных нужна лишь для того чтобы правильно задать название (id) view, потому что связывание осуществляется по наименованиям. Стоит обратить внимание, что данные это неполный срез реальных данных. Поэтому могут быть не стыковки по названиям, изображениям. В частности, изображений продукции всего 20 шт. Поэтому одна и та же картинка может быть у многих товаров.

API для экранов примера
Экран CATALOG для горизонтального списка (Новинки) URL depro/cron/news, метод GET. При чтении данных о новинках используется пагинация. Параметры пагинации передаются в заголовке.

Ответ

    {
        "product_id":4610,
        "catalog_id":15984,
        "product_name":"Термоусаживаемая трубка 20мм набор 6 цветов (пак 1м*20шт) APRO",
        "catalog_code":"ZRG-20kit",
        "picture":"depro/cronimg/picture_1.jpeg",
        "bar_code":"4824041010653",
        "oem":"",
        "price":175.98,
        "brand":"APRO",
        "product_code":"032578",
        "gift":0,
        "bonus":0,
        "new_product":1,
        "quantity":5
    },
    . . .

Список категорий для нераскрытого списка URL depro/cron/catalog.

Для раскрывающихся списков URL depro/cron/catalog_ex с параметром catalog_id в качестве значений которого используется id категории, которая раскрывается.

Ответ для всех запросов одинаков:

[
    {
        "catalog_id":15510,
        "parent_id":0,
        "catalog_name":"Кузов"
    },
    {
        "catalog_id":15584,
        "parent_id":0,
        "catalog_name":"Двигатель"
    },
    . . .
]

Экран PRODUCT_LIST URL depro/cron/product_list с параметрами: expandedLevel и catalog_id.
Здесь expandedLevel — уровень раскрытия каталога (0, 1 или 2), catalog_id — id выбанной категории.

Для категорий уровня 0 и 1 определяется список всех категорий, которые входят в выбранную, а затем по этому списку определяется перечень товаров входящих в какую нибудь категорию из этого списка.

Метод GET

Структура данных ответа аналогична как для горизонтального списка (Новинки)

Для выбора продукции по заданному баркоду используется URL depro/cron/product_barcode с
параметром barcode_scanner.

Для выбора продукции по строке поиска используется URL depro/cron/product_search с параметром product_name. Отбор осуществляется по условию LIKE. В product_name может быть одно или два слова.

Аналогичный запрос используется и для голосового задания текста поиска.

Экран DESCRIPT

Описание продукции: URL depro/cron/product_id с параметром product_id. Метод GET.

Ответ

{
    "product_id":2942,
    "catalog_id":15594,
    "product_name":"Датчик температуры ВАЗ 2110, 2111, 2112, ЗАЗ Sens 'Сенс' АВТОПРИБОР ",
    "catalog_code":"23.3828"
    ,"picture":"depro/cronimg/picture_16.jpeg",
    "bar_code":"2000000148472",
    "oem":"2112-3851010",
    "price":103.02,
    "brand":"Автоприбор, г. Калуга, Россия",
    "product_code":"027729",
    "gift":1,
    "bonus":0,
    "new_product":0,
    "quantity":16
}

Аналоги: URL depro/cron/product_analog с параметром product_id. Метод GET.

Ответ

[
    {
        "product_id":561,
        "catalog_id":15587,
        "product_name":"Насос водяной ВАЗ 2110, 2111, 2112 (помпа на 16 клап. мотор) AURORA",
        "catalog_code":"WP-LA2112",
        "picture":"depro/cronimg/picture_12.jpeg",
        "bar_code":"2900011680711",
        "oem":"2112-1307010",
        "price":188.16,
        "brand":"AURORA, Poland",
        "product_code":"016807",
        "gift":0,
        "bonus":1,
        "new_product":0,
        "quantity":15
    },
    . . .
]


Экран CHARACTERISTIC URL depro/cron/product_charact с параметром product_id. Метод GET.
Ответ

[
    {
        "prop_id":2764,
        "product_id":2942,
        "name":"Производитель",
        "value":"Автоприбор, г. Калуга, Россия"
    },
    {
        "prop_id":2765,
        "product_id":2942,
        "name":"Марки Авто для ИМ",
        "value":"ВАЗ,ЗАЗ"
    },
    . . .
]


Теперь рассмотрим наши действия написания приложения.

1. В студии создадим новый проект. Имя можно задать на свое усмотрение.

2. Развертывание ресурсов. Как уже упоминалось в первой статье файлы ресурсов (XML) используются обычные. Поэтому, чтобы не тратить время на известные вещи просто скачаем все необходимые ресурсы.

Разархивируем полученный файл res_example.zip. В студии удалим всё содержимое папки res.

Перенесем содержимое разархивированной папки res в папку res проекта. После этого, с учетом особенностей работы Android Studio, возможно нужно будет очистить проект и\или выполнить команду Invalid Caches/Restart.

3. Подключение библиотеки

В секции dependencies файла build.gradle модуля нужно задать:

    implementation 'github.com/deprosystem/depro:compon:3.0.1'

В секции android файла build.gradle модуля нужно задать:

    packagingOptions {
        exclude 'META-INF/DEPENDENCIES'
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

При выборе атрибута minSdkVersion нужно учитывать, что библиотека поддерживает minSdkVersion = 17. После изменения build.gradle нужно синхронизировать проект.

4. Создание необходимых классов (файлов). ПРи работе с библиотекой используется лишь 4 класса: MyDeclareScreens — описываются все экраны; MyParams — задаются необходимые параметры для приложения; MyApp — иницируется библиотека DePro; MainActivity — стартовая активность. Названия классов можно использовать собственные.

Создаем класс MyDeclareScreens.java. Его содержимое следующее:

public class MyDeclareScreens extends DeclareScreens {

    public final static String
            MAIN = "main", DRAWER = "DRAWER", CATALOG = "CATALOG",
            DESCRIPT = "DESCRIPT", CHARACTERISTIC = "CHARACTERISTIC",
            PRODUCT_LIST = "PRODUCT_LIST", PRODUCT_DESCRIPT = "PRODUCT_DESCRIPT",
            FITNESS = "FITNESS";

    @Override
    public void declare() {
        activity(MAIN, R.layout.activity_main)
                .navigator(finishDialog(R.string.attention, R.string.finishOk))
                .drawer(R.id.drawer, R.id.content_frame, R.id.left_drawer, null, DRAWER);

        fragment(DRAWER, R.layout.fragment_drawer)
                .menu(model(menu), view(R.id.recycler));

        fragment(CATALOG, R.layout.fragment_catalog)
                .navigator(handler(R.id.back, VH.OPEN_DRAWER))
                .component(TC.RECYCLER_HORIZONTAL, model(Api.NEWS_PROD).pagination().progress(R.id.progr),
                        view(R.id.recycler_news, R.layout.item_news_prod),
                        navigator(start(PRODUCT_DESCRIPT)))
                .component(TC.RECYCLER, model(Api.CATALOG),
                        view(R.id.recycler, "expandedLevel", new int[]{R.layout.item_catalog_type_1,
                                R.layout.item_catalog_type_2, R.layout.item_catalog_type_3})
                                .expanded(R.id.expand, R.id.expand, model(Api.CATALOG_EX, "catalog_id")),
                        navigator(start(PRODUCT_LIST)));

        activity(PRODUCT_LIST, R.layout.activity_product_list, "%1$s", "catalog_name").animate(AS.RL)
                .navigator(back(R.id.back))
                .component(TC.RECYCLER, model(Api.PRODUCT_LIST, "expandedLevel,catalog_id"),
                        view(R.id.recycler, R.layout.item_product_list)
                                .visibilityManager(visibility(R.id.bonus_i, "bonus"),
                                        visibility(R.id.gift_i, "gift"),
                                        visibility(R.id.newT, "new_product")),
                        navigator(start(PRODUCT_DESCRIPT)));

        activity(PRODUCT_DESCRIPT, R.layout.activity_product_descript, "%1$s", "catalog_name").animate(AS.RL)
                .navigator(back(R.id.back))
                .setValue(item(R.id.product_name, TS.PARAM, "product_name"))
                .component(TC.PAGER_F, view(R.id.pager, DESCRIPT, CHARACTERISTIC)
                        .setTab(R.id.tabs, R.array.descript_tab_name));

        fragment(DESCRIPT, R.layout.fragment_descript)
                .component(TC.PANEL, model(Api.PRODUCT_ID, "product_id"),
                        view(R.id.panel).visibilityManager(visibility(R.id.bonus, "bonus")))
                .component(TC.RECYCLER, model(Api.ANALOG_ID_PRODUCT,"product_id"),
                        view(R.id.recycler, R.layout.item_product_list).noDataView(R.id.not_analog)
                                .visibilityManager(visibility(R.id.bonus_i, "bonus"),
                                        visibility(R.id.gift_i, "gift"),
                                        visibility(R.id.newT, "new_product")),
                        navigator(start(0, PRODUCT_DESCRIPT, PS.RECORD),
                                handler(0, VH.BACK))) ;

        fragment(CHARACTERISTIC, R.layout.fragment_characteristic)
                .component(TC.RECYCLER, model(Api.CHARACT_ID_PRODUCT, "product_id"),
                        view(R.id.recycler, "2", new int[] {R.layout.item_property, R.layout.item_property_1}));

        fragment(FITNESS, R.layout.fragment_fitness)
                .navigator(handler(R.id.back, VH.OPEN_DRAWER))
                .component(TC.SPINNER, model(JSON, getString(R.string.clubs)),
                        view(R.id.spinner, R.layout.item_spin_drop, R.layout.item_spin_hider))
                .component(TC.RECYCLER, model(Api.FITNESS, "clubId"),
                        view(R.id.recycler, R.layout.item_fitness), null).eventFrom(R.id.spinner);
    }

    Menu menu = new Menu()
            .item(R.drawable.list, R.string.m_catalog, CATALOG, true)
            .divider()
            .item(R.drawable.ic_aura, R.string.fitness, FITNESS);
}

Позже опишем все используемые конструкции DePro. Для подключения импорта используйте клавиши alt+enter. Красным также будет выделен класс Api в котором заданы адреса для всех запросов. Его содержимое будет приведено далее.

Создаем класс MyParams.java. В большинстве случаев значения параметров по умолчанию достаточно. В нашем случае мы зададим лишь базовый URL.

public class MyParams extends AppParams {
    @Override
    public void setParams() {
        baseUrl = "https://deprosystem.com/";
    }
}

Меняем содержимое, созданного студией, класса MainActivity на следующее:

public class MainActivity extends BaseActivity {
    @Override
    public String getNameScreen() {
        return MyDeclareScreens.MAIN;
    }
}

В манифесте для MainActivity можно указать портретную ориентацию. В принципе библиотека поддерживает поворот экрана, но в данной версии для экранов типа activity() кроме стартового прописана портретная ориентация.

Создаем класс MyApp:

public class MyApp extends Application {
    private static MyApp instance;
    private Context context;

    public static MyApp getInstance() {
        if (instance == null) {
            instance = new MyApp();
        }
        return instance;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        instance = this;
        context = getApplicationContext();

        DeclareParam.build(context)
                .setAppParams(new MyParams())
                .setDeclareScreens(new MyDeclareScreens());
    }
}

Это обычный синглетон в методе onCreate которого задаются MyParams и MyDeclareScreens.
Не забудьте описать MyApp в манифесте.

На завершение создадим класс Api:

public class Api {
    public static final String CATALOG = "depro/cron/catalog",
            NEWS_PROD = "depro/cron/news_prod",
            PRODUCT_LIST = "depro/cron/product_list",
            PRODUCT_ID = "depro/cron/product_id",
            ANALOG_ID_PRODUCT = "depro/cron/product_analog",
            CHARACT_ID_PRODUCT = "depro/cron/product_charact",
            FITNESS = "depro/cron/fitness",
            CATALOG_EX = "depro/cron/catalog_ex";
}

5. Теперь можно запустить приложение на выполнение.

Если все введено правильно, то при старте приложения будут отображаться (и работать) все указанные экраны.

Как видим у нас на все приложение всего пять несложных классов. Причем четыре из них вспомогательные. Их содержимое не зависит от количества экранов. Описание, вообщем то не тривиальных, экранов тоже занимает немного места (менее 80 строк). Теперь покажем, что описание не только не большое, но и не сложное. Для этого опишем работу использованых компонентов библиотеки.

Как указывалось в первой статье экраны в библиотеке могут быль либо activity, либо fragment, которым задается наименование экрана (строка) и layout. Ссылки на экраны осуществляются по их именам. Выбор какой тип экрана использовать осуществляется обычным образом.

Экран MAIN

Итак, из дизайна видно, что стартовый экран имеет боковое меню и контейнер для фрагментов. В разметке R.layout.activity_main задан стандартный DrawerLayout. Поэтому и в описании экрана MAIN мы задаем компонент drawer, которому передаем id самого DrawerLayout и его контейнеров для боковой панели и фрагментов. Также указываем имя экрана (DRAWER), который будет отображаться в боковой панели. Навигатор finishDialog указывает, что перед выходом из приложения нужно выдать диалог — подтверждение.

Экран DRAWER

Содержит всего один компонент menu у которого в модели указана переменная типа Menu, а в представлении id элемента разметки типа RecyclerView, который и будет отображать меню. В самом меню указано, что пункт меню «CATALOG» будет отображаться в контейнере фрагментов при старте меню.

Экран CATALOG

Содержит один горизонтальный список «Новинки». Его модель при получении данных с сервера использует пагинацию, а прогресс отображает в R.id.progr. Если у Вас интернет быстрый, то Вы можете не заметить появление панели с прогрессом. Если хочете его посмотреть, то можете переключиться на более медленный интернет, либо изменить цвет фона у R.id.progr. В этом случае Вы, по крайней мере, увидете его мигание. Для пагинации используются параметры заданные в AppParams по умолчанию. Представлению задается R.id.recycler_news с RecyclerView и layout для айтемов. При клике на любой элемент списка стартует экран PRODUCT_DESCRIPT.

Список «Каталог» является раскрывающимся. Уровень раскрытия определен в поле «expandedLevel». Если оно не передается с исходными данными ничего страшного библиотека сама с этим разберется. Этим же параметром задается и какой layout из списка нужно использовать на каждом уровне раскрытия. То что список является раскрывающимся служит функционал expanded(...) в катором задется модель для получения данных с сервера для следующих уровней. В модели указывается адрес запроса Api.CATALOG_EX и наименование параметра запроса «catalog_id». В expanded также указывается R.id.expand — элемента разметки в айтемах по клику на который будет осуществляться раскрытие списка и id элемента который будет анимационно проворачиваться на 180 грудусов при раскрытии — закрытии списка. И, в завершение, в навигаторе указано, что при клике на айтем списка будет вызван экран PRODUCT_LIST.

В навигаторе, относящемся ко всему экрану указано, что при клике на элемент R.id.back откроется боковая панель.

Экран PRODUCT_LIST

При задании экрана указано, что в заголовке будет показано актуальное значение параметра «catalog_name» и анимация выхода будет с права на лево (AS.RL). Описание списка нам уже знакомо. Единственно, что для представления указывается visibilityManager, который управляет видимостью элементов разметки в зависимости от значений соответствующих данных. В навигаторе, относящемся ко всему экрану указано, что при клике на элемент R.id.back буде осуществлен возврат на предыдущий экран.

Экран PRODUCT_DESCRIPT

Новым для нас является компонент типа PAGER_F. Он связан с обычным ViewPager. Ему указывается список экранов (фрагментов), которые будут показываться, и подключен TabLayout (setTab).

Экран PRODUCT_DESCRIPT показывается во вкладке «ОПИСАНИЕ».

Компонент типа PANEL отображает данные полученные по модели. Список «Аналоги» отображает перечень аналогичной продукции, если он есть, или с помощью функционала noDataView() сообщение «Аналоги отсутствуют».

Экран CHARACTERISTIC

Показывает список характеристик продукта.

Экран FITNESS

Компонент типа SPINNER показывает список клубов с возможностью выбора нужного. Перечень клубов задается в модели в виде json-строки.

Список категорий занятий обычный. Функционал eventFrom(...) указывает, что при изменении компонента, связанного с R.id.spinner (в нашем случае спинером) необходимо обновить данные.
Код рассмотренного в статье приложения можно посмотреть на Github.

Статья носит ознакомительный характер. Более полно с возможностями библиотеки DePro можно в документации.
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 4

    0
    Нельзя создавать класс MyApp, его создаёт система
      0
      Предполагаю что под термином «система» ты подразумеваеш IDE Android Studio. Студия не создает класс MyApp. При создании нового проекта она создает только MainActivity. Поэтому, после создания нового проекта можеш смело создавать там класс MyApp. В принципе, название класса может быть любым. Нужно только, чтобы он наследовался от Application, в его методе onCreate иницировались классы MyParams и MyDeclareScreens (метод DeclareParam.build) и что бы он был описан в манифесте (в секции application атрибут android:name=".MyApp")
        0

        Нет, я имею ввиду саму операционную систему. Класс Application создаётся системой аналогично Activity. Если создать просто Application и нигде его не регистрировать lifecycle callbak не будут вызывать.

      0

      Есть пример экрана с recycler view с 8 разными view type, в которых ещё один recycler view, и это всё в некоторых случаях обновляется по diff callback а в некоторых через notify для изменения анимации

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое