В свободное время я немного увлекся android разработкой. Тема оказалась лично для меня интересной, но выделять много времени этому, конечно, возможности нет. Поэтому решил заниматься android-ом в качестве хобби.
Возможно из этого нового хобби родится несколько или более интересных приложений, возможно нет. Время покажет.
В связи с этим на сайте появляется новая рубрика «Android». Вряд ли она будет полезна профессиональным разработчикам или даже «джунам», но будет полезна лично мне.
Писать я буду на kotlin и jetpack compose, при этом сразу на material3.
После всех подготовительных работ, вроде «kotlin за 10 минут» и прочего, я прошел обучающий курс на udemy. Вот этот курс.
В рамках этого курса создавалось приложение «ShoppingList». Это список покупок и заметки с навигацией через ButtomNaviagtion. Курс неплохой, приложение написано с использованием MVVM паттерна.
Лично я изменил маленько дизайн почти всех экранов. Небольшие мелочи, вроде размеров, цветов, теней и прочего. Экран заметок сделал не через LazyColumn в виде списка, а в виде сетки с использованием LazyVerticalGrid. Добавил splashScreen и логотип приложения, добавил много настроек по выбору цвета (в курсе показан пример с одной).
Тут я просто опишу в отрыве от полного кода естественно то, где я делал по другому. Описано будет не все, ибо нет смысла писать о том, как сделать тень у карточки, но вот что-то более менее интересное обозначено будет.
FloatingActionButton
Такую вот кнопку на material3 рекомендуют делать, используя контейнер Box
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@Composable
fun MainScreen() {
Scaffold(
modifier = Modifier,
floatingActionButton = {
Box() {
FloatingActionButton(
onClick = { },
containerColor = MainBlue,
shape = CircleShape,
modifier = Modifier
.align(Alignment.Center)
.size(60.dp)
.offset(y = 50.dp)
) {
Icon(painter = painterResource(
id = R.drawable.add_icon),
contentDescription = "Add icon",
tint = Color.White
)
}
}
},
bottomBar = {
ButtonNavigation()
},
floatingActionButtonPosition = FabPosition.Center,
) {
}
}
Использование LazyVerticalGrid
LazyVerticalGrid(
columns = GridCells.Fixed(2),
contentPadding = PaddingValues(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(items = viewModel.noteList.value, itemContent = { noteItem ->
UiNoteItem(
viewModel.titleColor.value,
viewModel.descriptionColor.value,
viewModel.timeColor.value,
noteItem
) { event ->
viewModel.onEvent(event)
}
})
}
Настройки цвета через dataStoreManager
В курсе показано, как сделать экран настроек с возможностью выбора цвета для какого-то одного элемента. Я добавил множество настроек и запустил их все внутри одной корутины. Настройка сработала только для одного поля, вероятно это из-за асинхронности и борьбы за ресурсы, следовательно нужно было разделить запуск операции на отдельный вызов viewModelScope.launch, но тогда было бы много дублирующего кода.
Для viewModel настроек я сделал так:
val titleListColorItemListState = mutableStateOf<List<ColorItem>>(emptyList())
val productsListColorItemListState = mutableStateOf<List<ColorItem>>(emptyList())
val timeListColorListState = mutableStateOf<List<ColorItem>>(emptyList())
val titleNoteColorItemListState = mutableStateOf<List<ColorItem>>(emptyList())
val timeNoteColorListState = mutableStateOf<List<ColorItem>>(emptyList())
val noteDescriptionColorState = mutableStateOf<List<ColorItem>>(emptyList())
init {
loadSetting(DataStoreManager.LIST_TITLE_COLOR, titleListColorItemListState, "#FF141313")
loadSetting(DataStoreManager.PRODUCTS_TEXT_COLOR, productsListColorItemListState, "#FF141313")
loadSetting(DataStoreManager.TIME_LIST_COLOR, timeListColorListState, "#FF6B7FEB")
loadSetting(DataStoreManager.TITLE_NOTE_COLOR, titleNoteColorItemListState, "#FF141313")
loadSetting(DataStoreManager.NOTE_DESCRIPTION_COLOR, noteDescriptionColorState, "#FF3A3737")
loadSetting(DataStoreManager.TIME_NOTE_COLOR, timeNoteColorListState, "#FF6B7FEB")
}
private fun loadSetting(key: String, state: MutableState<List<ColorItem>>, defaultColor: String) {
viewModelScope.launch {
dataStoreManager.getStringPreference(key, defaultColor).collect { selectedColor ->
state.value = getColorItemList(selectedColor)
}
}
}
private fun getColorItemList(selectedColor: String) = ColorUtils.colorList.map { color ->
ColorItem(color, selectedColor == color)
}
Применяем функциональное программирование, в методе init вызываем одну и туже функцию, но с разными параметрами.
Соответственно для конечных viewModel других экранов, это будет выглядеть чуть проще.
Например для экрана заметок:
var titleColor = mutableStateOf("#FF141313")
var descriptionColor = mutableStateOf("#FF3A3737")
var timeColor = mutableStateOf("#FF6B7FEB")
init {
loadSetting(DataStoreManager.TITLE_NOTE_COLOR, titleColor, "#FF141313")
loadSetting(DataStoreManager.NOTE_DESCRIPTION_COLOR, descriptionColor, "#FF3A3737")
loadSetting(DataStoreManager.TIME_NOTE_COLOR, timeColor, "#FF6B7FEB")
}
private fun loadSetting(key: String, state: MutableState<String>, defaultColor: String) {
viewModelScope.launch {
dataStoreManager.getStringPreference(key, defaultColor).collect { color ->
state.value = color
}
}
}
В целом экран настроек я также немного поменял и сделал так:
@Composable
fun SettingsScreen(
viewModel: SettingsViewModel = hiltViewModel()
) {
val listTitleColor = viewModel.titleListColorItemListState.value
val productsColor = viewModel.productsListColorItemListState.value
val timeListColor = viewModel.timeListColorListState.value
val noteTitleColor = viewModel.titleNoteColorItemListState.value
val noteDescriptionColor = viewModel.noteDescriptionColorState.value
val noteTimeColor = viewModel.timeNoteColorListState.value
LazyColumn(
modifier = Modifier
.fillMaxSize()
.background(BackgroundColor),
contentPadding = PaddingValues(bottom = 100.dp)
) {
item {
SettingSection(title = "Цвет заголовков списка:", list = listTitleColor, viewModel, "ListTitle")
SettingSection(title = "Цвет списка продуктов:", list = productsColor, viewModel, "Products")
SettingSection(title = "Цвет времени списка:", list = timeListColor, viewModel, "TimeList")
SettingSection(title = "Цвет заголовка заметок:", list = noteTitleColor, viewModel, "NoteTitle")
SettingSection(title = "Цвет описания заметок:", list = noteDescriptionColor, viewModel, "NoteDescription")
SettingSection(title = "Цвет времени заметок:", list = noteTimeColor, viewModel, "NoteTime")
}
}
}
@Composable
fun SettingSection(title: String, list: List<ColorItem>, viewModel: SettingsViewModel, settingsType: String) {
Card(modifier = Modifier
.padding(5.dp)
.fillMaxSize(),
colors = CardDefaults.cardColors(containerColor = CardColor),
elevation = CardDefaults.cardElevation(defaultElevation = 15.dp)
) {
Text(
modifier = Modifier.padding(bottom = 8.dp, start = 5.dp),
text = title,
fontSize = 20.sp,
fontWeight = FontWeight.Bold
)
LazyRow(
modifier = Modifier
.padding(vertical = 10.dp)
) {
items(list) { item ->
UiSettingItem(item){color ->
if (settingsType == "ListTitle"){
viewModel.onEvent(SettingsEvent.OnTitleListSelectedColor(color))
} else if (settingsType == "Products"){
viewModel.onEvent(SettingsEvent.OnProductsTextSelectedColor(color))
} else if (settingsType == "TimeList"){
viewModel.onEvent(SettingsEvent.OnTimeListSelectedColor(color))
} else if (settingsType == "NoteTitle"){
viewModel.onEvent(SettingsEvent.OnTitleNoteSelectedColor(color))
} else if (settingsType == "NoteDescription") {
viewModel.onEvent(SettingsEvent.OnDescriptionNoteSelectedColor(color))
} else if (settingsType == "NoteTime") {
viewModel.onEvent(SettingsEvent.OnTimeNoteSelectedColor(color))
}
}
}
}
}
}
Вариант с передачей String параметра в Composable функцию и множеством if наверное не самый правильный, но показался самым простым.
Остальные изменения наверное не столь важны и интересны.
К сожалению выложить приложения в google play не получилось. С ноября 2023 года у google новое правило, перед выпуском приложения, оно должно пройти закрытое тестирование двадцатью разработчиками в течении 14 дней. Мое приложение крайне простое, там нечего тестировать 14 дней, да еще и в составе 20 человек, поэтому нет смысла «напрягать» кого-то ради этого. Для своих последующих приложений, которые возможно будут более интересными, я это сделаю. А пока завел аккаунт на MI developer. Это кстати бесплатно, в отличии от гугла, где за аккаунт разработчика нужно заплатить 25 долларов. Mi developer дает возможность публиковать приложения в «store» от xiaomi — «GetApps». Этот store официальный «партнер» google, он является доверительным, но правила публикации приложения там сильно проще. Особенно мне понравилось то, что тестирование приложения перед его релизом, проводится командой GetApps.
Приложение можно скачать из GetApps по этой ссылке.
Если будут проблемы с загрузкой через ссылку, то можно напрямую в приложении GetApps «вбить» ее в поиск или же имя пакета
id=com.andtree.shoppinglist
Скрины приложения: