Введение
Общее описание
Структура приложения
Уроки
Урок 1
Создание приложения, экраны, меню, список, локализация
Урок 2
Просмотр видео (youtube), панель ввода
Урок 3
Карты, PlusMinus, пользовательская обработка экранов
Урок 4
Завершение разработки, закрепление материала
Урок 5
Интро, авторизация, фото
Урок 6
Боковое меню, горизонтальный список, пагинация, раскрывающийся список, баркод-сканер, распознавание речи, поиск, листание страниц
Урок 7
Работа с базами данных SQLite, свайп, componentDateDiapason
Урок 8
Спиннер, календарь, пиккер и события
Урок 9
Работа с пуш сообщениями, BadgeTextView
Урок 10
Анимация, кастомные компоненты
Описание библиотеки
Приложения

Урок 7

На этом уроке мы научимся: работать с базой данных (SQLit), способом свайпить элементы списка и componentDateDiapason

Постановка задачи (задание урока)

Нужно описать экраны связанные с обработкой заказов.

Их дизайн приведен на следующих рисунках:

Рис. 1 экран ADD_PRODUCT
Рис. 1 экран ADD_PRODUCT
Рис. 2 экран ADD_PRODUCT
Рис. 2 экран ADD_PRODUCT
Рис. 3 экран ADD_PRODUCT
Рис. 3 экран ADD_PRODUCT
Рис. 4 экран ADD_PRODUCT
Рис. 4 экран ADD_PRODUCT
Рис. 5 экран ADD_PRODUCT
Рис. 5 экран ADD_PRODUCT
Рис. 6 экран ADD_PRODUCT
Рис. 6 экран ADD_PRODUCT
Рис. 7 экран ADD_PRODUCT
Рис. 7 экран ADD_PRODUCT
Рис. 8 экран ORDER_LIST
Рис. 8 экран ORDER_LIST
Рис. 9 экран ORDER_LIST
Рис. 9 экран ORDER_LIST
Рис. 10 экран ORDER_LIST
Рис. 10 экран ORDER_LIST
Рис. 11 экран ORDER_LIST
Рис. 11 экран ORDER_LIST
Рис. 12 экран ORDER_LIST
Рис. 12 экран ORDER_LIST
Рис. 13 экран ORDER_PRODUCT
Рис. 13 экран ORDER_PRODUCT
Рис. 14 экран ORDER_PRODUCT
Рис. 14 экран ORDER_PRODUCT
Рис. 15 экран ORDER_PRODUCT
Рис. 15 экран ORDER_PRODUCT
Рис. 16 экран ORDER_LIST
Рис. 16 экран ORDER_LIST

На рисунках 1 - 7 изображен экран ADD_PRODUCT для разных состояний. На рисунках 8 - 12 и 16 изображен экран ORDER_LIST. На рисунках 13 - 15 показан экран ORDER_PRODUCT.

Экран ADD_PRODUCT обеспечивает занесение информации о товаре в локальную базу данных заказов. Если открытых заказов нет, то будет показываться сообщение (рисунок 1). При нажатии кнопки "Создать новый" показывается панель для ввода названия заказа (рисунок 2). После ввода названия и клика на кнопку "Создать" в базу данных заносится информация о заказе, обновляется вид списка заказов и становится доступной кнопка "Добавить" (рисунок 3). Кнопками + - можно откорректировать количество товара в заказе (рисунок 5). В случае если остаток меньше заявленного количества товара выдается сообщение (рисунок 6). Одновременно пользователь может формировать несколько заказов. Товар будет заноситься в выбранный заказ (отмечен "птичкой" на рисунке 9). При клике на кнопку "Добавить" информация о товаре заносится в выбранный заказ, о чем сообщается во всплывающей панели (рисунок 4), которая исчезнет через 1,5 сек.

Экран ORDER_LIST показывает список открытых заказов, которые относятся к заданому диапазону дат. При клике на поле "С" появляется календарь с помощью которого можно задать диапазон дат для отбора списка заказов. При клике на стрелку осуществляется переход на экран ORDER_PRODUCT.

Экран ORDER_PRODUCT отображает список товаров в заказе. Здесь можно откорректировать количество товара, удалить товар и отправить заказ на сервер. После отправки заказа на сервер он вместе с входящими товарами удаляется из базы данных и осуществляется возврат на экран ORDER_LIST.

Описание API

Экран ORDER_PRODUCT
Отправка заказа на сервер: URL depro/crontoken/send_order, метод POST Параметры: "order_name,list_product(product_id;count)". 
Здесь указывается, что list_product содержит поля (product_id;count). Поле list_product это recycler. 
Из его списка будут браться все записи, а поля в записях только product_id и count

Данные отправляемые:
    {"order_name":"заказ 1","list_product":[{"product_id":652,"count":1},{"product_id":602,"count":1}]}
Ответ 
{"result":"oK"}

Описание базы данных и SQL запросов к ней

В качестве предустановленной базы данных используется SQLite. Можно использовать и другую базу данных. Как это осуществить описано в разделе "Работа с БД".

В приложении используется две таблицы: заказов (order_tab) и товары в заказе (product_order).

Таблица product_order

Поля таблицы: prod_ord INTEGER PRIMARY KEY, order_name TEXT, product_name TEXT, product_id INTEGER, count INTEGER, price REAL

Индекс: prod_ord_ind. Поля индекса: order_name

Таблица order_tab

Поля таблицы: ord_ind INTEGER PRIMARY KEY, order_name TEXT, status INTEGER, comment TEXT, date INTEGER

Индексов нет

При работе с базой данных для чтения данных в модель передается метод GET_DB, а вместо URL передается SQL запрос SELECT. Вставка строк в таблицу, изменение содержимого строк таблиц и удаление строк в конечном счете выполняется методами SQLite такими как replace, update и delete, которым нужны переменные типа ContentValues и условие WHERE с необходимыми параметрами. Поэтому в описании будем указывать метод и приводить параметры, которые позволят сформировать соответствующие переменные этих методов.

Экран ADD_PRODUCT
Чтение списка заказов из локальной базы данных. Метод GET_DB. SQL: SELECT * FROM order_tab.
Ответ 
[
    {
        "ord_ind":2,
        "order_name":"заказ 2",
        "status":null,
        "comment":null,
        "date":null
    },
    . . .
]
Для записи товара в локальную БД. Запись в таблицу product_order. 
    Параметры (список полей, которые записываются) "order_name,product_id,product_name,count,price". Метод POST_DB.
Ответ
{
    "order_name":"заказ 2",
    "product_id":"4610",
    "product_name":"Термоусаживаемая трубка 20мм набор 6 цветов (пак 1м*20шт) APRO",
    "count":"1",
    "price":"175,98"
}
Для записи нового заказа в локальную БД. Запись в таблицу order_tab. 
    Параметры (список полей, которые записываются) "order_name,date=SYSTEM_TIME". Метод POST_DB.
    Здесь date=SYSTEM_TIME указывает, что полю date нужно присвоить значение системного времени в миллисекундах.
Ответ
{
    "order_name":"заказ 1",
}

Экран ORDER_LIST
Чтение списка заказов из локальной базы данных. Метод GET_DB. SQL: SELECT * FROM order_tab WHERE date >= ? AND date <= ? ORDER BY date
Параметры: "from,before" указывают названия полей у компонента componentDateDiapason.
Ответ 
[
    {
        "ord_ind":2,
        "order_name":"заказ 2",
        "status":null,
        "comment":null,
        "date":null
    },
    . . .
]

Экран ORDER_PRODUCT
Чтение списка товаров. SQL: SELECT *, (price * count) AS amount FROM product_order WHERE order_name = ? ORDER BY product_name. Параметры: "order_name"
Ответ
[
    {
        "prod_ord":7,
        "order_name":"заказ 1",
        "product_name":"ДПДЗ ГАЗ 3110, 3302,  GEELY MK AURORA",
        "product_id":652,
        "count":1,
        "price":66.72,
        "amount":66.72,
        "row":1
    },
    . . .
]

Отправка заказа на сервер описана выше. После отправки выполняется:
Удаление товаров заказа: таблица product_order. Метод DEL_DB. Параметры для формирования условия WHERE : "product_id".
Ответ
{}
и удаление заказа: таблица order_tab. Метод DEL_DB. Параметры для формирования условия WHERE : "order_name".
Изменение количества товара. Выполняется при каждом изменении количества (в компоненте PlusMinus).
Запись в таблицу product_order. Метод UPDATE_DB. Параметры для формирования условия WHERE : "product_id". Параметры для формирования SET "count".
Ответ
{}

Описание базы данных выполняется в MyApp. Поэтому приведем измененный текст класса 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();

        ParamDB paramDB = new ParamDB(SQL.DB_NAME, 1);
        paramDB.addTable(SQL.PRODUCT_ORDER, SQL.PRODUCT_ORDER_FIELDS, SQL.PRODUCT_ORDER_INDEX_NAME, SQL.PRODUCT_ORDER_INDEX_COLUMN);
        paramDB.addTable(SQL.ORDER_TAB, SQL.ORDER_FIELDS);

        DeclareParam.build(context)
            .setNetworkParams(new MyParams())
            .setDeclareScreens(new MyDeclareScreens())
            .setDB(new DatabaseManager(context, paramDB));
    }
}

Здесь в дополнение к известным действиям создается переменная paramDB, в которой устанавливаются имя базы (SQL.DB_NAME), ее версия ( 1 ) и все необходимые таблицы. В DeclareParam.build для задания базы данных используется метод setDB

Все запросы к базе данных, в т.ч. и на создание необходимых таблиц находятся в классе SQL. Приведем его содержимое для всех экранов.

    public static String DB_NAME = "db_cron";

    public static String PRODUCT_ORDER = "product_order";
    public static String PRODUCT_ORDER_PARAM = "order_name,product_id,product_name,count,price";
    public static String PRODUCT_ORDER_INDEX_NAME = "prod_ord_ind";
    public static String PRODUCT_ORDER_INDEX_COLUMN = "order_name";
    public static String PRODUCT_ORDER_FIELDS = "prod_ord INTEGER PRIMARY KEY, order_name TEXT, product_name TEXT, product_id INTEGER, count INTEGER, price REAL";
    public static String PRODUCT_ORDER_WHERE = "product_id = ?";
    public static String PRODUCT_IN_ORDER = "SELECT *, (price * count) AS amount " +
            "FROM product_order WHERE order_name = ? ORDER BY product_name";

    public static String ORDER_TAB = "order_tab";
    public static String ORDER_WHERE = "order_name = ?";
    public static String ORDER_FIELDS = "ord_ind INTEGER PRIMARY KEY, order_name TEXT, status INTEGER, comment TEXT, date INTEGER";
    public static String ORDER_LIST = "SELECT * FROM order_tab WHERE date >= ? AND date <= ? ORDER BY date";
    public static String ORDER_LIST_ALL = "SELECT * FROM order_tab";

Предполагается, что вы знакомы с языком запросов SQL. Знание SQLite не обязательно

Теперь можно привести описание остальных экранов проекта.

    activity(ADD_PRODUCT, R.layout.activity_add_product, WorkAddProduct.class).animate(AS.RL)
        .navigator(back(R.id.back), show(R.id.create_new, R.id.new_order), hide(R.id.cancel, R.id.new_order))
        .plusMinus(R.id.count, R.id.plus, R.id.minus, null, new Multiply(R.id.amount, "price"))
        .component(TC.PANEL_ENTER, model(ARGUMENTS), view(R.id.panel),
            navigator(handler(R.id.add, VH.CLICK_SEND,
                model(POST_DB, SQL.PRODUCT_ORDER, SQL.PRODUCT_ORDER_PARAM),
                after(assignValue(R.id.inf_add_product), show(R.id.inf_add_product)), false)))
        .component(TC.PANEL_ENTER, null, view(R.id.new_order),
            navigator(handler(R.id.create_order, VH.CLICK_SEND,
                model(POST_DB, SQL.ORDER_TAB, "order_name,date=SYSTEM_TIME"),
                after(hide(0, R.id.new_order), actual(0, R.id.recycler)), false, R.id.order_name)))
        .list(model(GET_DB, SQL.ORDER_LIST_ALL), view(R.id.recycler, "select",
            new int[] {R.layout.item_order_log, R.layout.item_order_log_select}).selected().noDataView(R.id.no_data),
            navigator(handler(0, VH.SET_PARAM)))
        .enabled(R.id.add, R.id.recycler);

    fragment(ORDER_LIST, R.layout.fragment_order)
        .navigator(handler(R.id.back, VH.OPEN_DRAWER))
        .componentDateDiapason(R.id.diapason)
        .list(model(GET_DB, SQL.ORDER_LIST, "from,before"),
            view(R.id.recycler, R.layout.item_order_list).noDataView(R.id.no_data),
            navigator(start(ORDER_PRODUCT, PS.RECORD, after(actual(R.id.recycler))))).eventFrom(R.id.diapason);

    activity(ORDER_PRODUCT, R.layout.activity_order_product, "%1$s", "order_name").animate(AS.RL)
        .navigator(back(R.id.back))
        .plusMinus(R.id.count, R.id.plus, R.id.minus, navigator(handler(model(UPDATE_DB, SQL.PRODUCT_ORDER,
            "count", "product_id"))),
            new Multiply(R.id.amount, "price", "amount"))
        .list(model(GET_DB, SQL.PRODUCT_IN_ORDER, "order_name").row("row"),
            view(R.id.list_product, R.layout.item_order_product),
            navigator(handler(R.id.del, model(DEL_DB, SQL.PRODUCT_ORDER, "product_id"), after(actual()))))
        .componentTotal(R.id.total, R.id.list_product, R.id.count, null, "amount", "count")
        .component(TC.PANEL_ENTER, null, view(R.id.panel),
            navigator(handler(R.id.send, VH.CLICK_SEND, model(POST, Api.SEND_ORDER,
                "order_name,list_product(product_id;count)"),
                after(handler(model(DEL_DB, SQL.ORDER_TAB, "order_name")),
                    handler(model(DEL_DB, SQL.PRODUCT_ORDER, SQL.ORDER_WHERE, "order_name")),
                    handler(VH.RESULT_RECORD)))));

Экран ADD_PRODUCT для обработки нестандартного функционала использует класс WorkAddProduct, текст которого приведен ниже.

public class WorkAddProduct extends MoreWork {
    PanelEnterComponent panel;
    @Override
    public void changeValue(int viewId,  Field field, BaseComponent baseComponent) {
        panel = (PanelEnterComponent) baseComponent;
        if (panel.recordComponent != null) {
            View v = parentLayout.findViewById(R.id.more_residue);
            if (panel.recordComponent.getInt("quantity") < (int) field.value) {
                v.setVisibility(View.VISIBLE);
            } else {
                v.setVisibility(View.GONE);
            }
        }
    }
}

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

Объяснение работы новых компонентов

Экран ADD_PRODUCT

В компоненте plusMinus указано, что элементу разметки R.id.amount нужно установить значение равное произведению количества (count) на значение поля "price".

В компоненте типа PANEL_ENTER с view(R.id.panel) по клику на кнопку R.id.add информация заносится в базу данных: метод POST_DB, таблица product_order, заносятся значения полей, заданных в строке QL.PRODUCT_ORDER_PARAM ("order_name,product_id,product_name,count,price"). после получения ответа полученные данные связываются (binding) с всплывающей панелью R.id.inf_add_product (Внизу экрана на 1500 мсек появляется View с сообщением в какой заказ добавлен товар (рисунок 4))

После изменения количества в компоненте plusMinus осуществляется вызов метода changeValue класса WorkAddProduct. В нем сравнивается значение количества товара с наличием (поле "quantity"). Если заказывается больше чем имеется, то выводится сообщение (рисунок 6)

Работа компонента типа list не имеет особенностей и нам знакома

Экран ORDER_LIST

Здесь новым является компонент componentDateDiapason с помощью которого задаются начальная и конечная дата. Элемент R.id.diapason имеет тип DateDiapason. В нем в частности указываются поля id.from и id.before (типа TextView) в которых буду отображаться начальная и конечная дата диапазона. При клике на кнопку оК выбранный диапазон будет занесен в глобальный список параметров. Также будет сгенерировано событие на которое подписался recycler (дополнительный функционал eventFrom(R.id.diapason)). Это приведе к обновлению его данных (будут выбраны заказы попадающие в выбранный диапазон дат.)

Также новым является обработчик actual(R.id.recycler), который обеспечивает обновление данных в компоненте с viewId = R.id.recycler. В нашем случае будет обновлен список заказов после передачи заказ на сервер.

Экран ORDER_PRODUCT

Показывается список товаров в выбранном заказе (его название показывается тулбаре). В верху списка показывается общая стоимость заказа и общее количество товаров. Количество единиц товара по каждой позиции можно изменять (плюс - минус).

Нужно обратить внимание на лайоут элемента списка R.layout.item_order_product. В нем используется SwipeLayout, который является наследником ViewGroup. SwipeLayout можно задать аттрибуты: swipeViewId - id элемента который будет свайпиться; swipeRightViewId - id элемента который будет показан справа (при свайпе влево); swipeLeftViewId id элемента который будет показан слева (при свайпе в право)

При свайпе влево показывается корзина при клике на которую удаляется выбранный товар.

При клике на кнопку "Отправить" осуществляется отправка заказа на сервер. После получения ответа в опции after выполняется удаление из базы заказа и его товары, а также указывается на выход с экрана с параметром RESULT_RECORD. Это приводит к выполнению секции after обработчика start экрана ORDER_LIST.



Лучше пакета DePro может быть только искусственный интеллект
Задать вопрос
Отправить вопрос