From 366718a6e65df079f95aad26e9ef5f36e3e49284 Mon Sep 17 00:00:00 2001
From: Olsyy <@olsyy>
Date: Fri, 4 Apr 2025 10:46:58 +0300
Subject: [PATCH 1/2] Add Shimmering Loaders to Vacancies and Homepage screens
---
.idea/inspectionProfiles/Project_Default.xml | 4 +
.../com/codereview/feature_jobs/Homepage.kt | 89 ++++++++++++++++---
.../feature_jobs/HomepageViewModel.kt | 2 +
.../feature_vacansies/VacanciesViewModel.kt | 2 +
.../feature_vacansies/VacancyList.kt | 68 ++++++++++++--
5 files changed, 144 insertions(+), 21 deletions(-)
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
index cde3e19..7061a0d 100644
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -49,6 +49,10 @@
+
+
+
+
diff --git a/feature_jobs/src/main/java/com/codereview/feature_jobs/Homepage.kt b/feature_jobs/src/main/java/com/codereview/feature_jobs/Homepage.kt
index b68c576..ea602d2 100644
--- a/feature_jobs/src/main/java/com/codereview/feature_jobs/Homepage.kt
+++ b/feature_jobs/src/main/java/com/codereview/feature_jobs/Homepage.kt
@@ -1,6 +1,14 @@
package com.codereview.feature_jobs
+import android.util.Log
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.RepeatMode
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.infiniteRepeatable
+import androidx.compose.animation.core.rememberInfiniteTransition
+import androidx.compose.animation.core.tween
import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -12,6 +20,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.grid.GridCells
@@ -20,21 +29,29 @@ import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Refresh
-import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
@@ -52,21 +69,25 @@ fun HomeScreen(
onNavigateToVacancies: (String) -> Unit
) {
val state = viewModel.state.collectAsStateWithLifecycle()
-
+ Log.d("HomeScreenS", "HomeScreen create")
when (val currentState = state.value) {
- is HomeState.Loading -> HomeLoading()
+ is HomeState.Loading -> {
+ Log.d("HomeScreenS", "State: Loading")
+ HomeLoading()
+ }
is HomeState.Ready -> {
+ Log.d("HomeScreenS", "State: Ready")
HomePage(
currentState.data,
onNavigateToVacancies
)
}
+
is HomeState.Error -> HomeError(
errorMessage = currentState.message,
onRefreshJobs = { }
)
}
-
}
@Composable
@@ -209,15 +230,59 @@ fun JobCard(
}
@Composable
-fun HomeLoading(
- modifier: Modifier = Modifier
-) {
- Box(
- modifier = modifier
+fun HomeLoading() {
+ Column(
+ modifier = Modifier
+ .padding(15.dp)
.fillMaxSize(),
- contentAlignment = Alignment.Center
+ verticalArrangement = Arrangement.spacedBy(15.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
) {
- CircularProgressIndicator()
+ repeat(4) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(start = 16.dp, end = 16.dp)
+ .height(24.dp)
+ .shimmerEffect()
+ )
+ }
+ Spacer(modifier = Modifier.height(15.dp))
+ repeat(5) {
+ Box(
+ modifier = Modifier
+ .size(width = 190.dp, height = 120.dp)
+ .clip(shape = RoundedCornerShape(16.dp))
+ .shimmerEffect()
+ )
+ }
+ }
+}
+
+fun Modifier.shimmerEffect(): Modifier = composed {
+ var size by remember { mutableStateOf(IntSize.Zero) }
+
+ val transition = rememberInfiniteTransition()
+ val startOffsetX by transition.animateFloat(
+ initialValue = -size.width.toFloat(),
+ targetValue = size.width.toFloat(),
+ animationSpec = infiniteRepeatable(
+ animation = tween(durationMillis = 1300, easing = LinearEasing),
+ repeatMode = RepeatMode.Restart
+ )
+ )
+ background(
+ brush = Brush.linearGradient(
+ colors = listOf(
+ Color(0xFFE0E0E0),
+ Color(0xFFF5F5F5),
+ Color(0xFFE0E0E0)
+ ),
+ start = Offset(startOffsetX, 0f),
+ end = Offset(startOffsetX + size.width.toFloat(), size.height.toFloat())
+ )
+ ).onGloballyPositioned {
+ size = it.size
}
}
diff --git a/feature_jobs/src/main/java/com/codereview/feature_jobs/HomepageViewModel.kt b/feature_jobs/src/main/java/com/codereview/feature_jobs/HomepageViewModel.kt
index a3eebec..0bb22b7 100644
--- a/feature_jobs/src/main/java/com/codereview/feature_jobs/HomepageViewModel.kt
+++ b/feature_jobs/src/main/java/com/codereview/feature_jobs/HomepageViewModel.kt
@@ -7,6 +7,7 @@ import com.codereview.feature_jobs.state.HomeUiState
import com.codereview.repository.jobs_repository.JobRepository
import com.codereview.repository.jobs_repository.JobSpec
import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
@@ -29,6 +30,7 @@ class HomepageViewModel @Inject constructor(
init {
viewModelScope.launch {
_state.value = HomeState.Loading
+ delay(2000)
repository.getJobList()
.catch {
_state.emit(HomeState.Error(it.message))
diff --git a/feature_vacansies/src/main/java/com/codereview/feature_vacansies/VacanciesViewModel.kt b/feature_vacansies/src/main/java/com/codereview/feature_vacansies/VacanciesViewModel.kt
index 537c4e8..686aacc 100644
--- a/feature_vacansies/src/main/java/com/codereview/feature_vacansies/VacanciesViewModel.kt
+++ b/feature_vacansies/src/main/java/com/codereview/feature_vacansies/VacanciesViewModel.kt
@@ -12,6 +12,7 @@ import com.codereview.repository.vacancy_repository.Vacancy
import com.codereview.repository.vacancy_repository.VacancyRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
+import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -37,6 +38,7 @@ class VacanciesViewModel @Inject constructor(
) {
viewModelScope.launch {
_state.value = VacanciesState.Loading
+ delay(2000L)
Log.d("VacanciesViewModel", "getVacancies: ${uiState.value.isLoading}")
repo.getVacancyList(
specialities = specialities
diff --git a/feature_vacansies/src/main/java/com/codereview/feature_vacansies/VacancyList.kt b/feature_vacansies/src/main/java/com/codereview/feature_vacansies/VacancyList.kt
index 69d9062..0f2abe9 100644
--- a/feature_vacansies/src/main/java/com/codereview/feature_vacansies/VacancyList.kt
+++ b/feature_vacansies/src/main/java/com/codereview/feature_vacansies/VacancyList.kt
@@ -1,5 +1,11 @@
package com.codereview.feature_vacansies
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.RepeatMode
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.infiniteRepeatable
+import androidx.compose.animation.core.rememberInfiniteTransition
+import androidx.compose.animation.core.tween
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@@ -21,16 +27,25 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
import androidx.compose.ui.draw.clip
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
@@ -145,17 +160,52 @@ fun VacancyItem(
}
@Composable
-fun VacanciesLoading(
- modifier: Modifier = Modifier
-) {
- Box(
- modifier = modifier
- .fillMaxSize()
- .background(color = PurpleGrey80)
+fun VacanciesLoading() {
+ Column(
+ modifier = Modifier
+ .padding(15.dp)
+ .fillMaxSize(),
+ verticalArrangement = Arrangement.spacedBy(15.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
) {
- CircularProgressIndicator(
- modifier = Modifier.align(Alignment.Center)
+ repeat(3) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(300.dp)
+ .padding(start = 16.dp, end = 16.dp)
+ .clip(shape = RoundedCornerShape(32.dp))
+ .shimmerEffect()
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ }
+ }
+}
+
+fun Modifier.shimmerEffect(): Modifier = composed {
+ var size by remember { mutableStateOf(IntSize.Zero) }
+
+ val transition = rememberInfiniteTransition()
+ val startOffsetX by transition.animateFloat(
+ initialValue = -size.width.toFloat(),
+ targetValue = size.width.toFloat(),
+ animationSpec = infiniteRepeatable(
+ animation = tween(durationMillis = 1300, easing = LinearEasing),
+ repeatMode = RepeatMode.Restart
+ )
+ )
+ background(
+ brush = Brush.linearGradient(
+ colors = listOf(
+ Color(0xFFE0E0E0),
+ Color(0xFFF5F5F5),
+ Color(0xFFE0E0E0)
+ ),
+ start = Offset(startOffsetX, 0f),
+ end = Offset(startOffsetX + size.width.toFloat(), size.height.toFloat())
)
+ ).onGloballyPositioned {
+ size = it.size
}
}
From 3b7ac1c7a2a0ce5db7a9c7a00dca856fb627308f Mon Sep 17 00:00:00 2001
From: Olsyy <@olsyy>
Date: Sat, 5 Apr 2025 11:10:24 +0300
Subject: [PATCH 2/2] Move Modifier Extension with Shimmer Effect to Core
Module
---
.../java/com/codereview/core/theme/Color.kt | 3 ++
.../core/theme/ui/ModifierShimmerEffect.kt | 47 +++++++++++++++++++
.../com/codereview/feature_jobs/Homepage.kt | 45 +-----------------
.../feature_vacansies/VacancyList.kt | 44 +----------------
4 files changed, 53 insertions(+), 86 deletions(-)
create mode 100644 core/src/main/java/com/codereview/core/theme/ui/ModifierShimmerEffect.kt
diff --git a/core/src/main/java/com/codereview/core/theme/Color.kt b/core/src/main/java/com/codereview/core/theme/Color.kt
index 6e470a9..9fe005a 100644
--- a/core/src/main/java/com/codereview/core/theme/Color.kt
+++ b/core/src/main/java/com/codereview/core/theme/Color.kt
@@ -13,4 +13,7 @@ val Pink40 = Color(0xFF7D5260)
val WhiteSmoke = Color(0xF5F5F5F5)
val Whisper = Color(0xECECECEC)
+val LightGray = Color(0xFFE0E0E0)
+val OffWhite = Color(0xFFF5F5F5)
+
diff --git a/core/src/main/java/com/codereview/core/theme/ui/ModifierShimmerEffect.kt b/core/src/main/java/com/codereview/core/theme/ui/ModifierShimmerEffect.kt
new file mode 100644
index 0000000..fa528ce
--- /dev/null
+++ b/core/src/main/java/com/codereview/core/theme/ui/ModifierShimmerEffect.kt
@@ -0,0 +1,47 @@
+package com.codereview.core.theme.ui
+
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.RepeatMode
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.infiniteRepeatable
+import androidx.compose.animation.core.rememberInfiniteTransition
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.background
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.unit.IntSize
+
+fun Modifier.shimmerEffect(): Modifier = composed {
+ var size by remember { mutableStateOf(IntSize.Zero) }
+
+ val transition = rememberInfiniteTransition()
+ val startOffsetX by transition.animateFloat(
+ initialValue = -size.width.toFloat(),
+ targetValue = size.width.toFloat(),
+ animationSpec = infiniteRepeatable(
+ animation = tween(durationMillis = 1300, easing = LinearEasing),
+ repeatMode = RepeatMode.Restart
+ )
+ )
+ background(
+ brush = Brush.linearGradient(
+ colors = listOf(
+ Color(0xFFE0E0E0),
+ Color(0xFFF5F5F5),
+ Color(0xFFE0E0E0)
+ ),
+ start = Offset(startOffsetX, 0f),
+ end = Offset(startOffsetX + size.width.toFloat(), size.height.toFloat())
+ )
+ ).onGloballyPositioned {
+ size = it.size
+ }
+}
\ No newline at end of file
diff --git a/feature_jobs/src/main/java/com/codereview/feature_jobs/Homepage.kt b/feature_jobs/src/main/java/com/codereview/feature_jobs/Homepage.kt
index ea602d2..3be57cb 100644
--- a/feature_jobs/src/main/java/com/codereview/feature_jobs/Homepage.kt
+++ b/feature_jobs/src/main/java/com/codereview/feature_jobs/Homepage.kt
@@ -1,14 +1,7 @@
package com.codereview.feature_jobs
import android.util.Log
-import androidx.compose.animation.core.LinearEasing
-import androidx.compose.animation.core.RepeatMode
-import androidx.compose.animation.core.animateFloat
-import androidx.compose.animation.core.infiniteRepeatable
-import androidx.compose.animation.core.rememberInfiniteTransition
-import androidx.compose.animation.core.tween
import androidx.compose.foundation.Image
-import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -35,29 +28,21 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
import androidx.compose.ui.draw.clip
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.window.core.layout.WindowSizeClass
import androidx.window.core.layout.WindowWidthSizeClass
+import com.codereview.core.theme.ui.shimmerEffect
import com.codereview.feature_jobs.state.HomeState
import com.codereview.repository.jobs_repository.JobSpec
import com.codereview.core.R as coreR
@@ -75,6 +60,7 @@ fun HomeScreen(
Log.d("HomeScreenS", "State: Loading")
HomeLoading()
}
+
is HomeState.Ready -> {
Log.d("HomeScreenS", "State: Ready")
HomePage(
@@ -259,33 +245,6 @@ fun HomeLoading() {
}
}
-fun Modifier.shimmerEffect(): Modifier = composed {
- var size by remember { mutableStateOf(IntSize.Zero) }
-
- val transition = rememberInfiniteTransition()
- val startOffsetX by transition.animateFloat(
- initialValue = -size.width.toFloat(),
- targetValue = size.width.toFloat(),
- animationSpec = infiniteRepeatable(
- animation = tween(durationMillis = 1300, easing = LinearEasing),
- repeatMode = RepeatMode.Restart
- )
- )
- background(
- brush = Brush.linearGradient(
- colors = listOf(
- Color(0xFFE0E0E0),
- Color(0xFFF5F5F5),
- Color(0xFFE0E0E0)
- ),
- start = Offset(startOffsetX, 0f),
- end = Offset(startOffsetX + size.width.toFloat(), size.height.toFloat())
- )
- ).onGloballyPositioned {
- size = it.size
- }
-}
-
@Composable
fun HomeError(
modifier: Modifier = Modifier,
diff --git a/feature_vacansies/src/main/java/com/codereview/feature_vacansies/VacancyList.kt b/feature_vacansies/src/main/java/com/codereview/feature_vacansies/VacancyList.kt
index 0f2abe9..6a2e814 100644
--- a/feature_vacansies/src/main/java/com/codereview/feature_vacansies/VacancyList.kt
+++ b/feature_vacansies/src/main/java/com/codereview/feature_vacansies/VacancyList.kt
@@ -1,11 +1,5 @@
package com.codereview.feature_vacansies
-import androidx.compose.animation.core.LinearEasing
-import androidx.compose.animation.core.RepeatMode
-import androidx.compose.animation.core.animateFloat
-import androidx.compose.animation.core.infiniteRepeatable
-import androidx.compose.animation.core.rememberInfiniteTransition
-import androidx.compose.animation.core.tween
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@@ -25,33 +19,24 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Refresh
-import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
-import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
import androidx.compose.ui.draw.clip
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.codereview.core.theme.PurpleGrey80
import com.codereview.core.theme.Whisper
+import com.codereview.core.theme.ui.shimmerEffect
import com.codereview.feature_vacansies.state.VacanciesState
import com.codereview.repository.vacancy_repository.Vacancy
import com.codereview.core.R as coreR
@@ -182,33 +167,6 @@ fun VacanciesLoading() {
}
}
-fun Modifier.shimmerEffect(): Modifier = composed {
- var size by remember { mutableStateOf(IntSize.Zero) }
-
- val transition = rememberInfiniteTransition()
- val startOffsetX by transition.animateFloat(
- initialValue = -size.width.toFloat(),
- targetValue = size.width.toFloat(),
- animationSpec = infiniteRepeatable(
- animation = tween(durationMillis = 1300, easing = LinearEasing),
- repeatMode = RepeatMode.Restart
- )
- )
- background(
- brush = Brush.linearGradient(
- colors = listOf(
- Color(0xFFE0E0E0),
- Color(0xFFF5F5F5),
- Color(0xFFE0E0E0)
- ),
- start = Offset(startOffsetX, 0f),
- end = Offset(startOffsetX + size.width.toFloat(), size.height.toFloat())
- )
- ).onGloballyPositioned {
- size = it.size
- }
-}
-
@Composable
fun VacanciesError(
modifier: Modifier = Modifier,