Как создать приложение для изучения языка с помощью Compose — часть 1
23 января 2023 г.Это первая статья из новой серии, в которой я расскажу о своем пути создания приложения для изучения языка с помощью Jetpack Compose.
Я покажу вам свой ежедневный прогресс, как приложение развивалось с течением времени, почему я принял определенные решения и так далее. Я также поделюсь с вами новыми знаниями, которые я узнаю, которые могут быть вам полезны.
Когда я начал программировать, одним из моих первых проектов был веб-сайт, который я использовал для просмотра слов, которые я изучаю на других языках. Перенесемся на несколько лет вперед, и мне нужно что-то подобное. Однако на этот раз я создам его с помощью Jetpack Compose и постараюсь опубликовать в Play Store.
Если вы младший инженер, эта серия статей покажет вам мой процесс — как я прошел путь от ничего до создания приложения, готового к публикации в Play Store. Если вы старший инженер, мы надеемся, что эта серия статей научит вас чему-то новому о Jetpack Compose и даже заставит вас переосмыслить способы решения некоторых проблем.
Фон
Я назвал это приложение Lingua, что означает "язык" на латыни. Предполагается, что это будет комбинация Duolingo и Anki, а также некоторые другие вещи, которые, как мне кажется, будут полезны.
Одна из моих целей на 2023 год — выучить итальянский, и мне также нужно потратить часть своего времени на активное создание чего-то, а не просто пассивное изучение новых вещей. Я много читал о Compose, теперь пришло время применить эти знания на практике (позже я понял, что на самом деле очень мало знал о Compose).
Если я получу версию, которая выглядит хорошо и мне кажется, что она имеет смысл, я опубликую ее в Play Store.
Это первый черновик, который я создал в Figma; она далека от окончательной версии, но ведет меня в правильном направлении.
Я создаю это приложение итеративно, поэтому, как вы увидите, первые версии приложения будут выглядеть довольно уродливо, но это нормально, первоначальная цель — получить что-то, что работает. Позже я смогу вернуться к нему и улучшить дизайн.
Я разобью свои обновления по дням, чтобы вы имели разумное представление о том, сколько времени ушло на это создание. Я трачу на это около 1 часа в день.
Инструменты и библиотеки
Чтобы отслеживать свои задачи, я использую Notion. Для дизайна я использую Figma.
Я использую Kotlin и Jetpack Compose. Я буду упоминать другие библиотеки по мере их появления.
День 1
Ниже вы можете увидеть мой первый набросок главного экрана. У меня будет своего рода раздел прогресса, который покажет, сколько пользователь узнал за определенный день, за определенную неделю и за определенный месяц.
Я также хочу добавить функцию забастовки, чтобы мотивировать людей продолжать учиться. Данные очень ценны для меня, поэтому я также добавлю подробные диаграммы о вещах, которые могут быть полезны пользователю.
Вместо обычной «Практики», которая есть в большинстве приложений, я также хочу создать собственную практику, которая позволит вам выбирать, что вы хотите практиковать.
И, наконец, я хочу показать список курсов/колод, на которые подписан пользователь. С этого момента я буду называть эти карты «колодами», но название может измениться в будущем.
Мои экраны делятся на маршрут и экран. В приведенном выше примере я создал HomeRoute
и HomeScreen
. Маршрут — это то, что добавляется к навигационному графику, экрану и реальному визуальному контенту.
Здесь у нас есть основной NavHost
для приложения.
NavHost(navController = navController, startDestination = Routes.Home.route) {
composable(Routes.Home) { HomeRoute(navController) }
}
А здесь HomeRoute
, определяющий главный экран.
@Composable
fun HomeRoute(
navController: NavController,
) {
HomeScreen(...)
}
Сейчас я просто использую NavController
, но, возможно, абстрагируюсь от этого позже. Для маршрутов я только что определил запечатанный класс с моими маршрутами, на данный момент это соответствует моим потребностям, но я, вероятно, изменю это в будущем.
sealed class Routes(val route: String) {
object Home : Routes("/")
}
Имейте в виду, что это мое первое «большое» приложение с Compose, поэтому я обязательно буду делать ошибки и учиться по ходу дела.
Затем я создал другие компоненты, но пока просто присваиваю им жестко запрограммированные значения.
Стоит упомянуть, как я добился обрезки изображений флагов. Я использую катушку. У меня не было возможности обрезать изображение непосредственно, поэтому я обернул его Surface
и определил закругленный угол только для нижнего конечного угла, потому что верхний начальный угол уже обрезан картой.
Я также установил соотношение сторон 16/9, но я не на 100% доволен результатом.
Surface(
shape = RoundedCornerShape(bottomEnd = 8.dp),
) {
AsyncImage(
model = model.imageUrl,
contentDescription = "Icon",
// TODO: add placeholder
contentScale = ContentScale.FillBounds,
modifier = Modifier
.width(36.dp)
.aspectRatio(16 / 9f)
)
}
К концу первого дня я создал экран, который вы видите ниже. Он еще не работает, но это хороший шаг в том направлении, в котором я хочу двигаться.
День 2
На второй день я только что создал экран библиотеки, сейчас это довольно просто. В нем перечислены доступные колоды. В будущем я добавлю дополнительные параметры сортировки, чтобы пользователям было проще находить то, что им нужно.
У него также есть кнопка, с помощью которой пользователь может создать новую колоду.
Вот чего я добился к концу второго дня, ничего особенного. Просто LazyColumn
и FloatingActionButton
. На самом деле я также создал ViewModel
для этого экрана, поэтому мне просто нужно подключить его к источнику данных позже, и будут перечислены правильные данные.
День 3
На третий день я начал работать над экраном создания колоды. По сути, мне нужна как минимум одна колода с картами, чтобы иметь возможность разрабатывать остальную часть приложения, поэтому я сначала иду по этому пути.
Как вы можете видеть ниже, этот экран не так прост, как другие.
Во-первых, вы можете назвать свою колоду, я сначала создам ее.
После этого вы можете изменить его на общедоступный/приватный, я оставлю это на потом. Сейчас все будет общедоступно.
В правом верхнем углу вы можете увидеть флажок, это классная функция, но я не буду ее реализовывать прямо сейчас.
Посередине находится самая важная часть этого экрана — карты, составляющие колоду. На данный момент будет только один тип карточек, и это просто Input <-> Вывод.
Например, английское слово Bee на итальянском означает Ape, поэтому оно будет выглядеть примерно так: Bee <-> обезьяна Это позволяет мне делать несколько вещей:
- Покажите слово «Пчела», и пользователь должен ввести «Обезьяна».
- Покажите слово «Обезьяна», и пользователь должен ввести «Пчела».
- Если слов больше, я могу показать что-то вроде "Обезьяна", и пользователь должен будет выбрать слово из списка, например ("Яблоко", "Пчела", "Пирог").
По сути, это простая версия Duolingo. Позже я добавлю поддержку звука и изображения, но пока это просто дополнительная сложность.
Наконец, у нас есть кнопка для добавления новых карточек и кнопка сохранения для сохранения всего.
Я столкнулся с несколькими проблемами, например, как передать идентификатор колоды на этот экран? Я использую один и тот же экран для добавления и редактирования колод, поэтому идентификатор должен быть необязательным параметром.
Я решил эту проблему, создав новый объект в Routes
и добавив методы createRoute
и parse
.
object EditDeck : Routes("/deck/{id}/edit") {
fun createRoute(deckId: String?) = "/deck/$deckId/edit"
fun parse(bundle: Bundle?): String? = bundle?.getString("id")?.takeIf { it != "null" }
}
И в свои маршруты я добавил новый маршрут.
composable(Routes.EditDeck) {
val deckId = Routes.EditDeck.parse(it.arguments)
EditDeckRoute(navController = navController, deckId = deckId)
}
Затем, когда мой экран создан, я использую LaunchedEffect
для загрузки колоды в ViewModel
.
@Composable
fun EditDeckRoute(
navController: NavController,
deckId: String?,
viewModel: EditDeckViewModel = hiltViewModel()
) {
LaunchedEffect(deckId ?: "none") {
viewModel.loadDeck(deckId)
}
EditDeckScreen(...)
}
Код внутри LaunchedEffect
будет выполнен снова, если ключ изменится, в данном случае, если изменится идентификатор колоды, а это именно то, что я хочу.
К концу третьего дня я построил лишь небольшую часть этого экрана.
День 4
К четвертому дню я перестал работать на экране, чтобы создавать колоды, и переключился на экран, чтобы создавать карты. Колоды и карты являются основой этого приложения, все остальное в приложении будет вращаться вокруг них.
Пока я буду разрабатывать только тип «Текст» или, как я его называл ранее, Input <-> Вывод. Он будет содержать одно поле для ввода и одно поле для вывода (мне нужно придумать лучшие названия для этих вещей, лол).
Позже я также хочу добавить тип «Информация», который работает немного по-другому, но это на потом.
Этот экран очень похож на предыдущий, у меня есть EditDeckCardRoute
и
EditDeckCardScreen
. У нас также есть 2 ввода текста и FAB для сохранения карты.
Одно отличие состоит в том, что для создания карты мне нужен идентификатор колоды, а для изменения карты мне нужен идентификатор карты. Чтобы решить эту проблему, я создал 2 маршрута: EditCard
и AddCard
.
composable(Routes.AddCard) {
val deckId = Routes.AddCard.parse(it.arguments)
EditCardRoute(navController, deckId = deckId)
}
composable(Routes.EditCard) {
val cardId = Routes.EditCard.parse(it.arguments)
EditCardRoute(navController, cardId = cardId)
}
Вызывающий решает, какой маршрут вызывать, исходя из того, что он хочет сделать, но на стороне реализации я решил использовать тот же маршрут.
Поэтому при создании ViewModel
он либо вызывает функцию для создания новой карты, либо функцию для загрузки существующей карты.
fun load(deckId: String?, cardId: String?) {
when {
deckId == null && cardId == null -> {
Logger.e("deckId and cardId are null")
// navigate up
}
deckId != null -> createNewCard(deckId)
cardId != null -> loadCard(cardId)
}
}
Вот как выглядит этот экран к концу 4-го дня. Довольно простой экран, но теперь я могу, по крайней мере, создавать простые карты и добавлять их в заданную колоду.
В этом обновлении мы рассмотрели первые 4 дня работы этого приложения. Я поделился тем, что я буду строить и почему. Я также описал, что я строил каждый день, и немного о том, как это было построено.
Мне очень нравится эта новая серия, и я надеюсь, что вам тоже. Если вам понравилось это обновление, поделитесь им с друзьями и коллегами, ….
Следите за обновлениями.
Фото на обложке Келли Сиккема на Unsplash.
Также опубликовано здесь.
Оригинал