Перейти к основному содержимому

Простые приложения на Kotlin

Разработчику Архитектору

Простые приложения на Kotlin

Kotlin — это современный язык программирования, разработанный компанией JetBrains. Он полностью совместим с экосистемой Java, но предлагает более лаконичный синтаксис, безопасную работу с данными и продвинутые функции для работы с коллекциями. Язык особенно удобен для создания консольных утилит, скриптов автоматизации и простых приложений с графическим интерфейсом.

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


Генератор паролей

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

Код программы

import java.security.SecureRandom

fun generatePassword(length: Int): String {
val chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+-=[]{}|;:,.<>?"
val random = SecureRandom()
return (1..length)
.map { chars[random.nextInt(chars.length)] }
.joinToString("")
}

fun main() {
val passwordLength = 16
val password = generatePassword(passwordLength)
println("Сгенерированный пароль: $password")
}

Разбор кода

  • SecureRandom: Класс из пакета java.security обеспечивает криптографически стойкую генерацию случайных чисел. Это предпочтительнее использования класса Random для создания паролей.
  • Коллекция символов: Строка chars содержит все допустимые символы для пароля.
  • Диапазон и маппинг: Оператор (1..length) создает диапазон целых чисел от 1 до заданной длины. Метод map применяет лямбда-выражение к каждому элементу диапазона, выбирая случайный символ из строки chars.
  • joinToString: Метод объединяет список символов в одну строку без разделителей.
  • Интерполяция строк: Конструкция $password позволяет вставлять значение переменной прямо в строку вывода.

Сортировщик текстового файла

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

Код программы

import java.io.File
import java.util.Collections

fun sortWords(inputFile: String, outputFile: String) {
val file = File(inputFile)

if (!file.exists()) {
println("Файл не найден: $inputFile")
return
}

val content = file.readText()
val words = content.split("\\s+".toRegex()).filter { it.isNotEmpty() }

Collections.sort(words)

File(outputFile).writeText(words.joinToString("\n"))
println("Слова успешно сохранены в $outputFile")
}

fun main() {
sortWords("input.txt", "sorted_output.txt")
}

Разбор кода

  • Чтение файла: Метод readText() загружает всё содержимое файла в память как строку.
  • Разделение текста: Регулярное выражение \\s+ разделяет текст по пробельным символам (пробелы, табы, переносы строк). Фильтрация filter { it.isNotEmpty() } удаляет пустые элементы.
  • Сортировка: Метод Collections.sort() сортирует список слов в лексикографическом порядке.
  • Запись файла: Метод writeText() записывает отсортированный список слов в новый файл, разделяя их переносами строк.

Консольный калькулятор

Простой калькулятор выполняет арифметические операции (+, -, *, /) над двумя числами, введенными пользователем. Пример показывает обработку ввода, условные операторы и исключительные ситуации.

Код программы

fun calculate(a: Double, b: Double, operator: Char): Double? {
return when (operator) {
'+' -> a + b
'-' -> a - b
'*' -> a * b
'/' -> if (b != 0.0) a / b else null
else -> null
}
}

fun main() {
print("Введите первое число: ")
val num1 = readLine()?.toDoubleOrNull() ?: run {
println("Ошибка ввода числа"); return
}

print("Введите операцию (+, -, *, /): ")
val op = readLine()?.firstOrNull() ?: run {
println("Ошибка ввода операции"); return
}

print("Введите второе число: ")
val num2 = readLine()?.toDoubleOrNull() ?: run {
println("Ошибка ввода числа"); return
}

val result = calculate(num1, num2, op)

if (result != null) {
println("Результат: $result")
} else {
println("Неверная операция или деление на ноль.")
}
}

Разбор кода

  • when: Конструкция when работает как расширенный оператор switch, позволяя сопоставлять значения оператора с соответствующими действиями.
  • Обработка ошибок: Метод toDoubleOrNull() возвращает null, если строка не является числом. Оператор Elvis (?:) используется для обработки таких случаев и завершения выполнения функции.
  • Проверка деления: В случае деления проверяется, что делитель не равен нулю. Если условие нарушено, функция возвращает null.

Трекер задач в JSON

Приложение управляет списком задач, сохраняя их в формате JSON. Пример демонстрирует использование библиотек для сериализации и десериализации данных, а также работу с файловой системой. Для работы потребуется библиотека kotlinx.serialization.

Структура данных

import kotlinx.serialization.Serializable

@Serializable
data class Task(
val id: Int,
val title: String,
val isCompleted: Boolean
)

Код программы

import kotlinx.serialization.json.Json
import kotlinx.serialization.Serializable
import java.io.File

@Serializable
data class Task(val id: Int, val title: String, val isCompleted: Boolean)

fun loadTasks(): List<Task> {
val file = File("tasks.json")
if (!file.exists()) return emptyList()

val jsonContent = file.readText()
return Json.decodeFromString<List<Task>>(jsonContent)
}

fun saveTasks(tasks: List<Task>) {
val file = File("tasks.json")
val jsonContent = Json.encodeToString(tasks)
file.writeText(jsonContent)
}

fun addTask(tasks: MutableList<Task>, title: String) {
val newId = tasks.maxOfOrNull { it.id }?.plus(1) ?: 1
tasks.add(Task(newId, title, false))
saveTasks(tasks)
}

fun main() {
val tasks = loadTasks().toMutableList()

// Добавление задачи
addTask(tasks, "Изучить Kotlin")
addTask(tasks, "Написать тесты")

// Вывод списка
println("Список задач:")
tasks.forEach { task ->
val status = if (task.isCompleted) "[x]" else "[ ]"
println("$status ${task.id}. ${task.title}")
}
}

Разбор кода

  • Аннотация @Serializable: Указывает классу, что его экземпляры могут быть преобразованы в формат JSON.
  • Json.encodeToString / decodeFromString: Методы библиотеки kotlinx.serialization преобразуют объекты в строки JSON и обратно.
  • Работа с файлами: Функции loadTasks и saveTasks обеспечивают сохранение состояния приложения между запусками.
  • Генерация ID: Метод maxOfOrNull находит максимальный ID среди существующих задач, чтобы присвоить новому уникальный идентификатор.

Простой HTTP-сервер и клиент

Kotlin позволяет создавать простые сетевые приложения с использованием встроенных классов HttpURLConnection или сторонних библиотек. Ниже приведен пример сервера на основе стандартной библиотеки Java и клиента, отправляющего запрос.

Сервер

import java.net.ServerSocket
import java.net.Socket
import java.io.PrintWriter
import java.io.BufferedReader
import java.io.InputStreamReader

fun startServer(port: Int) {
ServerSocket(port).use { server ->
println("Сервер запущен на порту $port")

while (true) {
Socket().use { client ->
BufferedReader(InputStreamReader(client.inputStream)).use { reader ->
PrintWriter(client.outputStream, true).use { writer ->
val request = reader.readLine()
println("Получен запрос: $request")

val response = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nHello from Kotlin Server!"
writer.println(response)
}
}
}
}
}
}

fun main() {
startServer(8080)
}

Клиент

import java.net.HttpURLConnection
import java.net.URL

fun sendRequest(url: String) {
val connection = URL(url).openConnection() as HttpURLConnection
connection.requestMethod = "GET"

val responseCode = connection.responseCode
val inputStream = connection.inputStream
val reader = BufferedReader(InputStreamReader(inputStream))

var line: String?
val response = StringBuilder()

while (reader.readLine().also { line = it } != null) {
response.append(line)
}

println("Код ответа: $responseCode")
println("Ответ сервера: ${response.toString()}")

inputStream.close()
connection.disconnect()
}

fun main() {
sendRequest("http://localhost:8080")
}

Разбор кода

  • ServerSocket: Создает серверный сокет, который слушает входящие соединения на указанном порту.
  • Socket: Представляет соединение между клиентом и сервером.
  • HttpURLConnection: Класс для отправки HTTP-запросов и получения ответов.
  • Потоки ввода/вывода: BufferedReader и PrintWriter используются для чтения и записи текстовых данных через сеть.

Отправитель HTTP-запросов

Утилита для отправки произвольных HTTP-запросов с возможностью указания метода, заголовков и тела запроса.

Код программы

import java.net.HttpURLConnection
import java.net.URL
import java.io.OutputStream
import java.nio.charset.StandardCharsets

fun sendHttpRequest(method: String, url: String, headers: Map<String, String>, body: String?) {
val connection = URL(url).openConnection() as HttpURLConnection
connection.requestMethod = method

headers.forEach { (key, value) ->
connection.setRequestProperty(key, value)
}

if (body != null && method in listOf("POST", "PUT")) {
connection.doOutput = true
OutputStream(connection.outputStream).use { out ->
out.write(body.toByteArray(StandardCharsets.UTF_8))
}
}

val responseCode = connection.responseCode
val inputStream = connection.inputStream
val reader = BufferedReader(InputStreamReader(inputStream))

val response = StringBuilder()
var line: String?
while (reader.readLine().also { line = it } != null) {
response.append(line)
}

println("Метод: $method")
println("URL: $url")
println("Код ответа: $responseCode")
println("Тело ответа: ${response.toString()}")

inputStream.close()
connection.disconnect()
}

fun main() {
val headers = mapOf(
"Content-Type" to "application/json",
"Accept" to "application/json"
)
val body = """{"name": "Timur", "role": "Developer"}"""

sendHttpRequest("POST", "https://jsonplaceholder.typicode.com/posts", headers, body)
}

Разбор кода

  • Настройка заголовков: Цикл forEach устанавливает пользовательские заголовки в запросе.
  • Отправка тела: При использовании методов POST или PUT тело запроса записывается в поток вывода.
  • Чтение ответа: Данные считываются из потока ввода и собираются в строку для отображения.

Утилита для сканирования директорий

Программа выводит список всех файлов и поддиректорий в указанной папке рекурсивно.

Код программы

import java.io.File

fun scanDirectory(path: String, indent: Int = 0) {
val file = File(path)

if (!file.exists()) {
println("Директория не найдена: $path")
return
}

if (file.isDirectory) {
println("${" ".repeat(indent)}📁 ${file.name}/")
file.listFiles()?.forEach { subFile ->
scanDirectory(subFile.absolutePath, indent + 2)
}
} else {
println("${" ".repeat(indent)}📄 ${file.name} (${file.length()} байт)")
}
}

fun main() {
scanDirectory(".")
}

Разбор кода

  • Рекурсия: Функция вызывает сама себя для каждой поддиректории, увеличивая уровень отступа.
  • File.listFiles(): Возвращает массив файлов в текущей директории.
  • Отступы: Строка " ".repeat(indent) создает визуальный отступ для отображения структуры дерева.

Скрипт для создания резервного копирования файлов

Скрипт копирует файлы из одной директории в другую, создавая временную метку в имени папки назначения.

Код программы

import java.io.File
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

fun createBackup(sourceDir: String, backupDir: String) {
val source = File(sourceDir)
val timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss"))
val targetDir = File("$backupDir/backup_$timestamp")

if (!source.exists() || !source.isDirectory) {
println("Источник не существует или не является директорией: $sourceDir")
return
}

targetDir.mkdirs()

source.listFiles()?.forEach { file ->
if (file.isFile) {
val targetFile = File(targetDir, file.name)
file.copyTo(targetFile, overwrite = true)
println("Скопировано: ${file.name}")
}
}

println("Резервное копирование завершено в $targetDir")
}

fun main() {
createBackup("./data", "./backups")
}

Разбор кода

  • Дата и время: Класс LocalDateTime и форматтер DateTimeFormatter генерируют уникальное имя для папки бэкапа.
  • Копирование файлов: Метод copyTo копирует файл в новую директорию с возможностью перезаписи.
  • Создание директории: Метод mkdirs() создает всю цепочку директорий, если они отсутствуют.

Мониторинг дискового пространства

Утилита отображает информацию о свободном и занятом месте на дисках системы.

Код программы

import java.io.File

fun checkDiskSpace(path: String) {
val root = File(path)
val totalSpace = root.totalSpace
val freeSpace = root.freeSpace
val usableSpace = root.usableSpace

val usedSpace = totalSpace - usableSpace

println("Диск: $path")
println("Всего места: ${(totalSpace / (1024 * 1024 * 1024)).toInt()} ГБ")
println("Свободно: ${(freeSpace / (1024 * 1024 * 1024)).toInt()} ГБ")
println("Использовано: ${(usedSpace / (1024 * 1024 * 1024)).toInt()} ГБ")
println("Занято: %.2f%%".format((usedSpace.toDouble() / totalSpace.toDouble()) * 100))
}

fun main() {
checkDiskSpace("/")
checkDiskSpace("C:/") // Для Windows
}

Разбор кода

  • Свойства объекта File: Поля totalSpace, freeSpace, usableSpace содержат информацию о пространстве диска в байтах.
  • Конвертация единиц: Деление на 1024 * 1024 * 1024 переводит байты в гигабайты.
  • Форматирование вывода: Метод format позволяет вывести процент занятости с двумя знаками после запятой.

Парсер URL и проверка доступности ресурса

Программа анализирует URL, извлекает компоненты (протокол, хост, путь) и проверяет доступность ресурса.

Код программы

import java.net.URL
import java.net.HttpURLConnection

fun parseAndCheckUrl(urlString: String) {
try {
val url = URL(urlString)

println("Протокол: ${url.protocol}")
println("Хост: ${url.host}")
println("Порт: ${url.port}")
println("Путь: ${url.path}")
println("Запрос: ${url.query}")

val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "HEAD"
connection.connectTimeout = 5000
connection.readTimeout = 5000

val responseCode = connection.responseCode

if (responseCode == HttpURLConnection.HTTP_OK) {
println("Статус: Доступен ($responseCode)")
} else {
println("Статус: Недоступен ($responseCode)")
}

connection.disconnect()
} catch (e: Exception) {
println("Ошибка: ${e.message}")
}
}

fun main() {
parseAndCheckUrl("https://example.com/path/to/resource?key=value")
}

Разбор кода

  • Класс URL: Автоматически разбирает строку URL на компоненты.
  • HEAD запрос: Используется для проверки доступности ресурса без загрузки всего контента.
  • Обработка исключений: Блок try-catch перехватывает ошибки сети или неверного формата URL.

Конвертер форматов дат

Утилита преобразует строковое представление даты в различные форматы.

Код программы

import java.time.LocalDate
import java.time.format.DateTimeFormatter

fun convertDate(dateString: String, inputFormat: String, outputFormats: List<String>) {
val inputFormatter = DateTimeFormatter.ofPattern(inputFormat)
val date = LocalDate.parse(dateString, inputFormatter)

outputFormats.forEach { format ->
val formatter = DateTimeFormatter.ofPattern(format)
val converted = date.format(formatter)
println("$format -> $converted")
}
}

fun main() {
val inputDate = "2025-11-01"
val formats = listOf("dd/MM/yyyy", "MMMM dd, yyyy", "yyyy.MM.dd")

convertDate(inputDate, "yyyy-MM-dd", formats)
}

Разбор кода

  • LocalDate: Класс для работы с датами без учета времени.
  • DateTimeFormatter: Определяет шаблоны для парсинга и форматирования дат.
  • Параметризация: Список выходных форматов позволяет гибко задавать нужные варианты представления даты.

Утилита для просмотра запущенных процессов

Программа выводит список активных процессов операционной системы с указанием их имен и идентификаторов.

Код программы

import java.lang.ProcessBuilder
import java.io.BufferedReader
import java.io.InputStreamReader

fun listProcesses() {
val command = if (System.getProperty("os.name").contains("win")) {
arrayOf("cmd.exe", "/c", "tasklist")
} else {
arrayOf("ps", "-aux")
}

val processBuilder = ProcessBuilder(*command)
val process = processBuilder.start()

BufferedReader(InputStreamReader(process.inputStream)).use { reader ->
var line: String?
while (reader.readLine().also { line = it } != null) {
println(line)
}
}

process.waitFor()
}

fun main() {
listProcesses()
}

Разбор кода

  • ProcessBuilder: Запускает внешнюю команду операционной системы.
  • Определение ОС: Логика выбора команды зависит от названия операционной системы.
  • Чтение вывода: Вывод процесса считывается построчно и выводится в консоль.

Характерный пример для Kotlin

Одной из ключевых особенностей Kotlin является работа с nullable типами и безопасность операций. Приведенный пример демонстрирует использование оператора ?. (safe call), elvis-оператора ?: и функции let для безопасной работы с потенциально пустыми значениями.

Код программы

data class User(val name: String?, val age: Int?)

fun getUserInfo(user: User?) {
user?.let { u ->
val displayName = u.name ?: "Неизвестный пользователь"
val ageDisplay = u.age?.let { age -> "$age лет" } ?: "Возраст не указан"

println("Имя: $displayName")
println("Возраст: $ageDisplay")
} ?: run {
println("Пользователь не найден")
}
}

fun main() {
val user1 = User("Алексей", 30)
val user2 = User(null, null)
val user3: User? = null

getUserInfo(user1)
println("---")
getUserInfo(user2)
println("---")
getUserInfo(user3)
}

Разбор кода

  • Safe Call (?.): Вызывает метод только если объект не равен null.
  • Elvis Operator (?:): Возвращает альтернативное значение, если левая часть равна null.
  • Функция let: Позволяет выполнить блок кода с объектом, передавая его как параметр.
  • Блоки run: Выполняются при отсутствии объекта или при необходимости выполнить действие по умолчанию.

Этот подход делает код более читаемым и предотвращает возникновение NullPointerException, что является одним из главных преимуществ Kotlin перед Java.