🎯 Kotlin

Abschlussprojekt in Kotlin: Eine ausführliche Anleitung

Что это такое и зачем? (аналогия из реальной жизни)

Представь, что ты учишься в школе кулинарного искусства. Сначала ты учишься резать овощи (переменные), потом жарить яйца (условия), потом варить суп по рецепту (функции). Но в конце обучения тебе дают задание: придумать и приготовить собственное блюдо для выпускного экзамена. Ты можешь использовать любые продукты, любые техники, но блюдо должно быть съедобным, красивым и вкусным.

Итоговый практический проект в программировании — это то же самое. Это не просто задача на одну тему (например, "напиши цикл"). Это самостоятельная работа, где ты соединяешь всё, что выучил: переменные, функции, классы, списки, работу с файлами, обработку ошибок. Ты сам решаешь, как будет выглядеть программа, какие кнопки в ней будут и как она будет работать.

Важно: Итоговый проект — это не проверка того, как ты заучил синтаксис. Это проверка того, как ты умеешь думать как программист: разбивать большую задачу на маленькие, искать ошибки и доводить дело до конца.

Зачем это нужно? В реальной работе программиста никто не даёт тебе готовые кусочки кода, которые нужно просто вставить. Тебе дают задачу: "Сделай приложение для заметок" или "Напиши игру-кликер". И ты должен сам придумать архитектуру, выбрать инструменты и написать код, который будет работать.

В этой статье мы разберём, как спроектировать и написать итоговый проект на Kotlin. Мы не будем писать конкретную программу (ты сам выберешь тему), но мы разберём скелет, на который ты нанижешь свой код.

Теория: как это работает изнутри

Любой итоговый проект, даже самый сложный, состоит из трёх слоёв. Представь себе слоёный пирог:

  1. Слой данных (Model) — это то, с чем работает программа. Например, если ты пишешь "Телефонную книгу", данные — это контакты (имя, номер телефона, email). Если игру "Змейка" — это координаты змейки, яблока, счёт.
  2. Слой логики (Controller / Logic) — это правила. Как добавлять контакт? Как проверять, не съела ли змейка сама себя? Как сохранять данные в файл?
  3. Слой отображения (View / UI) — это то, что видит пользователь. Консольные сообщения, меню, кнопки (если графический интерфейс).

Почему их нужно разделять? Представь, что ты написал всю программу в одном файле, в одной функции main. Всё перемешано: и ввод с клавиатуры, и расчёты, и вывод. Через неделю ты захочешь добавить новую функцию (например, "поиск по имени"). Тебе придётся перечитывать 500 строк кода, чтобы понять, куда вставить новую строчку. А если слои разделены, ты просто идёшь в слой логики, добавляешь функцию поиска, а потом в слое отображения добавляешь пункт меню "Поиск".

В Kotlin для разделения слоёв мы используем классы и функции. Каждый класс отвечает за свою часть работы. Это называется принцип единственной ответственности (один класс — одна задача).

Ещё один важный момент: состояние программы. Программа не должна "забывать", что сделал пользователь. Если пользователь добавил контакт, программа должна его запомнить, пока работает. Для этого мы используем списки (MutableList) или базы данных. В простых проектах достаточно списка в памяти. В сложных — файлы или базы данных.

Синтаксис в Kotlin: все формы и варианты

Для итогового проекта тебе понадобятся не все возможности Kotlin, а только самые важные. Вот "джентльменский набор" инструментов, которые ты будешь использовать в 90% проектов.

1. Классы и конструкторы

Класс — это чертёж объекта. Например, чертёж "Контакт":

// Класс с конструктором по умолчанию
class Contact(
    val name: String,       // имя контакта (неизменяемое)
    var phone: String,      // телефон (можно менять)
    var email: String?      // email может быть null (необязательный)
) {
    // Дополнительные функции класса
    fun printInfo() {
        println("Имя: $name, Телефон: $phone, Email: ${email ?: "не указан"}")
    }
}

2. MutableList — изменяемый список

Это главный "контейнер" для хранения данных в памяти:

// Создаём пустой список контактов
val contacts = mutableListOf<Contact>()

// Добавляем контакт
contacts.add(Contact("Анна", "123-456", "anna@mail.com"))

// Удаляем по индексу
contacts.removeAt(0)

// Ищем по условию
val found = contacts.find { it.name == "Анна" }

3. When — мощная замена if-else

Для меню в консольном проекте это незаменимая вещь:

when (choice) {
    1 -> addContact()
    2 -> searchContact()
    3 -> deleteContact()
    0 -> println("Выход...")
    else -> println("Неверный ввод")
}

4. Функции с возвратом и без

Функции — это твои "инструменты". Каждая функция делает одно маленькое действие:

// Функция без возврата (Unit)
fun showMenu() {
    println("1. Добавить контакт")
    println("2. Показать все контакты")
}

// Функция с возвратом (String?)
fun getInput(prompt: String): String? {
    print(prompt)
    return readLine()
}

5. Обработка ошибок (try-catch)

Пользователь может ввести "abc" вместо числа. Программа не должна падать:

fun readInt(): Int {
    while (true) {
        try {
            return readLine()?.toInt() ?: 0
        } catch (e: NumberFormatException) {
            println("Ошибка! Введите число.")
        }
    }
}

6. Работа с файлами (для сохранения данных)

Чтобы данные не пропадали при закрытии программы:

import java.io.File

fun saveContacts(contacts: List<Contact>) {
    val file = File("contacts.txt")
    file.writeText("") // очищаем файл
    for (contact in contacts) {
        file.appendText("${contact.name},${contact.phone},${contact.email}
")
    }
}

fun loadContacts(): MutableList<Contact> {
    val file = File("contacts.txt")
    if (!file.exists()) return mutableListOf()
    val list = mutableListOf<Contact>()
    file.readLines().forEach { line ->
        val parts = line.split(",")
        if (parts.size == 3) {
            list.add(Contact(parts[0], parts[1], parts[2]))
        }
    }
    return list
}

Примеры кода — от простого к сложному

Мы будем строить один проект — "Консольный ежедневник". С каждым примером он будет становиться всё сложнее и функциональнее.

Пример 1: Минимальная структура (скелет)

// Самый простой скелет программы
fun main() {
    println("Добро пожаловать в Ежедневник!")
    while (true) {
        println("
1. Показать задачи")
        println("2. Добавить задачу")
        println("0. Выход")
        print("Ваш выбор: ")
        val choice = readLine() ?: "0"
        when (choice) {
            "1" -> println("Пока задач нет")
            "2" -> println("Добавление задачи (ещё не реализовано)")
            "0" -> {
                println("До свидания!")
                break
            }
            else -> println("Неверный ввод")
        }
    }
}

Объяснение: Это основа. Бесконечный цикл while (true) держит программу открытой, пока пользователь не выберет "0". when обрабатывает выбор. Пока функции только заглушки.

Пример 2: Добавляем класс Task

// Класс для хранения одной задачи
data class Task(
    val id: Int,           // уникальный номер
    val title: String,     // название задачи
    var isDone: Boolean    // выполнена или нет
) {
    // Специальная функция для красивого вывода
    fun display() {
        val status = if (isDone) "[✓]" else "[ ]"
        println("$id. $status $title")
    }
}

Объяснение: data class — это особый класс в Kotlin, который сам генерирует функции toString, equals и другие. Идеально для хранения данных. У каждой задачи есть id, название и статус.

Пример 3: Список задач и добавление

fun main() {
    val tasks = mutableListOf<Task>()  // список задач
    var nextId = 1                     // счётчик для id

    while (true) {
        println("
--- Ежедневник ---")
        println("1. Показать задачи")
        println("2. Добавить задачу")
        println("0. Выход")
        print("Ваш выбор: ")
        when (readLine()) {
            "1" -> {
                if (tasks.isEmpty()) {
                    println("Задач пока нет.")
                } else {
                    for (task in tasks) {
                        task.display()  // выводим каждую задачу
                    }
                }
            }
            "2" -> {
                print("Введите название задачи: ")
                val title = readLine() ?: "Без названия"
                tasks.add(Task(nextId, title, false))
                println("Задача добавлена! ID: $nextId")
                nextId++  // увеличиваем счётчик
            }
            "0" -> {
                println("Выход...")
                break
            }
            else -> println("Неверный ввод")
        }
    }
}

Объяснение: Теперь программа хранит задачи в списке. При добавлении мы создаём новый объект Task с уникальным id. При показе — проходим по всему списку циклом for.

Пример 4: Отмечаем задачу выполненной

// Добавляем в меню пункт 3
// Внутри when добавляем:
"3" -> {
    print("Введите ID задачи для отметки: ")
    val id = readLine()?.toIntOrNull()
    if (id == null) {
        println("Ошибка: введите число!")
    } else {
        // Ищем задачу по id
        val task = tasks.find { it.id == id }
        if (task != null) {
            task.isDone = true  // меняем статус
            println("Задача '$id' отмечена как выполненная!")
        } else {
            println("Задача с ID $id не найдена.")
        }
    }
}

Объяснение: toIntOrNull() — безопасный способ преобразовать строку в число. Если пользователь ввёл "abc", вернётся null. find ищет первый элемент, подходящий под условие.

Пример 5: Удаление задачи

// Добавляем пункт 4 в меню
"4" -> {
    print("Введите ID задачи для удаления: ")
    val id = readLine()?.toIntOrNull()
    if (id != null) {
        // removeIf удаляет все элементы, подходящие под условие
        val removed = tasks.removeIf { it.id == id }
        if (removed) {
            println("Задача удалена.")
        } else {
            println("Задача не найдена.")
        }
    } else {
        println("Ошибка: введите число!")
    }
}

Объяснение: removeIf — удобная функция, которая удаляет элементы по условию. Она возвращает true, если хотя бы один элемент был удалён.

Пример 6: Сохранение в файл

import java.io.File

// Функция сохранения
fun saveToFile(tasks: List<Task>, filename: String = "tasks.txt") {
    val file = File(filename)
    file.writeText("")  // очищаем файл
    for (task in tasks) {
        // Формат: id|название|isDone
        file.appendText("${task.id}|${task.title}|${task.isDone}
")
    }
    println("Сохранено ${tasks.size} задач.")
}

// Функция загрузки
fun loadFromFile(filename: String = "tasks.txt"): MutableList<Task> {
    val file = File(filename)
    if (!file.exists()) return mutableListOf()
    val list = mutableListOf<Task>()
    file.readLines().forEach { line ->
        val parts = line.split("|")
        if (parts.size == 3) {
            val id = parts[0].toIntOrNull() ?: return@forEach
            val isDone = parts[2].toBoolean()
            list.add(Task(id, parts[1], isDone))
        }
    }
    return list
}

// В main добавляем загрузку при старте:
val tasks = loadFromFile()
var nextId = if (tasks.isEmpty()) 1 else tasks.maxOf { it.id } + 1

// И сохранение перед выходом:
"0" -> {
    saveToFile(tasks)
    println("Выход...")
    break
}

Объяснение: Мы используем простой текстовый формат: каждая задача на отдельной строке, поля разделены символом "|". При загрузке мы восстанавливаем id, название и статус. maxOf находит максимальный id, чтобы новые задачи не получили уже существующий номер.

Пример 7: Полная программа с обработкой ошибок

import java.io.File

data class Task(val id: Int, val title: String, var isDone: Boolean) {
    fun display() {
        val status = if (isDone) "[✓]" else "[ ]"
        println("$id. $status $title")
    }
}

fun main() {
    val tasks = loadFromFile()
    var nextId = if (tasks.isEmpty()) 1 else tasks.maxOf { it.id } + 1

    while (true) {
        println("
=== ЕЖЕДНЕВНИК ===")
        println("1. Показать все задачи")
        println("2. Добавить задачу")
        println("3. Отметить выполненной")
        println("4. Удалить задачу")
        println("5. Показать только невыполненные")
        println("0. Выход и сохранение")
        print("> ")

        try {
            when (readLine()) {
                "1" -> tasks.forEach { it.display() }
                "2" -> {
                    print("Название: ")
                    val title = readLine() ?: "Без названия"
                    tasks.add(Task(nextId, title, false))
                    println("Добавлена задача #$nextId")
                    nextId++
                }
                "3" -> {
                    print("ID задачи: ")
                    val id = readLine()?.toInt() ?: throw NumberFormatException()
                    val task = tasks.find { it.id == id }
                    if (task != null) {
                        task.isDone = true
                        println("Отмечено!")
                    } else println("Не найдено")
                }
                "4" -> {
                    print("ID задачи: ")
                    val id = readLine()?.toInt() ?: throw NumberFormatException()
                    if (tasks.removeIf { it.id == id }) println("Удалено")
                    else println("Не найдено")
                }
                "5" -> tasks.filter { !it.isDone }.forEach { it.display() }
                "0" -> {
                    saveToFile(tasks)
                    println("Спасибо за использование!")
                    break
                }
                else -> println("Неверный пункт меню")
            }
        } catch (e: NumberFormatException) {
            println("Ошибка: нужно ввести число!")
        } catch (e: Exception) {
            println("Неизвестная ошибка: ${e.message}")
        }
    }
}

fun saveToFile(tasks: List<Task>, filename: String = "tasks.txt") {
    File(filename).writeText(tasks.joinToString("
") { "${it.id}|${it.title}|${it.isDone}" })
    println("Сохранено.")
}

fun loadFromFile(filename: String = "tasks.txt"): MutableList<Task> {
    val file = File(filename)
    if (!file.exists()) return mutableListOf()
    return file.readLines().mapNotNull { line ->
        val parts = line.split("|")
        if (parts.size == 3) {
            val id = parts[0].toIntOrNull() ?: return@mapNotNull null
            Task(id, parts[1], parts[2].toBoolean())
        } else null
    }.toMutableList()
}

Объяснение: Это уже полноценная программа. Обрати внимание на try-catch вокруг всего меню — если пользователь введёт букву вместо цифры, программа не упадёт, а выведет сообщение об ошибке. joinToString красиво собирает строки. mapNotNull отбрасывает некорректные строки при загрузке.

Сравнение с Python: чем похоже и чем отличается

Характеристика Kotlin Python
Типизация Статическая (тип указывается явно или выводится) Динамическая (тип определяется во время выполнения)
Скорость выполнения Высокая (компилируется в байт-код JVM) Средняя (интерпретируемый язык)
Null-безопасность Встроенная (типы с ? и !! операторы) Нет (любая переменная может быть None)
Классы Обязательны для ООП, есть data class Опциональны, можно писать функции без классов
Списки listOf (неизменяемый) / mutableListOf (изменяемый) list (изменяемый по умолчанию)
Циклы for, while, forEach for, while, list comprehension
Работа с файлами java.io.File (немногословно) open/with (лаконично)
Обработка ошибок try-catch-finally try-except-finally

Главное отличие для новичка: В Python ты можешь написать x = 5, а потом x = "строка", и это сработает. В Kotlin так нельзя — переменная сохраняет свой тип. Это кажется неудобным, но на самом деле защищает от ошибок. Представь, что ты ожидаешь число, а там строка — программа упадёт. Kotlin не даст тебе так ошибиться.

Что проще в Python: Меньше синтаксиса. Не нужно писать fun, val, фигурные скобки. Для быстрых скриптов Python удобнее.

Что проще в Kotlin: Чёткая структура. Когда проект становится большим, статическая типизация помогает не запутаться. Плюс Kotlin отлично подходит для Android-разработки.

Типичные ошибки и подводные камни

  1. Забыл про null.

    В Kotlin нельзя просто так взять и присвоить null. Если переменная может быть null, нужно писать String?. Самая частая ошибка: readLine() возвращает String?, а ты пытаешься сразу вызвать .toInt(). Решение: используй ?.toIntOrNull() или проверяй на null.

    Ошибка: val x = readLine().toInt() — не скомпилируется. Правильно: val x = readLine()?.toIntOrNull() ?: 0
  2. Путаница между val и var.

    val — неизменяемая ссылка (как final в Java). var — изменяемая. Если ты объявил список как val list = mutableListOf(), ты не можешь присвоить list = другойСписок, но можешь менять содержимое (list.add()).

  3. Бесконечный цикл без выхода.

    Если забыть break в пункте "Выход", программа никогда не закроется. Всегда проверяй, что у тебя есть условие выхода.

  4. Игнорирование исключений.

    Если пользователь введёт букву в поле для числа, программа упадёт с NumberFormatException. Всегда оборачивай ввод чисел в try-catch или используй безопасные функции типа toIntOrNull().

  5. Потеря данных при выходе.

    Если ты не сохраняешь данные в файл перед выходом, все изменения пропадут. Всегда вызывай функцию сохранения перед break.

  6. Сложная логика в одной функции.

    Новички часто пишут всю программу в main(). Это делает код нечитаемым. Разбивай на функции: showMenu(), addTask(), deleteTask().

  7. Неправильное использование when.

    В when можно использовать любые выражения, но если ты забыл else, и ни одно условие не подошло, программа ничего не сделает (но и не упадёт). Лучше всегда добавлять else для обработки неверного ввода.

Лучшие практики и советы профессионалов

  1. Начинай с бумаги.

    Прежде чем писать код, нарисуй схему программы. Какие будут пункты меню? Какие данные хранятся? Как они связаны? 10 минут планирования сэкономят 2 часа переписывания кода.

  2. Одна функция — одно действие.

    Функция должна делать только то, что написано в её названии. addTask() — только добавляет. saveToFile() — только сохраняет. Если функция делает и то, и другое, её сложно тестировать и изменять.

  3. Используй data class для данных.

    Вместо того чтобы хранить имя и телефон в двух отдельных списках, создай класс Contact. Это делает код понятнее и уменьшает количество ошибок.

  4. Пиши комментарии к сложным местам.

    Не комментируй очевидное (// прибавляем 1 к x), но объясняй, почему ты выбрал именно такой алгоритм. Это поможет тебе самому через месяц вспомнить, что ты делал.

  5. Тестируй каждую маленькую часть.

    Написал функцию добавления — запусти и проверь. Написал функцию удаления — проверь. Не пиши 200 строк кода, а потом пытайся найти ошибку. Тестируй постепенно.

  6. Используй константы для магических чисел.

    Вместо if (choice == 1) лучше завести константы: const val EXIT = 0. Тогда код легче читать и менять.

  7. Не бойся переписывать.

    Первый вариант программы почти всегда можно улучшить. Если ты видишь, что код стал запутанным, перепиши его. Это нормально. Профессионалы переписывают код чаще, чем пишут новый.

Вопросы для самопроверки

  1. Вопрос: Какие три слоя (уровня) есть в любой программе? Приведи пример для приложения "Список покупок".

    Ответ: Слой данных (класс Product с полями name, quantity, isBought), слой логики (функции addProduct, markAsBought, saveToFile), слой отображения (меню в консоли: показать список, добавить, выйти).

  2. Вопрос: Чем отличается val от var в Kotlin?

    Ответ: val — неизменяемая ссылка (нельзя переназначить), var — изменяемая. Но если val указывает на список, содержимое списка менять можно.

  3. Вопрос: Что такое data class и зачем он нужен?

    Ответ: Это класс, который автоматически генерирует toString(), equals(), hashCode() и copy(). Удобен для хранения данных без лишнего кода.

  4. Вопрос: Как безопасно преобразовать строку в число в Kotlin?

    Ответ: Использовать "123".toIntOrNull(). Если строка не является числом, вернётся null, и программа не упадёт.

  5. Вопрос: Почему в итоговом проекте важно разделять код на функции?

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

  6. Вопрос: Как сохранить данные между запусками программы?

    Ответ: Записать данные в файл (например, в текстовом формате CSV) перед выходом и загружать их при старте программы.

  7. Вопрос: Что произойдёт, если в when не указать else?

    Ответ: Если ни одно условие не совпадёт, программа просто ничего не сделает (пропустит блок). Ошибки не будет, но логика может нарушиться.

  8. Вопрос: В чём главное отличие Kotlin от Python для новичка?

    Ответ: Kotlin — статически типизированный (тип переменной фиксирован), Python — динамически типизированный. Kotlin требует больше кода, но даёт больше защиты от ошибок.

  9. Вопрос: Как найти элемент в списке по условию?

    Ответ: Использовать list.find { условие } — вернёт первый подходящий элемент или null. Или list.filter { условие } — вернёт список всех подходящих.

  10. Вопрос: Что такое "магическое число" и почему его стоит избегать?

    Ответ: Это число, значение которого непонятно из контекста (например, if (x == 7)). Лучше завести константу с понятным именем: const val MAX_TASKS = 7.

Итог и что изучить дальше

Мы разобрали, как построить итоговый проект на Kotlin. Ты узнал:

  • Что проект состоит из трёх слоёв: данные, логика, отображение.
  • Как использовать классы, списки, when, try-catch и работу с файлами.
  • Как избегать типичных ошибок и писать чистый код.
  • Чем Kotlin отличается от Python.

Теперь ты готов написать свой собственный проект. Вот несколько идей для вдохновения:

  • Телефонная книга (добавление, поиск, удаление контактов, сохранение в файл).
  • Игра "Угадай число"