- Регистрация
- 23 Авг 2023
- Сообщения
- 3,969
- Реакции
- 0
- Баллы
- 36
Ofline
Привет, Хабр! Я Android-разработчик-одиночка. За последний месяц я выпустил несколько версий своего приложения Todo Budget — комбайна для задач, финансов, заметок и помодоро-таймера на Jetpack Compose. Про сам процесс разработки я уже писал раньше, но сегодня другой разговор: в v5.0 я полностью переписал главный экран — и хочу объяснить, как именно это сделано, почему такой подход, и найти людей, готовых его по-настоящему потрепать.
До пятой версии главный экран был функциональным, но визуально скучным. Типичный Material Design без характера. Я получил несколько честных отзывов — в том числе жёстких — и решил переделать всё.
Новый Command Center строится на трёх принципах:
Ниже разберу каждый из этих элементов с реальным кодом.
Стандартного
Важный момент:
Для анимации фона я использую
Ключевая деталь: анимация идёт по оси X — начало и конец градиента сдвигаются в противоположные стороны. Это создаёт ощущение «дышащего» фона без лишних перерасчётов.
Эффект свечения на иконках — это тот же
Использование:
Отдельная история — виджет баланса. В Compose это делается через
Подводные камни Glance:
Это сознательное решение. Все данные живут в Room локально на устройстве. Никакого бэкенда, никакой синхронизации.
Причины:
Схема базы данных:
Миграции при обновлении версий — отдельная боль. Каждое изменение схемы требует явной миграции, иначе пользователь потеряет данные при обновлении. Я на это напоролся в v3.0 и потратил день на фикс.
Первая публикация получила минусы и жёсткие отзывы: «написано ИИ», «дизайн из нулевых», «вайбкод-слоп». Часть критики была справедливой. Я переосмыслил UI, переписал главный экран и выпустил v5.0.
Главный урок: публиковать рано — нормально. Получать жёсткий фидбек — нормально. Переделывать — тоже нормально. Единственное, что не нормально — игнорировать пользователей и не меняться.
Когда публикуешь обновление в открытый доступ, получаешь фидбек примерно так: «не работает», «плохо», «5 звёзд». Это мало помогает.
Закрытое тестирование устроено иначе. Мне нужны люди, которые:
Это именно то, чего не даёт автотестирование. Разработчик всегда знает, как пользоваться своим приложением. Живой пользователь — нет.
Что нужно от тестировщика:
Что я даю взамен: ранний доступ к новым фичам и возможность реально повлиять на следующий релиз.
В личные — пишите, пришлю APK или ссылку на закрытое тестирование в Play Market.
Телеграм
Приложение бесплатное, с ненавязчивой рекламой, без подписок. Таким и останется.
Если есть вопросы по реализации конкретных фич — спрашивайте в комментариях. Особенно интересно обсудить Glance-виджеты и работу с BlurMaskFilter на разных GPU.
Что было не так с прошлым UI
До пятой версии главный экран был функциональным, но визуально скучным. Типичный Material Design без характера. Я получил несколько честных отзывов — в том числе жёстких — и решил переделать всё.
Новый Command Center строится на трёх принципах:
Glass-морфизм — карточки с прозрачностью и эффектом свечения
Кастомная палитра — Electric Cyan, Gold, Emerald вместо дефолтного серого
Анимированные фоны — градиентные переходы между состояниями
Ниже разберу каждый из этих элементов с реальным кодом.
1. Glass-морфизм в Jetpack Compose
Стандартного
glass-компонента в Compose нет. Приходится собирать самому через Modifier + drawBehind + BlurMaskFilter.
Код:
fun Modifier.glassCard(
blurRadius: Float = 20f,
alpha: Float = 0.15f,
glowColor: Color = Color.Cyan
): Modifier = this
.drawBehind {
val paint = Paint().asFrameworkPaint().apply {
isAntiAlias = true
color = android.graphics.Color.TRANSPARENT
setShadowLayer(blurRadius, 0f, 0f, glowColor.copy(alpha = 0.6f).toArgb())
}
drawIntoCanvas { canvas ->
canvas.nativeCanvas.drawRoundRect(
0f, 0f, size.width, size.height,
24f, 24f, paint
)
}
}
.background(
color = Color.White.copy(alpha = alpha),
shape = RoundedCornerShape(24.dp)
)
.border(
width = 1.dp,
brush = Brush.linearGradient(
colors = listOf(
Color.White.copy(alpha = 0.4f),
Color.White.copy(alpha = 0.05f)
)
),
shape = RoundedCornerShape(24.dp)
)
Важный момент:
BlurMaskFilter не работает на всех устройствах одинаково. На некоторых старых GPU аппаратное ускорение отключает blur совсем. Поэтому всегда делайте fallback:
Код:
@Composable
fun GlassCard(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
val supportsBlur = remember {
android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S
}
Box(
modifier = modifier
.then(
if (supportsBlur) Modifier.glassCard()
else Modifier.background(
color = Color(0xFF1A1A2E).copy(alpha = 0.85f),
shape = RoundedCornerShape(24.dp)
)
)
.padding(16.dp)
) {
content()
}
}
2. Анимированный градиентный фон
Для анимации фона я использую
InfiniteTransition + animateFloat + кастомный Canvas:
Код:
@Composable
fun AnimatedGradientBackground(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
val infiniteTransition = rememberInfiniteTransition(label = "bg")
val shift by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(8000, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
),
label = "shift"
)
val colors = listOf(
Color(0xFF0A0A1A),
Color(0xFF0D1B2A),
Color(0xFF1A0A2E),
Color(0xFF0A1A0A)
)
Box(modifier = modifier) {
Canvas(modifier = Modifier.fillMaxSize()) {
val gradientBrush = Brush.linearGradient(
colors = colors,
start = Offset(size.width * shift, 0f),
end = Offset(size.width * (1f - shift), size.height)
)
drawRect(brush = gradientBrush)
}
content()
}
}
Ключевая деталь: анимация идёт по оси X — начало и конец градиента сдвигаются в противоположные стороны. Это создаёт ощущение «дышащего» фона без лишних перерасчётов.
3. Свечение иконок (Glow Effect)
Эффект свечения на иконках — это тот же
BlurMaskFilter, но применённый не к контейнеру, а к drawBehind конкретного элемента:
Код:
fun Modifier.glowEffect(
glowColor: Color,
glowRadius: Dp = 12.dp
): Modifier = this.drawBehind {
val radiusPx = glowRadius.toPx()
val paint = Paint().asFrameworkPaint().apply {
isAntiAlias = true
color = android.graphics.Color.TRANSPARENT
setShadowLayer(
radiusPx,
0f, 0f,
glowColor.copy(alpha = 0.8f).toArgb()
)
}
drawIntoCanvas { canvas ->
canvas.nativeCanvas.drawCircle(
center.x, center.y,
size.minDimension / 2f + radiusPx / 3,
paint
)
}
}
Использование:
Код:
Icon(
imageVector = Icons.Default.Star,
contentDescription = null,
tint = Color(0xFF00FFFF), // Electric Cyan
modifier = Modifier
.size(24.dp)
.glowEffect(glowColor = Color(0xFF00FFFF))
)
4. Виджет баланса на рабочем столе
Отдельная история — виджет баланса. В Compose это делается через
GlanceAppWidget (библиотека androidx.glance):
Код:
class BalanceWidget : GlanceAppWidget() {
override suspend fun provideGlance(context: Context, id: GlanceId) {
val balance = BalanceRepository(context).getBalance()
provideContent {
GlanceTheme {
BalanceWidgetContent(balance = balance)
}
}
}
}
@Composable
fun BalanceWidgetContent(balance: Double) {
Column(
modifier = GlanceModifier
.fillMaxSize()
.background(ColorProvider(Color(0xFF0D1B2A)))
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Баланс",
style = TextStyle(
color = ColorProvider(Color(0xFF00FFFF)),
fontSize = 12.sp
)
)
Text(
text = "${balance.formatAsCurrency()} ₽",
style = TextStyle(
color = ColorProvider(Color.White),
fontSize = 20.sp,
fontWeight = FontWeight.Bold
)
)
}
}
Подводные камни Glance:
Нет поддержкиCanvasи кастомной отрисовки — только базовые компоненты
Стейт виджета обновляется черезGlanceAppWidgetManager.updateIf<>(), а не через обычныйState
На Android 12+ виджеты автоматически получают скруглённые углы от системы — не нужно добавлятьRoundedCornerShape
5. Архитектура: почему нет сервера
Это сознательное решение. Все данные живут в Room локально на устройстве. Никакого бэкенда, никакой синхронизации.
Причины:
Не хочу держать сервер и платить за него
Не хочу собирать персональные данные пользователей
Финансовые данные — это чувствительная информация, и хранить её у себя на устройстве безопаснее для пользователя
Схема базы данных:
Код:
@Database(
entities = [Task::class, Transaction::class, Note::class, PomodoroSession::class],
version = 5,
exportSchema = true
)
abstract class AppDatabase : RoomDatabase() {
abstract fun taskDao(): TaskDao
abstract fun transactionDao(): TransactionDao
abstract fun noteDao(): NoteDao
abstract fun pomodoroDao(): PomodoroDao
}
Миграции при обновлении версий — отдельная боль. Каждое изменение схемы требует явной миграции, иначе пользователь потеряет данные при обновлении. Я на это напоролся в v3.0 и потратил день на фикс.
Код:
val MIGRATION_4_5 = object : Migration(4, 5) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"ALTER TABLE tasks ADD COLUMN pomodoro_count INTEGER NOT NULL DEFAULT 0"
)
}
}
Что я прошёл за этот месяц — честно
Первая публикация получила минусы и жёсткие отзывы: «написано ИИ», «дизайн из нулевых», «вайбкод-слоп». Часть критики была справедливой. Я переосмыслил UI, переписал главный экран и выпустил v5.0.
Главный урок: публиковать рано — нормально. Получать жёсткий фидбек — нормально. Переделывать — тоже нормально. Единственное, что не нормально — игнорировать пользователей и не меняться.
Почему нужны тестировщики, а не просто отзывы в сторе
Когда публикуешь обновление в открытый доступ, получаешь фидбек примерно так: «не работает», «плохо», «5 звёзд». Это мало помогает.
Закрытое тестирование устроено иначе. Мне нужны люди, которые:
Установят приложение и будут пользоваться им как реальным инструментом
Сообщат, если что-то упало, зависло или выглядит странно на их устройстве
Честно скажут, если новый дизайн неудобен — даже если он красивый
Это именно то, чего не даёт автотестирование. Разработчик всегда знает, как пользоваться своим приложением. Живой пользователь — нет.
Что нужно от тестировщика:
Android-устройство с версией 5.0+
10–15 минут в день в течение недели
Готовность написать пару строк о том, что сломалось или неудобно
Что я даю взамен: ранний доступ к новым фичам и возможность реально повлиять на следующий релиз.
В личные — пишите, пришлю APK или ссылку на закрытое тестирование в Play Market.
Телеграм
Приложение бесплатное, с ненавязчивой рекламой, без подписок. Таким и останется.
Если есть вопросы по реализации конкретных фич — спрашивайте в комментариях. Особенно интересно обсудить Glance-виджеты и работу с BlurMaskFilter на разных GPU.