Программирование мобильных устройств. Ч.1. Учебно-методическое пособие
.pdf
При этом инициализатор property_initializer, геттер и сеттер можно не указывать. Также необязательно указывать тип свойства, если он может быть выведен из инициализатора или из возвращаемого типа геттера.
Вы можете самостоятельно определить методы доступа для свойства. Если вы определяете пользовательский геттер, он будет вызываться каждый раз, когда вы обращаетесь к свойству.
class Rectangle(val width: Int, val height: Int) { val area: Int
get() = this.width * this.height
}
Если вы определяете пользовательский сеттер, он будет вызываться каждый раз, когда вы присваиваете значение свойству, за исключением его инициализации.
var stringRepresentation: String get() = this.toString()
set(value) { setDataFromString(value)
}
Если значение константного (read-only) свойства известно во время компиляции, его необходимо пометить ключевым словом const. Такие свойства должны соответствовать следующим требованиям и могут быть использованы в аннотациях:
-Находиться на самом высоком уровне или быть членами объявления object или вспомогательного объекта;
-Быть проинициализированными значением типа String или значением примитивного типа;
-Не иметь переопределённого геттера.
const val SUBSYSTEM_DEPRECATED: String = "This subsystem is deprecated"
@Deprecated(SUBSYSTEM_DEPRECATED) fun foo() { ... }
Обычно свойства, объявленные non-null типом, должны быть проинициализированы в конструкторе. Однако часто бывает так, что делать это неудобно. К примеру, свойства могут быть инициализированы через внедрение зависимостей или в установочном методе (ориг.: setup method) юнит-теста. В таком случае невозможно обеспечить non-null инициализацию в конструкторе, но всё равно необходимо избежать проверок на null при обращении внутри тела класса к такому свойству. Для того чтобы справиться с такой задачей, можно пометить свойство модификатором lateinit.
public class MyTest {
lateinit var subject: TestSubject
21
@SetUp fun setup() { subject = TestSubject()
}
@Test fun test() { subject.method()
}
}
Чтобы проверить, было ли проинициализировано lateinit var свойство, используйте .isInitialized метод ссылки на это свойство.
if (foo::bar.isInitialized) { println(foo.bar)
}
4.1.4. Итераторы
Для обхода элементов коллекции стандартная библиотека Kotlin поддерживает механизм итераторов – объектов, которые предоставляют доступ к элементам последовательно, не раскрывая базовую структуру коллекции. Итераторы полезны, когда необходимо обработать все элементы коллекции один за другим, например, вывести в лог их значения или обновить.
Итераторы доступны всем наследникам интерфейса Iterable<T>, включая Set и List, путём вызова функции iterator(). Итератор указывает на первый элемент коллекции, вызов функции next() возвращает этот элемент и перемещает позицию итератора на следующий элемент, если такой существует. Как только итератор проходит через последний элемент, его больше нельзя использовать для извлечения элементов, его также нельзя вернуть в предыдущее положение. Чтобы снова перебрать коллекцию, нужно создать новый итератор.
fun main() {
val numbers = listOf("one", "two", "three", "four") val numbersIterator = numbers.iterator()
while (numbersIterator.hasNext()) { println(numbersIterator.next())
}
}
Итераторы предоставляют доступ к элементам коллекции последовательно.
Также для итерации можно использовать стандартный цикл for.
fun main() {
val numbers = listOf("one", "two", "three", "four") for (item in numbers) {
println(item)
22
}
}
Также есть полезная функция forEach(), которая позволяет автоматически перебирать коллекцию и выполнять заданный код для каждого элемента.
fun main() {
val numbers = listOf("one", "two", "three", "four")
numbers.forEach { println(it)
}
}
Для списков существует специальная реализация итератора: ListIterator. Он поддерживает итерацию списков в обоих направлениях: вперёд и назад. Обратная итерация реализуется функциями hasPrevious() и previous(). Кроме того, ListIterator предоставляет информацию об индексах элементов с помощью функций nextIndex() и previousIndex().
fun main() {
val numbers = listOf("one", "two", "three", "four") val listIterator = numbers.listIterator()
while (listIterator.hasNext()) listIterator.next()
println("Iterating backwards:")
while (listIterator.hasPrevious()) {
print("Index: ${listIterator.previousIndex()}") println(", value: ${listIterator.previous()}")
}
}
Для итерации изменяемых коллекций существует MutableIterator, который расширяет Iterator с помощью функции удаления элементов remove(), что позволяет удалять элементы из коллекции во время итерации.
fun main() {
val numbers = mutableListOf("one", "two", "three", "four") val mutableIterator = numbers.iterator()
mutableIterator.next()
mutableIterator.remove()
println("After removal: $numbers") // [two, three, four]
}
23
Помимо удаления элементов, MutableListIterator позволяет добавлять и заменять элементы во время итерации.
fun main() {
val numbers = mutableListOf("one", "four", "four") val mutableListIterator = numbers.listIterator()
mutableListIterator.next()
mutableListIterator.add("two")
mutableListIterator.next()
mutableListIterator.set("three")
println(numbers) // [one, two, three, four]
}
Итераторы в Kotlin обеспечивают эффективный способ обхода коллекций и предоставляют различные возможности для обработки элементов вперед и назад. MutableIterator позволяет изменять коллекцию во время итерации.
24
5. Создание Игры «Жуки»
Целью занятия является реализация приложения «Жуки»: Жуки бегают по экрану. Игроку предлагается при помощи touchScreen-a уничтожить как можно большее число жуков. Обработка отдельного жука должна производиться в отдельном потоке. За каждый промах игроку начисляется штраф. Предусмотреть несколько видов насекомых. Попадание и промах должны иметь звуковое сопровождение. По окончании игры выводятся результаты.
Создайте новый проект в Android Studio с использованием Kotlin. Откройте макет (layout) для вашей игры. Создайте класс для представления объекта "Жук". Он может содержать координаты, тип жука и другие свойства. Реализуйте обработку событий нажатия на экран для уничтожения жуков. При помощи потоков обеспечьте движение жуков по экрану.
Один из способов обработки нажатия:
override fun onTouchEvent(event: MotionEvent): Boolean {
// Обработка нажатия
}
ВAndroid, как и в общем программировании, потоки представляют собой независимые последовательности исполнения кода. Однако, в Android особое внимание уделяется управлению потоками, так как некорректное использование потоков может привести к блокировке пользовательского интерфейса (UI) и другим проблемам. Вам может потребоваться выполнять фоновые задачи, такие как движение объектов в игре, в отдельных потоках, чтобы не замедлять основной поток UI.
ВKotlin, создание потока может быть выполнено несколькими способами.
Вданном контексте, мы рассмотрим создание потока для управления движением жуков в игре. Ваш класс `BugThread` может выглядеть следующим образом:
class BugThread : Thread() { private var isRunning = false override fun run() {
isRunning = true while (isRunning) {
//Логика движения жуков
//Обновление координат и отрисовка
sleep(16) // Задержка для контроля частоты обновления
}
}
fun stopThread() { isRunning = false
}
}
Важно управлять потоками, чтобы избежать утечек ресурсов и некорректного завершения. В приведенном выше примере метод `stopThread` используется для завершения потока. Он устанавливает флаг `isRunning` в `false`, что приводит к завершению цикла внутри метода `run`.
25
Важные моменты работы с потоками:
1.UI-поток (MainThread) – все взаимодействия с элементами пользовательского интерфейса (UI) должны выполняться в основном потоке (UIпотоке). Для обновления UI из фонового потока используйте `runOnUiThread`
или `Handler`.
2.Асинхронные задачи – для выполнения длительных операций в фоне, используйте асинхронные задачи, такие как `AsyncTask` или `Coroutine` в Kotlin.
3.Обработка событий – обработка событий, таких как нажатие на экран, должна выполняться в основном потоке. Вам нужно использовать механизмы обработки событий, такие как `onTouchEvent`, чтобы взаимодействовать с UI.
4.Синхронизация доступа к данным – в многопоточной среде, где несколько потоков могут обращаться к общим данным, важно правильно управлять доступом к этим данным. Можно использовать механизмы синхронизации, такие как `synchronized` или `Mutex` в Kotlin.
Пример использования потока для управления движением жуков внутри программы:
class GameActivity : AppCompatActivity() { private lateinit var bugThread: BugThread
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_game)
// Создание и запуск потока при создании активности bugThread = BugThread()
bugThread.start()
}
override fun onDestroy() { super.onDestroy()
// Остановка потока при уничтожении активности bugThread.stopThread()
}
}
Реализуйте механизм подсчета штрафов при промахах игрока.
var штрафы = 0
// В случае промаха штрафы++
Добавьте несколько видов жуков с разными свойствами (например, скорость) и используйте звуки при попадании и промахе.
val звукПопадания = MediaPlayer.create(this, R.raw.звук_попадания) val звукПромаха = MediaPlayer.create(this, R.raw.звук_промаха)
//В случае попадания звукПопадания.start()
//В случае промаха звукПромаха.start()
26
По окончании игры выводите результаты, такие как количество уничтоженных жуков, штрафы и другую статистику.
5.1.Дополнительные теоретические сведения
Впроцессе выполнения лабораторной работы будут полезные некоторые теоретические сведения, представленные ниже.
5.1.1. Звук в Android-приложениях
Звук является важным элементом игрового процесса, который может значительно улучшить пользовательский опыт. В разработке Androidприложений для работы со звуком используется класс `SoundPool`, который позволяет воспроизводить короткие звуковые эффекты.
SoundPool является инструментом для воспроизведения нескольких коротких звуковых файлов одновременно. Этот класс идеально подходит для игровых приложений, где часто требуется воспроизведение звуковых эффектов, таких как выстрелы, взрывы или звуки движения.
Для того чтобы начать работу с «SoundPool», необходимо выполнить следующие шаги:
1. Инициализация
Необходимо создать экземпляр класса «SoundPool», указав максимальное количество одновременно воспроизводимых звуков, тип аудиопотока и качество. Например:
val sounds = SoundPool.Builder()
.setMaxStreams(10)
.setAudioAttributes(AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.setUsage(AudioAttributes.USAGE_GAME)
.build())
.build()
2. Загрузка звуковых файлов
Звуковые файлы загружаются в память устройства с помощью метода «load», который возвращает идентификатор загруженного файла. Например:
val sExplosion = sounds.load(context, R.raw.explosion, 1)
3. Проигрывание звуковых эффектов:
После загрузки звуковых файлов можно начать их воспроизведение с помощью метода «play». Этот метод принимает идентификатор звукового файла, параметры громкости, баланса, приоритета, повторения и скорости воспроизведения. Например:
sounds.play(sExplosion, 1.0f, 1.0f, 0, 0, 1.5f)
27
Таким образом, с помощью класса «SoundPool» вы можете интегрировать звуковые эффекты в ваши Android-игры, тем самым улучшая визуальный и звуковой опыт пользователей.
5.1.2. Многопоточное программирование: класс Thread и интерфейс
Runnable
Многопоточное программирование в Kotlin предоставляет возможность параллельного выполнения задач, что может повысить производительность и эффективность приложений. Для создания и управления потоками есть два основных способа: с использованием класса «Thread» и с помощью интерфейса
«Runnable».
Класс «Thread» представляет собой поток исполнения. Он может быть создан путем наследования от класса «Thread» или путем реализации интерфейса «Runnable» и передачи экземпляра этого интерфейса в конструктор «Thread».
Опишем ниже несколько ключевых моментов, касающихся класса
«Thread».
Создание потока с использованием наследования:
class MyThread : Thread() { override fun run() {
// Код, который будет выполнен в потоке
}
}
Создание потока с использованием интерфейса «Runnable»:
class MyRunnable : Runnable { override fun run() {
// Код, который будет выполнен в потоке
}
}
val myThread = Thread(MyRunnable())
Запуск потока:
myThread.start()
В методе «run()» содержится код, который будет выполняться в потоке. Он должен быть переопределен в подклассе «Thread» или реализован в классе, который реализует интерфейс «Runnable».
Поток может находиться в различных состояниях, таких как `NEW`, `RUNNABLE`, `BLOCKED`, `WAITING`, `TIMED_WAITING` и `TERMINATED`. Управление состоянием потока происходит автоматически. Переход из одного состояния в другое называют жизненным циклом потока.
28
Интерфейс «Runnable» предоставляет более гибкий способ создания потоков, чем наследование от класса «Thread». Это позволяет классам реализовывать несколько интерфейсов и расширять другие классы при необходимости.
class MyRunnable : Runnable { override fun run() {
// Код, который будет выполнен в потоке
}
}
val myThread = Thread(MyRunnable())
Интерфейс «Runnable» имеет только один абстрактный метод «run()», который должен быть реализован классом, использующим этот интерфейс. В этом методе содержится код, который будет выполняться в потоке.
Преимущества интерфейса заключаются в гибкости подхода по созданию и использованию потоков, возможности классам реализовывать другие интерфейсы или расширять другие классы, а также в повышении читаемости кода и уменьшении связности.
К недостаткам можно отнести недоступность некоторых методов класса «Thread» при использовании интерфейса «Runnable», а также необходимость (в некоторых случаях) дополнительного управления состоянием потока вручную.
Многопоточное программирование с использованием класса «Thread» или интерфейса «Runnable» позволяет эффективно использовать ресурсы компьютера и создавать более отзывчивые и масштабируемые приложения.
5.1.3. Параллелизм
Процесс в Android представляет собой систему действий, выполняющую определенную функцию в вычислительной системе. Каждое приложение Android работает в своем собственном процессе.
Поток представляет собой базовую единицу выполнения в приложении Android. Операционная система выделяет время процессора для каждого потока, позволяя им выполняться параллельно. Потоки позволяют приложению обрабатывать несколько задач одновременно.
Асинхронная операция – это операция, которая может выполняться независимо от основного потока выполнения. В Android асинхронные операции часто используются для выполнения длительных операций, таких как загрузка данных из Интернета или обработка файлов.
Функции обратного вызова (Callback) используются для определения блоков кода, которые будут вызваны в ответ на определенные события или завершение асинхронных операций.
Корутины (Coroutines) представляют собой легковесные потоки выполнения, которые позволяют эффективно управлять асинхронными операциями и обрабатывать их последовательно.
29
CoroutineScope связывает корутины с жизненным циклом Androidприложения и предоставляет средства для создания, запуска и отмены корутин.
Dispatcher: управляет выполнением корутин и определяет, на каком потоке они будут выполняться. Например, Dispatcher.Main используется для выполнения корутин в главном (UI) потоке.
Job представляет собой задачу, которая может быть выполнена корутиной. У корутины может быть родительская задача (Parent Job) и дочерние задачи (Child Job), образуя иерархию выполнения корутин.
//Запуск корутины с использованием CoroutineScope val job = CoroutineScope(Dispatchers.IO).launch {
//Код корутины, выполняющийся в фоновом потоке
}
//Создание асинхронной операции с использованием async val deferred = CoroutineScope(Dispatchers.Default).async {
//Код асинхронной операции, выполняющийся в фоновом потоке
}
//Выполнение блокирующей операции с использованием runBlocking runBlocking {
//Код блокирующей операции, выполняющийся в основном потоке
}
Использование корутин позволяет эффективно управлять асинхронными операциями в Android-приложениях, обеспечивая параллельное выполнение задач и предотвращая блокировки UI. Корутины представляют собой мощный инструмент для реализации параллелизма и асинхронности в приложениях.
5.1.4. Пример использования Mutex и Semaphore для управления потоками
Mutex (Mutual Exclusion) – это синхронизационный механизм, который позволяет только одному потоку одновременно получать доступ к ресурсу. Он обеспечивает взаимное исключение для предотвращения конфликтов доступа к общему ресурсу. В Kotlin для реализации Mutex можно использовать мониторы объектов или классы из пакета `kotlinx.coroutines.sync`.
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.runBlocking
class MutexExample {
private val mutex = Mutex()
suspend fun accessResource() {
30
