Урок 7
На этом уроке мы научимся: работать с базой данных (SQLit), способом свайпить элементы списка и componentDateDiapason
Постановка задачи (задание урока)
Нужно описать экраны связанные с обработкой заказов.
Их дизайн приведен на следующих рисунках:
















На рисунках 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.