Академический Документы
Профессиональный Документы
Культура Документы
Рекомендовано
Методическим советом
в качестве учебного пособия
Хабаровск
Издательство ДВГУПС
2023
УДК 004.432 (075.8)
ББК З973.22я73
P 17
Авторы:
Рецензенты:
© ДВГУПС, 2023
2
ВВЕДЕНИЕ
3
мобильных устройств делают разработку мобильных приложений акту-
альной сферой деятельности, требующей актуальных навыков разработ-
ки приложений.
Пособие снабжено многочисленными примерами из курса информати-
ки и основ программирования, которые наглядно иллюстрируют различ-
ные аспекты разработки приложений для ОС Android. Также в пособии
рассмотрены возможности средств разработчика для ОС Android для ор-
ганизации сетевого взаимодействия, работы с базами данных, представле-
ния графического содержимого и других вопросов, с которыми встреча-
ются разработчики мобильных приложений. Включены разделы по лока-
лизации приложений и аннотированию.
Для закрепления материала, представленного в пособии, в конце каж-
дого раздела даны контрольные вопросы.
Труд авторов распределился следующим образом: И. В. Кузнецов
(гл. 7, 8, 14–16), М. С. Исаев (гл. 2–4), Ю. В. Пономарчук (гл. 1, 5, 6, 13),
А. А. Холодилов (гл. 9–11).
4
1. ПРОСТЕЙШЕЕ ПРИЛОЖЕНИЕ ДЛЯ ANDROID
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;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
Контрольные вопросы
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>
Контрольные вопросы
3. ВИДЫ РАЗМЕТОК
Существует большое число различных видов разметок приложений
для android, позволяющих построить пользовательский интерфейс наибо-
лее близко соответствующим требованиям заказчика. Все разметки явля-
ются наследниками класса ViewGroup. Android SDK предоставляет ряд
стандартных видов разметок, а также полезных в разработке элементов.
LinearLayout – линейная разметка, при использовании которой вид-
жеты располагаются друг за другом по вертикали или горизонтали как по-
казано на рис. 1.
<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>
<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>
<OnSwipe
app:touchAnchorId="@+id/view"
app:dragDirection="dragRight"
app:touchAnchorSide="right"
/>
</Transition>
</MotionScene>
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
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 и некоторые другие.
Контрольные вопросы
4. MATERIAL DESIGN
21
ной составляющей, что позволяет акцентировать внимание на значимых
элементах интерфейса.
Для использования элементов Material Design в проекте может потре-
боваться добавить библиотеку com.google.android.material.
Для этого в список зависимостей dependencies (файл build.gradle) по-
требуется добавить строку
implementation 'com.google.android.material:material:x.y.z'
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>
25
import android.widget.Toast;
import androidx.appcompat.app.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());
}
}
Контрольные вопросы
1. Создайте поле ввода паролей без фона с подсказкой «password».
2. Для чего используется размещение виджетов с различными пара-
метрами отображения тени?
3. Для чего используется метод findViewById?
4. Для чего используется класс Toast?
5. Создайте кнопку, по клику на нее выводите всплывающее сообще-
ние с текущим временем.
26
5. НАМЕРЕНИЯ. ОРГАНИЗАЦИЯ ПЕРЕХОДОВ
МЕЖДУ АКТИВНОСТЯМИ
startActivity(i);
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
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))
@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
6. ФРАГМЕНТЫ
Существует большое число ситуаций, когда требуется динамически
изменять содержимое активности в зависимости от различных факторов,
таких как команды пользователя, конфигурация устройства и т.д. Для ре-
шения данных задач возможно использование фрагментов.
Фрагменты – части пользовательского интерфейса, которые могут
использоваться многократно [5]. Фрагменты оперируют собственными
разметками, обладают собственным жизненным циклом и собственными
обработчиками событий. Отличительной особенностью макетов является то,
что они не могут существовать вне активности и при добавлении в актив-
ность включаются в ее иерархию элементов как дочерние компоненты.
Структурно фрагмент аналогичен активности и состоит из разметки и
класса фрагмента. Разметка фрагмента ничем не отличается от разметки
активности. При размещении же фрагмента на активности его вид может
претерпеть изменения в соответствии с ограничениями, накладываемыми
активностью (рис. 14).
.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)
}
}
}
}
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 BlankFragment() {
}
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
companion object {
@JvmStatic
fun newInstance(param1: String, param2: String) =
BlankFragment().apply {
arguments = Bundle().apply {
putString(ARG_PARAM1, param1)
putString(ARG_PARAM2, param2)
}
}
}
}
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() }
@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)
}
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()})
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()
})
Контрольные вопросы
//Kotlin
val v = Runnable { string="From Thread"; Log.d("Thread",string) }
v.run()
//Kotlin
thread { Log.d("Thread", "From Thread") }.start()
//либо
val t = Thread{Log.d("Thread", "From Thread")}
t.start()
45
В случае же создания класса-наследника Thread потребуется реализо-
вать метод run:
@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")
}
}
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
@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!"
}
}
}
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") }
}
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
}
}
Контрольные вопросы
@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()
}
}
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")
}
@Override
protected void onCancelled() {
super.onCancelled();
b.status.setText("CANCELLED");
}
//Kotlin
override fun onCancelled(s: String) {
b.status.setText("CANCELLED WITH MESSAGE: $s")
}
//Kotlin
protected override fun onProgressUpdate(vararg values: Int?) {
super.onProgressUpdate(*values)
b.progress.setText(values[0].toString())
}
//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()
}
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()
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-
core:x.y.z'
Для запуска сопрограммы потребуется использование метода launch:
GlobalScope.launch {
foo()
}
58
В случае же, когда нужно, чтобы сопрограммы выполнялись асин-
хронно и возвращали результаты вычислений, вместо launch возможно
использование async:
Контрольные вопросы
SharedPreferences s = getPreferences(MODE_PRIVATE);
//Kotlin
val s = getPreferences(MODE_PRIVATE)
Для того, чтобы получить все элементы, используется метод all, ко-
торый вернет карту из SharedPreference:
60
Добавление элементов реализуется с помощью группы методов put:
e.apply//аналогично Kotlin
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
@Override
public int getInt(String key, int defValue) {
return super.getInt(key, defValue);
}
}
//аналогично Kotlin
Контрольные вопросы
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"
}
companion object {
const val SQL_DELETE_ACCOUNT_ROLES = "DROP TABLE IF EXISTS
${AccountRoles.TABLE_NAME}"
sqLiteDatabase.execSQL(DatabaseContract.SQL_CREATE_ACCOUNTS)
sqLiteDatabase.execSQL(DatabaseContract.SQL_CREATE_ROLES)
sqLiteDatabase.execSQL(DatabaseContract.SQL_CREATE_ACCOUNT_ROLES)
}
sqLiteDatabase.execSQL(DatabaseContract.SQL_DELETE_ACCOUNTS)
sqLiteDatabase.execSQL(DatabaseContract.SQL_DELETE_ROLES)
sqLiteDatabase.execSQL(DatabaseContract.SQL_DELETE_ACCOUNT_ROLES)
onCreate(sqLiteDatabase)
}
companion object {
const val DB_VERSION = 1
const val DB_NAME = "SampleStore.db"
}
}
//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)
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)
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(),
что позволит каждому отдельному процессу создать собственный объект
этого класса и распространять информацию об изменении БД между ними.
73
//Применяется паттерн Builder
instance = Room
.databaseBuilder(context,MyDatabase::class.java, "my_database")
.build()
}
}
return instance
}
}
}
Контрольные вопросы
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
75
Второй способ создания Drawable заключается в определении XML-
ресурсов. Данный способ предпочтительным в ситуациях, когда не ожида-
ется изменения свойств графического объекта при его взаимодействии с
пользователем, но даже в ряде таких случаев использование XML-
ресурсов может оказаться оправданным. Объекты любых подклассов
Drawable, для которых возможен вызов метода inflate, могут быть
определены в XML и созданы в приложении.
Drawable расширяется рядом наследников с дополнительным функ-
ционалом, позволяющим добиться более эффективной работы.
Класс NinePathDrawable позволяет создавать растягиваемые изо-
бражения, что может оказаться полезным при их использовании в качест-
ве фоновых рисунков. Такие изображения должны храниться в файлах
формата .9.png. NinePath-изображения фактически представляют со-
бой стандартные изображения формата PNG с дополнительной границей в
1px. Изображения делятся на 9 областей, которые образуются путем соз-
дания по двум смежным границам изображения отрезков, которое про-
ецируют растягивающиеся области. Схема таких изображений показана на
рис. 17.
При использовании данного вида изображений в качестве фоновых,
они будут автоматически приведены к размеру виджета, в котором
они размещены.
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)
}
}
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);
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(new DrawView(this));
}
class DrawView extends SurfaceView implements
SurfaceHolder.Callback {
MyThread t;
@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;
@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;
}
}
setContentView(binding.root)
binding.surfaceView.holder.addCallback(this)
binding.surfaceView.setOnTouchListener { _, event ->
thread.setCoord(event.x, event.y)
super.onTouchEvent(event)
}
}
init {
running = false
x = drawView.getWidth() / 2
y = drawView.getHeight() / 2
r = 1
}
}
Контрольные вопросы
<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'
87
public String getTitle() {
return title;
}
@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();
}
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();
}
});
});
}
}
Контрольные вопросы
@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;
}
}
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 следующим образом:
@Override
public void onDestroy() {
super.onDestroy();
Log.d("SERVICE", "DONE");
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return binder;
}
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;
}
};
}
98
сервисом, а onServiceDisconnected() – в случае потери соединения с
сервисом. Последние два метода являются обязательными к реализации.
Для установления связи с сервисом используется метод bindService(),
принимающий в качестве аргументов намерение на работу с сервисом,
объект serviceConnection, а также различные флаги привязки.
Для описания связи сервисов и компонентов могут использоваться
объекты Messenger и язык определения интерфейсов Android (Android
Interface Language Definition, AIDL). Сервисы могут использовать меха-
низм уведомлений для связи с пользователем. Существует множество спо-
собов применений сервисов, которые требуют отдельного подробно-
го изучения.
Контрольные вопросы
99
Начиная с Android 8, для отображения уведомлений требуется созда-
ние каналов, в рамках которых будет проводиться работа с ними.
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);
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());
}
}
}
.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);
104
Существует большое число применений уведомлений, основанных на
рассмотренных в данном разделе. Каждое из них может потребовать изу-
чения дополнительного материала.
Контрольные вопросы
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()));
106
ных датчика, включая: точность данных, датчик, который сгенерировал
данные, временную метку, в которой были сгенерированы данные, и но-
вые данные, записанные датчиком;
− onAccuracyChanged() – метод обработки точности измерения.
Точность описывается одной из четырех констант:
SENSOR_STATUS_ACCURACY_LOW
SENSOR_STATUS_ACCURACY_MEDIUM
SENSOR_STATUS_ACCURACY_HIGH
SENSOR_STATUS_UNRELIABLE.
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);
}
}
108
Контрольные вопросы
109
ЗАКЛЮЧЕНИЕ
Понимание основополагающих принципов программирования наряду с
современными возможностями языков разработки приложений делают
умения и навыки студентов актуальными для ведения практической дея-
тельности в области реализации и верификации программного обеспече-
ния. Знание основ программирования необходимо как для разработчиков,
так и для специалистов смежных специальностей, таких как тестировщики
программного обеспечения, специалисты в области компьютерной безо-
пасности, инженеры информационных систем, разработчики баз данных и
многие другие.
Язык программирования Java, являясь современным средством разра-
ботки и де-факто промышленным стандартом, предоставляет широкий на-
бор средств для решения разнообразных задач, что делает владеющих им
специалистов востребованными на рынке труда. Знание основных прин-
ципов разработки приложений необходимо как для разработчиков непо-
средственно на Java, так и для тех, кто применяет языки программирова-
ния, использующие платформу Java, такие как Kotlin, Scala, Groovy и дру-
гие, применяемые в web-разработке, обработке больших данных, реализа-
ции мобильных приложений и многих других областях.
Изучение материалов пособия позволит освоить необходимый мини-
мальный уровень владения языком для дальнейшего совершенствования
навыков решения профессиональных задач, требующих углубленных зна-
ний как отдельных возможностей Java, так и предметной области.
Понимание аспектов разработки мобильных приложений делают уме-
ния и навыки студентов актуальными для ведения практической деятель-
ности в области реализации и верификации программного обеспечения.
Эти знания необходимы как для разработчиков, так и для специалистов
смежных специальностей, таких как тестировщики программного обеспе-
чения, специалисты в области компьютерной безопасности, инженеры
информационных систем и многие другие.
Операционная система Android, являясь доминирующей на рынке мо-
бильных устройств, предоставляет широкий набор средств для решения
разнообразных задач, что делает владеющих им специалистов востребо-
ванными на рынке труда. Знание принципов разработки мобильных при-
ложений необходимо как для профильных специалистов, так и для занятых
в смежных областях, так как web-разработка, тестирование и отладка про-
граммного обеспечения, информационная безопасность и многие другие.
Изучение материалов пособия позволит освоить необходимый мини-
мальный уровень владения языком для дальнейшего совершенствования
навыков решения профессиональных задач, требующих углубленных зна-
ний как отдельных возможностей ОС Android, так и предметной области.
110
БИБЛИОГРАФИЧЕСКИЙ СПИСОК
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
Учебное издание
Учебное пособие
112