Освой Android играючи

Сайт Александра Климова

Шкодим

/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000

Hello Kitty - первое приложение для Android (Compose)

Cat gives five

Здороваемся с вашим котом

После установки Android Studio (далее Студия) можно приступать к созданию своей первой программы.

Здесь мы рассмотрим пример на Compose, старый классический вариант доступен отдельно. Под классическим вариантом я подразумеваю старый способ написания приложений, который существовал с момента появления Android, когда пользовательский интерфейс создавался при помощи XML-файлов. Но сейчас все дружно стали писать по-новому, поэтому и приходится заново писать эту статью. Тем не менее стоит прочитать ту статью, так как часть полезной информации осталась там, я не стал повторяться.

В качестве языка программирования для Android используется Kotlin. Для классических приложений также можно использовать и Java, но в последние годы это удел одиночек.

По традиции, заложенной в прошлом веке, каждый программист должен был написать «Hello World!» (Здравствуй, Мир!) в качестве первой программы. Времена меняются, и программа «Hello World!» была встроена в среду разработки под Android в целях совместимости при разработке классических приложений. Гугл в 2021 году осмелилась нарушить традицию и изменить привычное выражение и стала использовать "Hello Android". Но компания отстала от меня на 10 лет, я начал выводить "Hello Kitty" в своих уроках гораздо раньше.

В Compose также здороваются с Android. Но уважающие себя программисты должны писать программу Hello Kitty! (Привет, киска!). Согласитесь, что здороваться с котёнком имеет больше здравого смысла, чем с андроидом (железякой).

Работа функции Greeting понятна - выводит текст "Hello Android". Вообще-то положено выводить текст "Hello World", но Гугл в 2021 году осмелилась нарушить традицию и изменить привычное выражение. Но компания отстала от меня на 10 лет, я начал выводить "Hello Kitty" гораздо раньше. Вот и в первом нашем проекте я также внёс единственное изменение.

Разобьём задачу на две части. Сначала запустим готовую программу Hello Androd! без написания кода, чтобы убедиться, что весь инструментарий корректно установился, и мы можем создавать и отлаживать программы. А потом уже напишем свою первую программу.

Создание нового проекта

Запускаем Студию и выбираем File | New | New Project.... Появится диалоговое окно мастера.

New Project

Окно имеет несколько разделов. В основном, мы будем использовать раздел Phone and Tablet.

Для compose-приложений имеется только один шаблон Empty Activity. Остальные относятся к классическим вариантам.

Empty Activity

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

Configure your project

Поле Name - понятное имя для приложения, которое будет отображаться в заголовке приложения. По умолчанию у вас уже может быть My Application. Заменим на Hello World. В принципе вы могли написать здесь и Здравствуй, мир!, но у Android есть замечательная возможность выводить нужные строки на телефонах с разными языками. Скажем, у американца на телефоне появится надпись на английском, а у русского - на русском. Поэтому в первоначальных настройках всегда используются английские варианты, а локализованные строки подготовите позже. Необходимо сразу вырабатывать привычку к правильному коду.

Поле Package name формирует специальный Java-пакет. В Java используется перевёрнутый вариант для наименования пакетов, поэтому сначала идёт ru, а потом уже название сайта. Пакет служит для уникальной идентификации вашего приложения, когда вы будете его распространять. Если сто человек напишет сто приложений с названием "Cat", то будет непонятно, где приложение, написанное разработчиком Василием Котовым. А приложение с именем пакета ru.vaskakotov.cat проще найти. Обратите внимание, что Гугл в своей документации использует пакет com.example в демонстрационных целях. Если вы будете просто копировать примеры из документации и в таком виде попытаетесь выложить в Google Play, то у вас ничего не выйдет - это название зарезервировано и запрещено к использованию в магазине приложений.

Третье поле Save location позволяет выбрать место на диске для создаваемого проекта. Вы можете создать на своём диске отдельную папку для своих проектов и хранить свои программы в ней. Студия запоминает последнюю папку и будет автоматически предлагать сохранение в ней. В случае необходимости вы можете задать другое местоположение для отдельного проекта через кнопку с значком папки.

В блоке Minimum SDK выбираем минимальную версию системы, под которую будет работать приложение. У меня в наличии телефон с минимальной версией Android 6.0, поэтому я выставляю этот вариант.

Если щёлкнуть по ссылке Help me choose, то откроется окно с графиком. Если вам интересно, можете посмотреть, но котиков там нет.

Поле Build configuration language - оставляем рекомендованный вариант Kotlin DSL (build.gradle.kts) [Recommended].

Мы закончили с первоначальной настройкой. Нажимаем кнопку Finish и смотрим, что дальше будет.

Содержание проекта

А дальше студия формирует проект и создаёт необходимую структуру из различных файлов и папок. Поначалу глаза разбегаются. Давайте разбираться.

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

Вкладка Android содержит две основные папки: app и Gradle Scripts. Первая папка app является отдельным модулем для приложения и содержит все необходимые файлы приложения - код, ресурсы картинок и т.п. Вторая папка служит для различных настроек, управления проектом и многих других вещей.

Вскользь рассмотрим один файл из папки Gradle Scripts. Разверните папку и откройте двойным щелчком файл build.gradle (Module :app).

В build.gradle прописано следующее (другие настройки пока не рассматриваем).


android {
    buildFeatures {
        compose true
    }
    ...
}

Эта запись показывает, что мы используем Compose.

Сейчас нас должна интересовать папка app. Раскройте её. В ней находятся папки: manifests, kotlin+java, res.

О назначении папок и файлов рассказано в статье с классическим вариантом создания приложения. Повторяться не будем.

Работа с проектом - Здравствуй, Мир!

Как уже говорилось, программа Hello Android! уже встроена в новый compose-проект, поэтому вам даже не нужно ничего писать. Просто нужно запустить проект и получить готовую программу!

Для изучения вам нужно файл - MainActivity.kt (скорее всего он уже открыт).

Вы можете увидеть приблизительный внешний вид приложения, не запуская его! В правом верхнем углу найдите три кнопки и выберите среднюю кнопку Split.

Split

Студия позволяет увидеть будущий вид экрана через аннотацию @Preview. Аннотация @Preview должна находиться до аннотации @Composable. Но есть небольшое ограничение у аннотации - составная функция не должна содержать параметры. По этой причине мы не сможем увидеть предыдущий пример, где функция использует один параметр. Пойдём на хитрость и вызовем функцию в другой функции.

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

Не будем пока изучать код, а просто нажмём на зелёный треугольник Run (Shift+F10) на панели инструментов в верхней части студии для запуска приложения.

Если вы не настроили эмулятор, значит вы не читали предыдущий урок. Настройте сначала эмулятор и запускайте проект снова. Либо подключайте реальное устройство.

Если всё сделали правильно, то в эмуляторе или на устройстве загрузится ваша программа. Поздравляю!

Итак, если программа запустилась, то увидите окно приложения с надписью Hello Android!.

Hello Android

Теперь посмотрим на код файла MainActivity.kt. Я приведу два варианта - старый, который существовал с самого начала и новый, который появился в версии студии Jellyfish.


// Старый вариант
package ru.alexanderklimov.helloworld

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import ru.alexanderklimov.helloworld.ui.theme.HelloWorldTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            HelloWorldTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    Greeting("Android")
                }
            }
        }
    }
}

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Text(
        text = "Hello $name!",
        modifier = modifier
    )
}

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    HelloWorldTheme {
        Greeting("Android")
    }
}

// Новый вариант package ru.alexanderklimov.jellycompose import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import ru.alexanderklimov.jellycompose.ui.theme.JellyComposeTheme class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { JellyComposeTheme { Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> Greeting( name = "Android", modifier = Modifier.padding(innerPadding) ) } } } } } @Composable fun Greeting(name: String, modifier: Modifier = Modifier) { Text( text = "Hello $name!", modifier = modifier ) } @Preview(showBackground = true) @Composable fun GreetingPreview() { JellyComposeTheme { Greeting("Android") } }

Разница фактически в одном месте - вместо Surface стало использоваться Scaffold. Сейчас это совсем не важно. А также добавили вызов enableEdgeToEdge() для поддержки управления жестами.

В первой строке идёт название пакета - его мы задавали при создании проекта (Package Name). Далее идут строки импорта необходимых классов для проекта. Для экономии места они свёрнуты в одну группу. Разверните её. Если однажды вы увидите, что имена классов выводятся серым цветом, значит они не используются в проекте (подсказка Unused import statement) и можете спокойно удалить лишние строки. Также они могут удаляться автоматически (настраивается).

Далее идёт объявление самого класса, который наследуется (двоеточие :) от абстрактного класса ComponentActivity. Это базовый класс для всех экранов приложения на Compose.

В самом классе мы видим метод onCreate() – он вызывается, когда приложение создаёт и отображает разметку активности. Метод помечен ключевым словом override (переопределён из базового класса). Ключевое слово может пригодиться вам. Если вы сделаете опечатку в имени метода, то компилятор сможет предупредить вас, сообщив об отсутствии такого метода у родительского класса ComponentActivity.

Строка super.onCreate(savedInstanceState) – это конструктор родительского класса, выполняющий необходимые операции для работы активности. Эту строчку вам не придётся трогать, оставляйте без изменений.

Вторая строчка setContent{} представляет больший интерес. Функция setContent() подключает другие функции, которые отвечают за внешний вид приложения. В частности, функция HelloWorldTheme() подключает определённую тему.

По поводу HelloWorldTheme - она задаёт тему, а её имя создаётся на основе имени вашего проекта. Если посмотрите на структуру проекта, то увидите, что кроме файла MainActivity.kt есть подпапка ui.theme с набором файлов, в том числе и Theme.kt. В нём можно найти описание используемой темы на основе MaterialTheme.

Функция Surface (старый вариант) отвечает за фон приложения. По умолчанию это белый цвет (в том же файле Theme.kt есть сноска на этот счёт.

А нас сейчас интересует функция Greeting() с аргументом Android. Нетрудно предположить, что если заменить это слово на другое, то получим приложение уже с изменённым текстом. Сама функция Greeting описана ниже. Обратите внимание, на аннотацию @Composable, функции с такой аннотацией можно вызывать только в составе других composable-функций или в функции setContent() (как в нашем примере). Подобные функции принято начинать с заглавной буквы. У функции два параметра - name и modifier.

Строковый параметр name добавляет имя к слову Hello. Второй параметр имеет тип по умолчанию Modifier, поэтому при вызове функции мы можем его опустить. Что и было сделано в нашем примере. О модификаторе поговорим позже. А пока просто порадуемся, что у нас получилось.

Hello Kitty!

Вы создали новую программу, но это ещё не повод считать себя программистом, так как вы не написали не единой строчки кода. Настало время набраться смелости и создать программу "Hello Kitty!".

Создаём новый проект. Снова выбираем шаблон Empty Activity и устанавливаем нужные настройки. Либо вы можете продолжить работу на старом проекте.

Мы уже знаем, что если заменить Android на Kitty, то получим нужный результат.


class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            HelloWorldTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    Greeting("Kitty")
                }
            }
        }
    }
}

Для предварительного просмотра внешнего вида приложения используется аннотация @Preview. Чтобы увидеть изменения в режиме "Design", нужно также поменять код и в функции GreetingPreview.


@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    HelloWorldTheme {
        Greeting("Kitty")
    }
}

Но это слишком просто. Давайте немного усложним пример. Для начала поменяем фон у экрана активности. Сейчас в функции Greeting находится всего один компонент Text. В реальности, у вас будет несколько компонентов, которые нужно размещать на экране определённым образом внутри какого-нибудь контейнера. Поместите курсор внутри строки кода со словом Text и нажмите комбинацию клавиш Alt+Enter.

Комбинация клавиш

В контекстном меню выберите последовательно Surround with widget | Surround with Column. Это самый популярный контейнер, который мы будем часто использовать дальше при изучении Compose.


@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Column {
        Text(
            text = "Hello $name!",
            modifier = modifier
        )
    }
}

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


Column(
    modifier = modifier
        .background(Color(0xFFEFB8C8))
) {
    Text(
        text = "Hello $name!",
        modifier = modifier
    )
}

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

Запустите пример. Вы увидите, что экран активности окрасится в розовый цвет. Получилось глаМУРненько.

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


Column(
    modifier = modifier
        .background(Color(0xFFEFB8C8)),
    horizontalAlignment = Alignment.CenterHorizontally
) {
    ...
}

Текст мелковат? У Text есть параметр fontSize, воспользуемся им.


Text(
    text = "Hello $name!",
    modifier = modifier,
    fontSize = 48.sp
)

Слишком близко к верхней границе экрана активности? Добавим к тексту отступы (можно задать индивидуально для каждой стороны или сразу для всех сторон).


Text(
    text = "Hello $name!",
    modifier = modifier.padding(16.dp),
    fontSize = 48.sp
)

Далее добавим картинку на экран активности. Находим подходящее изображение и копируем его в папку res/drawable. Картинку можете взять у меня.

Hello Kitty

Можно просто перетащить картинку из проводника мышкой в папку drawable, при этом картинка перенесётся. Если вы хотите перенести копию картинки, оставив оригинал в своей папке, то попробуйте следующий вариант - скопируйте вашу картинку в буфер, затем щёлкните правой кнопкой мыши на папке drawable в студии, выберите команду "Вставить". Сначала появится первое диалоговое окно для выбора папки, выбираем папку drawable.

Возможно первое окно у вас не появится, тогда в следующем диалоговом окне подтверждаем операцию копирования файла.

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

Запомните, что имена ресурсов должны начинаться с буквы и могут содержать буквы и цифры, а также знак нижнего подчёркивания. Другие символы типа тире, решётки и т.д. использовать нельзя.

Сразу после функции Text() добавляем новую функцию Image.


Text(
    ...
)

Image(
    painter = painterResource(id = R.drawable.pinkhellokitty),
    contentDescription = "Hello Kitty Image"
)

Вы создали программу Hello Kitty и автоматически стали программистом первого уровня. Поздравляю!

Hello Kitty Compose

Полностью код для приложения.


package ru.alexanderklimov.helloworld

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import ru.alexanderklimov.helloworld.ui.theme.HelloWorldTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            HelloWorldTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    Greeting("Kitty")
                }
            }
        }
    }
}

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Column(
        modifier = modifier
            .background(Color(0xFFEFB8C8)),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(
            text = "Hello $name!",
            modifier = modifier
                .padding(16.dp),
            fontSize = 48.sp
        )

        Image(
            painter = painterResource(id = R.drawable.pinkhellokitty),
            contentDescription = "Hello Kitty Image"
        )
    }
}

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    HelloWorldTheme {
        Greeting("Kitty")
    }
}

Здороваемся с вашим котом

Программа получилась замечательная, но у неё есть недостаток. Она показывает одну и ту же фразу "Hello Kitty!". Вряд ли ваш кот знает английский, да и здороваться лучше по имени. Не пытайтесь с котом мяукать, иначе разговор выглядит следующим образом.

Разговор с котом

Поздороваемся с котом по человечески.

Создадим две переменные. Первая будет содержать имя кота, а вторая - текст из текстового поля. А также разместим два новых компонента: текстовое поле (TextFild) и кнопку (Button).

С кнопкой всё понятно. При помощи Text мы задаём текст на кнопке, а в параметре onClick код для нажатия. В данном случае переменной для имени кота присваивается значение из текстового поля.

У текстового поля настроек чуть больше. Параметр modifier вам уже знаком. Мы задаём здесь отступы через padding. Необязательный параметр placeholder выводит текст при пустом поле. Другой необязательный параметр label выводит подсказку над полем. Параметр value содержит текст из текстового поля, который нас интересует. В нём может содержаться имя кота, которое ввёл пользователь. Параметр onValueChange отслеживает в режиме реального времени изменение текста.


var catName by remember { mutableStateOf("Kitty") }
var textValue by remember { mutableStateOf("") }

TextField(
    value = textValue,
    onValueChange = {
        textValue = it
    },
    label = { Text(text = "Введите имя кота") },
    placeholder = { Text(text = "Барсик") },
    modifier = modifier.padding(4.dp)
)

Button(
    onClick =
    {
        catName = textValue
    }
) {
    Text(text = "Поздороваться")
}

У функции Greeting можно убрать первый параметр name, так как теперь имя будем узнавать программно.

Теперь можно вводить имя любимого вашего кота и программа поздоровается с ним.

Код после правок.


package ru.alexanderklimov.helloworld

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import ru.alexanderklimov.helloworld.ui.theme.HelloWorldTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            HelloWorldTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    Greeting()
                }
            }
        }
    }
}

@Composable
fun Greeting(modifier: Modifier = Modifier) {
    Column(
        modifier = modifier
            .background(Color(0xFFEFB8C8)),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        var catName by remember { mutableStateOf("Kitty") }
        var textValue by remember { mutableStateOf("") }

        Text(
            text = "Hello $catName!",
            modifier = modifier
                .padding(16.dp),
            fontSize = 48.sp
        )

        Image(
            painter = painterResource(id = R.drawable.pinkhellokitty),
            contentDescription = "Hello Kitty Image"
        )

        TextField(
            value = textValue,
            onValueChange = {
                textValue = it
            },
            label = { Text(text = "Введите имя кота") },
            placeholder = { Text(text = "Барсик") },
            modifier = modifier.padding(4.dp)
        )

        Button(
            onClick =
            {
                catName = textValue
            }
        ) {
            Text(text = "Поздороваться")
        }
    }
}

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    HelloWorldTheme {
        Greeting()
    }
}

Шаблон для уроков

Если присмотреться, то можно увидеть, что определённая часть кода всегда остаётся неизменной. Для экономии места я буду использовать сокращённый вариант для примеров. Для начала полный код.


package ru.alexanderklimov.compose

import ...

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Content()
        }
    }
}

@Composable
fun Content()
{
    // Здесь код для примера
}

@Preview(showBackground = true)
@Composable
fun ContentPreview() {
    Content()
}

Для демонстрации простейших примеров достаточно показать код из Content(). На всякий случай также буду добавлять список импортированных классов, так как порой имена совпадают и на первых порах можно делать ошибочный импорт.


// Один из примеров
@Composable
fun Content() {
    Text(text = "Hello Kitty")
}

Дополнительное чтение

Классический вариант

Реклама