Вы находитесь на странице: 1из 112

Министерство транспорта Российской Федерации

Федеральное агентство железнодорожного транспорта


Федеральное государственное бюджетное образовательное учреждение
высшего образования
«Дальневосточный государственный университет путей сообщения»

Кафедра «Вычислительная техника и компьютерная графика»

РАЗРАБОТКА ПРИЛОЖЕНИЙ ДЛЯ ОС ANDROID

Рекомендовано
Методическим советом
в качестве учебного пособия

Хабаровск
Издательство ДВГУПС
2023
УДК 004.432 (075.8)
ББК З973.22я73
P 17
Авторы:

И. В. Кузнецов, М. С. Исаев, Ю. В. Пономарчук, А. А. Холодилов

Рецензенты:

начальник УМУ ФГБОУ ВО ХГУЭП


доцент кафедры цифрового государственного и корпоративного управления
Хабаровского государственного университета экономики и права,
кандидат экономических наук Е. Н. Тумилевич;

доктор технических наук, старший научный сотрудник


лаборатории Информационных технологий Вычислительного центра
Дальневосточного отделения Российской академии наук
И. А. Кривошеев

К 17 Разработка приложений для ОС Android : учебное пособие


/ И. В. Кузнецов, М. С. Исаев, Ю. В. Пономарчук, А. А. Холодилов.
– Хабаровск : Изд-во ДВГУПС, 2023. – 112 с. : ил.

Соответствует рабочей программе дисциплины «Разработка мобиль-


ных приложений».
Рассмотрены базовые компоненты языков программирования Java, Kotlin,
особенности реализации объектно-ориентированного подхода в этих языках, а
также и основные принципы разработки мобильных приложений. В пособие
включены разделы по разработке многопоточных приложений, организации кли-
ент-серверной архитектуры, принципов организации работы баз данных, пред-
ставления графического содержимого, работы с сервисами, сенсорами, и уве-
домлениями, представления графического интерфейса мобильных приложений.
Особое внимание уделено вопросам использования средств актуального стан-
дарта языка. Разделы пособия сопровождаются примерами и иллюстрациями.
Предназначено для студентов 2–4-го курсов всех форм обучения по техни-
ческим направлениям подготовки бакалавриата и магистратуры по укрупнен-
ной группе направления подготовки 09.00.00 «Информатика и вычислитель-
ная техника».

УДК 004.432 (075.8)


ББК З973.22я73

© ДВГУПС, 2023
2
ВВЕДЕНИЕ

Обучение студентов азам современного промышленного языка про-


граммирования и навыкам работы с системой разработки программных
приложений позволит заметно поднять их образовательный и культурный
уровень, облегчит восприятие современных инфокоммуникационных тех-
нологий и обеспечит их развитие как исследователей в технической сфере.
Язык программирования Java стремительно распространился в широ-
ких кругах сообщества благодаря сочетанию быстрого развития сети
Internet и телекоммуникационных технологий, возрастанию потребности
образования в новых подходах к программированию, успехам в создании
мобильных вычислительных устройств. Его особенности позволили срав-
нительно быстро привлечь интерес профессионалов из других областей
программирования к разработке кроссплатформенных приложений: неза-
висимость небольших мобильных программ в сочетании с генерацией ко-
да в процессе выполнения, переносимость, строгая типизация с поддерж-
кой динамических типов и системы «сборки мусора». Java предоставляет
мощную поддержку принципов объектно-ориентированного проектирова-
ния и программирования, сочетая простой и ясный синтаксис с надежной
и удобной в работе средой разработки приложений, что позволяет быстро
обучаться и создавать новые программы.
Пособие снабжено многочисленными примерами из курса информати-
ки и основ программирования, которые наглядно иллюстрируют различ-
ные возможности изучаемого объектно-ориентированного языка. Также в
пособии рассмотрены возможности Java для работы с файлами, исключе-
ниями, разработки многопоточных программ, организации сетевого взаи-
модействия с помощью сокетов, функционального программирования, а
также тестирования и верификации программного обеспечения. Включе-
ны разделы по локализации приложений и аннотированию.
Для закрепления материала, представленного в пособии, в конце каж-
дого раздела даны контрольные вопросы.
Обучение студентов базовым принципам разработки мобильных при-
ложений позволит поднять их образовательный и культурный уровень,
облегчит восприятие современных инфокоммуникационных технологий и
обеспечит их развитие как исследователей в технической сфере.
Операционная система Android получила широкое распространение
среди мобильных приложений благодаря открытости, простоте модифи-
кации, а также широким возможностям создания для нее прикладного
программного обеспечения. Ее особенности вкупе с растущим рынком

3
мобильных устройств делают разработку мобильных приложений акту-
альной сферой деятельности, требующей актуальных навыков разработ-
ки приложений.
Пособие снабжено многочисленными примерами из курса информати-
ки и основ программирования, которые наглядно иллюстрируют различ-
ные аспекты разработки приложений для ОС Android. Также в пособии
рассмотрены возможности средств разработчика для ОС Android для ор-
ганизации сетевого взаимодействия, работы с базами данных, представле-
ния графического содержимого и других вопросов, с которыми встреча-
ются разработчики мобильных приложений. Включены разделы по лока-
лизации приложений и аннотированию.
Для закрепления материала, представленного в пособии, в конце каж-
дого раздела даны контрольные вопросы.
Труд авторов распределился следующим образом: И. В. Кузнецов
(гл. 7, 8, 14–16), М. С. Исаев (гл. 2–4), Ю. В. Пономарчук (гл. 1, 5, 6, 13),
А. А. Холодилов (гл. 9–11).

4
1. ПРОСТЕЙШЕЕ ПРИЛОЖЕНИЕ ДЛЯ ANDROID

Android – операционная система с открытым исходным кодом, пред-


назначенная для смартфонов, планшетов, устройств, автомобилей, Интер-
нета Вещей и т.д. Базируется на ядре Linux. Использует среду выполне-
ния Android Runtime для выполнения приложений.
Для разработки приложений для платформы Android, как правило,
используются наборы средств, базирующиеся на следующих технологиях:
1) язык разметки XML;
2) языки программирования Kotlin, Java, Dart (также возможно
использование других ЯП высокого уровня с помощью дополнитель-
ных фреймворков);
3) язык программирования СУБД SQLite.
При разработке мобильных приложений требуется учитывать ряд
особенностей, накладывающих ограничения на выбор конкретных средств
разработки. Прежде всего, требуется учитывать, что мобильные устройст-
ва обладают значительно меньшим количеством вычислительных ресурсов,
что ставит вопрос оптимизации программного продукта, а также не позво-
ляет использовать мобильные устройства для выполнения требовательных
к ресурсам задач. Кроме того, мобильные устройства, как правило, не обла-
дают достаточным запасом энергии для выполнения длительной работы,
что ставит вопрос как об ограничении возможностей продолжительного
выполнения приложений, так и об их оптимизации с целью уменьшения
потребляемой энергии при их работе. Вторым фактором, оказывающим
значимое влияние на разработку мобильных приложений, является боль-
шое разнообразие устройств, обладающих различными характеристиками.
Устройства различных моделей отличаются как на уровне общих характе-
ристик, таких как частота или количество ядер процесса, так и, к примеру,
наборами доступных сенсоров, что ставит вопрос о проектировании при-
ложения таким доступным для наибольшего сегмента аудитории.
Большое разнообразие устройств при этом ставит проблему использо-
вания устаревших средств разработки, вследствие чего разработчику тре-
буется достичь поддержки приложения не только устройствами с различ-
ным аппаратным обеспечением, но также и с большим количеством вер-
сий операционной системы.
Android позволяет использовать большое количество языков програм-
мирования. На ранних этапах своего развития операционная система
использовала виртуальную машину Dalvik для запуска программ, напи-
санных на языке Java, в дальнейшем же Dalvik была заменена средой ис-
5
полнения Android Runtime. Также посредством средств Android
Native Development Kit возможна разработка программных продук-
тов на языке C.
Начиная с 2019 года приоритетным языком программирования для
Android был выбран Kotlin. Этот язык отличается большей простотой и
лаконичностью синтаксиса, нежели Java, сохраняя при этом высокую
скорость выполнения программ. Kotlin обладает совместимостью с Java,
что позволяет использовать оба языка в равной мере при разработке про-
грамм для Android. При этом знание Java также остается актуальным для
android-разработчика в силу большого объема программ, написанных ра-
нее, и которые необходимо поддерживать. Более подробно с этими языка-
ми Вы можете познакомиться с помощью специализированных ресурсов и
источников литературы. В дальнейшем же примеры кода будут приво-
диться как на Java, так и на Kotlin с использованием свободно распро-
страняемой среды разработки Android Studio.
Запишем простейшее приложение, выводящее сообщение «Hello,
World!» на экран смартфона. Для этого создадим проект в Android Studio:
Для создания проекта следует выполнить следующие действия:
1) нажмите кнопку «create a new project»;
2) во вкладке «Phone and Tablet» выберите шаблон проекта «Empty
Activity»;
3) укажите имя проекта и выберите язык программирования, который в
дальнейшем будет использоваться;
4) выберите минимальную версию комплекта разработчика (Software
Development Kit, SDK).
Будет сформирован проект со следующей структурой:

app
manifests
java
com.example.myapplication
com.example.myapplication (test)
com.example.myapplication (androidTest)
res
drawable
layout
mipmap
values
Gradle Scripts

6
Структурно проект android-приложения состоит из элементов App и
Gradle Scripts. Папка App содержит ресурсы и программные коды, а
также файл манифест приложения. Gradle Scripts файлы сценариев
системы сборки Gradle, а также необходимые для ее работы файлы кон-
фигурации. Система сборки Gradle отвечает за конфигурацию приложе-
ния, его сборку, загрузку и подключение необходимых для работы биб-
лиотек и т.д.
В сформированном в примере приложении будет автоматически сфор-
мирован класс MainActivity (см. листинги 1, 2).
Листинг 1 – класс MainActivity на языке Java
package com.example.myapplication;

import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}

Листинг 2 – класс MainActivity на языке Kotlin


import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle

class MainActivity : AppCompatActivity() {


override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}

Класс MainActivity выступает в качестве точки входа в приложение.


Старт работы приложения начинается с выполнения методы onCreate.
Данный метод содержит две команды – вызова метода суперкласса
onCreate и вызов метода setContentView, который отвечает за отобра-
жение представления. В качестве представления в данном случае исполь-
зуется файл activity_main.xml, описывающий конфигурацию активно-
7
сти MainActivity. Под активностью же понимается сущность, описы-
вающая содержимое экрана мобильного устройства и являющаяся анало-
гом фрейма или окна в других типах приложений. Активность функцио-
нально состоит из представления содержимого экрана, которое в боль-
шинстве случаев описывается с помощью разметок в файлах форма
та .xml, и логики ее работы, которая описывается программным кодом.
Замечание: в ряде ситуаций разметка может описываться не с помо-
щью файлов .xml, а другими способами, или не описываться вовсе.
В рассматриваемом примере разметка активности представлена сле-
дующим листингом:
Листинг 3 – разметка ActivityMain
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

В рассматриваемом примере в разметке указан виджет TextView с со-


держимым в виде сообщения «Hello, World!», а потому дальнейшая
модификация приложения не требуется.

Контрольные вопросы

1. Что такое Android Runtime?


2. Где хранятся в проекте файлы с программным кодом?
3. Что такое активность?
4. Какие языки программирования используются для программирова-
ния android-приложений?
5. Что такое Gradle?
8
2. РАЗМЕТКИ АКТИВНОСТЕЙ В ПРИЛОЖЕНИЯХ
ДЛЯ ANDROID
Разметкой называется описание структуры компонентов пользователь-
ского интерфейса, представляемого приложением, в частности, в активно-
сти [1]. В рамках разметки описывается то, как элементы интерфейса рас-
положены относительно друг друга, их иерархический порядок, а также
свойства отдельных компонентов.
Базовыми компонентами разметки является виджеты, реализуемые с
помощью класса View (представление) и его наследников.
Замечание: объекты класса View представляют собой описание эле-
ментов интерфейсов. Во многих источниках литературы такие элементы
называются виджетами.
К поставляемым по умолчанию виджетам относятся следующие:
1) TextView – предназначен для отображения текстовых сообщений;
2) EditText – предназначен для ввода текстовой информации, с помо-
щью атрибута android:inputType может быть специализирован для ввода
конкретного типа информации, такой как пароли, адреса электронной
почты, телефонные номера и т.д.;
3) Button – предназначен для получения команды пользователем на
выполнение заданного действия.
Замечание: Button представляет собой расширение класса TextView;
4) Floating Action Button – специальный вид кнопки, концептуально
предназначенный для предоставления возможности пользователю подать
команду на выполнение наиболее часто используемой или важной команды;
5) Chip –виджет, который хранит информацию о том, был ли он отме-
чен пользователем; совокупность виджетов Chip может быть упакована в
виджет-контейнер Chip Group;
6) ChexBox – виджет, которых хранит информацию о том, был ли он
отмечен пользователем; находится в одном из двух состояний – отмечен
или не отмечен; состояние ChexBox может изменяться многократно; в от-
личие от Chip, ChexBox, как правило, представляет свое состояние с по-
мощью отдельного графического элемента;
7) RadioButton – виджет, которых хранит информацию о том, был ли
он отмечен пользователем; в отличие от ChexBox, единичный элемент
RadioButton не может перейти из состояния «отмечено» в состояние «не-
отмечено»; в случае же упаковки нескольких таких виджетов в контейнер
RadioGroup в состоянии «отмечено» может находиться лишь один из них;
8) Switch и ToggleButton – виджеты, которые хранят информацию о
том, был ли он отмечен пользователем; концептуальное отличие Switch от
Button заключается в том, что Switch является объектом пользовательско-
9
го ввода и используется пользователем для установки состояния какого-
либо компонента приложения, в то время как ToggleButton является кноп-
кой и предназначен для обработки поступающих от пользователя команд;
данные виджеты могут быть представлены в виде выключателя и кнопки
питания соответственно – выключатель устанавливает одно из состояний
(например, включен или выключен свет в квартире), в то время как кнопка
питания предназначена для подачи команды (включить или выклю-
чить устройство);
9) ImageView – виджет, предназначенный для показа изображения;
10) VideoView – виджет, предназначенный для показа видеороликов;
11) WebView – виджет, предназначенный для показа содержимого
веб-ресурсов;
12) ProgressBar – виджет, предназначенный для отображения прогрес-
са выполнения какой-либо задачи;
13) RecyclerView – виджет, предназначенный для отображения списков;
14) NavigationView – виджет, предназначенный для отображения ме-
ню приложения;
15) TabLayout – предназначен для показа информации, структуриро-
ванной по отдельным вкладкам, и другие.
Все виджеты обладают рядом свойств, описывающих их положение
относительно друг друга, размеры, содержимое и другую информацию.
Данные свойства могут быть заданы как программно, так и в разметке с
помощью языка XML.
XML – расширяемый язык разметки (Extensible Markup Language).
Данный язык предназначен для хранения структурированной информации
об объектах и их совокупностях. Объекты в рамках XML описываются с
помощью тегов. В рамках разработки android-приложений ряд тегов (в ча-
стности, теги, описывающие виджеты) задан в SDK изначально, в общем
же случае теги описываются разработчиком. Документы в формате XML
описываются с соблюдением следующих правил.
1. Документ в формате XML в первой строке должен хранить деклара-
цию – строку с информацией о версии языка и используемой в документе
кодировке:
<?xml version="1.0" encoding="utf-8"?>

2. Все теги являются парными – описание любого объекта начинается с


открывающего тега и заканчивается закрывающим. Между открывающим
и закрывающим тегом помещается тело тега – непосредственно хранимая
информация, как показано в листинге 1:
<tag>Body</tag>
10
3. Если тело тега пусто, то допускается не использовать закрывающий
тег, а вместо этого дополнить открывающий символом /:
<tag/>

4. Имена тегов должны начинаться с буквы, и могут использовать бук-


вы, цифры и специальные знаки с соблюдением регистра:
<tag>Правильно</tag>
<Tag> Правильно </Tag>
<taG> Неправильно </tag>
<1aG> Неправильно </1ag>

5. Тег состоит из имени и атрибутов, которые могут принимать различ-


ные строковые значения. Атрибуты предназначены для хранения допол-
нительной информации об объекте и размещаются в открывающем теге:
<1aG attr1=”text”> Body </1ag>

6. Для представления атрибутов и тегов, а также в ситуациях, когда


может использоваться несколько тегов или атрибутов с одинаковым на-
званием, используются пространства имен. Пространство имен описыва-
ется атрибутом xmlns в открывающем теге элемента:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:onClick="clicked"
tools:context=".MainActivity" />

7. Элементы описываются иерархически. Дочерние элементы распола-


гаются в телах родительских. Все элементы в документе XML являются
дочерними к одному, который называется корневым:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:onClick="clicked"
11
tools:context=".MainActivity">
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="27dp"
android:layout_marginLeft="27dp"
android:layout_marginTop="40dp"
android:text="Button"

app:layout_constraintStart_toStartOf="@+id/editTextTextEmailAddress
2"

app:layout_constraintTop_toBottomOf="@+id/editTextTextEmailAddress2
" />
<EditText
android:id="@+id/editTextTextEmailAddress2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="85dp"
android:layout_marginLeft="85dp"
android:layout_marginBottom="271dp"
android:ems="10"
android:inputType="textEmailAddress"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

8. Информация, которая не должна определяться как элемент XML,


располагается в секциях CDATA:
<![CDATA[
текст в CDATA не определяется как часть XML, например, выражение
x<y будет определено как текст
]]>

9. Для отображения специальных символов, используемых в XML (на-


пример, кавычки <>) используются специальные идентификаторы:
Символ Идентификатор
< &lt;
> &gt;
& &amp;
‘ &apos;
“ &quot;
12
10. Комментарии в XML записываются в следующем виде:
<!-- Комментарий-->

В разработке приложений для android XML используется для хранения


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

Контрольные вопросы

1. Что такое разметка?


2. Что такое виджет?
3. Для чего используется язык XML и каковы его свойства?
4. Какие основные группы виджетов представлены в android-приложениях?
5. Для чего нужна секция CDATA?

3. ВИДЫ РАЗМЕТОК
Существует большое число различных видов разметок приложений
для android, позволяющих построить пользовательский интерфейс наибо-
лее близко соответствующим требованиям заказчика. Все разметки явля-
ются наследниками класса ViewGroup. Android SDK предоставляет ряд
стандартных видов разметок, а также полезных в разработке элементов.
LinearLayout – линейная разметка, при использовании которой вид-
жеты располагаются друг за другом по вертикали или горизонтали как по-
казано на рис. 1.

Рис. 1. Примеры линейной разметки

Для установки параметров выравнивания дочерних элементов может


использоваться атрибут gravity. Для установки ориентации разметки
(вертикальная, горизонтальная) используется атрибут orientation. Размеры
13
столбцов/строк могут быть заданы пропорционально относительно друг
друга с помощью атрибута weight. Размеры элементов будут вычислены
автоматически. Пример двух кнопок с весами 1 и 2 соответственно пока-
зан на рис. 2.
GridLayout – разметка, при которой виджеты располагаются в ячей-
ках, размер которых определяется в соответствии с размерами наиболь-
ших виджетов строк и столбцов. При этом внутри виджет ячейки может
принять любой размер. Ячейки, в которых размещаются виджеты, опреде-
ляются по номеру столбца и номеру строки с помощью атрибутов
layout_row и layout_column. Данная разметка является более совер-
шенной версией разметки TableLayout, в которой требуется отдельное
описание строк и столбцов. Пример использования GridLayout показан
на рис. 3.

Рис. 2. Пример использования ат- Рис. 3. Пример использования


рибута weight GridLayout

RelativeLayout – разметка, при ко-


торой расположение виджетов описыва-
ется относительно одного из виджетов,
выбирающегося базовым. Используются
атрибуты toStartOf, toLeftOf,
toRightOf, toEndOf и другие, в частно-
сти, виджеты могут быть выровнены от-
носительно других виджетов. Пример
RelativeLayout показан на рис. 4.
ConsraintLayout – разметка, кото-
Рис. 4. Пример использования рая, как и RelativeLayout, позициони-
RelativeLayout рует виджеты относительно друг друга и
14
краев экрана. От последней ConstraintLayout отличается лучшей про-
изводительностью и большим числом атрибутов, ориентированных на по-
зиционирование элементов. В частности, благодаря атрибутам
constraint_widthPercent и constraint_heigtthPercent возможна
установка размеров виджета в зависимости от ширины экрана. Пример
использования ConstraintLayout представлен на рис. 5.

Рис. 5. Пример использования ConstraintLayout

MotionLayout – расширенная версия ConstraintLayout, которую


рекомендуется использовать в случаях, когда требуется организовать
анимацию дочерних разметок при их переходах между состояниями.
MotionLayout структурно состоит из сцен – дочерних разметок – состоя-
ний, в которых находятся элементы. Сцена состоит из наборов ограниче-
ний (ConstraintSet) – наборов данных о позиционировании элементов,
применение которых приводит к смене расположения элементов на сцене
без пересоздания активности – набора состояний StateSet, описывающе-
го состояние разметки до, во время или после перехода, и набора перехо-
дов Transition, описывающего возможные переходы между элементами
StateSet или ConstraintSet. Пример настройки MotionLayout для
реализации перемещения виджета View вправо представлен в листинге 1 и
рис. 6.

Рис. 6. Пример использования MotionLayout


15
Листинг 1 – реализация MotionLayout
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.motion.widget.MotionLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/gridLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:columnCount="3"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:rowCount="3"
app:layoutDescription="@xml/activity_main_scene" >

<View
android:id="@+id/view"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@color/blue"
tools:layout_editor_absoluteX="200dp"
tools:layout_editor_absoluteY="100dp" />

</androidx.constraintlayout.motion.widget.MotionLayout>

<?xml version="1.0" encoding="utf-8"?>


<MotionScene
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:motion="http://schemas.android.com/apk/res-auto">

<ConstraintSet android:id="@+id/start">

<Constraint
android:id="@+id/view"
android:layout_width="100dp"
android:layout_height="100dp"
app:layout_editor_absoluteY="100dp"
app:layout_editor_absoluteX="100dp"
motion:layout_constraintTop_toTopOf="parent" />

16
</ConstraintSet>

<ConstraintSet android:id="@+id/end" >


<Constraint
android:id="@+id/view"
android:layout_width="100dp"
android:layout_height="100dp"
app:layout_editor_absoluteY="200dp"
app:layout_editor_absoluteX="100dp"
motion:layout_constraintEnd_toEndOf="parent"/>
</ConstraintSet>
<Transition
app:constraintSetStart="@+id/start"
app:constraintSetEnd="@+id/end"
app:autoTransition="animateToStart"
motion:duration="1000">

<OnSwipe
app:touchAnchorId="@+id/view"
app:dragDirection="dragRight"
app:touchAnchorSide="right"
/>
</Transition>
</MotionScene>

Замечание: на данный момент в большинстве случаев для увеличения


производительности и отзывчивости интерфейса рекомендуется избегать
использования разметок, отличных от
ConstraintLayout и MotionLayout.
FrameLayout предназначен для показа
различных сменяющих друг друга виджетов.
При использовании данного вида разметки
позиционирование виджетов осуществляет-
ся с помощью атрибута gravity. По умол-
чанию все виджеты помещаются в левый
верхний угол экрана, перекрывая друг друга.
Пример использования FrameLayout пока- Рис. 7. Пример использования
зан на рис. 7. FrameLayout

17
CoordinatorLayout – разметка, концептуальная задача которой за-
ключается в организации взаимодействия между дочерними элементами.
Внутри CoordinatorLayout помещаются элементы, которые должны
оказывать какое-либо воздействие друг на друга
при командах пользователя. Одним из частых
применений CoordinatorLayout является орга-
низация взаимодействия AppbatLayout и других
дочерних разметок. AppbarLayout в данном слу-
чае отвечает за расположение элементов в заголо-
вочной части активности, и должна изменять свое
состояние (как правило, сворачиваться) и давать
возможность другим дочерним активностям пред-
ставлять свое содержимое. Другим частым приме-
нением CoordinatorLayout является реализация
всплывающих уведомлений с помощью виджета
SnackBar, при котором элементы
CoordinatorLayout изменяют свое положение
при появлении или исчезновении SnackBar.
Рис. 8. Пример исполь- Пример использования CoordinatorLayout по-
зования казан на рис. 8.
CoordinatorLayout ConstraintLayout, MotionLayout и
CoordinatorLayout предоставляют большое число средств, которые мо-
гут быть использованы для создания качественного пользовательского ин-
терфейса, и в рамках данного учебного пособия описаны лишь базовые
элементы данных и других разметок. Для более подробного изучения ма-
териала вы можете ознакомиться с ресурсом [2].
При использовании ConstraintLayout для более качественного пози-
ционирования элементов могут применяться вспомогательные виджеты,
такие как Group, Chain, Barrier, Guideline, Flow и другие.
Group позволяет объединить несколько виджетов в группу с целью
проведения над ними общих операций, к которым могут относиться со-
крытие, блокировка и т.д.
Chain позволяет связать группу виджетов в последовательность и рав-
номерно распределить их в пространстве родительского виджета. Пример
использования Chain показан на рис. 9.

18
Рис. 9. Пример использования Chain

Barrier устанавливает линию, которую не могут пересечь указанные


разработчиком виджеты. Пример использования барьеров показан на
рис. 10.

Рис. 10. Пример использования Barrier


19
Guidelines позволяют создавать направляющие, которые могут быть
применены для создания общих для многих виджетов отступов, организа-
ции сеток расположения виджетов и т.д. Пример использования
Guidelines показан на рис. 11.

Рис. 11. Пример использования GuideLine

Рассмотренные виджеты, разметки и их свойства позволяют предо-


ставлять большой функционал проектирования UI, однако разнообразие
мобильных устройств не позволяет ограничиться одним макетом с разме-
рами виджетов, выраженными в процентах от экрана. Также может требо-
ваться изменять расположение виджетов в зависимости от ориентации
устройства. Для решения этих проблем устройства с разными характери-
стиками могут использовать разные ресурсы. Имена директории с ресур-
сами отмечаются квалификаторами, на основании которых происходит
выбор нужного файла с ресурсами для конкретного устройства. В частно-
сти, квалификатор land указывает, что разметки в данной директории
используется для альбомной ориентации устройства, в то время как ква-
лификатор sw600dp – что разметка не используется для устройств с шири-
ной экрана менее 600dp.
Наконец, для свойств различных элементов интерфейса – виджетов, тек-
ста и т.д. – используются различные системы измерения. К ним относятся:
− px (pixels) – пиксели;
− dp (density-independent pixels) – независимые от плотности
пиксели. Представляют собой абстрактную величину, основанную на фи-

20
зической плотности экрана 160dpi, где 1dp = 1px. Данные величины
рекомендованы для задания размеров виджетов;
− sp (scale-independent pixels) – независимые от масштабиро-
вания пиксели. Рекомендованы для задания размеров шрифтов;
− in (inches) – дюймы;
− mm (millimeters) – миллиметры;
− pt (points) – 1/72 дюйма.
Большинство устройств Android укладывается в стандартные размеры,
описываемые следующими квалификаторами:
− ldpi: приблизительно 120dpi;
− mdpi: приблизительно 160dpi;
− hdpi: приблизительно 240dpi;
− xhdpi: приблизительно 320dpi;
− xxhdpi: приблизительно 480dpi;
− xxxhdpi: приблизительно 640dpi и некоторые другие.

Контрольные вопросы

1. Переведите 20dp в inch.


2. В чем состоит отличие RelativeLayout от ConstraintLayout?
3. Для чего используется CoordinatorLayout?
4. Создайте экран, на котором будет размещен овал с возможностью
движения по маршруту в виде треугольника.
5. Для чего используется атрибут weight?

4. MATERIAL DESIGN

Дизайн пользовательских интерфейсов претерпевал существенные


изменения на этапах своего развития. Существует большое количество
методологий и стилей дизайна UI для различных платформ. К таковым
может быть отнесен Material Design. Данный стиль дизайна предполагает
ориентирование на тактильные поверхности, такие как бумага. Элементы
интерфейса могут находиться на разной высоте, отбрасывая тени и отра-
жая свет. Материальный дизайн руководствуется методами полиграфиче-
ского дизайна – типографикой, сетками, пространством, масштабом, цве-
том и изображениями, чтобы создать иерархию, смысл и фокус, которые
погрузят зрителей в опыт [3]. Отдельное внимание уделяется анимацион-

21
ной составляющей, что позволяет акцентировать внимание на значимых
элементах интерфейса.
Для использования элементов Material Design в проекте может потре-
боваться добавить библиотеку com.google.android.material.
Для этого в список зависимостей dependencies (файл build.gradle) по-
требуется добавить строку

implementation 'com.google.android.material:material:x.y.z'

где x, y, z – числа, описывающие версию библиотеки.


Рассмотрим примеры применения основных
компонентов интерфейса с использованием
Material. Кнопки и их наследники не требуют ка-
ких-либо дополнительных изменений – они авто-
матически преобразуются к
com.google.android.material.button.Mater
ialButton в случаях, когда используется тема, от-
личная от Bridge
Theme.MaterialComponents.*.
Текстовые поля представлены группой виджетов
com.google.android.material.textfield.*.
Структурно данные текстовые поля состоят из не-
посредственно текстового поля (TextInputField
или MaterialAutocompleteTextView) и макета
TextInputLayout, который отвечает за стилиза-
цию текстового поля. В рамках макета могут быть
Рис. 12. Пример заданы иконки, возможности выпадающего меню,
использования рамки т.д. Пример использования TextInput показан
TextInputField на рис. 12.
В листинге 1 представлена реализация данно-
го примера.
Листинг 1 – реализация TextInputField
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/gridLayout"
android:layout_width="match_parent"

22
android:layout_height="match_parent"
android:paddingLeft="16dp"
android:paddingRight="16dp"
app:layoutDescription="@xml/activity_main_scene">

<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textInputLayout"

style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox
"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="314dp"
android:hint="Login"

app:boxBackgroundColor="@color/design_default_color_background"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/guideline4"
app:layout_constraintStart_toStartOf="@+id/guideline3">

<com.google.android.material.textfield.TextInputEditText
android:id="@+id/login"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:inputType="textPersonName" />
</com.google.android.material.textfield.TextInputLayout>

<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textInputLayout2"

style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox
"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="11dp"

app:boxBackgroundColor="@color/design_default_color_background"
app:endIconMode="password_toggle"
app:layout_constraintEnd_toStartOf="@+id/guideline4"
app:layout_constraintStart_toStartOf="@+id/guideline3"
app:layout_constraintTop_toBottomOf="@+id/textInputLayout">

<com.google.android.material.textfield.TextInputEditText
android:id="@+id/password"
23
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:hint="Password"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>

<Button
android:id="@+id/button4"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="39dp"
android:text="Login"
app:layout_constraintEnd_toStartOf="@+id/guideline4"
app:layout_constraintStart_toStartOf="@+id/guideline3"
app:layout_constraintTop_toBottomOf="@+id/textInputLayout2"
/>

<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.15" />

<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.85" />

</androidx.constraintlayout.widget.ConstraintLayout>

В дальнейшем доступ к элементам интерфейса осуществляется анало-


гично доступу к стандартным виджетам.
Для того, чтобы обратиться к элементу интерфейса требуется объявить
в классе активности объект класса соответствующего виджету. В частно-
сти, для кнопки будет использоваться класс Button. Для того чтобы ука-
зать, что созданный объект класса описывает конкретную кнопку из раз-
метки, вместо конструктора использовать метод findViewById, в качест-
ве которого подается идентификатор (атрибут android:id) виджета.
Для обработки команд, поданных пользователем с помощью элементов
24
интерфейса, используется механизм обратного вы-
зова: в ответ на действие пользователя – например,
на клик по кнопке – будет вызван соответствую-
щий данному событию метод. В частности, для об-
работки клика по кнопке для соответствующего
объекта в классе активности будет вызван метод
setOnClickListener. Результат работы примера
показан на рис. 13.
Пример реализации обработки клика по кнопке
показан в листингах 2 и 3.
Листинг 1 – реализация MainActivity (Kotlin)
import android.os.Bundle
import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import
androidx.appcompat.app.AppCompatActivity Рис. 13. Пример
использования элемен-
class MainActivity : AppCompatActivity() { тов Material Design
lateinit var button:Button;
lateinit var l: TextView;
lateinit var p: TextView;
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button = findViewById(R.id.button4)
l = findViewById(R.id.login)
p = findViewById(R.id.password)
button.setOnClickListener {
Toast.makeText(this, """
Login: ${l.text}
Password: ${p.text}
""".trimIndent(),
Toast.LENGTH_LONG).show()
}
}
}
Листинг 1 – реализация MainActivity (Java)
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

25
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

Button button;
TextView l, p;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
button = findViewById(R.id.button4);
l = findViewById(R.id.login);
p = findViewById(R.id.password);
button.setOnClickListener(e-> Toast.makeText(this,"Login: "
+l.getText().toString()
+"\nPassword: "
+p.getText().toString(),Toast.LENGTH_LONG).show());
}
}

Использование метода findViewById является самым простым с точ-


ки зрения синтаксиса. Более эффективные способы организации доступа к
элементам интерфейса будут рассмотрены в следующих разделах.
Замечание: на данный момент вышла в свет методология Material You –
развитие Material Design, направленное на персонализацию дизайна поль-
зовательского интерфейса. Она расширяет возможности предшествующей
методологии, оставляя в силе ее принципы.
Элементы библиотеки Material Design обладают большим количеством
атрибутов, позволяющих строить качественные богатые содержимым
пользовательские интерфейсы. Более подробно с содержимым библиотеки
предлагается самостоятельно.

Контрольные вопросы
1. Создайте поле ввода паролей без фона с подсказкой «password».
2. Для чего используется размещение виджетов с различными пара-
метрами отображения тени?
3. Для чего используется метод findViewById?
4. Для чего используется класс Toast?
5. Создайте кнопку, по клику на нее выводите всплывающее сообще-
ние с текущим временем.
26
5. НАМЕРЕНИЯ. ОРГАНИЗАЦИЯ ПЕРЕХОДОВ
МЕЖДУ АКТИВНОСТЯМИ

Намерения (Intent`s) – это механизм обмена сообщениями между ком-


понентами приложения. Намерения могут быть использованы для реше-
ния многих задач, наиболее известными из которых являются запуск но-
вых активностей, запуск сервисов, а также рассылка широковещательных
сообщений между приложениями.
Намерения описываются с помощью объектов класса Intent. Объект
класса Intent в простейшем случае создается следующим образом:

val i = Intent(this, MainActivity2::class.java)//Создание Intent


(Kotlin)
Intent i = new Intent(this, MainActivity2.class);//Создание
Intent (Java)

Данное намерение предполагает запуск активности MainActivity2.


Для запуска новой активности намерение используется как аргумент ме-
тода startActivity:

startActivity(i);

В данном примере используется объект, получивший в конструкторе


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

i.putExtra("val", 1);//Java и Kotlin

Данный метод автоматически добавит заданную информацию объекту


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

int i = (int) getIntent().getExtras().get("val");//Java


val i = intent.extras?.get("val")//Kotlin

27
Намерения подразделяются на явные и неявные. Явным называется
намерение, в котором явным образом указывается то, какое приложение
будет удовлетворять создаваемому намерению. Как правило, для этого
прямо указывается имя пакета либо имя класса целевого приложения.
В большинстве ситуаций явные намерения используются для работы не-
посредственно в приложении – для запуска активностей, служб и т.д.
Неявные намерения используются в тех случаях, когда известно дейст-
вие, которое необходимо выполнить, но не конкретный компонент, кото-
рый должен это сделать. К таким действиям могут быть отнесены отправ-
ка сообщения по электронной почте, показ маршрута в приложении-
навигаторе и т.д. При использовании данного вида намерений операцион-
ная система ищет подходящие для решения поставленной задачи прило-
жения с помощью фильтров намерений.
Фильтр намерений – это секция манифеста приложения, в которой
указывается, какие действия может обрабатывать приложение. В случае
использования неявного намерения операционная система проверяет
фильтры приложений на предмет наличия возможности обработки тре-
буемого действия. В случае, если несколько приложений могут обрабаты-
вать одно и то же действие, пользователю будет выведено диалоговое ок-
но с предложением выбора того приложения, которое будет его обрабаты-
вать. Если в фильтре намерений не указано действие, то его можно будет
обработать только с помощью явного намерения. Запуск приложения так-
же описан в фильтре намерений с действием MAIN и категорией LAUNCHER
для нужной активности:

<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER"
/>
</intent-filter>
</activity>

Приведем пример использования неявного намерения с целью получе-


ния доступа к камере устройства.
Для получения разрешения на использование камеры в манифесте
приложения записывается соответствующее разрешение:

<uses-feature android:name="android.hardware.camera"
android:required="true"/>
28
Для получения использования камеры будет вызван метод
startActivityForResult. Данный метод не только запускает другую
активность, но и по окончанию действий на ней оповещает исходную
активность о факте завершении действий. Данное событие обрабатывается
с помощью метода onActivityResult.
Камера является одним из стандартных компонентов, доступ к кото-
рым может быть осуществлен с помощью намерения на выполнение дейст-
вия MediaStore.ACTION_IMAGE_CAPTURE. В случае если на устройстве
установлено несколько клиентов для обработки камеры, то пользователю
будет предложено выбрать нужный. Метод startActivityForResult
вызывается в блоке try-catch для того, чтобы обработать ситуацию не-
доступности камеры.
Метод onActivityResult осуществляет обработку результатов рабо-
ты активности, вызванной с помощью намерения. В рассматриваемом
примере, с помощью константы REQUEST_IMAGE_CAPTURE проверяется,
что была вызвана камера, а с помощью константы RESULT_OK – что
изображение успешно получено. Если это так, то изображение устанавли-
вается на виджет ImageView. Непосредственно же получение изображе-
ния получается с помощью метода получения дополнительной информа-
ции намерения.
Листинг 1 – Пример получения доступа к камере (Java)
public class MainActivity extends AppCompatActivity {
static final int REQUEST_IMAGE_CAPTURE = 1;
ImageView imageView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
imageView = findViewById(R.id.imageView);
findViewById(R.id.button).setOnClickListener(l-> {
try {
startActivityForResult(new
Intent(MediaStore.ACTION_IMAGE_CAPTURE), REQUEST_IMAGE_CAPTURE);
}catch (ActivityNotFoundException e) {
Toast.makeText(this,"Camera is not available!",
Toast.LENGTH_LONG).show();
}
});
}
@Override

29
protected void onActivityResult(int requestCode, int
resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_IMAGE_CAPTURE&&resultCode ==
RESULT_OK)
imageView.setImageBitmap((Bitmap)data.getExtras().get("data"));
}
}
//Kotlin
import android.content.ActivityNotFoundException
import android.content.Intent
import android.graphics.Bitmap
import android.os.Bundle
import android.provider.MediaStore
import android.widget.Button
import android.widget.ImageView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {


val REQUEST_IMAGE_CAPTURE = 1
lateinit var imageView: ImageView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
imageView = findViewById(R.id.imageView)
findViewById<Button>(R.id.button).setOnClickListener {
try {
startActivityForResult(Intent(MediaStore.ACTION_IMAGE_CAPTURE),
REQUEST_IMAGE_CAPTURE)
} catch (e: ActivityNotFoundException) {
Toast.makeText(this,"Camera is not
availabe!",Toast.LENGTH_LONG ).show()
}}
}
override fun onActivityResult(requestCode: Int, resultCode:
Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode==1&&resultCode== RESULT_OK)
imageView.setImageBitmap(data!!.extras!!.get("data") as
Bitmap)
}
}

30
Замечание: startActivityForResult, предложенная выше, на данный мо-
мент является устаревшей и демонстрируется для показа кода, который
может встретиться в старых проектах. На данный момент вместо исполь-
зования startActivityForResult (и соответствующей обработки результатов)
используются объекты, описывающие так называемые контракты. Пример
использования контракта представлен в листинге 2.
Листинг 2 – пример запуска камеры с помощью контракта
val contract =
registerForActivityResult(ActivityResultContracts.StartActivityForR
esult()){
if(it.resultCode== RESULT_OK){

findViewById<ImageView>(R.id.imageView).setImageBitmap(it?.data?.ex
tras?.get("data") as Bitmap)
}
}
contract.launch(Intent(MediaStore.ACTION_IMAGE_CAPTURE))

Существует большое количество действий, предусмотренных SDK по


умолчанию. В случае если требуется создать собственное действие, то оно
описывается в фильтре намерений с указанием имени пакета в виде стро-
ковой константы:
"edu.festu.example.sample.CUSTON_ACTION”
В дальнейшем же указание действия для приложений, которые вызы-
вают намерения на их выполнение, происходит в обычном режиме.
Намерения могут быть применены для рассылки широковещательных
сообщений (Broadcast) с использованием метода sendBroadcast.
На принимающей стороне для приема должен быть реализован наследник
класса BroadcastReceiver. В манифесте же следует добавить секцию
<receiver> с указанием намерений, которые будут обрабатываться прием-
ником, с помощью фильтра намерений:
<receiver
android:name=".MyReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<!--здесь код опущен -->
</intent-filter>
</receiver>

Наследник же класс BroadcastReceiver требует лишь переопределения


метода onReceive, который будет обрабатывать получаемые сообщения:
31
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;

public class MyReceiver extends BroadcastReceiver {

@Override
public void onReceive(Context context, Intent intent) {
// TODO: This method is called when the BroadcastReceiver
is receiving
// an Intent broadcast.
throw new UnsupportedOperationException("Not yet
implemented");
}
}

//KOTLIN
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent

class MyReceiver : BroadcastReceiver() {

override fun onReceive(context: Context, intent: Intent) {


// This method is called when the BroadcastReceiver is
receiving an Intent broadcast.
TODO("MyReceiver.onReceive() is not implemented")
}
}

Также намерения могут использоваться при работе с сервисами – фо-


новыми задачами, которые могут выполняться вне зависимости от статуса
работы остального приложения – посредством метода startService.
Сервисы следует вызывать лишь посредством явных намерений, посколь-
ку использование неявных намерений влечет собой проблемы безопасно-
сти – пользователь не сможет контролировать то, какой именно сервис
вызывается. Подробнее сервисы и принципы работы с ними будут описа-
ны в следующих разделах.
Намерения могут дополняться рядом метаданных, называемых флагами.
Флаги могут указывать системе, как запускать активность (например, к ка-
кой задаче должно принадлежать действие) и как обращаться с ней после ее
запуска (например, принадлежит ли она к списку недавних активностей).
32
Намерения содержать большое число параметров, не рассмотренных в
рамках данного раздела. Более подробно изучить намерения можно с по-
мощью таких ресурсов, как [1, 4].
Контрольные вопросы
1. В чем состоит отличие явных намерений от неявных?
2. Для чего используется метод registerForActivityResult?
3. Какая информация необходима для перехода между активностями в
рамках одного приложения?
4. Для чего используется класс Intent?
5. Каким образом передать данные при осуществлении намерения?

6. ФРАГМЕНТЫ
Существует большое число ситуаций, когда требуется динамически
изменять содержимое активности в зависимости от различных факторов,
таких как команды пользователя, конфигурация устройства и т.д. Для ре-
шения данных задач возможно использование фрагментов.
Фрагменты – части пользовательского интерфейса, которые могут
использоваться многократно [5]. Фрагменты оперируют собственными
разметками, обладают собственным жизненным циклом и собственными
обработчиками событий. Отличительной особенностью макетов является то,
что они не могут существовать вне активности и при добавлении в актив-
ность включаются в ее иерархию элементов как дочерние компоненты.
Структурно фрагмент аналогичен активности и состоит из разметки и
класса фрагмента. Разметка фрагмента ничем не отличается от разметки
активности. При размещении же фрагмента на активности его вид может
претерпеть изменения в соответствии с ограничениями, накладываемыми
активностью (рис. 14).

Рис. 14. Пример макета фрагмента, макета активности и итогового ви-


да экрана
33
Рисунок 14 демонстрирует использование элемента <fragment>.
Использование данного элемента является устаревшей практикой, и в но-
вых проектах рекомендуется использовать элементы пространства
AndroidX, которые будут рассмотрены далее.
Для использования фрагментов в список может потребоваться доба-
вить зависимость:
implementation "androidx.fragment:fragment:<version>" //Java
implementation "androidx.fragment:fragment-ktx:<version>"
Далее на активности понадобится разместить элемент
androidx.fragment.app.FragmentContainerView. Данный элемент
может как принимать требуемый для отображения фрагмент посредством
атрибута android:name, так и получать его программными средствами.
Пример использования FragmentContainerView представлен в листин-
ге 1.
Листинг 1 – FragmentContainerView
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragmentContainerView"
android:layout_width="300dp"
android:layout_height="300dp"
android:layout_marginEnd="43dp"
android:layout_marginRight="43dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:name="com.example.myapplication.BlankFragment"/>

В случае если фрагмент требуется определить программными средст-


вами, то в приведенном выше листинге достаточно не указывать имя
фрагмента, а в классе активности, где будет размещен фрагмент, задать
фрагмент посредством менеджера фрагментов (листинг 2):
Листинг 2 – Программное создание фрагментов
//Java
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (savedInstanceState==null)
{
getSupportFragmentManager()
.beginTransaction()
34
.setReorderingAllowed(true)

.add(R.id.fragmentContainerView,BlankFragment.class,null)
.commit();
}
}
//Kotlin
class ExampleActivity :
AppCompatActivity(R.layout.example_activity) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
supportFragmentManager.commit {
setReorderingAllowed(true)
add<ExampleFragment>(R.id.fragment_container_view)
}
}
}
}

Рассмотрим данный код подробнее. Логическое выражение


savedInstaceState==null проверяется для того, чтобы убедиться, что
фрагмент будет создан лишь один раз при первоначальном создании
активности. Если же активность (и, соответственно, фрагмент) создава-
лись ранее, то достаточно восстановить уже созданный фрагмент
из savedInstanceState.
Метод getSupportedFragmentManager возвращает объект-наследник
абстрактного класса FragmentManager, который предоставляет функцио-
нал по работе с фрагментами. Метод beginTransaction открывает транзак-
цию на работу с фрагментами.
Замечание: Транзакция – это совокупность связанных между собой
операций, характеризуемых четырьмя свойствами: атомарность, непроти-
воречивость, локализация и продолжительность [6].
Использование механизма транзакций позволяет гарантировать, что
необходимые действия по работе с фрагментами будут выполнены в пол-
ном объеме. Метод setReorderingAllowed устанавливает разрешение
на проведение оптимизаций операций по работе с фрагментами. Согласно
рекомендациям разработчиков, при работе с транзакциями фрагментов
данный метод всегда должен устанавливать true. Метод add добавляет в
описанный ранее контейнер фрагмент BlankFragment. Значение null
для дополнительных аргументов передается по причине их отсутствия.

35
Если же для создания фрагмента нужны дополнительные данные, то они
могут быть переданы в качестве объекта Bundle. Метод commit указывает,
что описанная транзакция должна быть выполнена. Следует понимать, что
транзакция может быть выполнена с некоторой задержкой, что обуслов-
лено возможной занятостью потока, в котором она будет выполняться.
Транзакция описывается объектом класса FragmentTransaction.
Данный класс предоставляет ряд методов для работы с фрагментами, сре-
ди которых важно отметить следующие:
− add() – добавляет фрагмент на активность;
− remove() – удаляет фрагмент из активности;
− replace() – заменяет один фрагмент другим; заменяемый фраг-
мент удаляется;
− hide() – делает фрагмент невидимым (но не удаляет его);
− show() – отображает скрытый (не удаленный) фрагмент;
− detach() – удаляет фрагмент из иерархии компонентов активности
и удаляет из UI и перестает отображать; открепленный фрагмент все еще
управляется менеджером фрагментов;
− attach() – повторно прикрепляет фрагмент, восстанавливая иерар-
хию компонентов, присоединяет его к пользовательскому интерфейсу
и отображает.
Использование данных методов переводит фрагмент между различны-
ми состояниями жизненного цикла. Жизненный цикл фрагмента похож на
жизненный цикл активности, но имеет ряд отличий. При изучении жиз-
ненного цикла фрагментов важно помнить, что они тесно связаны с ком-
понентами, на которых они будут отображены.
Как для активности, так и для фрагмента возможны следующие со-
стояния, описываемые классом Lifecycle.State. Описания состояний
для активности и фрагмента приведены в табл. 1.

Таблица 1
Состояния жизненного цикла для активности или фрагмента

Состояние Описание
INITIALIZED Объект класса создан, метод onCreate еще не вызван
CREATED Вызван метод onCreate, метод onStop еще не вызван
STARTED Объект функционирует, вызван метод onStart
RESUMED Возобновляет деятельность объекта, вызван метод onResume
DESTROYED Вызван onDestroy, объект уничтожен

36
Фрагмент, как и любой другой ком-
понент, переходит между состояниями
жизненного цикла от состояния
CREATED к состоянию DESTROYED.
Фрагмент может переходить между со-
стояниями в обоих направлениях.
Состояние фрагмента соотносится с со-
стоянием объекта, на котором он ото-
бражен в соответствии с рис. 15. Важно
учитывать, что перед методом
onCreate вызывается метод
onAttach, прикрепляющий фрагмент к
активности, а после onDestroy –
onDetach, открепляющий от активно-
сти. Данные методы в рамках жизнен-
ного цикла не рассматриваются.
Рис. 15. Жизненные циклы фрагмен-
Управление компонентами фраг- та и активности
ментами осуществляется с помощью
наследников класса Fragment аналогично управлению компонентами
активности. Вид класса фрагмента представлен в листинге 3.
Листинг 3 – Пример класса фрагмента
import android.os.Bundle;
import androidx.fragment.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

public class BlankFragment extends Fragment {

private static final String ARG_PARAM1 = "param1";


private static final String ARG_PARAM2 = "param2";

private String mParam1;


private String mParam2;

public BlankFragment() {
}

public static BlankFragment newInstance(String param1, String

37
param2) {
BlankFragment fragment = new BlankFragment();
Bundle args = new Bundle();
args.putString(ARG_PARAM1, param1);
args.putString(ARG_PARAM2, param2);
fragment.setArguments(args);
return fragment;
}

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
mParam1 = getArguments().getString(ARG_PARAM1);
mParam2 = getArguments().getString(ARG_PARAM2);
}
}

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup
container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_blank, container,
false);
}
}

//Kotlin
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup

private const val ARG_PARAM1 = "param1"


private const val ARG_PARAM2 = "param2"

class BlankFragment : Fragment() {


private var param1: String? = null
private var param2: String? = null

override fun onCreate(savedInstanceState: Bundle?) {


super.onCreate(savedInstanceState)
arguments?.let {
param1 = it.getString(ARG_PARAM1)
param2 = it.getString(ARG_PARAM2)
38
}
}

override fun onCreateView(


inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_blank, container,
false)
}

companion object {
@JvmStatic
fun newInstance(param1: String, param2: String) =
BlankFragment().apply {
arguments = Bundle().apply {
putString(ARG_PARAM1, param1)
putString(ARG_PARAM2, param2)
}
}
}
}

Особого внимания в приведенном коде заслуживают методы


onCreate и onCreateView. Метод onCreate предназначен для органи-
зации работы компонентов, не связанных с пользовательским интерфей-
сом. Метод onCreateView предназначен для размещения компонентов
фрагмента в рамках виджета активности. Для этого используется метод
inflate объекта класса LayoutInflater. Доступ к элементам фрагмен-
та возможен после создания View, посредством получения доступа к ком-
поненту, на котором размещен фрагмент. Кроме того, в классе фрагмента
возможно переопределение метода onViewCreated, в рамках которого
обращения к компонентам фрагмента возможно посредством аргумента
view, для которого будет вызываться метод findViewById.
В качестве альтернативы возможно использование привязок компо-
нентов (View binding). Использование данного объекта на текущий мо-
мент является более предпочтительным, нежели findViewById по при-
чине типобезопасности и null-безопасности.
Для включения возможности View binding в файле build.gradle в сек-
ции android требуется добавить указать возможность привязки компонентов:
buildFeatures {
viewBinding true
}
39
После этого как для активностей, так и для фрагментов будет сгенери-
рован класс XBinding, где X – инвертированное (по словам) имя класса.
В частности, для фрагмента BlankFragment будет сгенерирован класс
FragmentBlankBinding. В случае работы с активностями метод при
этом изменится метод onCreate:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ResultProfileBinding.inflate(getLayoutInflater());
View view = binding. BlankFragmentBinding ();
setContentView(view);
}
//Kotlin
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = BlankFragmentBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
}

Аналогичным способом будет изменен метод onCreateView


для фрагментов:

Override
public View onCreateView (LayoutInflater inflater,
ViewGroup container,
Bundle savedInstanceState) {
binding = BlankFragmentBinding.inflate(inflater, container,
false);
View view = binding.getRoot();
return view;
}
//Kotlin
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = BlankFragmentBinding.inflate(inflater, container,
false)
val view = binding.root
return view
}

40
В дальнейшем доступ к фрагментам будет осуществляться по их иден-
тификаторам в форме полей объекта BlankFragmentBinding:
resultProfileBinding.button.setOnClickListener(l-
>Toast.makeText(getContext(), "Clicked!",
Toast.LENGTH_LONG).show());
//Kotlin
activityMainBinding.button.setOnClickListener {
Toast.makeText(applicationContext, "Clicked",
Toast.LENGTH_LONG).show() }

В случаях, когда необходимо обеспечить взаимодействие фрагмента с


активностью, достаточно использовать метод getActivity, который вер-
нет объект класса FragmentActivity – активность-родителя – на базе
которого можно взаимодействовать с элементами активности. Однако
реализация прямого взаимодействия фрагментов и активностей или фраг-
ментов друг с другом не рекомендуется по причине того, что это противо-
речит определению фрагмента как автономного переиспользуемого ком-
понента. Для передачи информации между фрагментами и активностями
предлагается использовать один из двух способов: использование модели
представлений (ViewModel) или использование Fragment Result API.
В первом случае потребуется описать класс-наследник ViewModel, ко-
торый будет оперировать передаваемыми между фрагментами данными:
public class ItemModel extends ViewModel {
private MutableLiveData<String>data= new MutableLiveData<>();
public MutableLiveData<String> getData() {
return data;
}
public void setData(MutableLiveData<String> data) {
this.data = data;
}
public void add(String string) {
data.setValue(string);
}
}
//Kotlin
class ItemModel : ViewModel() {
var data = MutableLiveData<String>()

fun add(string: String) {


data.value = string
}
}
41
Далее в компонентах, которые будут посылать и принимать данные,
потребуется добавить поле – объект данного класса, который будет созда-
ваться с помощью объекта ViewModelProvider. Для активности это вы-
полняется следующим образом:

itemModel = new ViewModelProvider(this).get(ItemModel.class);


//Kotlin
private val viewModel: ItemModel by viewModels() //Либо аналогично
команде из Java

Наконец, потребуется определить обработчики события получения


данных в компонентах, которые должны их обрабатывать и методы, кото-
рые будут отправлять в компонентах-отправителях:

@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle
savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
itemModel = new
ViewModelProvider(requireActivity()).get(ItemModel.class);
resultProfileBinding.button.setOnClickListener(l-
>itemModel.add("Sended From Fragment"));

}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
viewModel = new ViewModelProvider(this).get(ItemModel.class);
viewModel.getData().observe(this,i->
Toast.makeText(this,viewModel.getData().getValue(),Toast.LENGTH_LON
G).show());
}

//Kotlin
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
itemModel = ViewModelProvider(this).get(ItemModel::class.java)
setContentView(activityMainBinding.root)

itemModel.data.observe(this,{Toast.makeText(this,itemModel.data.val
ue,Toast.LENGTH_LONG).show()})
}
42
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding =
FragmentBlankBinding.inflate(layoutInflater,container,false)
binding.button.setOnClickListener { itemModel.add("Sended From
Fragment!") }
return inflater.inflate(R.layout.fragment_blank, container,
false)
}

Метод observe в примерах отвечает за поведение в случае изменения


данных во ViewModel.
В случае если нескольким фрагментам требуется оперировать общими
данными, то они также могут быть заданы во ViewModel. Различные же
фрагменты будут оперировать с теми из данных, которые будут попадать
в их область видимости, путем указания необходимого метода-геттера для
объектов ViewModel компонентов:

viewModel.getData().observe(this,i->
Toast.makeText(this,viewModel.getData().getValue(),Toast.LENGTH_LON
G).show());
//Koltin
itemModel.data.observe(this,{Toast.makeText(this,itemModel.data.val
ue,Toast.LENGTH_LONG).show()})

Альтернативой ViewModel является использование Fragment Result


API. Данное средство предназначено для ситуаций, когда требуется еди-
норазово переслать данные между компонентами.
Для этого во фрагменте-отправителе требуется задать информацию с
помощью объекта Bundle:

Bundle b = new Bundle();


b.putString("key","data");
getParentFragmentManager().setFragmentResult("requestKey", b);
//
setFragmentResult("requestKey", bundleOf("key" to "data"))

43
В компоненте-получателе информации потребуется определить обра-
ботчик события FragmentResultListener и переопределить метод
onFragmentResult в соответствии с поставленными задачами:

getSupportFragmentManager().setFragmentResultListener("requestKey",
this, (requestKey, result) -> Toast.
makeText(getApplicationContext(),

result.getString("key"),Toast.LENGTH_LONG).show());
//
supportFragmentManager.setFragmentResultListener("requestKey",
this, { requestKey: String?, result: Bundle ->
Toast.makeText(applicationContext,
result.getString("key"), Toast.LENGTH_LONG).show()
})

Метод setFragmentResultListener задает обработчик события


FragmentResult, который будет получать данные Bundle по соответст-
вующему ключу requestKey. Далее потребуется лишь обработать
Bundle в соответствии с собственными нуждами.

Контрольные вопросы

1. Для чего используются привязки представлений?


2. Что такое транзакция?
3. Создайте два фрагмента, каждый из которых содержит кнопку, за-
меняющую представляемый экраном фрагмент на другой.
4. Каким образом фрагмент отображается на активности?
5. Что описывает класс ViewModel? Для чего используются объекты
этого класса?

7. ПОТОКИ. БАЗОВЫЕ ПОНЯТИЯ

Замечание: при изучении данного раздела рекомендуется изучить ба-


зовый материал, касающийся параллелизации вычислений.
Введем понятие потока.
Поток – это дочерний процесс, использующий вычислительные ресур-
сы процесса-родителя.
Выполнение многопоточных вычислений является крайне важной те-
мой мобильной разработки. Выполнение длительных и/или ресурсоемких
44
задач рекомендуется помещать в отдельные потоки, что обусловлено как
меньшим количеством вычислительных ресурсов мобильных устройств по
сравнению с персональными компьютерами, так и большими требования-
ми по обеспечению непрерывности работы приложения.
Средства Android SDK и JDK предоставляют большое число средств
по организации многопоточных вычислений. Базовыми элементами для их
осуществления являются интерфейс Runnable и класс Thread.
Функциональный интерфейс Runnable предназначен для описания за-
дач, которые должны быть выполнены в потоке. Данная задача описыва-
ется в методе run.

Runnable r = new Runnable() {


@Override
public void run() {
Log.d("Thread","From Thread!");
}
};
r.run();

//Kotlin
val v = Runnable { string="From Thread"; Log.d("Thread",string) }
v.run()

Важно помнить, что Runnable лишь описывает задачу, которая долж-


на быть выполнена, но не выполняет ее в отдельном потоке самостоятель-
но. Для того чтобы Runnable выполнялись в отдельных потоках, потре-
буется использование класса Thread или его наследников.
Класс Thread – базовое средство JDK для работы с потоками. Данный
класс реализует (implements) Runnable по умолчанию, а также может
принимать созданные ранее Runnable для их выполнения при своем соз-
дании. Для запуска задачи в отдельном потоке для объекта Thread требу-
ется вызвать метод start():

Thread t = new Thread(()->Log.d("Thread", "From Thread"));


t.start();

//Kotlin
thread { Log.d("Thread", "From Thread") }.start()
//либо
val t = Thread{Log.d("Thread", "From Thread")}
t.start()

45
В случае же создания класса-наследника Thread потребуется реализо-
вать метод run:

class MyT extends Thread {

@Override
public void run() {
super.run();
Log.d("Thread","From thread!");
}
}

//Kotlin
class MyT:Thread() {
override fun run() {
super.run()
Log.d("Thread","From Thread")
}
}

При работе с потоками важно помнить, что из стороннего потока нель-


зя взаимодействовать с элементами пользовательского интерфейса (UI) –
работа с UI доступна только в рамках основного потока. Для того чтобы
передавать необходимую информацию для изменения UI в основной по-
ток, Android SDK предоставляет ряд методов:
− Activity.runOnUiThread(Runnable);
− View.post(Runnable);
− View.postDelayed(Runnable, long).
Метод runOnUiThread позволяет запустить задачу Runnable в рам-
ках основного потока, что в свою очередь делает возможным изменение
элементов активности (листинг 1):
Листинг 1 – Пример использования runOnUiThread
public class MainActivity extends AppCompatActivity {

ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
new MyT().start();

46
}
class MyT extends Thread {

@Override
public void run() {
super.run();
runOnUiThread(()->binding.textView.setText("From
Thread"));
}
}
}
//Kotlin
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.example.myapplication.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {


lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
MyT().start()
}
inner class MyT:Thread() {
override fun run() {
super.run()
runOnUiThread { binding.textView.text = "From Thread" }
}
}
}

Методы View.post и View.postDelayed рекомендуется использо-


вать в тех случаях, когда необходимо отправить некоторую задачу для из-
менения определенного элемента view (в отличие от runOnUithread),
которая также будет выполнена в основном потоке:

class MyT extends Thread {

@Override
public void run() {
super.run();
binding.textView.post(()-
>binding.textView.setText("From Thread!"));
47
}
}
//Kotlin
inner class MyT:Thread() {
override fun run() {
super.run()
binding.textView.post{
binding.textView2.text = "From thread!"
}
}
}

JDK предоставляет ряд средств для более качественного управления


потоками. Интерфейс ThreadFactory предоставляет средства для созда-
ния потоков. Преимуществом использования данного интерфейса являет-
ся возможность добавления функционала для автоматической дополни-
тельной настройки потоков: добавления счетчика количества созданных
потоков, автоматическое указание имени потоков и т.д. Пример использо-
вания ThreadFacrory представлен в листинге 2.
Листинг 2 – Пример использования ThreadFactory
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
MyFactory factory = new MyFactory();
Thread t = factory.newThread(()->Log.d("Thread", "From
Thread"));
}
class MyFactory implements ThreadFactory {

int count=0;
String name;

@Override
public Thread newThread(Runnable r) {
return new Thread(r,name+"-"+count);
}
}
//Kotlin
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

48
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val factory = MyFactory()
val t = factory.newThread { Log.d("Thread", "From Thread") }
}
internal class MyFactory : ThreadFactory {
var count = 0
var name: String? = null
override fun newThread(r: Runnable): Thread {
return Thread(r, "$name-$count") }
}

Все потоки объединяются в группы, которые описываются классами-


наследниками класса ThreadGroup. Группа потоков состоит из потоков
или других групп. Элементы группы потоков может управлять дочерними
потоками или группами, но не может управлять родительскими группами.
Также класс ThreadGroup предоставляет ряд методов для управления
всеми потоками группы. Более подробно с ThreadGroup можно ознако-
миться с помощью документации [7].
Пример использования ThreadGroup представлен в листинге 2.

public class MainActivity extends AppCompatActivity {

class MyT extends Thread {


boolean running;
public MyT(@Nullable ThreadGroup group, @Nullable String
name) {
super(group, name);
}
@Override
public void run() {
super.run();
running = true;
int x = 0;
while (running)
x=(x+1)%100000;
}
public void stopThread(){
running = false;
}
}

ActivityMainBinding binding;

49
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
ThreadGroup tg = new ThreadGroup("My Group!");
//Два потока с бесконечной задачей
MyT t = new MyT(tg, "First");
MyT t2 = new MyT(tg, "Second");
Log.d("NUM",Integer.toString(tg.activeCount()));
t.start();
t2.start();
Log.d("NUM",Integer.toString(tg.activeCount()));
MyT[] threads = new MyT[tg.activeCount()];
tg.enumerate(threads);
for (MyT th:threads)
th.stopThread();
}
}

//Kotlin
class MainActivity : AppCompatActivity() {
internal inner class MyT(group: ThreadGroup, name: String) :
Thread(group, name) {
private var running = false
override fun run() {
super.run()
running = true
var x = 0
while (running) x = (x + 1) % 100000
}

fun stopThread() {
running = false
}
}

private var binding: ActivityMainBinding? = null


override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding!!.root)
val tg = ThreadGroup("My Group!")
//Два потока с бесконечной задачей
val t = MyT(tg, "First")
50
val t2 = MyT(tg, "Second")
Log.d("NUM", tg.activeCount().toString())
t.start()
t2.start()
Log.d("NUM", tg.activeCount().toString())
val threads = arrayOfNulls<MyT>(tg.activeCount())
tg.enumerate(threads)
for (th in threads) th!!.stopThread()
}
}

Рассмотренные средства являются базовыми при создании многопо-


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

Контрольные вопросы

1. Что такое поток?


2. Какие классы используются для объявления потоков?
3. Напишите программу, проводящую параллельное вычисление сум-
мы, разности, произведения и среднего геометрического для прямоуголь-
ной матрицы.
4. Воздайте поток, вычисляющий факториал числа 150.
5. Для чего используются методы View.post и runOnUiThread? В чем
их отличие?

8. ПОТОКИ В ANDROID. СОПРОГРАММЫ

Android SDK предоставляет ряд компонентов для работы с потоками


на более качественном уровне, нежели стандартные реализации пото-
ков. К ним относятся интерфейс AsyncTask, а также компоненты биб-
лиотеки java.util.concurrent: Executor, Callable, Future,
Flow и другие.
AsyncTask – это абстрактный, предназначенный для описания фоно-
вой задачи. На текущий момент он признан устаревшим в пользу стан-
дартных компонентов Java и Kotlin, однако длительная история его
использования делает важным понимание принципов его работы.
AsyncTask описывается следующей сигнатурой:

public abstract class AsyncTask<Params, Progress, Result>


51
Наследники данного класса должны определить реализацию метода
doInBackground, описывающего вычисления, которые должны быть вы-
полнены в отдельном потоке. Также возможно переопределение методов
onPreExecute и onPostExecute, которые выполняются до проведения и
после проведения фонового вычисления соответственно, а также метода
onProgressUpdate, позволяющего выполнять вычисления в потоке-
родителе в случаях, если будет вызван метод publishProgress. Также
возможно переопределение перегруженного метода onCancelled, кото-
рый отвечает за поведение при завершении выполнения метода
doInBackground. Рассмотрим данные методы подробнее.
Метод onPreExecute предназначен для проведения подготовитель-
ных вычислений. Данный метод вызывается до вызова doInBackground
и имеет доступ к UI. Данный метод не имеет аргументов и описывается
аналогично следующему примеру:

@Override
protected void onPreExecute() {
super.onPreExecute();
b.status.setText("STARTING");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

//Kotlin
override fun onPreExecute() {
b.status.setText("STARTING")
try {
Thread.sleep(1000)
} catch (e: InterruptedException) {
e.printStackTrace()
}
}

Метод onPostExecute вызывается после успешного выполнения фо-


новой задачи и может не быть вызван в случаях, когда выполнение фоно-
вой задачи было отменено. В качестве аргумента используется обобщение
Result. Имеет доступ к UI, что показано в следующем примере:

52
@Override
protected void onPostExecute(String s) {
super.onPostExecute(s);
b.status.setText("ENDED WITH SUM: "+s);
}
//Kotlin
override fun onPostExecute(s: String) {
super.onPostExecute(s)
b.status.setText("ENDED WITH SUM: $s")
}

Метод onCancelled вызывается в случае подачи команды на завер-


шение выполнения фоновой задачи с помощью метода cancel. Обладает
доступом к UI. Имеется две перегруженные версии этого метода, которые
могут игнорировать или обрабатывать Result:
@Override
protected void onCancelled(String s) {
super.onCancelled(s);
b.status.setText("CANCELLED WITH MESSAGE: "+s);
}

@Override
protected void onCancelled() {
super.onCancelled();
b.status.setText("CANCELLED");
}

//Kotlin
override fun onCancelled(s: String) {
b.status.setText("CANCELLED WITH MESSAGE: $s")
}

override fun onCancelled() {


super.onCancelled()
b.status.setText("CANCELLED")
}

Непосредственно описание фоновой задачи заключается в методе


doInBackground, который принимает в качестве аргумента обобщение
Params. В качестве же возвращаемого значения данного метода выступа-
ет Result. Данный метод не имеет доступ к UI и выполняется в другом
потоке. В случае же, если необходимо отобразить некоторые промежу-
точные данные, то это возможно с помощью метода publishProgress.
53
Метод publishProgress принимает в качестве аргумента обобщение
Progress и вызывает метод onProgressUpdate, который обладает дос-
тупом к UI:
@Override
protected void onProgressUpdate(Integer... values) {
super.onProgressUpdate(values);
b.progress.setText(String.valueOf(values[0]));
}

//Kotlin
protected override fun onProgressUpdate(vararg values: Int?) {
super.onProgressUpdate(*values)
b.progress.setText(values[0].toString())
}

Выполнение фоновой задачи вызывается в основном потоке с помо-


щью метода execute.
new MyAsync().execute();//аналогично для Kotlin

Замечание: на данный момент AsyncTask является устаревшим ин-


терфейсом. Однако, понимание принципов его работы важно по причине
большого числа проектов, использующих старую кодовую базу.
На смену AsyncTask пришли библиотеки java.util.concurrent и
kotlin concurrency. Библиотека concurrent предоставляет функцио-
нал интерфейсов Future, Executor, Callable и ряда других.
В случае, когда от потока ожидается получение некоторого результата,
возможно использование интерфейса Callable, пример которого приве-
ден в листинге 1.
Листинг 1 – Пример реализации Callable
Callable v = new Callable() {
@Override
public Object call() throws Exception {
return Integer.MAX_VALUE;
}
};
//Kotlin
val v = Callable() {
@Throws(Exception::class)
fun call(): Any {
return Int.MAX_VALUE
}
}
54
В дальнейшем методы, описанные в классах, реализующих Callable,
могут быть выполнены для объектов классов, реализующих интерфейс
Future. Данный интерфейс предназначен для выполнения асинхронных
операций и предоставляет возможность отмены проведения вычисления,
получения результата после завершения задачи и проверки того, что вы-
числение было завершено или отменено. Следует помнить, что использо-
вание метода get() для получения значения делает вычисление син-
хронным. Важным классом, реализующим Future, является FutureTask,
который, реализуя также Runnable, может быть выполнен в сторон-
нем потоке.
Начиная с JDK 8 возможно использование класса
CompletableFuture, предоставляющего расширенный функционал
Future и CompletionStage и дающего возможность обрабатывать про-
межуточные результаты вычислений, а также комбинировать вычисления
в цепочки, как показано в листинге 8.
Листинг 8 – Пример использования CompletableFuture
try {
Log.d("COMPLETABLE", CompletableFuture
.supplyAsync(new Supplier<String>() {
@Override
public String get() {
return "Hello There!\n";
}
}).thenApply(new Function<String, String>() {
@Override
public String apply(String s) {
return s + "General";
}
}).thenApply(new Function<String, String>() {
@Override
public String apply(String s) {
return s + " Kenobi!";
}
}).get());
} catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
}

//kotlin
try {
Log.d("COMPLETABLE", CompletableFuture
.supplyAsync { "Hello There!\n" }.thenApply {

55
(fun(s: String): String {
return s + "General"
}).toString()
}.thenApply {
(fun(s: String): String {
return "$s Kenobi!"
}).toString()
}.get().toString())
} catch (e: ExecutionException) {
e.printStackTrace()
} catch (e: InterruptedException) {
e.printStackTrace()
}

Кроме того, результаты вычислений различных методов класса


CompletableFuture могут быть объединены с помощью таких методов,
как thenCombine(). CompletableFuture предоставляет также и мно-
гие другие методы для работы, которые здесь не рассматриваются.
Использование потоков может быть более эффективным при разделении
описания задачи, которую должен выполнить поток, и непосредственно
описания потока. Для этого может быть применены объекты-исполнители.
Интерфейс Executor требует реализации единственного мето-
да execute():
Callable<Integer> t = ()->Integer.MAX_VALUE;
FutureTask<Integer> f = new FutureTask<>(t);
Executor e = command -> new Thread(command).start();
e.execute(f);
try {
Log.d("EXECUTOR", String.valueOf(f.get()));
} catch (ExecutionException | InterruptedException
executionException) {
executionException.printStackTrace();
}
//Kotlin
val t = Callable { Int.MAX_VALUE }
val f = FutureTask(t)
val e = Executor { command: Runnable? -> Thread(command).start() }
e.execute(f)
try {
Log.d("EXECUTOR", f.get().toString())
} catch (executionException: ExecutionException) {
executionException.printStackTrace()
} catch (executionException: InterruptedException) {
executionException.printStackTrace()
}
56
Благодаря Executor возможно многоразовое выполнение одного и то-
го же Runnable или Callable более лаконичными средствами. Данный
интерфейс имеет наследника ExetucorService, который обладает до-
полнительным функционалом по управлению работой исполнителя.
Кроме того, он поддерживает получение Callable для выполнения и мо-
жет возвращать задачи для отслеживания их состояния. Пример использо-
вания класса ExecutorService представлен в листинге 2.
Листинг 2 – Пример использования ExecutorService
Callable<Integer> t = ()->Integer.MAX_VALUE;
ExecutorService s = Executors.newSingleThreadExecutor();

try {
Log.d("EXECUTORSERVICE", String.valueOf(s.submit(t).get()));
} catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
}
s.shutdown();
//Kotlin
val t = Callable { Int.MAX_VALUE }
val s = Executors.newSingleThreadExecutor()

try {
Log.d("EXECUTORSERVICE", s.submit(t).get().toString())
} catch (e: ExecutionException) {
e.printStackTrace()
} catch (e: InterruptedException) {
e.printStackTrace()
}
s.shutdown()

В листинге 9 для получения ExecutorService используется метод


newSingleThreadExecutor статического класса Executors. Следует
обратить внимание на то, что для завершения работы ExecutorService
необходимо вызывать метод shutdown().
Наконец, интерфейс ScheduledExecutorService расширяет
ExecutorService расписанием выполнения задач Runnable или
Callable после заданной задержки, а также позволяет выполнять их по-
вторно в заданное время.

Runnable task = () -> {


int c = new Random().nextInt();
57
int x= IntStream.range(c, c+10).sum();
System.out.println(Thread.currentThread().getName()+" val "+x);
};
new ScheduledThreadPoolExecutor(1).scheduleAtFixedRate(task,1,2,
TimeUnit.SECONDS);
//Kotlin
val task = Runnable {
val c = Random().nextInt()
val x = IntStream.range(c, c + 10).sum()
println(Thread.currentThread().name + " val " + x)
}
ScheduledThreadPoolExecutor(1).scheduleAtFixedRate(task, 1, 2,
TimeUnit.SECONDS)

В дополнение к средствам java.util.concurrency в рамках Kotlin


возможно использование сопрограмм (coroutins). Сопрограммы основаны
на принципах приостанавливаемых вычислений: функция может приоста-
новить свое выполнение в какой-то момент и возобновить его позже.
Для обозначения приостанавливаемой функции используется ключевое
слово suspend. Пример синтаксиса приостанавливаемой функции пред-
ставлен ниже:

suspend fun foo(): Int {


return 1;
}

Приостанавливаемые функции могут быть использованы только в со-


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

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-
core:x.y.z'
Для запуска сопрограммы потребуется использование метода launch:
GlobalScope.launch {
foo()
}

launch возвращает объект типа Job. launch не возвращает значений.

58
В случае же, когда нужно, чтобы сопрограммы выполнялись асин-
хронно и возвращали результаты вычислений, вместо launch возможно
использование async:

val t = GlobalScope.async { foo()}

async возвращает объект типа Deffered<T> – упрощенный аналог


java.util.concurrent.Future, работа с которым происходит анало-
гичным образом.
Многопоточные вычисления требуют качественного контроля синхро-
низации потоков. Данная проблема выходит за границы данного раздела и
предлагается к самостоятельному изучению.

Контрольные вопросы

1. Какие методы должны быть описаны при реализации интерфейса


AsyncTask?
2. Для чего используется Executor?
3. Что такое сопрограмма? В чем заключается ее отличие от потока?
4. В чем состоит отличие интерфейса Runnable от интерфейса Callable?
5. Напишите программу, вычисляющую факториал числа 150. Исполь-
зуйте Executor.

9. ТИПЫ РЕСУРСОВ. SHAREDPREFERENCES

В ряде ситуаций требуется хранить небольшое число информации в


течение длительного времени. К такой информации могут быть отнесены
настройки приложения и данные о его состоянии. Хранение такого рода
информации в базе данных не оправдано по причине малого объема ин-
формации для хранения и небольшого спектра операций, которые будут
выполняться над этими данными. Оптимальным решением данной задачи
является использование SharedPreferences.
SharedPreferences представляет собой хранилище данных в формате
«ключ-значение». SharedPreferences сохраняются в отдельном, автома-
тически создаваемом файле, и не теряются при перезапуске приложения.
Preference – базовый компонент настроек, характеризующий один из
параметров настроек приложения. Хранит данные в формате «ключ-
значение» и может быть получен (и задан) в других местах программы.
59
Замечание: на текущий момент для работы с Preference использу-
ются средства AndroidX. Библиотека android.preference объявлена
устаревшей, а ее функционал перенесен в androidx.preference.
Доступ к SharedPreference организуется при помощи метода
getPreference, который возвращает соответствующий объект:

SharedPreferences s = getPreferences(MODE_PRIVATE);
//Kotlin
val s = getPreferences(MODE_PRIVATE)

Константа MODE_PRIVATE отвечает за модификатор доступа к


SharedPrefence, согласно которому к ним имеет доступ только проек-
тируемое приложение. Существуют также и другие режимы, которые на
текущий момент объявлены устаревшими.
В случае же, если одно приложение имеет доступ к нескольким фай-
лам, то вместо getPreferences используется метод
getSharedPreferences, который кроме модификатора доступа также
получает имя файла.
Для получения данных достаточно использовать методы-геттеры с
указанием ключа, по которому будет происходить выбор значения, и зна-
чения, которое будет возвращено в случае, если соответствующей пары не
будет найдено:

s.getString("key","default"); //аналогично Kotlin

Для того, чтобы получить все элементы, используется метод all, ко-
торый вернет карту из SharedPreference:

Map l = s.getAll();//аналогично Kotlin

Важно помнить, что ключ в SharedPreference всегда является стро-


кой, в то время как тип значения может оказаться любым, что накладывает
дополнительные требования по их обработке.
Для того чтобы задать значения в SharedPreference используется
объект класса-реализации внутреннего интерфейса Editor, который воз-
вращается при вызове метода edit:

SharedPreferences.Editor e = s.edit();//аналогично Kotlin

60
Добавление элементов реализуется с помощью группы методов put:

e.putString("key", "value");//аналогично Kotlin


e.putString("key","value")

Для сохранения изменений указывается метод apply или commit в за-


висимости от того, требуется ли синхронное выполнение данной опера-
ции, и требуется ли возвращаемое значение по факту выполнения опера-
ции сохранения:

e.apply//аналогично Kotlin

Для удаления элемента из SharedPreference используется метод


remove, если же требуется удалить все значения – clear.
SharedPreferences могут обрабатывать события изменения значе-
ния. Для этого достаточно добавить соответствующий обработчик объекту
SharedPreference. При этом важно помнить, что первоначальное до-
бавление пары не считается изменением и не будет зарегистрировано об-
работчиком. Снять же обработчик событий можно с помощью метода
unregisterOnSharedPreferenceChangeListener:

SharedPreferences s = getPreferences(MODE_PRIVATE);
SharedPreferences.Editor e = s.edit();
e.putString("key", "value");
e.apply();
s.registerOnSharedPreferenceChangeListener(this);
e.putString("key", "value2");
s.unregisterOnSharedPreferenceChangeListener(this);
e.apply();
//аналогично для Kotlin

В данном примере реализация метода onSharedPreferenceChanged


приведена в классе MainActivity, для которого указано, что он реализует
интерфейс SharedPreferences.OnSharedPreferenceChangeListener.
В случае данного интерфейса рекомендуется использовать «сильные»
ссылки на обработчики событий. Это обосновано тем, что
SharedPreferenceManager не сохраняет ссылку на обработчик. Среди
возможных способов решения данной проблемы – использование внутрен-
них классов либо реализация интерфейса непосредственно активностью.
SharedPreference является рекомендованным способом хранения
объектов Preference, однако в ряде ситуаций требуется создание собст-
61
венного хранилища данных. В этом случае понадобится создать класс-
наследник PreferenceDataStore и переопределить необходимые гетте-
ры и сеттеры:

class MyDataStore extends PreferenceDataStore {


@Override
public void putInt(String key, int value) {
super.putInt(key, value);
}

@Override
public int getInt(String key, int defValue) {
return super.getInt(key, defValue);
}
}
//аналогично Kotlin

Одним из частых случаев применения Preference является хранение


настроек приложения. Android SDK предоставляет для этого следую-
щие классы:
− EditTextPreference;
− ListPreference;
− MultiSelectListPreference;
− SeekBarPreference;
− SwitchPreferenceCompat;
− CheckBoxPreference;
− PreferenceFragmentCompat;
− PreferenceScreen;
− PreferenceCategory и другие.
Эти классы делают возможным быстрое создание качественного ин-
терфейса установки настроек приложения. Их применение опирается на
рассмотренный в разделе материал и предлагается к самостоятельно-
му изучению.

Контрольные вопросы

1. Для чего используется метод getPreferrence?


2. Каким образом можно сохранить данные в Preference?
3. Каким образом можно получить данные из Preference?
4. За что отвечают режимы доступа к Preferece?
5. Сохраните в Preference пару “оценка”:5.
62
10. РАБОТА С БАЗАМИ ДАННЫХ. СУБД SQLite

Введем несколько эквивалентных определений базы данных.


Базой данных называется набор данных, структура которых описыва-
ет характеристики объектов и отношения между ними, в рамках одной или
нескольких частей приложения [8].
Согласно же ГОСТ 34.321-96, база данных – это совокупность взаи-
мосвязанных данных, организованных в соответствии со схемой базы
данных таким образом, чтобы с ними мог работать пользователь.
И, наконец, ГОСТ 20886-85 [9] дает следующее определение базы
данных: совокупность данных, организованных по определенным прави-
лам, предусматривающим общие принципы описания, хранения и мани-
пулирования данными, независимая от прикладных программ.
На основе данных определений важно сделать вывод о том, что база
данных не является набором каких-либо программных или аппаратных
средств. База данных предоставляет лишь описание набора хранимых
данных. Конкретная реализация же базы данных осуществляется с помо-
щью системы управления базами данных.
В рамках Android для работы с базами данных могут использоваться
средства пакета android.database.sqlite и Room Persistence
Library. В данном разделе будет рассмотрен первый способ.
Перед описанием работы в рамках android.database.sqlite сле-
дует отметить, что хотя он и предоставляет широкий спектр возможностей
по работе с БД, в большинстве случаев рекомендуется использовать набор
Room API. Причиной этому является низкий уровень описания запросов в
рамках sqlite.database. Не выполняется автоматическая проверка не-
обработанных SQL-запросов на этапе компиляции, что ставит вопрос об
их непосредственном контроле, из-за чего возрастает риск ошибок рабо-
ты. Кроме того, передаваемые и принимаемые данные требуется вручную
обрабатывать для преобразования к типам, используемым в программе,
или наоборот в базе данных.
Опишем работу с данным пакетом на примере базы данных пользова-
телей, состоящей из двух отношений: учетные записи пользователей и их
роли. Каждый пользователь может иметь несколько ролей. Таблица ролей
является справочной. Из описания видно, что отношения имеют тип связи
многие-ко-многим. Данный вид связи может быть описан с помощью
промежуточной таблицы, хранящей ссылки на исходные (см. листинг 1).

63
Листинг 1 – Пример создания промежуточной таблицы
CREATE TABLE "ROLES" (
"ID" INTEGER,
"ROLE_NAME" TEXT NOT NULL UNIQUE,
PRIMARY KEY("ID" AUTOINCREMENT)
)
CREATE TABLE "ACCOUNTS" (
"ID" INTEGER,
"login" TEXT NOT NULL UNIQUE,
"password" INTEGER NOT NULL,
PRIMARY KEY("ID" AUTOINCREMENT)
);
CREATE TABLE "ACCOUNT_ROLES" (
"ACCOUNT_ID" INTEGER NOT NULL,
"ROLE_ID" INTEGER NOT NULL,
CONSTRAINT "ACCOUNT_FKEY" FOREIGN KEY("ROLE_ID") REFERENCES
"ACCOUNTS.ID",
CONSTRAINT "ROLE_FKEY" FOREIGN KEY("ACCOUNT_ID") REFERENCES
"ROLES.ID"
);
Для работы с базой данных рекомендуется создать класс-контракт,
описывающий требуемую схему. В данный класс вносятся имена таблиц и
столбцов, а также методы для создания и управления базой данных. Такой
подход позволяет организовать централизованное управление БД с воз-
можностью простого уведомления других компонентов программы об из-
менениях в схеме данных.
Пример класса-контракта представлен в листинге 2.
Листинг 2 – Пример класса-контракта
//Kotlin
class DatabaseContract {
object Accounts : BaseColumns {
const val TABLE_NAME = "ACCOUNTS"
const val COLUMN_LOGIN = "login"
const val COLUMN_PASSWORD = "password"
}

object Roles : BaseColumns {


const val TABLE_NAME = "ROLES"
const val COLUMN_NAME = "name"
}

object AccountRoles : BaseColumns {


64
const val TABLE_NAME = "ACCOUNT_ROLES"
const val COLUMN_ACCOUNT = "ACCOUNT_ID"
const val COLUMN_ROLE = "ROLE_ID"
}

companion object {
const val SQL_DELETE_ACCOUNT_ROLES = "DROP TABLE IF EXISTS
${AccountRoles.TABLE_NAME}"

const val SQL_CREATE_ACCOUNTS =


"CREATE TABLE ${Accounts.TABLE_NAME}
(${BaseColumns._ID} INTEGER PRIMARY KEY,${Accounts.COLUMN_LOGIN}
TEXT,${Accounts.COLUMN_PASSWORD} TEXT)"

const val SQL_DELETE_ACCOUNTS = "DROP TABLE IF EXISTS


${Accounts.TABLE_NAME}"

const val SQL_CREATE_ROLES =


"CREATE TABLE ${Roles.TABLE_NAME} (${BaseColumns._ID}
INTEGER PRIMARY KEY,${Roles.COLUMN_NAME} TEXT, TEXT)"

const val SQL_DELETE_ROLES = "DROP TABLE IF EXISTS


${Accounts.TABLE_NAME}"
const val SQL_CREATE_ACCOUNT_ROLES =
"""CREATE TABLE ${AccountRoles.TABLE_NAME}
(${BaseColumns._ID} INTEGER PRIMARY
KEY,${AccountRoles.COLUMN_ACCOUNT} INTEGER NOT
NULL,${AccountRoles.COLUMN_ROLE} INTEGER NOT NULL, CONSTRAINT
"ACCOUNT_FKEY" FOREIGN KEY("ROLE_ID") REFERENCES "ACCOUNTS.ID",
CONSTRAINT "ROLE_FKEY" FOREIGN KEY("ACCOUNT_ID") REFERENCES
"ROLES.ID"
)"""
}}

Класс-контракт содержит внутренние классы, описывающие отноше-


ния базы данных с помощью констант – имен таблиц и столбцов. Реализа-
ция интерфейса BaseColumns дает возможность работы с внешним клю-
чом _ID. Это не является обязательным условием описания отношения,
однако может помочь в ряде ситуаций. В этом же классе находятся строки
SQL-запросов для создания и удаления базы.
После описания схемы данных требуется описать методы, предназна-
ченные для непосредственного с данной базой взаимодействия – ее созда-
ния, обновления и удаления. Для решения этой задачи применяется класс-
наследник SQLiteOpenHelper (листинг 3).
65
Листинг 3 – Пример расширения SQliteOpenHelper
//Kotlin
class DatabaseHelper(context: Context?) :
SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) {
override fun onCreate(sqLiteDatabase: SQLiteDatabase) {

sqLiteDatabase.execSQL(DatabaseContract.SQL_CREATE_ACCOUNTS)
sqLiteDatabase.execSQL(DatabaseContract.SQL_CREATE_ROLES)

sqLiteDatabase.execSQL(DatabaseContract.SQL_CREATE_ACCOUNT_ROLES)
}

override fun onUpgrade(sqLiteDatabase: SQLiteDatabase, i: Int,


i1: Int) {

sqLiteDatabase.execSQL(DatabaseContract.SQL_DELETE_ACCOUNTS)
sqLiteDatabase.execSQL(DatabaseContract.SQL_DELETE_ROLES)

sqLiteDatabase.execSQL(DatabaseContract.SQL_DELETE_ACCOUNT_ROLES)
onCreate(sqLiteDatabase)
}

override fun onDowngrade(sqLiteDatabase: SQLiteDatabase,


oldVersion: Int, newVersion: Int) {
onUpgrade(sqLiteDatabase, oldVersion, newVersion)
}

companion object {
const val DB_VERSION = 1
const val DB_NAME = "SampleStore.db"
}
}

Для дальнейшей работы с базой данных требуется создать объект


класса-наследника SQLiteOpenHelper, а затем создать объект класса
SQLiteDatabase в режиме чтения или в режиме записи.
Замечание: режим записи позволяет также осуществлять и чте-
ние данных.

//Kotlin
val dbHelper = DatabaseHelper(baseContext)
val db: SQLiteDatabase = dbHelper.writableDatabase

66
Для вставки данных в таблицу используется метод insert. Данные,
соответствующие столбцам, описываются с помощью объектов класса
ContentValues в виде пары ключ-значение:
//Kotlin
val values = ContentValues()
values.put(DatabaseContract.Accounts.COLUMN_LOGIN, "sampleuser")
values.put(DatabaseContract.Accounts.COLUMN_PASSWORD, "samplepass")
db.insert(DatabaseContract.Accounts.TABLE_NAME, null, values)

Для выборки данных описываются столбцы, которые будут получены,


условия выбора, группировки, сортировки значений. Используется метод
query, который возвращает объект типа Cursor – интерфейса, реализации
которого позволяют работать с двумерными таблицами:
//Kotlin
val projection = arrayOf(
BaseColumns._ID,
DatabaseContract.Accounts.COLUMN_LOGIN,
DatabaseContract.Accounts.COLUMN_PASSWORD
)

val selection = DatabaseContract.Accounts.TABLE_NAME + " = ?"


val selectionArgs = arrayOf("user")
val sortOrder = DatabaseContract.Accounts.COLUMN_LOGIN + " DESC"
val cursor = db.query(
DatabaseContract.Accounts.TABLE_NAME, // The table to
query
projection, // The array of columns to return (pass null
to get all)
selection, // The columns for the WHERE clause
selectionArgs, // The values for the WHERE clause
null, // don't group the rows
null, // don't filter by row groups
sortOrder // The sort order
)

В данном примере используются заполнители для строк: вместо знака ? в


строке selection будет подставлено значение из массива selectionArgs.
Полученные данные в дальнейшем обрабатываются посредством обхо-
да записей, на которые указывает курсор. По окончанию работы курсор
должен быть закрыт.
while(cursor.moveToNext()) {
//Обработка полученных данных
67
}
cursor.close();
//аналогично для Kotlin

Сходным образом описываются запросы на удаление и редактирование


данных. Метод delete в качестве результата возвращает количество уда-
ленных строк.
val selection = DatabaseContract.Accounts.TABLE_NAME + " LIKE ?"
val selectionArgs = arrayOf("Accounts")
val deletedRows = db.delete(DatabaseContract.Accounts.TABLE_NAME,
selection, selectionArgs)

Метод update используется для реализации запросов типа UPDATE и


возвращает количество измененных записей.
String title = "ACCS";
ContentValues values = new ContentValues();
values.put(DatabaseContract.Accounts.TABLE_NAME, title);
String selection = DatabaseContract.Accounts.TABLE_NAME + " LIKE
?";
String[] selectionArgs = { "Accounts" };

int count = db.update(


DatabaseContract.Accounts.TABLE_NAME,
values,
selection,
selectionArgs);

Для выполнения собственных SQL-запросов используется метод


executeSQL().
Работу с базой данных крайне рекомендуется выполнять в стороннем
потоке. Это обусловлено тем, что основной поток приложения предназна-
чен в первую очередь для работы с UI и не позволяет выполнять длитель-
ные операции. Работа с БД в стороннем потоке может быть организована
следующим образом (листинг 4):
Листинг 4 – Пример реализации работы с БД в стороннем потоке
//Kotlin
val r = Runnable {
val dbHelper = DatabaseHelper(baseContext)
val db: SQLiteDatabase = dbHelper.writableDatabase
val values = ContentValues()
values.put(DatabaseContract.Accounts.COLUMN_LOGIN, "admin")
68
values.put(DatabaseContract.Accounts.COLUMN_PASSWORD, "admin")
db.insert(DatabaseContract.Accounts.TABLE_NAME, null, values)
runOnUiThread { binding.status.text = "INSERT EXECUTED" }
}
val task = FutureTask<Void?>(r, null)
Executors.newSingleThreadExecutor().execute(task)

По окончанию работы необходимо также закрывать соединение с ба-


зой данных:
dbHelper.close();
// аналогично для Kotlin

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


Контрольные вопросы
1. Что такое база данных?
2. Что такое класс-контракт?
3. Для чего используются объекты класса Cursor?
4. Обратитесь к таблице базы данных, выведите все записи в лог при-
ложения.
5. Для чего используется интерфейс BaseColumns?

11. РАБОТА С БАЗАМИ ДАННЫХ. Room Persistence Library


Альтернативой непосредственному использованию SQLiteOpenHelper
является применение Room Persistence Library. Room предоставляет
дополнительный уровень абстракции над SQLite, что обеспечивает сво-
бодный доступ к базе данных. В частности, Room предоставляет следую-
щие возможности:
− проверку SQL-запросов во время компиляции;
− механизм аннотаций для уменьшения количества шаблонного кода;
− оптимизированные пути миграции баз данных.
Основными элементами Room являются Entity (сущность), DAO
(Database Access Object, Объект доступа к базе данных), и Database (ба-
за данных).
Сущности – объекты классов, которые соответствуют записям в базе
данных. В рамках Room API сущности используются для передачи в базу
данных или получения из нее информации.
DAO – объект, описывающие механизмы доступа к базе данных.
Посредством DAO описывается то, какие методы для работы с хранили-
щем данных доступны приложению.
69
Объекты классов-наследников RoomDatabase описывают базу дан-
ных и служат основной точкой доступа для соединения с сохраняемыми
данными приложения.
Рассмотрим эти компоненты подробнее на примере простой схемы
данных, описывающей успеваемость студентов. Выделяются сущности
Student, Group, Specialization, Scores и Course.
Студент может состоять в нескольких группах т.к. может учиться по
нескольким образовательным программам. Группа студентов содержит в
себе данные о годе поступления и направлении обучения. Группа состоит
из нескольких студентов, а потому наблюдается отношение M:N, которое
разрешается посредством промежуточного отношения student_group.
Данное же отношение при этом позволяет указать оценки, которые полу-
чил студент по предметам при обучении в определенной группе (рис. 16).

Рис. 16. Пример схемы данных

70
Замечание: при работе непосредственно с реляционными СУБД отно-
шения описываются с помощью таблиц. Сущности также описывают за-
писи из таблиц. В большинстве случаев понятие таблица используется
как синоним понятия отношение. Однако в строгой формулировке поня-
тия таблицы и отношения не тождественны и обладают некоторыми
различиями, которые опускаются при работе с СУБД.
Отношения базы данных описываются посредством классов, помечен-
ных аннотацией @Entity:

//Kotlin
@Entity(tableName = "STUDENT_GROUP", foreignKeys = [
ForeignKey(entity = Group::class,
parentColumns = arrayOf("ID_GROUP"),
childColumns = arrayOf("GROUP_ID"),
onUpdate = ForeignKey.CASCADE,
onDelete = ForeignKey.CASCADE),
ForeignKey(entity = Student::class,
parentColumns = arrayOf("ID_STUDENT"),
childColumns = arrayOf("STUDENT_ID"),
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE)])
data class StudentGroup (
@PrimaryKey(autoGenerate = true) @ColumnInfo(name =
"ID_STUDENT_GROUP") var id:Int,
@ColumnInfo(name ="GROUP_ID") var group:Int,
@ColumnInfo(name ="STUDENT_ID") var student:Int)

Данная аннотация может принимать такие параметры, как имя табли-


цы базы данных, список первичных и внешних ключей, индексов и т.д.
Члены классов, помеченных данной аннотацией, могут быть отмечены ря-
дом аннотаций, к которым относятся:
− @NonNull – поле не может принимать значение Null;
− @ColumnInfo – описывает информацию о столбце таблицы, может в
качестве атрибутов принимать характеристики как имя, значение по
умолчанию и другие;
− @PrimaryKey – поле является первичным ключом с возможностью
автогенерации (необходимость автогенерации указывается в виде аргу-
мента аннотации). Каждая сущность должна содержать как минимум
1 поле с данной аннотацией;
− @ForeignKey – указывает, что по этому полю организован внешний
ключ. Принимает в качестве аргументов ссылку на класс-родитель, список

71
столбцов, характеризующих первичный ключ, список столбцов, характе-
ризующих внешний ключ и стратегии обновления и удаления данных;
− @Embedded – указывает, что поля класса, включенного в класс-
сущность в виде поля, являются полями описываемого отношения.
Без этой аннотации возможно использование в виде полей лишь совмес-
тимых с БД типов;
− @Ignore – указывает, что поле класса не должно учитываться при
построении логики базы данных;
− @Relation – указывает, что поле класса связано с некоторой сущ-
ностью. Указываются столбцы, по которым организуется отношение,
класс, описывающий объединение таблиц (при необходимости), сущность,
объекты которой будут храниться и список получаемых атрибутов.
Сам класс должен хранить поле объекта-родителя и коллекцию (список
для 1:M и множество для M:N) дочерних элементов и другие.
Для взаимодействия с данными определяются DAO (database access
object). Для этого описываются интерфейсы, маркируемые аннотаци-
ей @Dao.
Интерфейсы Dao не реализуются пользователем и предназначены
для описания взаимодействия с данными на уровне абстракций.
DAO реализуются средствами Room на этапе компиляции автоматиче-
ски. DAO не содержат свойств, и содержат лишь методы, необходимые для
работы с данными.
Для взаимодействия с данными предоставляется ряд аннотаций:
− @Insert – метод предназначен для вставки данных в таблицу.
В качестве аргументов может принимать сущность базы данных для рабо-
ты и действие при конфликте данных;
− @Update – метод предназначен для редактирования данных в табли-
це. Может принимать в качестве аргумента сущность базы данных;
− @Delete – метод предназначен для удаления данных из таблицы.
Может принимать в качестве аргумента сущность базы данных;
− @Query – метод используется для выполнения SQL-запроса, пере-
данного в качестве аргумента. Аргументы методов указываются непо-
средственно в теле SQL-запроса в виде “:аргумент”. @Query поддержи-
вает использование запросов SELECT, INSERT, UPDATE и DELETE.
В качестве возвращаемых значений описанных методов могут исполь-
зоваться как Entity, так и классы POJO.
Для того, чтобы выполнить метод мог исполнить несколько SQL-
запросов, он может быть отмечен аннотацией @Transactional. Это озна-
чает, что метод должен выполнить транзакцию из нескольких запросов.
72
Одно из применений @Transactional – описание методов, возвращаю-
щих классы с аннотацией @Relation.
Замечание: методы @Dao могут также возвращать объекты Cursor,
однако такая практика не рекомендована по причине отсутствия гарантий
безопасности работы с Cursor.
Для описания конфигурации базы данных используется абстрактный
класс-наследник RoomDatabase. Данный класс служит основной точкой
доступа приложения к сохраняемым данным. Класс базы данных должен
удовлетворять следующим условиям:
Класс должен быть отмечен аннотацией @Database, которая включает
массив сущностей, содержащий список всех сущностей данных, связан-
ных с базой данных.
Для каждого класса DAO, связанного с базой данных, класс базы дан-
ных должен определить абстрактный метод, который имеет нулевые аргу-
менты и возвращает экземпляр класса DAO.
Замечание: для создания объекта класса RoomDatabase настоятельно ре-
комендуется использовать шаблон Одиночка, что обусловлено высокими за-
тратами на создание таких объектов. В случае если планируется работа при-
ложения в виде нескольких процессов, при создании объекта RoomDatabase
рекомендуется включить опцию enableMultiInstanceInvalidation(),
что позволит каждому отдельному процессу создать собственный объект
этого класса и распространять информацию об изменении БД между ними.

@Database(entities = [Student::class], version = 1)


abstract class MyDatabase: RoomDatabase() {
abstract fun getStudentDao(): StudentDao
//Double-checked singleton
//В Kotlin возможно использование ключевого object для создания
singleton
//Данная реализация приведена по причине наглядности своей ра-
боты
companion object {
@Volatile
private lateinit var instance: MyDatabase

fun getInstance(context: Context): MyDatabase {


if (! ::instance.isInitialized)
synchronized(MyDatabase::class.java) {
if (! ::instance.isInitialized) {

73
//Применяется паттерн Builder
instance = Room

.databaseBuilder(context,MyDatabase::class.java, "my_database")
.build()
}
}
return instance
}
}
}

Дальнейшее взаимодействие с БД происходит посредством объекта


RoomDatabase.
Кроме описанных, Room Persistence Library обладает широким
возможностей, таких как работа с представлениями и индексами, которые
предлагаются к самостоятельному изучению.

Контрольные вопросы

1. Что такое сущность?


2. Что такое DAO?
3. Для чего используется аннотация @Transactional?
4. По какой причине при создании объектов класса RoomDatabase ре-
комендуется использовать шаблон Одиночка?
5. Обратитесь к таблице базы данных, выведите все строки в лог при-
ложения. Используйте Room API.

12. ОСНОВЫ РАБОТЫ С ГРАФИЧЕСКИМИ ЭЛЕМЕНТАМИ

Одной из значимых областей разработки приложений для ОС Android


является работа с графическими компонентами. Для этого применяется
широкий спектр компонентов, базовыми из которых являются Canvas,
SurfaceView и Drawable. Применение этих и других элементов позво-
ляет создавать нестандартные элементы интерфейса, описывать сложные
взаимодействия графических элементов, управлять параметрами их ото-
бражения и т. д.

74
Класс Drawable представляет собой абстракцию объекта, который
должен быть нарисован. Создание объекта Drawable может быть осуще-
ствлено одним из двух способов:
1) путем развертывания изображения (в виде Bitmap), сохраненного
в проекте;
2) путем развертывания XML-ресурса, хранящего свойства изображения.
XML-ресурсы могут также описывать векторные изображения. Вектор-
ные изображения описываются как совокупность графических примити-
вов и их свойств. Средства Android Studio предоставляют возможность
импорта векторных изображений в форматах SVG и PSD.
Для растровых изображений поддерживаются форматы PNG, JPEG,
GIF. Предпочтительно использование формата PNG, в то время как формат
GIF не рекомендуется к применению.
Изображения, сохраненные в проекте в папке res/drawable, могут
быть автоматически сжаты без потерь с помощью инструмента aapt, что
приведет к получению изображения меньшего размера с качеством, анало-
гичным оригиналу. Из этого следует, что файлы изображений могут изме-
няться в процессе сборки. В случаях, когда требуется чтение изображений
в виде битовых потоков, следует помещать их в папку res/raw, в которой
aapt не проводит оптимизаций.
В дальнейшем изображения используются по правилам ресурсов, что
показано в листинге 1.
Листинг 1 – Пример использования изображения
//kotlin
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.example.myapplication.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {


lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.imageView.setImageResource(R.drawable.ic_right)
}
}

75
Второй способ создания Drawable заключается в определении XML-
ресурсов. Данный способ предпочтительным в ситуациях, когда не ожида-
ется изменения свойств графического объекта при его взаимодействии с
пользователем, но даже в ряде таких случаев использование XML-
ресурсов может оказаться оправданным. Объекты любых подклассов
Drawable, для которых возможен вызов метода inflate, могут быть
определены в XML и созданы в приложении.
Drawable расширяется рядом наследников с дополнительным функ-
ционалом, позволяющим добиться более эффективной работы.
Класс NinePathDrawable позволяет создавать растягиваемые изо-
бражения, что может оказаться полезным при их использовании в качест-
ве фоновых рисунков. Такие изображения должны храниться в файлах
формата .9.png. NinePath-изображения фактически представляют со-
бой стандартные изображения формата PNG с дополнительной границей в
1px. Изображения делятся на 9 областей, которые образуются путем соз-
дания по двум смежным границам изображения отрезков, которое про-
ецируют растягивающиеся области. Схема таких изображений показана на
рис. 17.
При использовании данного вида изображений в качестве фоновых,
они будут автоматически приведены к размеру виджета, в котором
они размещены.

Рис. 17. Схема NinePath-изображения


76
ShapeDrawable используется в ситуациях, когда требуется динамиче-
ски рисовать изображения. Объекты данного класса могут быть использо-
ваны всюду, где используется Drawable. Данный класс содержит метод
draw(), что упрощает его использование в работе с графикой в рамках
классов-наследников view, как показано в листинге 2.
Листинг 2 – Пример расширения класса View для работы с ShapeDrawable
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.drawable.ShapeDrawable
import android.graphics.drawable.shapes.RectShape
import android.view.View

class CustomDrawable(context: Context) : View(context) {


private val drawable: ShapeDrawable
override fun onDraw(canvas: Canvas) {
drawable.draw(canvas)
}

init {
val x = 100
val y = 100
val width = 600
val height = 600
drawable = ShapeDrawable(RectShape())
// If the color isn't set, the shape uses black as the
default.
drawable.paint.color = Color.RED
// If the bounds aren't set, the shape can't be drawn.
drawable.setBounds(x, y, x + width, y + height)
}
}

class MainActivity : AppCompatActivity() {


override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(CustomDrawable(this))
}
}

77
Для рисования изображений используются объекты классов Canvas и
Paint. Canvas описывает холст, на котором будут отрисованы изображе-
ния. Paint описывает параметры создания изображения. Рассмотрим эти
элементы подробнее.
Canvas описывает пространство, на котором будут изображены тре-
буемые объекты. Данный класс содержит такие методы, как drawArc,
drawCircle, drawRect и другие, предназначенные для рисования графи-
ческих примитивов. К ним относятся следующие:
− прямоугольник;
− круг;
− линия;
− точка;
− дуга;
− эллипс и другие, основанные на представленных выше.
Кроме того, на Canvas может быть отрисован Bitmap, кроме того,
холст можно закрасить определенным цветом. Также на Canvas может
быть нарисован текст. При этом следует помнить, что текст, нарисован-
ный на Canvas, будет являться рисунком, не будет поддерживающим
средства работы с текстом.
С помощью объектов класса Paint описываются параметры рисования
объектов на холсте. К таковым относятся следующие:
− стиль – заполнение, контур, заполнение и контур;
− размер текста;
− выравнивание текста;
− наличие антиалиасинга и другие.
Замечание: Canvas и Paint представляют собой аналогию холста и
кисти: на холсте указывается, что должно быть нарисовано, а с помощью
кисти – то, каким образом это должно быть нарисовано.
В простейшей реализации использование Canvas и Paint задается
аналогично листингу 3.
Листинг 3 – Пример использования Canvas и Paint
//java
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(new DrawView(this));

}
class DrawView extends View{

78
public DrawView(Context context) {
super(context);
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint p = new Paint();
p.setAntiAlias(true);
p.setTextSize(48);
p.setColor(Color.RED);
p.setStyle(Paint.Style.FILL_AND_STROKE);
canvas.drawText("APPLE", 55, 55, p);
}
}
}

В данном листинге внутренний класс DrawView – наследник View и


является реализацией собственного виджета. Для отрисовки содержимого
во View используется метод обратного вызова onDraw, принимающий в
качестве аргумента объект Canvas – это холст, представляющий собой
изображение виджета на активности.
В рамках данного листинга рисование происходит в основном потоке.
Для обновления изображения на Canvas используется метод invalidate(),
что не является оптимальным решением в случае необходимости отрисов-
ки динамического, требовательного к вычислительным ресурсам прило-
жения. Использование Canvas на View может быть полезно при создании
собственных виджетов или статических изображений. Для работы с дина-
мическим графическим содержимым рекомендуется использовать вид-
жет SurfaceView.
SurfaceView предоставляет поверхность для рисования, которая под-
держивает рисование в фоновых потоках. Это возможно благодаря тому,
что в основе SurfaceView находится объект класса Surface, а не
Canvas. Кроме того, классы-наследники SurfaceView могут также реа-
лизовывать интерфейс SurfaceHolder.Callback для получения возмож-
ности обработки событий, связанных с SurfaceView. К ним относятся:
− surfaceCreated – SurfaceView был создан и готов к использованию;
− surfaceChanged – формат или размер SurfaceView был изменен.
Данный метод будет вызван как минимум один раз – после вызова
surfaceCreated;
− surfaceDestroyed – вызывается непосредственно перед тем, как
SurfaceView будет уничтожен.
79
В большом числе ситуаций для работы с SurfaceView также создает-
ся класс, описывающий вычисления в стороннем потоке. В данном случае
наследник SurfaceView будет отвечать за описание поверхности, а реа-
лизация потока - за действия на ней, как показано в листинге 4.
Листинг 4 – Пример использования SurfaceView
//java
public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(new DrawView(this));

}
class DrawView extends SurfaceView implements
SurfaceHolder.Callback {

MyThread t;

public DrawView(Context context) {


super(context);
getHolder().addCallback(this);

@Override
public boolean onTouchEvent(MotionEvent event) {
t.setCoord(event.getX(),event.getY());
return super.onTouchEvent(event);
}

@Override
public void surfaceCreated(@NonNull SurfaceHolder holder) {
Log.d("CREATED","SURFACE");
t = new MyThread(getHolder(), this);
t.setRunning(true);
t.start();
}

@Override
public void surfaceChanged(@NonNull SurfaceHolder holder,
int format, int width, int height) {

80
}

@Override
public void surfaceDestroyed(@NonNull SurfaceHolder holder)
{
boolean retry = true;
t.setRunning(false);
while (retry) {
try {
t.join();
retry = false;
} catch (InterruptedException e) {
}
}
}
}
class MyThread extends Thread{
int x;
int y;
int r;

private SurfaceHolder surfaceHolder;


private DrawView drawView;
private boolean running;
public MyThread(SurfaceHolder surfaceHolder, DrawView
drawView) {
this.surfaceHolder = surfaceHolder;
this.drawView = drawView;
this.running = false;
x = drawView.getWidth()/2;
y = drawView.getHeight()/2;
r = 1;
}

public void setRunning(boolean running) {


this.running = running;
}

@Override
public void run() {
Log.d("THREAD","STARTED");
Canvas c;
while (running)
{
81
c = surfaceHolder.lockCanvas();
Paint p = new Paint();
p.setColor(Color.argb(x,x*y,(2+x*y)/x,y));
c.drawCircle(x,y,r,p);
r = (r%256)+1;
surfaceHolder.unlockCanvasAndPost(c);
}

}
void setCoord(float x, float y)
{
this.x = Math.round(x);
this.y = Math.round(y);
this.r = 1;
}
}

В данном примере в методе surfaceDestroyed происходит ожидание


окончания работы фонового потока, который предназначен для рисования.
В методе surfaceCreated происходит создание стороннего потока и за-
пуск его выполнения. Метод surfaceChanged не содержит команд, что
означает, что не требуется ничего делать при изменении surfaceView.
Конструктор класса DrawView содержит команду addCallback, благода-
ря которой указывается, что обработка событий с surfaceView будет
происходить в методах, описанных в методах класса DrawView.
В классе MyThread содержится поле running, выполняющее роль фла-
га работы. В методе run содержится описание непосредственно рисования.
Внутри цикла while с помощью метода lockCanvas происходит получе-
ние Canvas поверхности, причем данный холст блокируется для измене-
ний извне до тех пор, пока не будет окончена работа в текущем потоке.
Для указания того, что работа с Canvas окончена и нужно опублико-
вать изменения Canvas, используется метод unlockCanvasAndPost.
SurfaceView может быть размещена внутри виджета SurfaceView,
что позволяет размещать графические элемента на конкретных участках
активности. Реализация фоновой работы для SurfaceView требует пони-
мания работы со средствами многопоточных вычислений. В простейшем
случае для этого достаточно расширения Thread, однако в более сложных
задачах для этого может потребоваться использование рассмотренных в
предыдущих разделах средств (листинг 5).
82
Листинг 5 – Пример использования виджета SurfaceView
class MainActivity : AppCompatActivity(), SurfaceHolder.Callback {
private lateinit var thread: MyThread
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)

setContentView(binding.root)
binding.surfaceView.holder.addCallback(this)
binding.surfaceView.setOnTouchListener { _, event ->
thread.setCoord(event.x, event.y)
super.onTouchEvent(event)
}
}

override fun surfaceCreated(p0: SurfaceHolder) {


thread = MyThread(binding.surfaceView)
thread.setRunning(true)
thread.start()
}

override fun surfaceChanged(p0: SurfaceHolder, p1: Int, p2:


Int, p3: Int) {
}

override fun surfaceDestroyed(p0: SurfaceHolder) {


var retry = true
thread.setRunning(false)
while (retry) {
try {
thread.join()
retry = false
} catch (e: InterruptedException) {
}
}
}
}
class MyThread(private val drawView: SurfaceView) :
Thread() {
var x: Int
var y: Int
var r: Int
private var running: Boolean
fun setRunning(running: Boolean) {
this.running = running
83
}

override fun run() {


Log.d("THREAD", "STARTED")
var c: Canvas
while (running) {
c = drawView.holder.lockCanvas()
val p = Paint()
p.setColor(Color.argb(x, x * y, (2 + x * y) / x, y))
c.drawCircle(x.toFloat(), y.toFloat(), r.toFloat(), p)
r = r % 256 + 1
drawView.holder.unlockCanvasAndPost(c)
}
}

fun setCoord(x: Float, y: Float) {


this.x = Math.round(x)
this.y = Math.round(y)
r = 1
}

init {
running = false
x = drawView.getWidth() / 2
y = drawView.getHeight() / 2
r = 1
}
}

Наконец, отметим, что SurfaceView является наследником View, что


подразумевает возможность обработки в рамках реализации поверхности
(в листинге 4 это DrawView) событий по взаимодействию с пользовате-
лем: кликов, прикосновений к экрану, как показано на примере переопре-
деления метода onTouchEvent, и другие. Метод onTouchEvent предна-
значен для получения координат точки касания экрана с последующей пе-
редачей их в фоновый поток для указания новой точки рисования графи-
ческого объекта (в рассмотренном примере – окружности).

Контрольные вопросы

1. Для чего используется метод surfaceChanged?


2. Какая часть NinePath-изображения не подвержена растяже-
нию/сжатию?
84
3. Что такое холст? Что такое сцена?
4. Каким образом возможно осуществить рисование штриховой линии
на сцене?
5. Что такое графический примитив? Какие примитивы представляют-
ся Android SDK?

13. ОСНОВЫ СЕТЕВОГО ВЗАИМОДЕЙСТВИЯ

Одним из значимых разделов разработки приложений является органи-


зация сетевого взаимодействия, в рамках которого мобильное приложение
выступает в роли клиента.
Для организации соединения с сетевым ресурсом в рамках Android
возможно использование средств JDK, таких как HTTPUrlConntection,
однако в большинстве случаев рекомендуется использовать библиоте-
ку Retrofit.
Для предоставления возможности работы с сетью Интернет в манифе-
сте проекта следует описать соответствующие разрешения:

<uses-permission android:name="android.permission.INTERNET" />


<uses-permission
android:name="android.permission.ACCESS_NETWORK_STATE" />

Замечание: Взаимодействие с сетевым ресурсом по умолчанию


осуществляется посредством протокола HTTPS, работа протокола HTTP
требует указания возможности передачи незашифрованного трафика:

<application
...
...

android:networkSecurityConfig="@xml/network_security_config"
...
...>
...
...
</application>
//или
android:usesCleartextTraffic="true"

85
Для организации соединения с сетевым ресурсом в рамках Android
возможно использование средств JDK, таких как HTTPUrlConntection,
однако в большинстве случаев рекомендуется использовать библиоте-
ку Retrofit.
Retrofit основан на библиотеке Apache OkHttp. Последняя предостав-
ляет широкий функционал по организации клиент-серверного взаимодей-
ствия, однако при этом оперирует более низким уровнем взаимодействия,
что делает ее менее удобной в использовании.
Для подключения Retrofit необходимо добавить соответствующую
запись в список зависимостей. Кроме того, возможно добавления ряда за-
висимостей для доступа к дополнительному функционалу, такому как де-
сериализация ответов сервера с использованием ряда библиотек. В рас-
сматриваемом будет использоваться библиотека Gson:

implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'

Работа Retrofit основана на двух основных понятиях. Первым из них


является API. В данном контексте под API понимается набор методов для
работы с удаленным сервисом. API описывается в интерфейсах. Пример
простейшего интерфейса показан в листинге 1.
Листинг 1 – Пример описания API
public interface SampleAPI {
@GET("students/")
Call<List<Student>> getStudents();
}

Аннотация @GET метода getStudent показывается, что будет исполь-


зоваться GET-запрос по адресу <основной адрес>/students, т.е. в ка-
честве аргумента указан относительный путь, используемый для доступа к
сервису. При передаче в аннотацию, путь всегда должен начинаться со
знака /.
Объект типа Call<T> предназначен для вызовов методов Retrofit с
целью обращения к удаленному сервису. Обобщение T используется для
обозначения типа, описывающего успешный ответ сервера.
Обращение может быть осуществлено как синхронно, с помощью мето-
да execute(), так и асинхронно с помощью метода enqueue(), а также
рядом других для контроля работы обращения. Метод enqueue() прини-
мает в качестве аргумента реализацию интерфейса Callback<T>, состоя-
86
щего из методов onResponse(), вызываемого при успешном ответе серве-
ра, и метода onFailure(), вызываемого при ошибке работы обращения.
Вторым важным элементом клиента Retrofit является непосредственно
объект класса Retrofit, предназначенный для организации клиент-
серверного взаимодействия. Объекты Retrofit создаются с помощью
внутреннего класса-строителя и добавить функционал, который потребу-
ется при взаимодействии. В частности, возможно добавлений конвертеров
объектов в форматы JSON/XML. Объекты Retrofit должны получать при
создании информацию о расположении удаленного сервиса в виде URL.
Доступ же к конкретным элементам этого сервиса осуществляется по-
средством API. Пример объекта Retrofit представлен в листинге 2.
Листинг 2 – Пример создания объекта Retrofit
Retrofit r = new Retrofit.Builder().baseUrl("<baseURL>")
.addConverterFactory(GsonConverterFactory.create())
.build();
Приведем пример работы с Retrofit на примере получения шаблонных данных
(листинг 3).

Листинг 3 – Пример работы с Retrofit


//java
public class Post{
int id;
int userId;
String title;
String body;

public int getId() {


return id;
}

public void setId(int id) {


this.id = id;
}

public int getUserId() {


return userId;
}

public void setUserId(int userId) {


this.userId = userId;
}

87
public String getTitle() {
return title;
}

public void setTitle(String title) {


this.title = title;
}

public String getBody() {


return body;
}

public void setBody(String body) {


this.body = body;
}
}

public interface SampleAPI {


@GET("posts/")
Call<List<Post>> getPosts();
}

public class RecycleAdapter extends


RecyclerView.Adapter<RecycleAdapter.ViewHolder> {
ItemBinding binding;
List<Post> postList;
public RecycleAdapter(List<Post> postList) {
this.postList = postList;

@NonNull
@Override
public RecycleAdapter.ViewHolder onCreateViewHolder(@NonNull
ViewGroup parent, int viewType) {
binding =
ItemBinding.inflate(LayoutInflater.from(parent.getContext()));
return new ViewHolder(binding.getRoot());
}

@Override
public void onBindViewHolder(@NonNull RecycleAdapter.ViewHolder
holder, int position) {
binding.titleTV.setText(postList.get(position).title);
88
binding.bodyTV.setText(postList.get(position).body);

binding.idTV.setText(String.valueOf(postList.get(position).id));

binding.userIDTV.setText(String.valueOf(postList.get(position).user
Id));

@Override
public int getItemCount() {
return postList.size();
}

class ViewHolder extends RecyclerView.ViewHolder {

public ViewHolder(@NonNull View itemView) {


super(itemView);
}
}
}

public class MainActivity extends AppCompatActivity {

ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
Retrofit r = new Retrofit.Builder().
addConverterFactory(GsonConverterFactory.create()).

baseUrl("https://jsonplaceholder.typicode.com").build();
binding.postsButton.setOnClickListener(l->{
Log.d("BUTTON",
String.valueOf(Thread.currentThread().getId()));
r.create(SampleAPI.class).getPosts().enqueue(new
Callback<List<Post>>() {
@Override
public void onResponse(Call<List<Post>> call,
Response<List<Post>> response) {

89
RecycleAdapter adapter = new
RecycleAdapter(response.body());
binding.postView.setLayoutManager(new
LinearLayoutManager(MainActivity.this));
binding.postView.setAdapter(adapter);
}

@Override
public void onFailure(Call<List<Post>> call,
Throwable t) {
Log.d("FAILURE",
String.valueOf(Thread.currentThread().getId()));
t.printStackTrace();
}
});
});

}
}

При работе с методом enqueue() следует помнить, что асинхронное


выполнение запроса не является выполнением запроса в другом потоке.
При выполнении многопоточных вычислений следует помнить об особен-
ностях работы потоков в рамках ОС Android, а также с особым вниманием
относиться к обработке результатов выполнения запросов.
Аналогично описанному выше примеру реализуются и прочие типы
HTTP-запросов, что делает возможной организацию REST-взаимодейст-
вия. Для реализации других видов запросов используются аннотации
POST, PUT, DELETE и другие. Для организации по динамически форми-
руемым путям используется аннотация PATCH. Для передачи параметров
запроса используется аннотация QUERY. Для передачи тела запроса
используется аннотация BODY, для передачи заголовка – HEAD.
Кроме рассмотренных Retrofit располагает и рядом других аннота-
ций, предназначенных для качественной настройки сетевого взаимодейст-
вия. Данные аннотации предлагается изучить самостоятельно.

Контрольные вопросы

1. Какие разрешения добавляются в манифест приложения для работы


с сетью?
2. Для чего используется аннотация PATCH библиотеки Retrofit?
3. Для чего предназначен интерфейс Call<T>?
90
4. Обратитесь к сетевому ресурсу, выведите ответ, полученный по за-
просу типа GET. Ответ сервиса должен быть представлен в формате XML.
5. Для чего используется метод baseUrl()?

14. СЕРВИСЫ В ANDROID OS

Под сервисом понимается компонент приложения, предназначенный


для выполнения длительных фоновых операций. К таковым могут быть
отнесены загрузка файлов в облачный сервис или из него, проигрывание
музыки и т.д.
Однажды запущенный сервис будет работать до тех пор, пока не
исполнит свою задачу, или пока его не остановят вручную. Сервис про-
должит свою работу, в том числе и после закрытия приложения, его за-
пустившего. К одному и тому же сервису может иметь доступ несколь-
ко приложений.
Сервисы не требуют наличия графического интерфейса. Сервисы
обладают более высоким приоритетом по сравнению с бездействующими
активностями, а потому имеют меньшую вероятность быть остановлен-
ными операционной системой.
Замечание: единственный случай, когда ОС может остановить сер-
вис – это необходимость выделить большее число ресурсов компонентам,
работающим в активном (не фоновом) режиме. В этом случае остановлен-
ный сервис будет автоматически перезапущен при наличии ресурсов для
его работы.
Существует три типа сервисов:
− видимые;
− фоновые;
− связанные.
Видимые сервисы предназначены для выполнения заметных для поль-
зователя операций, таких как проигрывание музыки. Такой вид сервисов
должен отображать уведомление о его работе. Видимые сервисы продолжа-
ют работу даже в то время, когда пользователь с ними не взаимодействует.
Замечание: видимый сервис должен уведомить пользователя о своей
работе (с помощью Notification) в течение 5 секунд после запуска сво-
ей работы.
Фоновые сервисы используются для операций, выполнение которых
не требует уведомления пользователя и которые ему не заметны, напри-
мер, для оптимизации хранения данных приложения. В большинстве слу-
чаев на работу фоновых сервисов наложен ряд ограничений, когда прило-
91
жение не активно. Настоятельно не рекомендуется давать фоновым серви-
сам доступ к информации, которая не является необходимой для их работы.
Сервис называется связанным, когда компонент приложения привя-
зывается к нему с помощью вызова bindService(). Связанный сервис
предоставляет клиент-серверный интерфейс для взаимодействия с компо-
нентами приложений, его использующих. Связанный сервис работает
только до тех пор, пока к нему привязан другой компонент приложения.
Несколько компонентов могут привязываться к сервису одновременно, но,
когда все они отключаются, сервис уничтожается.
Для описания сервиса используются наследники класса Service.
При расширении данного класса требуется переопределить метод onBind,
возвращающий объект класса, реализующего интерфейс IBinder.
Данный метод обратного вызова описывает поведение при привязке сер-
виса к компоненту приложения и должен предоставлять компоненту при-
ложения интерфейс для взаимодействия с сервисом. В случае, когда тре-
буется запретить привязку сервиса, данному методу следует возвращать
null. Кроме того, в большинстве случаев требуется переопределение сле-
дующих методов:
− onCreate() – выполняется, когда объект класса сервиса был соз-
дан. Семантически аналогичен методу onCreate активности;
− onStartCommand() – выполняется при вызове startService(),
когда компонентом запрашивается запуск работы сервиса. Во время вы-
полнения этого метода сервис готов запустить фоновую задачу. При реа-
лизации этого метода также должна быть обеспечена возможность оста-
новки сервиса с помощью метода stopSelf() или stopService().
В случае реализации привязки к компоненту данный метод переопреде-
лять не требуется;
− OnDestroy() – вызывается перед тем, как сервис будет уничтожен.
Также в рамках сервиса возможно переопределение методов для обра-
ботки нехватки памяти для работы, изменении настроек, изменении тре-
буемой задачи и т.д.
Сервисы должны быть отмечены в манифесте приложения внутри тега
<application> с помощью тега <service>:

<application>
<service android:name=”.SampleService”
android:exported=”true”>
…..
</service>
</application>
92
Атрибут exported указывает на то, возможно ли использовать сервис
в сторонних приложениях. Кроме того, возможно указать, может ли быть
создан сервис с помощью атрибута enabled, задать иконку сервиса и т.д.
<service> также содержит атрибут description, с помощью которого
задается краткое описание сервиса, которое будет видно пользователям
при просмотре информации о сервисе в списке запущенных сервисов.
Пример реализации сервиса получения скорости интернет-соединения
представлен в листинге 1.
Листинг 1 – Пример реализации сервиса
public class SampleService extends Service {

@Override
public void onCreate() {
super.onCreate();
Log.d("SERVICE", "CREATED");
}
@Override
public int onStartCommand(Intent intent, int flags, int
startId) {
Log.d("SERVICE", "STARTED");
Executor e = Executors.newSingleThreadExecutor();
Runnable r = ()->{
ConnectivityManager m = (ConnectivityManager)
getSystemService(CONNECTIVITY_SERVICE);
Network[]ni = m.getAllNetworks();
Function<Network, String> f = network -> "UID:
"+m.getNetworkCapabilities(network).getOwnerUid()+" UP:
"+m.getNetworkCapabilities(network).getLinkUpstreamBandwidthKbps()+
" kbps | DOWN:
"+m.getNetworkCapabilities(network).getLinkDownstreamBandwidthKbps(
)+" kbps";
List<String> speeds =
Arrays.stream(ni).map(f).collect(Collectors.toList());
speeds.forEach(x->Log.d("NETWORK", x));
try {
Thread.sleep(500);
} catch (InterruptedException interruptedException)
{
interruptedException.printStackTrace();
}
};
e.execute(r);

93
stopSelf(startId);
return START_NOT_STICKY;
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d("SERVICE", "DONE");
}

@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
}

Рассмотрим этот пример подробнее. Методы onCreate() и onDelete()


уведомляют пользователя о создании и остановке сервиса соответственно.
Метод onBind() возвращает null по причине того, что данный сервис не
должен быть доступен для других приложений.
Метод OnStartCommand() уведомляет пользователя о старте работы
сервиса, а также содержит объекты Executor и Runnable, задача кото-
рых – в стороннем потоке получить список доступных устройству мо-
бильных сетей и считать значения скорости приема и отправки данных.
Данный сервис должен перезапуститься при его остановке со стороны
системы, а потому onStartCommand() возвращает флаг START_STICKY.
Метод принимает в качестве аргументов намерение, запускающее сервис,
флаги запуска сервиса и идентификатор запуска сервиса.
Флаги запуска приложения указывают на дополнительные параметры
запуска приложения и могут принимать значение 0 или комбинацию из
флагов START_FLAG_REDELIVERY (используется, если передаваемое на-
мерение подается повторно по причине того, что сервис ранее вернул
START_REDELIVER_INTENT, но было прерван до вызова stopSelf(int)
для этого намерения) или START_FLAG_RETRY (устанавливается, если
произошло непредвиденное завершение работы сервиса, который работал
в режиме START_STICKY).
Флаг START_STICKY указывает, что сервис должен быть перезапущен
в случае его остановки с помощью метода onStartCommand() без пере-
дачи ему последнего объекта Intent в случае, если нет ожидающих наме-
рений для запуска сервиса – в этом случае намерения будут реализованы.

94
Флаг START_NON_STICKY указывает, что сервис не должен будет пере-
запускаться в случае его остановки.
Флаг START_REDELIVER_INTENT указывает, что сервис должен быть
перезапущен в случае его остановки с помощью метода onStartCommand()
с передачей ему последнего объекта Intent, если сервис завершил свою
работу преждевременно и был явно вызван его запуск, или если работа
сервиса была завершена до вызова метода stopSelf(int). В таком слу-
чае будет снова передано последнее полученное сервисов намерение, если
оно не было должным образом обработано. Другие ожидающие намерения
будут запускаться по очереди.
START_STICKY подходит для ситуаций, когда не требуется выполнения
команд приложения, а сервисы обрабатывают свое состояние самостоятель-
но, например, в случае проигрывания музыки. START_REDELIVER_INTENT
подходит в случаях, когда требуется убедиться, что сервис выполнил свою
задачу, например при передаче файлов. В случаях, когда перезапускать
сервис нет необходимости или это можно сделать явно в приложении,
подходит флаг START_NOT_STICKY.
Запуск сервиса производится с помощью метода startService(), ко-
торый принимает в качестве аргумента намерение. Остановка работы сер-
виса производится с помощью метода storService(int) или, если сер-
вис должен остановить свою работу, – с помощью метода stopSelf().
При вызове одного из этих методов операционная система остановит ра-
боту сервиса при ближайшей возможности это сделать.
Следует помнить, что работа сервиса может быть запрошена несколь-
кими компонентами, а потому следует корректно вызывать остановку его
работы. В этом случае требуется проверять, не было ли после запроса на
остановку сервиса запросов на старт его работы. Для этого используется
метод stopSelf(int startId). Этот метод принимает идентификатор
запроса на запуск startId, переданный в метод onStartCommand(), кото-
рому соответствует запрос на остановку. Если сервис получит новый запрос
на запуск до того, как сможете, будет вызван stopSelf(int startId),
идентификаторы запуска не совпадут, и сервис не будет остановлен.
В случае, когда требуется, чтобы сервис был доступен для других при-
ложений, в методе onBind() должен быть описан объект, реализующий
интерфейс IBinder. Также возможно использование объектов Messenger
или использование AIDL (Android Interface Definition Language). В данном
разделе рассмотрим первый способ.
Способ привязки сервиса на основе реализации IBinder используется
в тех ситуациях, когда не требуется межпроцессного взаимодействия,
95
т.е. сервис будет использоваться локальным приложением и не будет
использоваться другими приложениями. В этом случае приложение будет
выступать в качестве клиента сервиса и получит доступ к его общедос-
тупным методам.
Исправим пример из листинга 1 следующим образом:

public class SampleService extends Service {


private final IBinder binder = new SampleBinder();
String r;
@Override
public void onCreate() {
super.onCreate();
Log.d("SERVICE", "CREATED");
r="";
Executor e = Executors.newSingleThreadExecutor();
Runnable task = ()->{
ConnectivityManager m = (ConnectivityManager)
getSystemService(CONNECTIVITY_SERVICE);
Network[]ni = m.getAllNetworks();
Function<Network, String> f = network -> "UID:
"+m.getNetworkCapabilities(network).getOwnerUid()+" UP:
"+m.getNetworkCapabilities(network).getLinkUpstreamBandwidthKbps()+
" kbps | DOWN:
"+m.getNetworkCapabilities(network).getLinkDownstreamBandwidthKbps(
)+" kbps";
r =
Arrays.stream(ni).map(f).collect(Collectors.joining("\n"));
};
e.execute(task);
}

@Override
public void onDestroy() {
super.onDestroy();
Log.d("SERVICE", "DONE");
}

@Nullable
@Override
public IBinder onBind(Intent intent) {
return binder;
}

public class SampleBinder extends Binder{


96
SampleService getService(){
return SampleService.this;
}
}
public String doWork(){
return r;
}
}

В данном классе метод onDestroy() также уведомляет о завершении


работы сервиса. Метод onCreate() содержит теперь команды для полу-
чения скоростей интернет-соединения, которая ранее находилась в методе
onStartCommand(). В данном примере метод onStartCommand() не
используется, а потому сервис будет существовать только в то время, ко-
гда используется другими компонентами. Сервис будет уничтожен в тот
момент, когда у него не останется привязанных компонентов. В рамках
одного сервиса возможно использование как метода onStartCommand(),
так и метода onBind() в зависимости от нужд разработчика.
В качестве компонента, использующего приложение – в рамках рас-
сматриваемого примера это MainActivity, используется объект клас-
са, реализующего интерфейс ServiceConnection. Объекты данного
класса должны реализовывать два метода: onServiceConnected() и
onServiceDisconnected(). Пример реализации представлен в листин-
ге 2.
Листинг 2 – Пример использования ServiceConnection.
public class MainActivity extends AppCompatActivity {
SampleService s;
boolean flag = true;
ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
binding.startButton.setOnClickListener(l->{
if (flag) {
Log.d("BUTTON", "WORK");

Toast.makeText(this,s.doWork(),Toast.LENGTH_LONG).show();
}

97
});
}
@Override
protected void onStart() {
super.onStart();
Intent intent = new Intent(this, SampleService.class);
bindService(intent, connection, Context.BIND_AUTO_CREATE);
}
@Override
protected void onStop() {
super.onStop();
unbindService(connection);
flag = false;
}
private ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder
service) {
SampleService.SampleBinder binder =
(SampleService.SampleBinder) service;
s = binder.getService();
flag = true;
}

@Override
public void onServiceDisconnected(ComponentName name) {
flag = false;
}
};
}

Интерфейс ServiceConnection позволяет переопределить четыре ме-


тода: onServiceConnected(), onServiceDisconnected(),
onBindDied(), onNullBinding(). Метод onNullBinding() использу-
ется для обработки ситуации, когда метод сервиса onBind() возвращает
null, т.е. когда привязка компонентов к сервису запрещена. Метод
onBindDied() используется для ситуаций, когда привязка к сервису от-
ключается. В этом случае для восстановления соединения (например, если
привязанное к сервису приложение было обновлено) необходимо провести
повторную привязку к сервису. Метод onServiceConnected() исполь-
зуется для определения поведения в случае установления соединения с

98
сервисом, а onServiceDisconnected() – в случае потери соединения с
сервисом. Последние два метода являются обязательными к реализации.
Для установления связи с сервисом используется метод bindService(),
принимающий в качестве аргументов намерение на работу с сервисом,
объект serviceConnection, а также различные флаги привязки.
Для описания связи сервисов и компонентов могут использоваться
объекты Messenger и язык определения интерфейсов Android (Android
Interface Language Definition, AIDL). Сервисы могут использовать меха-
низм уведомлений для связи с пользователем. Существует множество спо-
собов применений сервисов, которые требуют отдельного подробно-
го изучения.

Контрольные вопросы

1. Что такое сервис?


2. Какие виды сервисов существуют?
3. Какое требование накладывается на работу видимого сервиса?
4. За что отвечает метод onBind()?
5. За что отвечает метод onStartCommand()?

15. РАБОТА С УВЕДОМЛЕНИЯМИ

Уведомлением называется сообщение, которое отображается опера-


ционной системой вне интерфейса приложения для предоставления поль-
зователю актуальной информации из него. К таковой информации могут
относиться сообщения из социальных сетей, письма электронной почты,
прогресс проигрывания музыкальной композиции и т.д. В отличие от
Toast или Snackbar, уведомления позволяют выводить информацию и в
том случае, если приложение работает в фоновом режиме.
Уведомление структурно состоит из шести компонентов:
1) малой иконки;
2) имени приложения, которое отправило уведомление;
3) времени получения уведомления;
4) большой иконки, которая может быть отображена в уведомлении;
5) заголовка уведомления, который может быть отображен в уведом-
лении;
6) текста уведомления, который может быть отображен в уведомлении.

99
Начиная с Android 8, для отображения уведомлений требуется созда-
ние каналов, в рамках которых будет проводиться работа с ними.

NotificationChannel channel = new


NotificationChannel(CHANNEL_ID,"SAMPLE CHANNEL
NAME",NotificationManager.IMPORTANCE_DEFAULT);
channel.setDescription("this is a sample channel");
NotificationManager manager =
getSystemService(NotificationManager.class);
manager.createNotificationChannel(channel);

Для каналов указываются параметры отображения уведомлений, кото-


рые в его рамках. К таковым могут относиться параметры отображения
уведомления на экране блокировки, свечения светодиода, указывающего
на наличие уведомлений в канале, проигрываемый звук при отображении
уведомления, уровень важности и т.д.
Замечание: Начиная с Android 8.1 (уровень API 27), приложения не
могут проигрывать звук уведомления чаще одного раза в секунду. В том
случае, если приложение отображает несколько уведомлений в одну се-
кунду, то все они штатно отобразятся, будет проигран звук лишь перво-
го уведомления.
Непосредственно уведомление создается с помощью класса
Notification. Класс Notification содержит внутренний класс Builder,
предоставляющий возможности гибкого построения уведомлений.

Notification notification = new Notification


.Builder(MainActivity.this,CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notify)
.setContentTitle("Sample Title")
.setContentText("Sample Text")
.setLargeIcon(BitmapFactory.decodeResource(getResources(),
R.drawable.notification))
.build();

Отображение уведомлений возможно с помощью менеджера уведом-


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

NotificationManager manager =
getSystemService(NotificationManager.class);
manager.createNotificationChannel(channel);
manager.notify(NOTIFY_ID, notification);

100
Для перехода к активности из уведомления требуется описание соот-
ветствующего явного намерения.
Intent intent = new Intent(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
Intent.FLAG_ACTIVITY_CLEAR_TASK);

Комбинация из флагов позволяет корректно описать поведение прило-


жения при переходе из уведомления. В приведенном ниже примере пред-
полагается, что пользователь переходит по уведомлению из иного прило-
жения, а не того, которое его опубликовало, а потому намерение создает
новую задачу, не добавляя ее в стек переходов.
Сформированное намерение подается в качестве аргумента в объект
класса PendingIntent, которое в свою очередь подается в качестве аргу-
мента метода setContentIntent (листинг 1).
Листинг 1 – Пример работы NotificationCompat
NotificationCompat.Builder notification = new
NotificationCompat.Builder(MainActivity.this,CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notify)
.setContentTitle("Sample Title")
.setContentText("Sample Text")
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.setLargeIcon(BitmapFactory.decodeResource(getResources(),R.drawabl
e.notification))
.addAction(new NotificationCompat.
Action.
Builder(IconCompat.

createWithResource(MainActivity.this,R.drawable.ic_timer),
"Set 500ms
Timer", pendingTimerIntent)
.build())
.addAction(new NotificationCompat.
Action.Builder(IconCompat.

createWithResource(MainActivity.this,R.drawable.ic_answer),
"Answer", pendingAnswerIntent)
.build())
.addAction(new NotificationCompat.
Action.Builder(IconCompat.
createWithResource(MainActivity.this,R.drawable.ic_close),
"Ignore", pendingIgnoreIntent)
.build());
101
Метод setAutoCancel позволяет автоматически удалять уведомление
после того, как пользователь нажмет на него.
Метод setContentIntent задает действие, которое будет выполнять-
ся по умолчанию при клике пользователя по уведомлению. Уведомление
также может содержать до трех кнопок действий. Действие описывается
иконкой, его отображающей (может не использоваться на ряде устройств),
заголовком и намерением, которое должно будет исполниться при выбо-
ре действия.
По умолчанию уведомления отображаются в однострочном виде.
В случае если текст уведомления не может быть размещен в одну строку,
то он будет приведен к виду однострочного с помощью знака многоточия.
Полный текст уведомления отображаться при этом не будет. В случае ес-
ли требуется отобразить на уведомлении большое число содержимого,
требуется сделать его расширяемым.
Для решения этой задачи используется метод setStyle(), прини-
мающий в качестве аргумента шаблон отображения полной формы уве-
домления. Существует ряд стандартных шаблонов уведомлений, пред-
ставленных в классе NotificationCompat.
Пример получения изображения с помощью камеры устройства с по-
следующим помещением его в уведомление показан в листинге 2.
Листинг 2 – Пример размещения изображения в уведомлении
public class MainActivity extends AppCompatActivity {
private static final int NOTIFY_ID = 101;
static final int REQUEST_IMAGE_CAPTURE = 1;
NotificationChannel channel;
NotificationManager manager;
private static final String CHANNEL_ID = "EXAMPLE CHANNEL";
ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
channel = new NotificationChannel(CHANNEL_ID,"SAMPLE CHANNEL
NAME",NotificationManager.IMPORTANCE_DEFAULT);
channel.setDescription("this is a sample channel");
manager = getSystemService(NotificationManager.class);
manager.createNotificationChannel(channel);
binding.makeNotifyButton.setOnClickListener(l->{
Intent i = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
startActivityForResult(i,REQUEST_IMAGE_CAPTURE);
102
});
}
@Override
public void onActivityResult(int requestCode, int statusCode,
Intent intentData){
super.onActivityResult(requestCode,statusCode,intentData);
if
(requestCode==REQUEST_IMAGE_CAPTURE&&statusCode==RESULT_OK)
{
Bitmap b = (Bitmap)intentData.getExtras().get("data");
NotificationCompat.Builder notification = new
NotificationCompat.Builder(MainActivity.this,CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notify)
.setContentTitle("Sample Title")
.setContentText("Sample Text")
.setAutoCancel(true)
.setLargeIcon(b)
.setStyle(new NotificationCompat
.BigPictureStyle()
.bigLargeIcon(null)
.bigPicture(b));
manager.notify(NOTIFY_ID, notification.build());
}
}
}

Следует помнить, что размер развернутого уведомления также ограни-


чен и его содержимое также может быть «обрезано».
Аналогичным образом создается уведомление с большим количест-
вом текста:

NotificationCompat.Builder notification = new NotificationCompat


.Builder(MainActivity.this,CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notify)
.setContentTitle("Lorem Ipsum")
.setStyle(new NotificationCompat
.BigTextStyle()
.setBigContentTitle("THIS IS LOREM")
.setSummaryText("LOREM LOREM")
.bigText(getString(R.string.lorem_ipsum)));

Уведомления могут быть объединены в группы. Группа уведомлений


не является синонимом канала уведомлений. Уведомление может быть
привязано к группе с помощью метода setGroup(), принимающего в ка-
103
честве аргумента строковой идентификатор группы. Кроме того, создается
отдельное уведомление, агрегирующее описанные в группе. Для него вы-
зывается метод setGroupSummary, принимающий в качестве аргумента
истину (листинг 3).
Листинг 3 – Пример работы с группой уведомлений
NotificationCompat.Builder notification = new
NotificationCompat.Builder(MainActivity.this,CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notify)
.setContentTitle("Lorem Ipsum")
.setStyle(new NotificationCompat
.BigTextStyle()
.setBigContentTitle("THIS IS LOREM")
.setSummaryText("LOREM LOREM")

.bigText(getString(R.string.lorem_ipsum)))
.setGroup(GROUP_ID);
NotificationCompat.Builder notification2 = new
NotificationCompat.Builder(MainActivity.this,CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notify)
.setContentTitle("Lorem Ipsum 2")
.setStyle(new NotificationCompat
.BigTextStyle()
.setBigContentTitle("THIS IS OTHER
LOREM")
.setSummaryText("LOREM IPSUM")

.bigText(getString(R.string.lorem_ipsum)))
.setGroup(GROUP_ID);

NotificationCompat.Builder notification3 = new


NotificationCompat.Builder(MainActivity.this,CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notify)
.setContentTitle("Lorem Ipsum 3")
.setContentText("2 new notification")
.setGroup(GROUP_ID)
.setGroupSummary(true);
manager.notify(NOTIFY_ID, notification.build());
manager.notify(NOTIFY_ID+1, notification2.build());
manager.notify(NOTIFY_ID+2, notification3.build());

104
Существует большое число применений уведомлений, основанных на
рассмотренных в данном разделе. Каждое из них может потребовать изу-
чения дополнительного материала.

Контрольные вопросы

1. Что такое уведомление?


2. Что такое канал уведомлений? Что такое группа уведомлений?
3. Какова структура уведомления?
4. Каким образом осуществляется переход от уведомления к активно-
сти при клике по нему?
5. Создайте уведомление, содержащее изображение; при клике по нему
переходите на активность приложения.

16. ОСНОВЫ РАБОТЫ С СЕНСОРАМИ

Датчик (сенсор, sensor) – это средство измерений, предназначенное


для выработки сигнала измерительной информации в форме, удобной
для передачи, дальнейшего преобразования, обработки и (или) хранения,
но не поддающейся непосредственному восприятию наблюдате-
лем [ГОСТ Р 51086-97].
Подавляющее большинство мобильных устройств содержат в себе
большое число сенсоров, которые измеряют движение, ориентацию и раз-
личные параметры окружающей среды. К измеряемым данным могут от-
носиться скорость движения устройства, температура и влажность окру-
жающей среды, координаты местоположения устройства, его расположе-
ние в пространстве и т.д.
ОС Android поддерживает три группы датчиков.
Датчики движения: акселерометры, гироскопы, датчики силы тяже-
сти и вектора вращения.
Датчики окружающей среды: барометры, фотометры, термометры и т.д.
Датчики положения: датчики ориентации, магнитометры и т.п.
Замечание: Android поддерживает 13 типов сенсоров. Сенсоры делят-
ся на аппаратные – физические компоненты устройства – и программные,
являющиеся эмуляцией аппаратных сенсоров и моделирующие их работу
на основе данных аппаратных сенсоров.

105
Для работы с сенсорами предлагается использование фреймворка
Sensor. Данный фреймворк является частью пакета android.hardware
и включает в себя следующие компоненты:
− SensorManager: применяется для обеспечения доступа к сенсору;
− Sensor: используется для программного описания определенного
сенсора. Этот класс предоставляет различные методы, позволяющие опре-
делить возможности датчика;
− SensorEvent: используется для описания событий, которые могут
произойти при работе с сенсором;
− SensorEventListener: применяется для создания двух методов
обратного вызова, которые получают уведомления (события датчика) при
изменении значений датчика или при изменении точности датчика.
Работа с сенсорами в рамках приложения разделяется на два этапа.
I. Определение наличия сенсоров и возможности работы с ними – это
связано с тем, что мобильные устройства отличаются большим разнообра-
зием и могут содержать разные наборы сенсоров. Лишь малый процент
устройств содержит представителей всех 13 групп сенсоров.
II. Мониторинг событий сенсоров: связано с необходимостью получе-
ния данных сенсоров. События сенсоров происходят каждый раз, когда
происходит изменение параметров, измеряемых сенсором.
Для определения имеющихся на устройстве датчиков возможно ис-
пользование объекта SensorManager, получаемый посредством метода
getSystemService():

SensorManager s =
(SensorManager)getSystemService(Context.SENSOR_SERVICE);
s.getSensorList(Sensor.TYPE_ALL).forEach(x->
Log.d("SENSOR",x.getName()));

Метод getSensorList() возвращает список объектов класса Sensor,


предоставляющих данные о находящихся в устройстве сенсорах. К тако-
вым данным относится название сенсора, его поставщик, диапазон изме-
ряемых значений и т.д.
Для обработки данных, получаемых сенсором, используются объекты,
реализующие SensorEventListener. Данный интерфейс содержит два
обязательных к реализации метода:
− onSensorChanged() – метод обработки изменения значения изме-
ряемой сенсором величины. В качестве аргумента данного метода высту-
пает объект SensorEvent, который содержит информацию о новых дан-

106
ных датчика, включая: точность данных, датчик, который сгенерировал
данные, временную метку, в которой были сгенерированы данные, и но-
вые данные, записанные датчиком;
− onAccuracyChanged() – метод обработки точности измерения.
Точность описывается одной из четырех констант:

SENSOR_STATUS_ACCURACY_LOW
SENSOR_STATUS_ACCURACY_MEDIUM
SENSOR_STATUS_ACCURACY_HIGH
SENSOR_STATUS_UNRELIABLE.

Пример обработки данных сенсора представлен в листинге 1. Рассмот-


рим его подробнее.
Листинг 1 – Пример получения данных сенсора
public class MainActivity extends AppCompatActivity implements
SensorEventListener{

SensorManager sm;
Sensor sensor;
ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());

setContentView(binding.getRoot());
sm = (SensorManager)
getSystemService(Context.SENSOR_SERVICE);
sensor = sm.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
}
@Override
public void onSensorChanged(SensorEvent event) {
binding.xAxis.setText("x: "+ event.values[0]);
binding.yAxis.setText("y: "+ event.values[1]);
binding.zAxis.setText("z: "+ event.values[2]);

@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {

107
@Override
protected void onResume() {
super.onResume();
sm.registerListener(this,
sensor,SensorManager.SENSOR_DELAY_NORMAL);
}

@Override
protected void onStop() {
super.onStop();
sm.unregisterListener(this);
}
}

В методе onCreate() происходит инициализация объекта класса


SensorManager с помощью метода getSystemService(). Данный ме-
тод принимает в качестве аргумента константу SENSOR_SERVICE, а ре-
зультат работы метода приводится к типу SensorManager.
Для объекта SensorManager вызывается метод getDefaultSensor(),
принимающий в качестве аргумента константу TYPE_GYROSCOPE. Данный
метод предназначен для получения сенсора заданного типа, используемый
по умолчанию (т.к. в устройстве может быть несколько сенсоров одного
вида) - в данном случае это гироскоп.
Рассматриваемая в листинге активность реализует методы
SensorEventListener. При изменении точности работы сенсора не
предполагается никаких действий, в то время как при изменении считы-
ваемых значений они передаются для вывода на TextView.
Наконец, прослушивание сенсора регистрируется в методе
onResume() с помощью команды registerListener() объекта
SensorManager. Данная команда принимает в качестве аргументов объ-
ект класса, реализующего SensorEventListener, объект, описывающий
сенсор и число - задержка получения данных в виде константы
SENSOR_DELAY_NORMAL. Данное число описывает, с какой частотой дан-
ные, полученные с помощью сенсора, будут отправлены в приложение с
помощью метода onSensorChanged(). Снятие регистрации производит-
ся в методе onStop() с помощью метода unregisterListener().
Более подробное рассмотрение принципов работы с сенсорами требует
самостоятельного изучения и может оказаться полезным во множестве
областей применения.

108
Контрольные вопросы

1. Что такое датчик?


2. Какие группы сенсоров представлены в Android?
3. За что отвечает метод onAccuracyChanged()?
4. Для чего используется класс SensorManager?
5. Обратитесь к датчику освещенности устройства, выведите информацию
об уровне освещенности в лог приложения или всплывающее сообщение.

109
ЗАКЛЮЧЕНИЕ
Понимание основополагающих принципов программирования наряду с
современными возможностями языков разработки приложений делают
умения и навыки студентов актуальными для ведения практической дея-
тельности в области реализации и верификации программного обеспече-
ния. Знание основ программирования необходимо как для разработчиков,
так и для специалистов смежных специальностей, таких как тестировщики
программного обеспечения, специалисты в области компьютерной безо-
пасности, инженеры информационных систем, разработчики баз данных и
многие другие.
Язык программирования Java, являясь современным средством разра-
ботки и де-факто промышленным стандартом, предоставляет широкий на-
бор средств для решения разнообразных задач, что делает владеющих им
специалистов востребованными на рынке труда. Знание основных прин-
ципов разработки приложений необходимо как для разработчиков непо-
средственно на Java, так и для тех, кто применяет языки программирова-
ния, использующие платформу Java, такие как Kotlin, Scala, Groovy и дру-
гие, применяемые в web-разработке, обработке больших данных, реализа-
ции мобильных приложений и многих других областях.
Изучение материалов пособия позволит освоить необходимый мини-
мальный уровень владения языком для дальнейшего совершенствования
навыков решения профессиональных задач, требующих углубленных зна-
ний как отдельных возможностей Java, так и предметной области.
Понимание аспектов разработки мобильных приложений делают уме-
ния и навыки студентов актуальными для ведения практической деятель-
ности в области реализации и верификации программного обеспечения.
Эти знания необходимы как для разработчиков, так и для специалистов
смежных специальностей, таких как тестировщики программного обеспе-
чения, специалисты в области компьютерной безопасности, инженеры
информационных систем и многие другие.
Операционная система Android, являясь доминирующей на рынке мо-
бильных устройств, предоставляет широкий набор средств для решения
разнообразных задач, что делает владеющих им специалистов востребо-
ванными на рынке труда. Знание принципов разработки мобильных при-
ложений необходимо как для профильных специалистов, так и для занятых
в смежных областях, так как web-разработка, тестирование и отладка про-
граммного обеспечения, информационная безопасность и многие другие.
Изучение материалов пособия позволит освоить необходимый мини-
мальный уровень владения языком для дальнейшего совершенствования
навыков решения профессиональных задач, требующих углубленных зна-
ний как отдельных возможностей ОС Android, так и предметной области.
110
БИБЛИОГРАФИЧЕСКИЙ СПИСОК

1. Документация операционной системы Android. – URL :


https://developer.android.com (дата обращения: 31.10.2022).
2. Программирование для профессионалов / Филлипс Билл, Стюарт
Крис, Марсикано Кристин, Гарднер Брайан Android. – 4-е издание. – Санкт-
Петербург : Питер, 2021. – 704 с. – (Серия «Для профессионалов»).
3. Документация методологии Material Design. – URL : http://material.io
(дата обращения : 31.10.2022).
4. Дейтел, П. Android для разработчиков / П. Дейтел, Х. Дейтел,
А. Уолд. – 3-е изд. – Санкт-Петербург : Питер, 2016. – 512 с.
5. Fragments | Android Developers. – URL : https://developer.android.com
/guide/fragments?hl=en (дата обращения: 31.10.2022).
6. ГОСТ 34.321-96 Эталонная модель управления данными.
7. Документация языка Java. – URL : https://docs.oracle.com/en/java
/javase/19/docs/api/index.html (дата обращения: 31.10.2022).
8. ISO 2382:2015 Information Technology. Vocabulary. – URL :
https://www.iso.org/standard/63598.html (дата обращения: 31.10.2022).
9. Организация данных в системах обработки данных. Термины и опре-
деления / ГОСТ 20886-85. Взамен ГОСТ 20886-75; введ. 1986-07-01.
– Москва : Стандартинформ, 2005. – 27 с.
10. ГОСТ Р 51086-97 «Датчики и преобразователи физических вели-
чин электронные. Термины и определения».

111
ОГЛАВЛЕНИЕ

ВВЕДЕНИЕ .................................................................................................................. 3
1. ПРОСТЕЙШЕЕ ПРИЛОЖЕНИЕ ДЛЯ ANDROID .............................................. 5
2. РАЗМЕТКИ АКТИВНОСТЕЙ В ПРИЛОЖЕНИЯХ ДЛЯ ANDROID .............. 9
3. ВИДЫ РАЗМЕТОК ............................................................................................... 13
4. MATERIAL DESIGN ............................................................................................. 21
5. НАМЕРЕНИЯ. ОРГАНИЗАЦИЯ ПЕРЕХОДОВ
МЕЖДУ АКТИВНОСТЯМИ ............................................................................... 27
6. ФРАГМЕНТЫ ........................................................................................................ 33
7. ПОТОКИ. БАЗОВЫЕ ПОНЯТИЯ ....................................................................... 44
8. ПОТОКИ В ANDROID. СОПРОГРАММЫ ....................................................... 51
9. ТИПЫ РЕСУРСОВ. SHAREDPREFERENCES .................................................. 59
10. РАБОТА С БАЗАМИ ДАННЫХ. СУБД SQLite .............................................. 63
11. РАБОТА С БАЗАМИ ДАННЫХ. Room Persistence Library ........................... 69
12. ОСНОВЫ РАБОТЫ С ГРАФИЧЕСКИМИ ЭЛЕМЕНТАМИ......................... 74
13. ОСНОВЫ СЕТЕВОГО ВЗАИМОДЕЙСТВИЯ ................................................ 85
14. СЕРВИСЫ В ANDROID OS ............................................................................... 91
15. РАБОТА С УВЕДОМЛЕНИЯМИ ..................................................................... 99
16. ОСНОВЫ РАБОТЫ С СЕНСОРАМИ ............................................................ 105
ЗАКЛЮЧЕНИЕ ....................................................................................................... 110
БИБЛИОГРАФИЧЕСКИЙ СПИСОК .................................................................... 111

Учебное издание

Кузнецов Иван Владимирович


Исаев Михаил Сергеевич
Пономарчук Юлия Викторовна
Холодилов Александр Андреевич

РАЗРАБОТКА ПРИЛОЖЕНИЙ ДЛЯ ОС ANDROID

Учебное пособие

Технический редактор С. С. Заикина

Отпечатано методом прямого репродуцирования


————————————————————————————
План 2023 г. Поз. 9.20. Подписано в печать 17.02.2023. Формат 6084/16.
Усл. печ. л. 6,5. Зак. 24. Тираж 10 экз. Цена 763 р.
————————————————————————————
Отпечатано в Издательстве ДВГУПС
680021, г. Хабаровск, ул. Серышева, 47.

112

Вам также может понравиться