Abschlussprojekt in Kotlin: Eine ausführliche Anleitung
Что это такое и зачем? (аналогия из реальной жизни)
Представь, что ты учишься в школе кулинарного искусства. Сначала ты учишься резать овощи (переменные), потом жарить яйца (условия), потом варить суп по рецепту (функции). Но в конце обучения тебе дают задание: придумать и приготовить собственное блюдо для выпускного экзамена. Ты можешь использовать любые продукты, любые техники, но блюдо должно быть съедобным, красивым и вкусным.
Итоговый практический проект в программировании — это то же самое. Это не просто задача на одну тему (например, "напиши цикл"). Это самостоятельная работа, где ты соединяешь всё, что выучил: переменные, функции, классы, списки, работу с файлами, обработку ошибок. Ты сам решаешь, как будет выглядеть программа, какие кнопки в ней будут и как она будет работать.
Важно: Итоговый проект — это не проверка того, как ты заучил синтаксис. Это проверка того, как ты умеешь думать как программист: разбивать большую задачу на маленькие, искать ошибки и доводить дело до конца.
Зачем это нужно? В реальной работе программиста никто не даёт тебе готовые кусочки кода, которые нужно просто вставить. Тебе дают задачу: "Сделай приложение для заметок" или "Напиши игру-кликер". И ты должен сам придумать архитектуру, выбрать инструменты и написать код, который будет работать.
В этой статье мы разберём, как спроектировать и написать итоговый проект на Kotlin. Мы не будем писать конкретную программу (ты сам выберешь тему), но мы разберём скелет, на который ты нанижешь свой код.
Теория: как это работает изнутри
Любой итоговый проект, даже самый сложный, состоит из трёх слоёв. Представь себе слоёный пирог:
- Слой данных (Model) — это то, с чем работает программа. Например, если ты пишешь "Телефонную книгу", данные — это контакты (имя, номер телефона, email). Если игру "Змейка" — это координаты змейки, яблока, счёт.
- Слой логики (Controller / Logic) — это правила. Как добавлять контакт? Как проверять, не съела ли змейка сама себя? Как сохранять данные в файл?
- Слой отображения (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-разработки.
Типичные ошибки и подводные камни
-
Забыл про null.
В Kotlin нельзя просто так взять и присвоить null. Если переменная может быть null, нужно писать
String?. Самая частая ошибка:readLine()возвращаетString?, а ты пытаешься сразу вызвать.toInt(). Решение: используй?.toIntOrNull()или проверяй на null.Ошибка:
val x = readLine().toInt()— не скомпилируется. Правильно:val x = readLine()?.toIntOrNull() ?: 0 -
Путаница между val и var.
val— неизменяемая ссылка (как final в Java).var— изменяемая. Если ты объявил список какval list = mutableListOf(), ты не можешь присвоитьlist = другойСписок, но можешь менять содержимое (list.add()). -
Бесконечный цикл без выхода.
Если забыть
breakв пункте "Выход", программа никогда не закроется. Всегда проверяй, что у тебя есть условие выхода. -
Игнорирование исключений.
Если пользователь введёт букву в поле для числа, программа упадёт с
NumberFormatException. Всегда оборачивай ввод чисел вtry-catchили используй безопасные функции типаtoIntOrNull(). -
Потеря данных при выходе.
Если ты не сохраняешь данные в файл перед выходом, все изменения пропадут. Всегда вызывай функцию сохранения перед
break. -
Сложная логика в одной функции.
Новички часто пишут всю программу в
main(). Это делает код нечитаемым. Разбивай на функции:showMenu(),addTask(),deleteTask(). -
Неправильное использование when.
В
whenможно использовать любые выражения, но если ты забылelse, и ни одно условие не подошло, программа ничего не сделает (но и не упадёт). Лучше всегда добавлятьelseдля обработки неверного ввода.
Лучшие практики и советы профессионалов
-
Начинай с бумаги.
Прежде чем писать код, нарисуй схему программы. Какие будут пункты меню? Какие данные хранятся? Как они связаны? 10 минут планирования сэкономят 2 часа переписывания кода.
-
Одна функция — одно действие.
Функция должна делать только то, что написано в её названии.
addTask()— только добавляет.saveToFile()— только сохраняет. Если функция делает и то, и другое, её сложно тестировать и изменять. -
Используй data class для данных.
Вместо того чтобы хранить имя и телефон в двух отдельных списках, создай класс
Contact. Это делает код понятнее и уменьшает количество ошибок. -
Пиши комментарии к сложным местам.
Не комментируй очевидное (
// прибавляем 1 к x), но объясняй, почему ты выбрал именно такой алгоритм. Это поможет тебе самому через месяц вспомнить, что ты делал. -
Тестируй каждую маленькую часть.
Написал функцию добавления — запусти и проверь. Написал функцию удаления — проверь. Не пиши 200 строк кода, а потом пытайся найти ошибку. Тестируй постепенно.
-
Используй константы для магических чисел.
Вместо
if (choice == 1)лучше завести константы:const val EXIT = 0. Тогда код легче читать и менять. -
Не бойся переписывать.
Первый вариант программы почти всегда можно улучшить. Если ты видишь, что код стал запутанным, перепиши его. Это нормально. Профессионалы переписывают код чаще, чем пишут новый.
Вопросы для самопроверки
-
Вопрос: Какие три слоя (уровня) есть в любой программе? Приведи пример для приложения "Список покупок".
Ответ: Слой данных (класс Product с полями name, quantity, isBought), слой логики (функции addProduct, markAsBought, saveToFile), слой отображения (меню в консоли: показать список, добавить, выйти).
-
Вопрос: Чем отличается
valотvarв Kotlin?Ответ:
val— неизменяемая ссылка (нельзя переназначить),var— изменяемая. Но еслиvalуказывает на список, содержимое списка менять можно. -
Вопрос: Что такое
data classи зачем он нужен?Ответ: Это класс, который автоматически генерирует
toString(),equals(),hashCode()иcopy(). Удобен для хранения данных без лишнего кода. -
Вопрос: Как безопасно преобразовать строку в число в Kotlin?
Ответ: Использовать
"123".toIntOrNull(). Если строка не является числом, вернётсяnull, и программа не упадёт. -
Вопрос: Почему в итоговом проекте важно разделять код на функции?
Ответ: Чтобы код был читаемым, переиспользуемым и легко тестируемым. Каждая функция отвечает за одно действие.
-
Вопрос: Как сохранить данные между запусками программы?
Ответ: Записать данные в файл (например, в текстовом формате CSV) перед выходом и загружать их при старте программы.
-
Вопрос: Что произойдёт, если в
whenне указатьelse?Ответ: Если ни одно условие не совпадёт, программа просто ничего не сделает (пропустит блок). Ошибки не будет, но логика может нарушиться.
-
Вопрос: В чём главное отличие Kotlin от Python для новичка?
Ответ: Kotlin — статически типизированный (тип переменной фиксирован), Python — динамически типизированный. Kotlin требует больше кода, но даёт больше защиты от ошибок.
-
Вопрос: Как найти элемент в списке по условию?
Ответ: Использовать
list.find { условие }— вернёт первый подходящий элемент илиnull. Илиlist.filter { условие }— вернёт список всех подходящих. -
Вопрос: Что такое "магическое число" и почему его стоит избегать?
Ответ: Это число, значение которого непонятно из контекста (например,
if (x == 7)). Лучше завести константу с понятным именем:const val MAX_TASKS = 7.
Итог и что изучить дальше
Мы разобрали, как построить итоговый проект на Kotlin. Ты узнал:
- Что проект состоит из трёх слоёв: данные, логика, отображение.
- Как использовать классы, списки, when, try-catch и работу с файлами.
- Как избегать типичных ошибок и писать чистый код.
- Чем Kotlin отличается от Python.
Теперь ты готов написать свой собственный проект. Вот несколько идей для вдохновения:
- Телефонная книга (добавление, поиск, удаление контактов, сохранение в файл).
- Игра "Угадай число"