diff --git a/README.md b/README.md index 5477fb10..e18a48c9 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ UI Components includes several custom views for Android platform to make develop * [TimelineView](libraries/timeline-view): **TimelineView** for creating a timeline and show actions on it. * [TimelineViewCompose](libraries/timeline-view-compose): **TimelineView** for creating a timeline for compose and show actions on it. * [FitOptionMessage](libraries/fit-option-message-view): **FitOptionMessageView** for displaying text views with clipped imageviews. - +* [Shimmer](libraries/shimmer-compose): **Shimmer** for displaying shimmer for loading state. License -------- Copyright 2022 Trendyol.com diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt index 014db04c..4e627995 100644 --- a/buildSrc/src/main/kotlin/Dependencies.kt +++ b/buildSrc/src/main/kotlin/Dependencies.kt @@ -15,6 +15,7 @@ object Dependencies { const val composeBom = "androidx.compose:compose-bom:2023.05.01" const val composeActivity = "androidx.activity:activity-compose:1.3.0-alpha07" const val composeMaterial = "androidx.compose.material:material" + const val composeUiUtil = "androidx.compose.ui:ui-util" const val composeUiTooling = "androidx.compose.ui:ui-tooling" const val composeRuntime = "androidx.compose.runtime:runtime" const val composeCoil = "io.coil-kt:coil-compose:2.2.1" diff --git a/libraries/shimmer-compose/.gitignore b/libraries/shimmer-compose/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/libraries/shimmer-compose/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/libraries/shimmer-compose/README.md b/libraries/shimmer-compose/README.md new file mode 100644 index 00000000..23925e96 --- /dev/null +++ b/libraries/shimmer-compose/README.md @@ -0,0 +1,76 @@ +shimmerComposeVersion = **shimmer-compose-1.0.0** [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) + +# Shimmer Modifier + +The `shimmer` modifier allows you to display a shimmer with a custom shape for loading state. + +# Parameters + +The `Shimmer` modifier accepts the following parameters: + + +| Parameter Name | Type | Description | +|-------------------------|-------------|--------------------------------| +| isVisible | Boolean | It determines if the shimmer isVisible or not | +| isWipeOffAnimationEnabled | Boolean | It enables or disables wipe-off animation of the shimmer | +| shape | Shape | It determines the shimmer's shape | + +# Usage + +```kotlin +val title by mutableStateOf("") + +Text( + text = title, + modifier = Modifier + .fillMaxWidth() + .shimmer(title == "") +) +``` + +# Installation + +- To implement **Shimmer** to your Android project via Gradle, you need to add JitPack repository to your root build.gradle. + +```groovy + allprojects { + repositories { + // ... + + maven { url 'https://jitpack.io' } + } + } +``` + +Open your module-level `build.gradle` file and add the `Shimmer` dependency: + +```groovy +dependencies { + implementation "com.github.Trendyol.android-ui-components:shimmer-compose:$shimmerComposeVersion" +} +``` + +# Contributors + +This library is maintained mainly by Trendyol Android Team members but also other Android lovers contributes. + +# License +-------- + Copyright 2023 Trendyol.com + + Copyright 2022 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + Placeholder.kt file in androidx project has been renamed to Shimmer.kt and modified to fit our needs + https://github.com/androidx/androidx/blob/8da8b8938ee44b72bd258b15eb623680b3fcbb29/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/Placeholder.kt \ No newline at end of file diff --git a/libraries/shimmer-compose/build.gradle.kts b/libraries/shimmer-compose/build.gradle.kts new file mode 100644 index 00000000..1537bc0a --- /dev/null +++ b/libraries/shimmer-compose/build.gradle.kts @@ -0,0 +1,47 @@ +plugins { + id(Plugins.androidLibrary) + id(Plugins.kotlinAndroid) + id(Plugins.mavenPublish) +} + +android { + namespace = "com.trendyol.shimmercompose" + compileSdk = Configs.compileSdkVersion + + defaultConfig { + minSdk = Configs.minSdkVersion + targetSdk = Configs.targetSdkVersion + + vectorDrawables.useSupportLibrary = true + } + + buildTypes { + getByName("release") { + isMinifyEnabled = false + setProguardFiles( + mutableListOf( + getDefaultProguardFile("proguard-android.txt"), + "proguard-rules.pro" + ) + ) + } + } + buildFeatures { + compose = true + } + + composeOptions { + kotlinCompilerExtensionVersion = Dependencies.kotlinCompilerExtensionVersion + } + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + implementation(platform(Dependencies.composeBom)) + implementation(Dependencies.composeMaterial) + implementation(Dependencies.composeUiUtil) + implementation(Dependencies.composeUiTooling) + implementation(Dependencies.composeRuntime) +} \ No newline at end of file diff --git a/libraries/shimmer-compose/src/main/java/com/trendyol/uicomponents/shimmercompose/Shimmer.kt b/libraries/shimmer-compose/src/main/java/com/trendyol/uicomponents/shimmercompose/Shimmer.kt new file mode 100644 index 00000000..1983f64c --- /dev/null +++ b/libraries/shimmer-compose/src/main/java/com/trendyol/uicomponents/shimmercompose/Shimmer.kt @@ -0,0 +1,403 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.trendyol.uicomponents.shimmercompose + +import androidx.compose.animation.core.withInfiniteAnimationFrameMillis +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.draw.DrawModifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.compositeOver +import androidx.compose.ui.graphics.drawOutline +import androidx.compose.ui.graphics.drawscope.ContentDrawScope +import androidx.compose.ui.layout.LayoutCoordinates +import androidx.compose.ui.layout.OnGloballyPositionedModifier +import androidx.compose.ui.layout.positionInRoot +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.debugInspectorInfo +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.util.lerp +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.isActive +import kotlin.math.max + +@Stable +class PlaceholderState internal constructor( + private val isVisible: State, + private val maxScreenDimension: Float +) { + suspend fun startShimmer() { + coroutineScope { + while (isActive) { + withInfiniteAnimationFrameMillis { + frameMillis.value = it + } + } + } + } + + val placeholderProgression: Float by derivedStateOf { + val absoluteProgression = + (frameMillis.value.mod(PLACEHOLDER_GAP_BETWEEN_ANIMATION_LOOPS_MS).coerceAtMost( + PLACEHOLDER_PROGRESSION_DURATION_MS + ).toFloat() / + PLACEHOLDER_PROGRESSION_DURATION_MS) + val progression = lerp(-maxScreenDimension, maxScreenDimension, absoluteProgression) + progression + } + + + val isShowContent: Boolean by derivedStateOf { + placeholderStage == PlaceholderStage.ShowContent + } + + val isWipeOff: Boolean by derivedStateOf { + placeholderStage == PlaceholderStage.WipeOff + } + + internal val gradientWidth: Float by derivedStateOf { + maxScreenDimension + } + + internal var placeholderStage: PlaceholderStage = + if (isVisible.value) PlaceholderStage.ShowPlaceholder + else PlaceholderStage.ShowContent + get() { + if (field != PlaceholderStage.ShowContent) { + if (startOfNextPlaceholderAnimation == 0L) { + startOfNextPlaceholderAnimation = + (frameMillis.value.div(PLACEHOLDER_GAP_BETWEEN_ANIMATION_LOOPS_MS) + 1) * + PLACEHOLDER_GAP_BETWEEN_ANIMATION_LOOPS_MS + } else if (frameMillis.value >= startOfNextPlaceholderAnimation) { + field = checkForStageTransition( + field, + isVisible.value.not(), + ) + startOfNextPlaceholderAnimation = + (frameMillis.value.div(PLACEHOLDER_GAP_BETWEEN_ANIMATION_LOOPS_MS) + 1) * + PLACEHOLDER_GAP_BETWEEN_ANIMATION_LOOPS_MS + } + } + return field + } + + private val frameMillis = mutableStateOf(0L) + + private var startOfNextPlaceholderAnimation = 0L +} + +@Composable +fun rememberShimmerState(isVisible: Boolean): PlaceholderState { + val maxScreenDimension = with(LocalDensity.current) { + Dp(max(screenHeightDp(), screenWidthDp()).toFloat()).toPx() + } + val isShimmerVisibleState = rememberUpdatedState(isVisible) + return remember { + PlaceholderState( + isShimmerVisibleState, + maxScreenDimension + ) + } +} + +private fun checkForStageTransition( + placeholderStage: PlaceholderStage, + contentReady: Boolean +): PlaceholderStage { + return if (placeholderStage == PlaceholderStage.ShowPlaceholder && contentReady) { + PlaceholderStage.WipeOff + } else if (placeholderStage == PlaceholderStage.WipeOff) { + PlaceholderStage.ShowContent + } else placeholderStage +} + +fun Modifier.shimmer( + isVisible: Boolean, + isWipeOffAnimationEnabled: Boolean = true, + shape: Shape? = null, +): Modifier = composed { + + val shimmerState = rememberShimmerState(isVisible) + + if (!shimmerState.isShowContent) { + LaunchedEffect(shimmerState) { + shimmerState.startShimmer() + } + } + + then( + Modifier + .placeholderShimmer(shimmerState, shape) + .placeholder(shimmerState, isWipeOffAnimationEnabled, shape) + ) +} + +fun Modifier.placeholder( + placeholderState: PlaceholderState, + isWipeOffAnimationEnabled: Boolean, + shape: Shape? = null, + color: Color? = null +): Modifier = composed( + inspectorInfo = debugInspectorInfo { + name = "placeholder" + properties["placeholderState"] = placeholderState + properties["shape"] = shape + properties["color"] = color + } +) { + PlaceholderModifier( + placeholderState = placeholderState, + isWipeOffAnimationEnabled = isWipeOffAnimationEnabled, + color = color ?: MaterialTheme.colors.onSurface.copy(alpha = 0.1f) + .compositeOver(MaterialTheme.colors.surface), + shape = shape ?: MaterialTheme.shapes.small + ) +} + +fun Modifier.placeholderShimmer( + placeholderState: PlaceholderState, + shape: Shape? = null, + color: Color? = null +): Modifier = composed( + inspectorInfo = debugInspectorInfo { + name = "placeholderShimmer" + properties["placeholderState"] = placeholderState + properties["shape"] = shape + properties["color"] = color + } +) { + PlaceholderShimmerModifier( + placeholderState = placeholderState, + color = color ?: MaterialTheme.colors.onSurface, + shape = shape ?: MaterialTheme.shapes.small + ) +} + +@Immutable +@JvmInline +internal value class PlaceholderStage internal constructor(internal val type: Int) { + + companion object { + val ShowPlaceholder = PlaceholderStage(0) + val WipeOff = PlaceholderStage(1) + val ShowContent = PlaceholderStage(2) + } + + override fun toString(): String { + return when (this) { + ShowPlaceholder -> "PlaceholderStage.ShowPlaceholder" + WipeOff -> "PlaceholderStage.WipeOff" + else -> "PlaceholderStage.ShowContent" + } + } +} + +private fun wipeOffBrush( + color: Color, + offset: Offset, + placeholderState: PlaceholderState +): Brush { + return Brush.linearGradient( + colors = listOf( + Color.Transparent, + color + ), + start = Offset( + x = placeholderState.placeholderProgression - offset.x, + y = placeholderState.placeholderProgression - offset.y + ), + end = Offset( + x = placeholderState.placeholderProgression - offset.x + placeholderState.gradientWidth.div( + 1.4f + ), + y = placeholderState.placeholderProgression - offset.y + placeholderState.gradientWidth.div( + 1.4f + ) + ) + ) +} + +private abstract class AbstractPlaceholderModifier( + private val alpha: Float = 1.0f, + private val shape: Shape +) : DrawModifier, OnGloballyPositionedModifier { + + private var offset by mutableStateOf(Offset.Zero) + + private var lastSize: Size? = null + private var lastLayoutDirection: LayoutDirection? = null + private var lastOutline: Outline? = null + + override fun onGloballyPositioned(coordinates: LayoutCoordinates) { + offset = coordinates.positionInRoot() + } + + abstract fun generateBrush(offset: Offset): Brush? + + override fun ContentDrawScope.draw() { + val brush = generateBrush(offset) + + drawContent() + if (brush != null) { + if (shape === RectangleShape) { + drawRect(brush) + } else { + drawOutline(brush) + } + } + } + + private fun ContentDrawScope.drawOutline(brush: Brush) { + val outline = + if (size == lastSize && layoutDirection == lastLayoutDirection) { + lastOutline!! + } else { + shape.createOutline(size, layoutDirection, this) + } + drawOutline(outline, brush = brush, alpha = alpha) + lastOutline = outline + lastSize = size + } +} + +private class PlaceholderModifier constructor( + private val placeholderState: PlaceholderState, + private val isWipeOffAnimationEnabled: Boolean, + private val color: Color, + alpha: Float = 1.0f, + val shape: Shape +) : AbstractPlaceholderModifier(alpha, shape) { + override fun generateBrush(offset: Offset): Brush? { + return when (placeholderState.placeholderStage) { + PlaceholderStage.ShowPlaceholder -> { + SolidColor(color) + } + + PlaceholderStage.WipeOff -> { + takeIf { isWipeOffAnimationEnabled }?.let { + wipeOffBrush(color, offset, placeholderState) + } + } + + else -> { + null + } + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as PlaceholderModifier + + if (placeholderState != other.placeholderState) return false + if (color != other.color) return false + if (shape != other.shape) return false + + return true + } + + override fun hashCode(): Int { + var result = placeholderState.hashCode() + result = 31 * result + color.hashCode() + result = 31 * result + shape.hashCode() + return result + } +} + +private class PlaceholderShimmerModifier constructor( + private val placeholderState: PlaceholderState, + private val color: Color, + alpha: Float = 1.0f, + val shape: Shape +) : AbstractPlaceholderModifier(alpha, shape) { + override fun generateBrush(offset: Offset): Brush? { + return if (placeholderState.placeholderStage == PlaceholderStage.ShowPlaceholder) { + Brush.linearGradient( + start = Offset( + x = placeholderState.placeholderProgression - offset.x, + y = placeholderState.placeholderProgression - offset.y + ), + end = Offset( + x = placeholderState.placeholderProgression - offset.x + + placeholderState.gradientWidth, + y = placeholderState.placeholderProgression - offset.y + + placeholderState.gradientWidth + ), + colorStops = listOf( + 0f to color.copy(alpha = 0f), + 0.65f to color.copy(alpha = 0.13f), + 1f to color.copy(alpha = 0f), + ).toTypedArray() + ) + } else { + null + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as PlaceholderShimmerModifier + + if (placeholderState != other.placeholderState) return false + if (color != other.color) return false + if (shape != other.shape) return false + + return true + } + + override fun hashCode(): Int { + var result = placeholderState.hashCode() + result = 31 * result + color.hashCode() + result = 31 * result + shape.hashCode() + return result + } +} + +@Composable +internal fun screenHeightDp() = LocalContext.current.resources.configuration.screenHeightDp + +@Composable +internal fun screenWidthDp() = LocalContext.current.resources.configuration.screenWidthDp + +internal const val PLACEHOLDER_PROGRESSION_DURATION_MS = 800L +internal const val PLACEHOLDER_DELAY_BETWEEN_PROGRESSIONS_MS = 800L +internal const val PLACEHOLDER_GAP_BETWEEN_ANIMATION_LOOPS_MS = + PLACEHOLDER_PROGRESSION_DURATION_MS + PLACEHOLDER_DELAY_BETWEEN_PROGRESSIONS_MS diff --git a/sample-compose/build.gradle.kts b/sample-compose/build.gradle.kts index 0b88502f..8e0c2e30 100644 --- a/sample-compose/build.gradle.kts +++ b/sample-compose/build.gradle.kts @@ -49,4 +49,5 @@ dependencies { implementation(projects.libraries.timelineViewCompose) implementation(projects.libraries.ratingBarCompose) + implementation(projects.libraries.shimmerCompose) } diff --git a/sample-compose/src/main/java/com/trendyol/uicomponents/samplecompose/ComponentsScreen.kt b/sample-compose/src/main/java/com/trendyol/uicomponents/samplecompose/ComponentsScreen.kt index a9699bda..c920d1f3 100644 --- a/sample-compose/src/main/java/com/trendyol/uicomponents/samplecompose/ComponentsScreen.kt +++ b/sample-compose/src/main/java/com/trendyol/uicomponents/samplecompose/ComponentsScreen.kt @@ -35,6 +35,10 @@ fun ComponentsScreen( ComponentItem(title = "RatingBar") { onItemClick(Route.RatingBar) } + + ComponentItem(title = "Shimmer") { + onItemClick(Route.Shimmer) + } } } diff --git a/sample-compose/src/main/java/com/trendyol/uicomponents/samplecompose/Route.kt b/sample-compose/src/main/java/com/trendyol/uicomponents/samplecompose/Route.kt index 9c34bd6b..37307c30 100644 --- a/sample-compose/src/main/java/com/trendyol/uicomponents/samplecompose/Route.kt +++ b/sample-compose/src/main/java/com/trendyol/uicomponents/samplecompose/Route.kt @@ -7,4 +7,6 @@ sealed class Route(val destination: String) { object TimelineView : Route("timelineView") object RatingBar : Route("ratingbar") + + object Shimmer : Route("shimmer") } diff --git a/sample-compose/src/main/java/com/trendyol/uicomponents/samplecompose/SampleComposeNavGraph.kt b/sample-compose/src/main/java/com/trendyol/uicomponents/samplecompose/SampleComposeNavGraph.kt index 5f7ddee8..165bc88c 100644 --- a/sample-compose/src/main/java/com/trendyol/uicomponents/samplecompose/SampleComposeNavGraph.kt +++ b/sample-compose/src/main/java/com/trendyol/uicomponents/samplecompose/SampleComposeNavGraph.kt @@ -32,5 +32,9 @@ fun SampleComposeNavGraph( composable(Route.RatingBar.destination) { RatingBarScreen() } + + composable(Route.Shimmer.destination) { + ShimmerScreen() + } } } \ No newline at end of file diff --git a/sample-compose/src/main/java/com/trendyol/uicomponents/samplecompose/ShimmerScreen.kt b/sample-compose/src/main/java/com/trendyol/uicomponents/samplecompose/ShimmerScreen.kt new file mode 100644 index 00000000..5037f571 --- /dev/null +++ b/sample-compose/src/main/java/com/trendyol/uicomponents/samplecompose/ShimmerScreen.kt @@ -0,0 +1,126 @@ +package com.trendyol.uicomponents.samplecompose + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.material.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.compose.viewModel +import coil.compose.AsyncImage +import com.trendyol.uicomponents.samplecompose.ui.theme.ColorPrimary +import com.trendyol.uicomponents.shimmercompose.shimmer +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +@Composable +fun ShimmerScreen(viewModel: ShimmerViewModel = viewModel()) = with(viewModel) { + Scaffold( + topBar = { + TopAppBar( + backgroundColor = ColorPrimary, + elevation = 0.dp + ) { + Text( + modifier = Modifier.padding(start = 16.dp), + text = "Shimmer", + style = MaterialTheme.typography.h6.copy(color = Color.White) + ) + } + } + ) { paddingValues -> + LazyVerticalGrid( + modifier = Modifier.padding(paddingValues), + columns = GridCells.Fixed(2), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + contentPadding = PaddingValues(16.dp) + ) { + items(20) { + ListItem(uiState.item, uiState.isLoading) + } + } + } +} + +@Composable +fun ListItem(item: Item, isLoading: Boolean) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + AsyncImage( + model = item.imageUrl, + contentDescription = null, + modifier = Modifier + .aspectRatio(1f) + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .shimmer(isLoading) + ) + + Text( + text = item.title, + modifier = Modifier + .fillMaxWidth() + .shimmer(isLoading), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } +} + +@Stable +interface ShimmerUiState { + val item: Item + val isLoading: Boolean +} + +@Stable +data class Item( + val title: String, + val imageUrl: String +) + +class MutableShimmerUiState : ShimmerUiState { + override var item: Item by mutableStateOf(Item("", "")) + override var isLoading: Boolean by mutableStateOf(true) +} + +class ShimmerViewModel : ViewModel() { + private val _uiState = MutableShimmerUiState() + val uiState: ShimmerUiState = _uiState + + init { + fetchItem() + } + + private fun fetchItem() = viewModelScope.launch { + delay(1_500) + _uiState.isLoading = false + _uiState.item = Item( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit", + "https://via.placeholder.com/600/92c952" + ) + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 2c1c2eef..c273793b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -13,6 +13,7 @@ include(":libraries:touch-delegator") include(":libraries:fit-option-message-view") include(":libraries:timeline-view-compose") include(":libraries:rating-bar-compose") +include(":libraries:shimmer-compose") rootProject.name = "trendyol-android-ui-components"