Урок 1

На протяжении четырех уроков мы напишем приложение для склада (магазина). За основу взят реальный проект “склад техника”, который имеется в маркете. Отличие от реального проекта только в том, что используется тестовый сервер.

Как указывалось выше файлы ресурсов не отличаются от обычных. Поэтому в рамках уроков мы не будем отвлекаться на их разработку, а скачаем с сервера.

Можно скачать сразу все ресурсы для всех уроков по приложению, либо скачивать файлы ресурсов для каждого урока отдельно.

Скачать все ресурсы

Рекомендуется скачать сразу все ресурсы, чтобы на каждом уроке не отвлекаться на действия по их развертыванию.

На этом уроке мы научимся: создавать приложения, описывать экраны, работать с такими компонентами как нижнее меню, прокручиваемый список, панель, контейнер фрагментов. А также способы локализации приложений.

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

В приложении имеется 4 группы экранов:

- продукция,

- сервисы по ремонту,

- о нас,

- новости.

На данном этапе приложение должно быть локализовано для трех языков: украинского, русского, английского. Предполагается в дальнейшем расширить количество языков.

Дизайн начальных экранов этих групп приведен на рисунках ниже:


Рис. 1 экран HOME

Рис. 2 экран REPAIRS_MAIN

Рис. 3 экран ABOUT

Рис. 4 экран NEWS

Во время загрузки картинок с сервера и в случае их отсутствия показывать иконку sms.png.

При клике на текст “полное описание” (рис. 3) показывается дополнительное описание, а текст меняется на “свернуть описание”. При повторном клике на этот текст (“свернуть описание”) возвращаемся в исходное состояние.

На рисунках ниже приведен дизайн: заставки при отсутствии новостей; панели выбора языка; панели с сообщениями об ошибках сети и сервера; сплэш экрана, который показывается во время загрузки приложения


Рис. 5 экран NEWS

Рис. 6 экран HOME

Рис. 7 экран HOME

Рис. 8 заставка SPLASH

Для всех запросов будет использоваться базовый URL: "http://examples.delta.branderstudio.com/"

Описание API

Экран HOME URL "depro/products/categories", ответ
[
    {
        "categoryId":49,
        "title":"Линии переработки ТБО",
        "imagePath":"images/icon-conveyor_nut.png",
        "count":12,
        "order":0
    }, 
    {
        "categoryId":53,
        "title":"Линии переработки фруктов/овощей/ягод",
        "imagePath":"images/icon-conveyor_berry.png",
        "count":21,
        "order":1
    },
    ....
]

Экран ABOUT URL depro/org/about, ответ
{
    "orgId":1,
    "mainImagePath":"images/about_us.png",
    "text1":"ООО «Системы модернизации складов»\\n«Системы модернизации складов» – украинская компания-производитель.\\nГлавная Миссия компании: способствовать ....",
    "text2":"Комплексные решения компании «СМС» – это разработка, проектирование....",
    "videoLink":"C6d_iP_RZrc",
    "phone":"067 890 98 76"
}

Экран NEWS URL "depro/news/list",ответ
[
    {
        "newsId":1539,
        "title":"Официально открыта фирма в Грузии — Motion Systems LLC",
        "date":"2019-04-21T17:04:38-0",
        "mainImagePath":"images/IMG-dd209ba862386381e870c31b9f48cd37-V.jpg"
    },
    {
        "newsId":1551,
        "title":"ПУЭТ День карьер",
        "date":"2019-04-24T07:04:33-0",
        "mainImagePath":"images/PUET-foto2.jpg"},
    ....
]

Общие решения по разработке приложения.

Из анализа первых 4-х экранов видно, что должна быть активность с нижним меню и 4-е фрагмента, которые будут загружаться при клике на соответствующие кнопки меню. Тулбар можно разместить в активити, а можно и во фрагментах. В данном приложении размещаем его во фрагментах. Хотя в библиотеке есть компонент ToolBar, но здесь мы будем использовать обычный RelativeLayout.

Наши действия по разработке приложения

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

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

3. Изменение файла build.gradle

Откорректированный файл имеет вид:

apply plugin: 'com.android.application'
android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "com.dpcsa.stock"
        minSdkVersion 17
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
           minifyEnabled false
           proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    
    packagingOptions {
        exclude 'META-INF/DEPENDENCIES'
    }
    allprojects { repositories { jcenter() } }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
    implementation 'com.android.support:appcompat-v7:28.0.0'

    implementation 'github.com/depro49/depro:compon:1.0.0'
}

Здесь синим цветом выделены добавленные и измененные строки. Так как в приложении мы не будем использовать класс ConstraintLayout, то из секции dependencies удалена строка

implementation 'com.android.support.constraint:constraint-layout:1.1.3'

После изменения build.gradle нужно синхронизировать проект

4. Создание необходимых классов (файлов). Названия классов можно использовать собственные.

Файл StockDeclareScreens.java

public class StockDeclareScreens extends DeclareScreens {
    public final static String MAIN = "main", HOME = "home", 
        REPAIRS_MAIN = "repairs_main", ABOUT = "ABOUT", NEWS = "news";

    @Override
    public void declare() {
        activity(MAIN, R.layout.activity_main)
            .fragmentsContainer(R.id.content_frame)
            .navigator(handler(R.id.apply, VH.SET_LOCALE))
            .menuBottom(R.id.nav, HOME, REPAIRS_MAIN, ABOUT, NEWS)
            .component(TC.RECYCLER,            // меню вибору мови
                model(JSON, getString(R.string.jsonListLang)),
                view(R.id.recycler, new int[] {R.layout.item_lang, R.layout.item_lang_sel}).selected("id_language"));
    }
}

В этом файле будут описываться все экраны приложения. Сейчас описан только главный экран. В нем описаны строковые константы MAIN, HOME, REPAIRS_MAIN, ABOUT, NEWS, которые служат названием используемых в приложении экранов. Названия экранов можете выбирать на свой вкус. В примере используются названия присвоенные дизайнерами.

В методе declare() в строке activity(MAIN, R.layout.activity_main) указывается, что у нас есть экран с именем MAIN и id лайоута activity_main (файл разметки).

Можете посмотреть его содержимого в ресурсах. Там имеются стандартные элементы FrameLayout, который используется в качестве контейнера для фрагментов и RadioGroup с кнопками меню. Также там имеются элементы библиотеки SheetBottom - всплывающая панель для которой можно задать атрибутом viewId - View которая будет отображаться при раскрытии элемента и, при необходимости атрибутом negativeViewId можно указать элемент при клике на который панель закроется. В данном уроке используются панели для выбора локали (дизайн на рис. 6) и для сообщения об ошибках (рис. 7).

В строке .fragmentsContainer(R.id.content_frame) указывается контейнер для фрагментов.

Строка .menuBottom(R.id.nav, HOME, REPAIRS_MAIN, ABOUT, NEWS) указывает, что на экране будет нижнее меню с id элемента RadioGroup разметки. Также указывается перечень экранов, которые будут вызываться при клике на ту или иную кнопку. Порядок в перечне соответствует порядку кнопок в RadioGroup. Стартовым будет экран для которого в соответствующей кнопке указано android:checked="true". В нашем случае это экран HOME.

Строки начиная с .component(TC.RECYCLER, .... Описывают список (на это указывает тип компонента TC.RECYCLER) языков, которые будут показываться в панели с id="@+id/lang" (находится в подключаемом лайоуте view_lang).

В строках view(R.id.recycler, new int[] {R.layout.item_lang, R.layout.item_lang_sel}) указывается, что для отображения списка будет использован элемент разметки R.id.recycler. А вид каждого айтема задается R.layout.item_lang и R.layout.item_lang_sel. Строка .selected("id_language") указывает, что в списке будет выделяться некоторые строки по полю в данных с именем id_language.

Данные для списка задаются в строке model(JSON, getString(R.string.jsonListLang)). В ней указывается что в R.string.jsonListLang будут данные в формате JSON.

На языковой панели также имеется кнопка (TextView) с id="@+id/apply". В строке .navigator(handler(R.id.apply, VH.SET_LOCALE)) указывается, что при клике на эту кнопку нужно установить выбранную локаль.

Файл StockAppParams.java

public class StockAppParams extends AppParams {
    @Override
    public void setParams() {
        baseUrl =  "http://examples.delta.branderstudio.com/";

        progressViewId = R.layout.dialog_progress;
        errorDialogViewId = R.id.error_dialog;

        idStringDefaultErrorTitle = R.string.er_title_def;
        idStringERRORINMESSAGE = R.string.er_message;
        idStringNOCONNECTION_TITLE = R.string.er_connect_title;
        idStringNOCONNECTIONERROR = R.string.er_connect;
        idStringTIMEOUT = R.string.er_timeout;
        idStringSERVERERROR = R.string.er_server_error;
        idStringJSONSYNTAXERROR = R.string.er_json_syntax;

        nameLanguageInHeader = "Language";
        nameLanguageInParam = "id_language";
        initialLanguage = "uk";
    }
}

В этом файле указываются параметры общие для всего приложения. В нашем случае это следующие параметры.

baseUrl - указывает адрес сайта.

В библиотеке предусмотрено показывать прогресс при выполнении операций ввода-вывода данных. Прогресс можно показывать либо в пользовательском классе, либо в библиотечном. В первом случае устанавливается параметр classProgress, во втором указывается id лайоута с прогрессом. Мы используем второй вариант и задаем параметр progressLayoutId.

Аналогично отображаются сообщения об ошибках ввода-вывода: либо класс пользователя, либо id View на экране разметки. Мы используем второй вариант и задаем параметр errorDialogViewId. Его значение R.id.error_dialog буде для всех экранов, где предусматривается вывод сообщения об ошибках. Фрагменты используют R.id.error_dialog своей activity.

После указания этих параметров вывод прогресса и ошибок осуществляется автоматически.

Сообщение об ошибке приходит с сервера в формате: {“title”:”текст заголовка”,”message”:”текст сообщения”}. В R.id.error_dialog присутствуют элементы с соответствующими id (R.id.title и R.id.message). В принципе, можно передавать и другие данные и использовать другие названия данных. Лишь бы они совпадали с названиями полей в R.id.error_dialog. При этом нужно учитывать следующее.

Кроме ошибок, которые приходят с сервера, существуют ошибки, которые обнаруживаются на смартфоне, например, time out, отсутствие интернета и др. В этом случае библиотека формирует свои сообщения об ошибках в указанном выше формате. Поэтому, если с сервера будут приходить другие названия, то в разметке нужно предусмотреть R.id.title и R.id.message, либо эти названия указать в алиасах.

С учетом этого понятно как заполнять параметры которые начинаются с “idString...”.

И последняя группа параметров связана с локализацией: nameLanguageInHeader - задает название локали в заголовках запросов, initialLanguage - стартовую локаль. Локаль может передаваться и в параметрах. Для этого служит nameLanguageInParam.

Файл StockApp.java

public class StockApp extends Application {
    private static StockApp instance;
    private Context context;
    public static StockApp getInstance() {
        if (instance == null) {
            instance = new StockApp();
        }
        return instance;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        instance = this;
        context = getApplicationContext();
        DeclareParam.build(context)
            .setNetworkParams(new StockAppParams())
            .setDeclareScreens(new StockDeclareScreens());
    }
}

Здесь в конструкторе инициируется библиотека (DeclareParam.build(context)) и ей передаются классы StockAppParams и StockDeclareScreens.

Файл MainActivity.java

Изменяем файл MainActivity следующим образом:

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

Здесь в методе getNameScreen указывается название стартового экрана (MAIN)

Файл API.java

public class API {
    public static String CATEGORIES = "depro/products/categories";
    public static String NEWS = "depro/news/list";
    public static String ABOUT = "depro/org/about";
}

В нем указываются адреса на сервере для данных.

Файл AndroidManifest.xml

Наконец внесем исправления в манифест:

    <application
        android:allowBackup="true"
        android:icon="@drawable/icon"
        android:label="@string/app_name"
        android:name=".StockApp"
        android:supportsRtl="true"
        android:usesCleartextTraffic="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity"
            android:screenOrientation="portrait"
            android:theme="@style/SplashTheme"
            android:windowSoftInputMode="adjustPan"
            android:launchMode="singleTask">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
    <uses-permission android:name="android.permission.INTERNET"/>

Здесь использованы все стандартные атрибуты

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

Если все было сделано правильно, то вы получите следующий экран:

При тапе на элементы нижнего меню в логах будете получать сообщения типа:

SMPL_APP: 0003 No screen with name home

Это библиотека сообщает об отсутствии описания соответствующих экранов.Так как мы не меняли названия тегов (параметр NAME_LOG_APP в файле StockAppParams), то сообщения выдаются с тегами по умолчанию.

6. Опишем остальные экраны

Так как эти экраны будут ссылаться на другие экраны, то нужно дополнить список экранов. Чтобы постоянно не корректировать этот список введем сразу названия всех экранов. Для этого в StockDeclareScreens.java добавим нужные строковые константы:

SERVICE = "service", CATEGORY = "category", PRODUCT = "product", ITEM_FORM_REPAIR = "item_form_repair", ITEM_FORM = "item_form", 
REPAIRS_CALC = "repairs_calc", ITEM_FORM_SERVICE = "ITEM_FORM_SERVICE", 
COUNTRY_CODE_PH = "COUNTRY_CODE_PH", COMMENT = "COMMENT", THANKS = "THANKS", NEWS_DETAIL = "NEWS_DETAIL", WRITE_US = "WRITE_US", 
BACK_THANKS = "BACK_THANKS", YOUTUBE = "YOUTUBE"

Добавим в метод declare() класса StockDeclareScreens.java описание экрана HOME:

    fragment(HOME, R.layout.fragment_home)
        .setValue(item(R.id.lang_txt, TS.LOCALE))
        .navigator(show(R.id.sel_lang, R.id.lang, true))
            .component(TC.RECYCLER,
                model(API.CATEGORIES, 1).sort("order"),
                view(R.id.recycler, R.layout.item_home),
                navigator(start(CATEGORY, PS.RECORD)));

Перед дальнейшим изучением материала нужно ознакомиться с файлом разметки экрана fragment_home и разметкой элементов списка R.layout.item_home.

В описании экрана многие элементы нам знакомы, поэтому опишем только новые.

В некоторых случаях необходимо установить элементам разметки значения, которые не приходят с данными (модели). Для этого используется setValue. Им можно присвоить значения сразу нескольким элементам. Каждое присвоение описывается конструкцией item(id элемента, тип присвоения [, значение]). В нашем случае элементу с id lang_txt присваивается значение текущей локали.

Обработчик show указывает, что при клике на R.id.sel_lang нужно показать элемент R.id.lang (панель выбора языка), который находится в activity (MAIN).

Данные для списка задаются model(API.CATEGORIES, 1). Здесь в API.CATEGORIES находится адрес (URL) на сервере.

В библиотеке имеется две модели кэширования. Обе они задаются конструкцией model(url, duration). Первая модель кеширования работает если duration > 1. В этом случае при запросе на получение данных проверяется выполнялся ли предыдущий запрос меньше чем duration миллисекунд тому. Если да то данные берутся из кэша, если больше то данные читаются с сервера (и заносятся в кэш.). Вторая модель используется если duration = 1. В этом случае при запросе на получение данных компоненту сразу передаются данные из кэша (при их наличии) потом читаются данные с сервера, обновляется кэш и полученные данные передаются в компонент.

В соответствии с постановкой задачи использован второй способ кэширования данных, а именно: model(API.CATEGORIES, 1).

Обработчик навигатора start(CATEGORY, PS.RECORD)) указывает, что при клике на какой нибудь элемент списка будет вызван экран CATEGORY и ему будет передана запись, соответствующая элементу на который кликнули.

Элементы списка отображаются в соответствии с R.layout.item_home. В этом лайоуте имеется элемент библиотеки ComponImageView. Ему можно указать кастомные атрибуты: placeholder, blur и oval. В остальном его поведение наследуется от AppCompatImageView. В нашем случае задан атрибут app:placeholder="@drawable/sms" для выполнения условий постановки задачи (Во время загрузки картинок с сервера и в случае их отсутствия показывать иконку sms.png).

Экран REPAIRS_MAIN (рис. 2) вообще простой. Он описывается следующим образом:

    fragment(REPAIRS_MAIN, R.layout.fragment_repairs_main)
        .setValue(item(R.id.lang_txt, TS.LOCALE))
        .navigator(show(R.id.sel_lang, R.id.lang, true),
            start(R.id.apply, SERVICE, true));

Здесь все нам знакомое. Требует пояснения обработчик навигатора start(R.id.apply, SERVICE, true). В ряде случаев при клике на элемент разметки действие выполняется достаточно долго. Чтобы исключить повторное нажатие на этот элемент блокируют (делают недоступной для кликов). Третий параметр (true) как раз и указывает на необходимость блокировать R.id.apply после клика.

Также добавим описание экрана ABOUT

    fragment(ABOUT, R.layout.fragment_about)
        .setValue(item(R.id.lang_txt, TS.LOCALE))
        .navigator(show(R.id.sel_lang, R.id.lang, true), start(R.id.apply, WRITE_US))
        .component(TC.PANEL,
            model(API.ABOUT),
            view(R.id.panel),
            navigator(start(R.id.video, YOUTUBE),
                handler(R.id.phone, VH.DIAL_UP),
                showHide(R.id.full_desc, R.id.text2, R.string.hide, R.string.full_desc)));

Здесь у нас появляется компонент нового типа - PANEL. Под панелью понимается View (LinearLayout, RelativeLayout, ...). Данные полученные по модели заносятся в соответствующие элементы панели по совпадению имен.

Обработчик handler(R.id.phone, VH.DIAL_UP) обеспечивает вызов стандартной звонилки при клике на R.id.phone (TextView или его наследник). При этом в качестве номера выступает содержимое R.id.phone. Обработчик showHide(R.id.full_desc, R.id.text2, R.string.hide, R.string.full_desc) обеспечивает показ\скрытие элемента R.id.text2 при клике на R.id.full_desc (TextView или его наследник). При этом текст R.id.full_desc будет устанавливаться R.string.hide или R.string.full_desc.

И наконец опишем экран NEWS

    fragment(NEWS, R.layout.fragment_news)
        .setValue(item(R.id.lang_txt, TS.LOCALE))
        .navigator(show(R.id.sel_lang, R.id.lang, true))
        .component(TC.RECYCLER,
            model(API.NEWS).errorShowView(R.id.error_view),
            view(R.id.recycler,  R.layout.item_news).noDataView(R.id.no_news),
            navigator(start(0, NEWS_DETAIL)));

Здесь нужно пояснить конструкции errorShowView(R.id.error_view) и noDataView(R.id.no_news. noDataView указывает элемент который будет показан в случае отсутствия данных для списка. errorShowView указывает элемент в который будет выводиться сообщение об ошибке ввода-вывода. Если указан этот функционал для модели, то сообщения об ошибке будут выводиться не в соответствии с заданными в классе StockAppParams.java параметрами, а в элемент который указан errorShowView. По непонятным мне причинам дизайнеры решили для двух экранов сообщение об ошибках показывать не так как на всех экранах. Поэтому здесь и применен функционал errorShowView.

7. Запуск приложения

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

В логах можно посмотреть сообщения приложения. Если Вы не задавали параметры NAME_LOG_NET и NAME_LOG_APP (в классе StockAppParams.java), то сообщения будут с тегами по умолчанию: SMPL_NET и SMPL_APP.



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