[Разработка под Android, Kotlin] Пишем комикс-приключение на Kotlin
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Всем привет! Сегодня вас ждет легкая статья, которая расскажет как написать простую мобильную игру-викторину на Kotlin. Здесь я наглядно покажу как выглядит Kotlin для мобильной разработки и предложу свои идеи о том, как можно структурировать подобный проект. Что же, не буду томить вас графоманией, вперед! Сейчас вы увидите остов идеи, которая ярко воспылала, но быстро прогорела. Мы с моим другом-дизайнером придумали сделать простую мобильную игру в текстовом формате. Жанр планировался приключенческий, а скупой текст должен был быть подогрет уникальным картинками в определенном стиле. К сожалению, дальше скелета приложения дело не продвинулось, поэтому я решил вынести его на публику. Вдруг кто-то найдет новые мысли. Сразу оговорюсь, вряд ли проект можно назвать серьезным решением, и для действительно больших приложений, возможно, стоит рассмотреть более сложные абстракции. Приложение стоит воспринимать как некий MVP.Структура папокДля начала поговорим о структуре папок. Вряд ли здесь будет что-то инновационное, но я считаю структуру папок в проекте одной из самых важных и интересных вещей в программировании.
Структура папок в приложенииВ корне проекта лежит 2 Activity, которые у нас будут использоваться в приложении.StartActivity отвечает за стартовый экран приложения, где можно начать игру и потенциально разместить какие-то глобальные элементы управления (настройки, кнопочки “поделиться” и т.п.).MainActivity, которую корректнее было бы назвать как-то вроде GameActivity, будет отвечать за рендер вопросов викторины, вариантов ответов и соответствующих картинок.Папка Entity содержит некоторые доменные сущности. Файл json с самим сюжетом игры, о котором я расскажу чуть позже, будет превращаться как раз в набор моделей из папки Entity.Папка dao (от сокращения data access object) в проекте нужна для содержания сущностей, которые предоставляют доступ к данным. Именно здесь будут лежать классы, которые будут отвечать за хранение данных в Runtime, и классы, которые смогут превратить набор структурированных данных в объекты.Папка core будет содержать объекты, которые относятся непосредственно к ходу игры. По итогу таких объектов вышло достаточно мало, однако здесь кроется потенциал для расширения.Папка UI отвечает, как многие уже догадались, за какие-то интерфейсные сущности. В частности, сюда стоит помещать presenter-ы для наших activity.Модель данныхВ нашем MVP сами данные представляют собой json-файл, который содержит массив объектов. Вот пример одного из таких объектов:
{
"id": 2,
"type": "Q",
"question": "После получаса ходьбы вы наткнулись на затушенное кострище. Здесь были люди!",
"answers": [
{
"next_question_id": 5,
"answer": "Попытаться обнаружить следы"
},
{
"next_question_id": 3,
"answer": "Потрогать, вдруг кострище еще теплое"
}
]
}
Давайте разберем, что здесь для чего:
- Id - это некоторый уникальный идентификатор. Не буду вновь повторяться и рассказывать для чего вообще существуют уникальные идентификаторы. Лишь скажу, что в приложении он будет использоваться для поиска следующего вопроса
- type - строка состоящая из одного символа. Литера Q означает, что это шаг типа "question" и он имеет возможность перехода к следующему вопросу. Существуют еще типы F (Fail) и S (Success). Это особые типы вопросов, которые говорят нам о том, что игра закончена поражением или победой
- question - просто текст вопроса, который будет выведен на экране
- answers - массив ответов на текущий вопрос. Он может содержать от 0 до 4 ответов. Массив пуст, когда текущий вопрос имеет тип F или S. В объектах ответов мы имеем поля, указывающие на следующий вопрос, и текст ответа, который будет отображаться на экране
JSON файл с игрой содержится в папочке assests. Подразумевалось, что изначально данные игры будут храниться непосредственно в приложении. Однако, теоретически, мы можем получать этот JSON по сети, сохраняя его локально или в sqllite. Либо же мы можем организовать общение приложения по сети с некоторым сервером, сохраняя этот же протокол.Тут же, в модели данных, можно прикрепить ссылки на картинки, если мы их хотели бы менять. Пока это логика не реализована, она лишь планировалась. Однако ассоциацию между картинкой и вопросом хранить лучше именно здесь.Управление игрой и ее состояниемДля начала, давайте разберемся как мы будем хранить модель данных в runtime. Для этого был придуман интерфейс Store.
public interface Store {
fun getAllQuestions(): List<Question>
fun getQuestionById(id: Int): Question
fun init(context: Context): Store
}
Этот интерфейс расширяем и содержит набор методов, которые позволяют нам работать с нашими данными. Используя фабричный метод, мы можем получить какую-то определенную реализацию хранилища. В нашем MVP реализована версия хранилища с локальным JSON-ом, который превратится в коллекцию объектов, содержащихся в оперативной памяти. Теоретически, мы можем создать еще несколько реализаций, в которых, например, класс будет обращаться в sqllite или же на сервер за следующим вопросом. Все, что нам потребуется изменить в приложении - лишь написать новую реализацию хранилища.
class StoreFactory {
companion object {
fun getStore(ctx: Context): Store {
return LocalStore().init(ctx)
}
}
}
Выше я немного слукавил. Если мы хотим работать с сервером по сети или же доставать объекты из локальной базы данных, то нам определенно потребуются асинхронные операции. В противном случае, наш интерфейс будет блокироваться, и мы будем получать ошибки ANR. Для обхода этой проблемы нам нужно выполнять все IO - операции не в главном потоке. Поскольку я не очень много занимался production разработкой под андроид, то для решения этой проблемы я посоветую популярный RX. Надеюсь, более опытные разработчики под мобильные устройства предложат альтернативы. Дальше мы создаем класс игры:
class Game {
private lateinit var store: Store
private lateinit var question: Question
fun init(context: Context) {
this.store = StoreFactory.getStore(context)
question = store.getQuestionById(1)
}
fun isGameEnd(): Boolean {
return isSuccess() || isFail()
}
fun isSuccess(): Boolean {
return question.isSuccess()
}
fun isFail(): Boolean {
return question.isFail()
}
fun chooseAnswer(numberOfAnswer: Int) {
val answer: Answer = question.getAnswers()[numberOfAnswer - 1]
question = store.getQuestionById(answer.getNextQuestionId())
}
fun getQuestion(): Question {
return question
}
}
Класс содержит в себе 2 поля: экземпляр класса Store, о котором мы уже успели поговорить, и экземпляр текущего вопроса, на котором находится пользователь. Помимо этого, игра может закончится (когда type текущего вопрос F или S). Также в данном классе существует метод, который получит выбранный ответ и поменяет текущий вопрос на следующий.Достаточно много логики уже содержится в модели данных. Например информация о том, закончилась ли игра, как она закончилась. При таком подходе нам остается просто отобразить информацию на экране.Слегка неочевидный пункт содержится в модели вопроса (Question.kt). Давайте обратим внимание на реализацию геттера ответов:
fun getAnswers(): List<Answer> {
val list: MutableList<Answer> = ArrayList(this.answers)
val shouldAdd: Int = 4 - list.size
for (i in 1..shouldAdd) {
list.add(Answer("", -1))
}
return list
}
Из этого метода мы всегда возвращаем List с 4 вариантами ответа, даже если вопрос предполагает, что их 2. Мы просто добиваем нашу коллекцию пустыми объектами с невалидными id. На слое представления они просто не будут отображены, это не изменит логику поведения экрана. Это небольшой костыль для того, чтобы нам было проще рендерить объекты.Рендер игрыТут все достаточно прозаично, мы просто отображаем вопрос и 4 варианта ответа, вешаем обработчики кликов на них и даем пользователю играть. При каждом выборе ответа мы обновляем страницу с текущим вопросом, отвечает у нас за это presenter:
private fun updateView() {
if (game.isGameEnd()) {
showEndGame()
return
}
activity.setQuestion(game.getQuestion().getText())
val answers: List<Answer> = game.getQuestion().getAnswers()
answers.forEachIndexed {idx, answer -> activity.setAnswer(idx + 1, answer.getText())}
}
fun chooseAnswer(number: Int) {
game.chooseAnswer(number)
updateView()
}
Ну и когда игра закончена, мы возвращаем пользователя на StartActivity, передавая результат игры (текст текущего вопроса) в Intent:
private fun showEndGame() {
val intent = Intent(activity, StartActivity::class.java).apply {
putExtra(StartActivity.RESULT_MESSAGE, game.getQuestion().getText())
}
activity.startActivity(intent)
}
Вместо заключенияКак я уже говорил, вряд ли это приложение можно назвать максимально production-ready. Здесь много допущений и мест, где срезаются углы. Это скорее MVP, скелет, который можно развить. Ниже я напишу то, что можно было бы улучшить или добавить:
- Дизайн. Его тут вообще нет, есть пространство для маневра
- Текущие UI-элементы неплохо отображаются на моем эмуляторе, однако на реальном смартфоне есть проблемы и конфликты с темой. Над этим можно поработать
- Добавить различные картинки в модель данных, заменять картинки на экране
- Стоило бы перенести тяжелые операции парсинга json - файла в фон, добавить loader на время этого процесса
- Добавить возможность сохранять данные из json в sqllite и работать с этим
Ну и ссылка на полные исходники для тех, кто хочет посмотреть на проект комплексно.
===========
Источник:
habr.com
===========
Похожие новости:
- [Информационная безопасность, Open source, Разработка под Android, Управление сообществом, Смартфоны] Android окукливается и сообщество потворствует этому
- [Разработка мобильных приложений, Разработка под Android, Kotlin] Пишем свой профайлер для анализа производительности приложения на Android
- [Python, Разработка под iOS, Разработка под Android, Big Data] Разработка большого проекта за 6 месяцев: как не облажаться
- [Анализ и проектирование систем, Разработка под Android] «Оливье в каждой семьей свой»: или как мы придумали ещё одну многомодульную архитектуру
- Доступна сборка Android-x86 8.1-r6
- [Программирование, Разработка под Android, Kotlin] Более безопасный способ сбора потоков данных из пользовательских интерфейсов Android (перевод)
- [Информационная безопасность, Смартфоны, IT-компании] Google автоматически устанавливала на смартфоны жителей Массачусетса приложение для уведомлений о коронавирусе
- [Карьера в IT-индустрии] Начни своё путешествие по миру Luxoft и получи шанс выиграть поездку на Камчатку: Open day митап
- [Программирование, Обработка изображений, Big Data, Машинное обучение] Помогите прочитать, что здесь написано? (OCR)
- [Разработка мобильных приложений, Разработка под Android, Повышение конверсии] Персонализация инвайтов в приложении с использованием AppsFlyer
Теги для поиска: #_razrabotka_pod_android (Разработка под Android), #_kotlin, #_kotlin, #_android, #_quiz, #_mvp, #_razrabotka_pod_android (
Разработка под Android
), #_kotlin
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 25-Ноя 20:49
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Всем привет! Сегодня вас ждет легкая статья, которая расскажет как написать простую мобильную игру-викторину на Kotlin. Здесь я наглядно покажу как выглядит Kotlin для мобильной разработки и предложу свои идеи о том, как можно структурировать подобный проект. Что же, не буду томить вас графоманией, вперед! Сейчас вы увидите остов идеи, которая ярко воспылала, но быстро прогорела. Мы с моим другом-дизайнером придумали сделать простую мобильную игру в текстовом формате. Жанр планировался приключенческий, а скупой текст должен был быть подогрет уникальным картинками в определенном стиле. К сожалению, дальше скелета приложения дело не продвинулось, поэтому я решил вынести его на публику. Вдруг кто-то найдет новые мысли. Сразу оговорюсь, вряд ли проект можно назвать серьезным решением, и для действительно больших приложений, возможно, стоит рассмотреть более сложные абстракции. Приложение стоит воспринимать как некий MVP.Структура папокДля начала поговорим о структуре папок. Вряд ли здесь будет что-то инновационное, но я считаю структуру папок в проекте одной из самых важных и интересных вещей в программировании. Структура папок в приложенииВ корне проекта лежит 2 Activity, которые у нас будут использоваться в приложении.StartActivity отвечает за стартовый экран приложения, где можно начать игру и потенциально разместить какие-то глобальные элементы управления (настройки, кнопочки “поделиться” и т.п.).MainActivity, которую корректнее было бы назвать как-то вроде GameActivity, будет отвечать за рендер вопросов викторины, вариантов ответов и соответствующих картинок.Папка Entity содержит некоторые доменные сущности. Файл json с самим сюжетом игры, о котором я расскажу чуть позже, будет превращаться как раз в набор моделей из папки Entity.Папка dao (от сокращения data access object) в проекте нужна для содержания сущностей, которые предоставляют доступ к данным. Именно здесь будут лежать классы, которые будут отвечать за хранение данных в Runtime, и классы, которые смогут превратить набор структурированных данных в объекты.Папка core будет содержать объекты, которые относятся непосредственно к ходу игры. По итогу таких объектов вышло достаточно мало, однако здесь кроется потенциал для расширения.Папка UI отвечает, как многие уже догадались, за какие-то интерфейсные сущности. В частности, сюда стоит помещать presenter-ы для наших activity.Модель данныхВ нашем MVP сами данные представляют собой json-файл, который содержит массив объектов. Вот пример одного из таких объектов: {
"id": 2, "type": "Q", "question": "После получаса ходьбы вы наткнулись на затушенное кострище. Здесь были люди!", "answers": [ { "next_question_id": 5, "answer": "Попытаться обнаружить следы" }, { "next_question_id": 3, "answer": "Потрогать, вдруг кострище еще теплое" } ] }
public interface Store {
fun getAllQuestions(): List<Question> fun getQuestionById(id: Int): Question fun init(context: Context): Store } class StoreFactory {
companion object { fun getStore(ctx: Context): Store { return LocalStore().init(ctx) } } } class Game {
private lateinit var store: Store private lateinit var question: Question fun init(context: Context) { this.store = StoreFactory.getStore(context) question = store.getQuestionById(1) } fun isGameEnd(): Boolean { return isSuccess() || isFail() } fun isSuccess(): Boolean { return question.isSuccess() } fun isFail(): Boolean { return question.isFail() } fun chooseAnswer(numberOfAnswer: Int) { val answer: Answer = question.getAnswers()[numberOfAnswer - 1] question = store.getQuestionById(answer.getNextQuestionId()) } fun getQuestion(): Question { return question } } fun getAnswers(): List<Answer> {
val list: MutableList<Answer> = ArrayList(this.answers) val shouldAdd: Int = 4 - list.size for (i in 1..shouldAdd) { list.add(Answer("", -1)) } return list } private fun updateView() {
if (game.isGameEnd()) { showEndGame() return } activity.setQuestion(game.getQuestion().getText()) val answers: List<Answer> = game.getQuestion().getAnswers() answers.forEachIndexed {idx, answer -> activity.setAnswer(idx + 1, answer.getText())} } fun chooseAnswer(number: Int) { game.chooseAnswer(number) updateView() } private fun showEndGame() {
val intent = Intent(activity, StartActivity::class.java).apply { putExtra(StartActivity.RESULT_MESSAGE, game.getQuestion().getText()) } activity.startActivity(intent) }
=========== Источник: habr.com =========== Похожие новости:
Разработка под Android ), #_kotlin |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 25-Ноя 20:49
Часовой пояс: UTC + 5