Android и Kotlin — разработка мобильных приложений
Android — самая массовая мобильная платформа в мире. Разработка ведётся на языке Kotlin в официальной среде Android Studio: от первого «Привет, мир» на эмуляторе до публикации приложения в Google Play. Курс шаг за шагом проведёт от установки инструментов до релиза.
1. Что такое Android-разработка
Android — операционная система на базе Linux, которая работает на смартфонах, планшетах, часах и телевизорах. Приложения компилируются в установочный файл .apk или современный формат .aab (Android App Bundle) и распространяются через Google Play.
Официальный язык — Kotlin: он лаконичен, безопасен по типам и полностью совместим с Java. Для сборки используется система Gradle, а интерфейс современные приложения строят на Jetpack Compose — декларативном тулките, где экран описывается кодом, а не XML-файлами.
// Простейшая программа на Kotlin
fun main() {
println("Привет, мир!")
}2. Установка Android Studio
Скачай Android Studio с официального сайта developer.android.com/studio. Установщик предложит JDK, SDK и набор инструментов командной строки. После первого запуска мастер доустановит недостающие компоненты — это займёт несколько минут.
# Проверка установки SDK-инструментов
sdkmanager --list
sdkmanager "platform-tools" "platforms;android-35"
sdkmanager "system-images;android-35;google_apis;x86_64"Лицензии компонентов принимаются при первом запуске. Если работаешь из терминала, прими их командой sdkmanager --licenses.
3. Первый проект и структура
В Android Studio выбери New Project → Empty Activity. IDE сгенерирует проект, который уже можно запустить. Разберись со структурой — без неё легко потеряться:
| Папка | Что внутри |
|---|---|
app/src/main/java | код Kotlin: активити и классы |
app/src/main/res | ресурсы: картинки, строки, темы |
AndroidManifest.xml | описание приложения и разрешения |
build.gradle.kts | настройки сборки и зависимости |
// app/build.gradle.kts
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "com.example.myapp"
compileSdk = 35
}Точка входа — активити. Когда система запускает приложение, она вызывает его метод onCreate.
4. Основы Kotlin
Синтаксис Kotlin близок к Java и C#, но проще. Ключевое отличие — безопасность от нулевых ссылок: тип String? означает «строка или null», и компилятор заставит проверить null перед обращением.
val name = "Анна" // val — неизменяемая переменная
var count = 0 // var — изменяемая
fun greet(name: String): String {
return "Привет, $name!"
}
var maybe: String? = null // nullable-тип
val length = maybe?.length ?: 0 // ?. — безопасный вызов, ?: — значение по умолчанию?: называется «элвис» — он похож на человечка с причёской. Если слева null, берётся значение справа.В интерполяцию строк напрямую подставляются переменные: "Итог: $count". Это удобно при выводе данных в интерфейс.
5. Манифест, Activity и Jetpack Compose
Файл AndroidManifest.xml описывает приложение, его активити и разрешения. Активити с фильтром LAUNCHER — стартовый экран, с которого начинается приложение.
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="@string/app_name"
android:theme="@style/Theme.MyApp">
<activity android:name=".MainActivity" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>Современный интерфейс строится на Compose: функция @Composable описывает экран декларативно.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Greeting()
}
}
}
@Composable
fun Greeting() {
Text("Привет, Android!")
}android:exported обязателен с Android 12. Для активити, запускаемой системой, ставь true — иначе сборка упадёт с ошибкой.6. Вёрстка и списки: Compose и RecyclerView
В Compose списки делает компонент LazyColumn — он создаёт только видимые элементы, поэтому легко тянет тысяч элементов.
val items = listOf("Первый", "Второй", "Третий")
LazyColumn {
items(items) { item ->
Text(
text = item,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
)
}
}В старых проектах используют XML-вёрстку и виджет RecyclerView с адаптером. Основные блоки XML-разметки: контейнер LinearLayout (строка или колонка), FrameLayout (наложение) и ConstraintLayout (гибкие связи).
// Пример XML-разметки экрана
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="match_parent" />7. Интенты и навигация
Интент (Intent) — это сообщение «что нужно сделать». Явный интент открывает конкретный экран внутри приложения, неявный — делегирует действие системе.
// Переход на другой экран
val intent = Intent(this, DetailActivity::class.java)
intent.putExtra("post_id", id)
startActivity(intent)
// Открыть внешнюю ссылку
val url = Uri.parse("https://it-uroki.ru")
val browser = Intent(Intent.ACTION_VIEW, url)
startActivity(browser)Данные передаются через putExtra и читаются в принимающей активити через getStringExtra или getIntExtra.
8. Работа с сетью
Стандартный инструмент — Retrofit от Square: он превращает HTTP-запросы в вызовы функций. С корутинами используются suspend-функции, которые не блокируют поток интерфейса.
data class Post(val id: Int, val title: String)
interface ApiService {
@GET("posts")
suspend fun getPosts(): List<Post>
}
val api = Retrofit.Builder()
.baseUrl("https://jsonplaceholder.typicode.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ApiService::class.java)Вызвать API можно из корутины:
lifecycleScope.launch {
val posts = api.getPosts()
recyclerAdapter.submitList(posts)
}<uses-permission android:name="android.permission.INTERNET" />. Без него запросы молча завершаются ошибкой.10.0.2.2 вместо localhost — так эмулятор видит хост-машину. Работа с реальным сайтом по HTTP требует флага usesCleartextTraffic.9. Локальное хранилище: SQLite и Room
Room — библиотека поверх SQLite с проверкой SQL-запросов на этапе компиляции. Данные описываются сущностями (@Entity), обращения — интерфейсом DAO (@Dao).
@Entity(tableName = "notes")
data class Note(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val text: String
)
@Dao
interface NoteDao {
@Insert
suspend fun insert(note: Note)
@Query("SELECT * FROM notes")
fun getAll(): Flow<List<Note>>
}Объект базы создаётся один раз:
@Database(entities = [Note::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun noteDao(): NoteDao
}Тип Flow — реактивный поток: интерфейс автоматически обновится при любом изменении базы без ручных вызовов.
SharedPreferences или DataStore. Room включают, когда данные структурированы и их нужно фильтровать запросами.10. Эмулятор и отладка
Настрой виртуальное устройство через Device Manager: выбери образ системы (лучше Google APIs), размер экрана и версию Android. Запуск эмулятора — горячая клавиша Shift + F10 или кнопка Run.
Устройство и комьютер связывает инструмент adb (Android Debug Bridge):
# Полезные команды adb
adb devices # список подключённых устройств
adb shell # интерактивная консоль
adb install app-release.apk # установить APK вручную
adb logcat -s AndroidRuntime:E # только ошибки приложенияЛоги пишутся функцией Log.d("Метка", "сообщение") и просматриваются в панели Logcat. Точки останова ставятся кликом у номера строки — с ними можно пошагово пройти код.
11. Публикация в Google Play
Релизная сборка подписывается ключом .jks, который связывает приложение с аккаунтом разработчика. Настрой подпись в build.gradle.kts:
android {
signingConfigs {
create("release") {
storeFile = file("keystore.jks")
storePassword = "пароль"
keyAlias = "mykey"
keyPassword = "пароль"
}
}
}Дальше путь такой: Build → Generate Signed Bundle / APK → выбери .aab → зарегистрируй аккаунт разработчика (разовый платёж ~25$) → загрузи сборку в Google Play Console → заполни описание, скриншоты и опубликуй.
.jks в надёжном месте вне проекта (например, в зашифрованном менеджере паролей).12. ViewModel и состояние экрана
ViewModel переживает поворот экрана и хранит данные, не связанные с представлением. Компонент не теряет состояние при изменении конфигурации — это стандартная практика современного Android.
class CounterViewModel : ViewModel() {
private val _count = MutableStateFlow(0)
val count: StateFlow<Int> = _count.asStateFlow()
fun increment() {
_count.value += 1
}
}В Compose-экране получаем экземпляр так:
@Composable
fun CounterScreen(viewModel: CounterViewModel =
viewModel()) {
val count by viewModel.count.collectAsState()
Button(onClick = { viewModel.increment() }) {
Text("Счёт: $count")
}
}- не храните ссылки на UI в ViewModel — только данные и логика;
- поток
StateFlowавтоматически обновляет интерфейс при изменениях; - создавайте ViewModel с фабрикой, если нужны параметры (id, аргументы).
13. Корутины и потоковые данные
Корутины Kotlin — способ писать асинхронный код без коллбэков. Запросы к сети, чтение базы и обновление UI выполняются не блокируя поток интерфейса.
lifecycleScope.launch {
val data = repository.load() // suspend-функция
uiState.value = UiState.Success(data)
}Основные понятия:
| Понятие | Смысл |
|---|---|
| CoroutineScope | жизненный цикл корутин (например, lifecycleScope) |
| Dispatchers.IO | пул для сетевых и дисковых операций |
| suspend | функция может приостановиться без блокировки |
| withContext | смена контекста внутри корутины |
Dispatchers.Main (или use collectAsState), иначе упадёт CalledFromWrongThreadException.14. Работа с данными: StateFlow, Flow и база
Flow — холодный поток значений: данные появляются по мере подписки. Комбинируя Flow с Room и Retrofit, получаем реактивные экраны, обновляющиеся сами.
@Query("SELECT * FROM notes ORDER BY id DESC")
fun getAll(): Flow<List<Note>>
// в ViewModel
val notes = dao.getAll().stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
initialValue = emptyList()
)Сбор во ViewModel и отображение через collectAsState — канонический паттерн: подписка сама закрывается, когда экран исчез.
15. Тесты и отладка приложения
Юнит-тесты проверяют ViewModel и Dao, UI-тесты — композдаблы и взаимодействие. Тесты делают рефакторинг безопасным.
@Test
fun increment_increases_count() {
val vm = CounterViewModel()
vm.increment()
assertEquals(1, vm.count.value)
}- юнит-тесты на JVM — для логики бизнеса и ViewModel;
- тесты с Robolectric — для Android-компонентов вне эмулятора;
- UI-тесты с Compose Test API — проходы по экранам;
- покрытые тестами модули рефакторятся быстрее.
16. DI на Hilt: управляем зависимостями
Инжекция зависимостей (DI) делает код тестируемым и гибким: сервисы, база, сеть создаются «извне», а не прямо в классах. Hilt — стандартный DI-фреймворк от Google для Android.
@HiltAndroidApp
class App : Application()
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides @Singleton
fun provideDb(): AppDatabase = Room.databaseBuilder(...).build()
}@HiltAndroidApp— в классе Application;@AndroidEntryPoint— в Activity и Fragment;@Injectв конструкторе — автоматическая фабрика;ViewModelинжектится через factory, объявленный в модуле.
17. Push-уведомления (FCM)
Push-уведомления возвращают пользователей в приложение: лента, заказы, сообщения. Стандарт для Android — Firebase Cloud Messaging (FCM): сервер шлёт сообщение, приложение его показывает.
- подключите Firebase (google-services.json) и добавьте зависимость FCM;
- создайте службу-наследник
FirebaseMessagingService; - получите токен устройства:
FirebaseMessaging.getInstance().token; - передайте токен на свой сервер — по нему шлёте пуш;
- отправляйте сообщения:
POST /v1/projects/.../messages:send.
{
"message": {
"token": "USER_TOKEN",
"notification": {
"title": "Скидка!",
"body": "Только сегодня −30%"
}
}
}18. Камера и работа с файлами
Фото профиля, сканы документов, сохранение файлов — частое требование в приложениях. Современные API Android построены на Activity Result Contracts и файловых провайдерах.
// Запрос на выбор фото из галереи
val pick = registerForActivityResult(
ActivityResultContracts.GetContent()) { uri ->
// uri — доступ только к выбранному файлу
}
// Съёмка с камеры с сохранением в файл
val take = registerForActivityResult(
ActivityResultContracts.TakePicture()) { success -> ... }
- галерея:
GetContent— не требует разрешений; - камера:
TakePicture— работает через URI и FileProvider; - чтение файлов:
contentResolver.openInputStream(uri); - права на хранилище: для общего доступа — MANAGE_EXTERNAL_STORAGE не выдаётся без причины.
19. Производительность и профилирование
Медленное приложение теряет пользователей: зависания (ANR) и тормоза караются низкими оценками. Профилируйте заранее: утечки памяти, лишние перерисовки, тяжёлые операции на главном потоке.
| Инструмент | Что показывает |
|---|---|
| Profiler (Android Studio) | CPU, память, сеть, энергия |
| LeakCanary | утечки по ссылкам на Activity/ViewModel |
| StrictMode | дисковые и сетевые операции на главном потоке |
| Systrace | как фреймы тратят время |
- не держите тяжёлые объекты в ViewModel дольше жизни экрана;
- переиспользуйте списки: RecyclerView.holder, не пересоздавайте при скролле;
- работа с сетью и файлами — только в корутинах с Dispatchers.IO;
- проверяйте память на больших bitmap: масштабируйте изображения.
20. WorkManager и фоновые задачи
WorkManager — современный API для отложенных и периодических фоновых задач: синхронизация данных, загрузка, очистка кэша. Он переживает перезапуск приложения и учитывает ограничения сети и батареи.
val request = OneTimeWorkRequestBuilder<SyncWorker>()
.setConstraints(Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build())
.build()
WorkManager.getInstance(context).enqueue(request)- периодические задачи с интервалом не меньше 15 минут;
- уникальные имена предотвращают дублирование работ;
- результаты наблюдаются через LiveData или Flow;
- для точных по времени событий используйте AlarmManager — он затратнее для батареи.
21. Безопасность: Keystore и шифрование
Хранить токены и пароли в SharedPreferences опасно: их может прочитать любой, у кого есть устройство. Android Keystore держит ключи в защищённом хранилище и не отдаёт их наружу.
- AndroidKeyStore генерирует и использует ключи без экспорта из хранилища;
- EncryptedSharedPreferences шифрует данные на диске «из коробки»;
- BiometricPrompt добавляет подтверждение биометрией;
- секреты удаляйте сразу после использования — не храните лишнего.
22. Аналитика и crash-репорты
Публикация — не финиш. Crashlytics собирает падения, аналитика — поведение пользователей. Без них вы не увидите, что приложение делает на реальных устройствах.
- Crashlytics даёт стек ошибки, данные устройства и версию приложения;
- события аналитики (экран, покупка, воронка) — в отдельные события;
- внутренняя дистрибуция рассылает бета-сборки на тестовые устройства;
- после релиза следите за ростом падений — откатывайте проблемные версии.
23. Мультимодульность и Clean Architecture
Когда приложение разрастается, один модуль становится узким местом: все правят всё и конфликты в Git неизбежны. Разбиение на модули даёт изоляцию и быстрее сборку.
- типовая разбивка: app, core, feature, data, domain;
- данные «стучатся» наверх через интерфейсы, а не напрямую;
- изменение одной фичи пересобирает только её модуль;
- модульные тесты запускаются без эмулятора и работают быстрее.
Что дальше
Закрепи язык: курс IntelliJ IDEA поможет освоить IDE, а Git — начать версионировать код. Для серверной части приложения пригодится REST API, для аналитики данных — SQL. Смежные мобильные платформы: iOS (Swift), кроссплатформенные Flutter и React Native.