diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b5e0c10..e6e95b1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -63,19 +63,16 @@ dependencies { implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.compose) implementation(libs.androidx.activity.compose) - implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.compose.ui) implementation(libs.androidx.compose.ui.graphics) implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.material3) - androidTestImplementation(platform(libs.androidx.compose.bom)) - androidTestImplementation(libs.androidx.compose.ui.test.junit4) debugImplementation(libs.androidx.compose.ui.tooling) - debugImplementation(libs.androidx.compose.ui.test.manifest) implementation(libs.androidx.startup) implementation(libs.hilt) ksp(libs.hilt.compiler) implementation(libs.timber) + implementation(libs.coil.compose) } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a2b9a43..a679ef4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + + + Unit, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .heightIn(max = 900.dp) + .clip(RoundedCornerShape(16.dp)) + .clickable(onClick = onArtworkItemSelect), + ) { + NetworkImage( + imageUrl = artwork.imageUrl, + contentDescription = "Artwork by ${artwork.artistName}", + modifier = Modifier.fillMaxWidth(), + contentScale = ContentScale.FillWidth, + ) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 20.dp, start = 20.dp) + .align(Alignment.TopStart), + verticalAlignment = Alignment.CenterVertically, + ) { + Image( + painter = painterResource(id = R.drawable.placeholder), + contentDescription = "Profile picture of ${artwork.artistName}", + modifier = Modifier + .size(28.dp) + .clip(CircleShape), + contentScale = ContentScale.Crop, + ) + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = artwork.artistName, + style = Paragraph2, + color = Gray0, + modifier = Modifier.weight(1f), + ) + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 20.dp, end = 20.dp, bottom = 20.dp) + .align(Alignment.BottomStart), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = artwork.title, + color = Gray0, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + style = Heading4, + ) + } + } +} + +@ComponentPreview +@Composable +private fun ArtworkItemPreview() { + ZiineTheme { + ArtworkItem( + artwork = UiArtwork( + id = 1, + imageUrl = "https://example.com/artwork.jpg", + artistName = "Artist Name", + title = "Artwork Name", + ), + onArtworkItemSelect = {}, + ) + } +} diff --git a/presentation/src/main/kotlin/com/nexters/ziine/android/presentation/artworks/ArtworksNavigation.kt b/presentation/src/main/kotlin/com/nexters/ziine/android/presentation/artworks/ArtworksNavigation.kt index 87f61b4..e86d661 100644 --- a/presentation/src/main/kotlin/com/nexters/ziine/android/presentation/artworks/ArtworksNavigation.kt +++ b/presentation/src/main/kotlin/com/nexters/ziine/android/presentation/artworks/ArtworksNavigation.kt @@ -1,6 +1,5 @@ package com.nexters.ziine.android.presentation.artworks -import androidx.compose.ui.Modifier import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions @@ -11,10 +10,8 @@ fun NavController.navigateToArtworks(navOptions: NavOptions) { navigate(MainTabRoute.Artworks, navOptions) } -fun NavGraphBuilder.artworksScreen(modifier: Modifier = Modifier) { +fun NavGraphBuilder.artworksScreen() { composable { - ArtworksRoute( - modifier = modifier, - ) + ArtworksRoute() } } diff --git a/presentation/src/main/kotlin/com/nexters/ziine/android/presentation/artworks/ArtworksScreen.kt b/presentation/src/main/kotlin/com/nexters/ziine/android/presentation/artworks/ArtworksScreen.kt index 26f68de..8414caf 100644 --- a/presentation/src/main/kotlin/com/nexters/ziine/android/presentation/artworks/ArtworksScreen.kt +++ b/presentation/src/main/kotlin/com/nexters/ziine/android/presentation/artworks/ArtworksScreen.kt @@ -1,35 +1,92 @@ package com.nexters.ziine.android.presentation.artworks +import android.content.Context +import android.os.Build +import android.os.VibrationEffect +import android.os.Vibrator +import android.os.VibratorManager +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.Text +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.nexters.ziine.android.presentation.artworks.viewmodel.ArtworksUiAction +import com.nexters.ziine.android.presentation.artworks.viewmodel.ArtworksUiState +import com.nexters.ziine.android.presentation.artworks.viewmodel.ArtworksViewModel +import com.nexters.ziine.android.presentation.preview.ArtworksPreviewParameterProvider import com.nexters.ziine.android.presentation.preview.DevicePreview import com.nexters.ziine.android.presentation.ui.theme.ZiineTheme @Composable -internal fun ArtworksRoute(modifier: Modifier = Modifier) { +internal fun ArtworksRoute(artworksViewModel: ArtworksViewModel = hiltViewModel()) { + val artworksUiState by artworksViewModel.uiState.collectAsStateWithLifecycle() + ArtworksScreen( - modifier = modifier, + uiState = artworksUiState, + onAction = artworksViewModel::onAction, ) } @Composable -internal fun ArtworksScreen(modifier: Modifier = Modifier) { +internal fun ArtworksScreen( + uiState: ArtworksUiState, + onAction: (ArtworksUiAction) -> Unit, +) { + val context = LocalContext.current + val vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val vibratorManager = + context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager + vibratorManager.defaultVibrator + } else { + @Suppress("DEPRECATION") + context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator + } + Box( - modifier = modifier.fillMaxSize(), + modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center, ) { - Text(text = "작품목록") + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + items( + items = uiState.artworks, + key = { artwork -> artwork.id }, + ) { artwork -> + ArtworkItem( + artwork = artwork, + onArtworkItemSelect = { + vibrator.vibrate(VibrationEffect.createOneShot(50, VibrationEffect.DEFAULT_AMPLITUDE)) + onAction(ArtworksUiAction.OnArtworkItemSelect(artworkId = artwork.id)) + }, + ) + } + } } } @DevicePreview @Composable -private fun ArtworksScreenPreview() { +private fun ArtworksScreenPreview( + @PreviewParameter(ArtworksPreviewParameterProvider::class) + uiState: ArtworksUiState, +) { ZiineTheme { - ArtworksScreen() + ArtworksScreen( + uiState = uiState, + onAction = {}, + ) } } diff --git a/presentation/src/main/kotlin/com/nexters/ziine/android/presentation/artworks/model/UiArtwork.kt b/presentation/src/main/kotlin/com/nexters/ziine/android/presentation/artworks/model/UiArtwork.kt new file mode 100644 index 0000000..02a12c1 --- /dev/null +++ b/presentation/src/main/kotlin/com/nexters/ziine/android/presentation/artworks/model/UiArtwork.kt @@ -0,0 +1,8 @@ +package com.nexters.ziine.android.presentation.artworks.model + +data class UiArtwork( + val id: Int, + val artistName: String, + val imageUrl: String, + val title: String, +) diff --git a/presentation/src/main/kotlin/com/nexters/ziine/android/presentation/artworks/viewmodel/ArtworksUiAction.kt b/presentation/src/main/kotlin/com/nexters/ziine/android/presentation/artworks/viewmodel/ArtworksUiAction.kt new file mode 100644 index 0000000..c7e59e7 --- /dev/null +++ b/presentation/src/main/kotlin/com/nexters/ziine/android/presentation/artworks/viewmodel/ArtworksUiAction.kt @@ -0,0 +1,5 @@ +package com.nexters.ziine.android.presentation.artworks.viewmodel + +sealed interface ArtworksUiAction { + data class OnArtworkItemSelect(val artworkId: Int) : ArtworksUiAction +} diff --git a/presentation/src/main/kotlin/com/nexters/ziine/android/presentation/artworks/viewmodel/ArtworksUiEvent.kt b/presentation/src/main/kotlin/com/nexters/ziine/android/presentation/artworks/viewmodel/ArtworksUiEvent.kt new file mode 100644 index 0000000..65ef879 --- /dev/null +++ b/presentation/src/main/kotlin/com/nexters/ziine/android/presentation/artworks/viewmodel/ArtworksUiEvent.kt @@ -0,0 +1,5 @@ +package com.nexters.ziine.android.presentation.artworks.viewmodel + +sealed interface ArtworksUiEvent { + data class NavigateToArtworkDetail(val artworkId: Int) : ArtworksUiEvent +} diff --git a/presentation/src/main/kotlin/com/nexters/ziine/android/presentation/artworks/viewmodel/ArtworksUiState.kt b/presentation/src/main/kotlin/com/nexters/ziine/android/presentation/artworks/viewmodel/ArtworksUiState.kt new file mode 100644 index 0000000..c5b7d36 --- /dev/null +++ b/presentation/src/main/kotlin/com/nexters/ziine/android/presentation/artworks/viewmodel/ArtworksUiState.kt @@ -0,0 +1,10 @@ +package com.nexters.ziine.android.presentation.artworks.viewmodel + +import com.nexters.ziine.android.presentation.artworks.model.UiArtwork +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +data class ArtworksUiState( + val isLoading: Boolean = false, + val artworks: ImmutableList = persistentListOf(), +) diff --git a/presentation/src/main/kotlin/com/nexters/ziine/android/presentation/artworks/viewmodel/ArtworksViewModel.kt b/presentation/src/main/kotlin/com/nexters/ziine/android/presentation/artworks/viewmodel/ArtworksViewModel.kt new file mode 100644 index 0000000..68b1d20 --- /dev/null +++ b/presentation/src/main/kotlin/com/nexters/ziine/android/presentation/artworks/viewmodel/ArtworksViewModel.kt @@ -0,0 +1,90 @@ +package com.nexters.ziine.android.presentation.artworks.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.nexters.ziine.android.presentation.artworks.model.UiArtwork +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class ArtworksViewModel + @Inject + constructor() : ViewModel() { + private val _uiState = MutableStateFlow(ArtworksUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + private val _uiEvent = Channel() + val uiEvent: Flow = _uiEvent.receiveAsFlow() + + fun onAction(action: ArtworksUiAction) { + when (action) { + is ArtworksUiAction.OnArtworkItemSelect -> navigateToArtworkDetail(action.artworkId) + } + } + + init { + fetchAlbumList() + } + + private fun fetchAlbumList() { + viewModelScope.launch { + _uiState.update { + it.copy( + artworks = persistentListOf( + UiArtwork( + id = 1, + artistName = "Artist 1", + imageUrl = "https://placehold.co/600x400/png", + title = "Artwork 1", + ), + UiArtwork( + id = 2, + artistName = "Artist 2", + imageUrl = "https://placehold.co/400x600/png", + title = "Artwork 2", + ), + UiArtwork( + id = 3, + artistName = "Artist 3", + imageUrl = "https://placehold.co/500x500/png", + title = "Artwork 3", + ), + UiArtwork( + id = 4, + artistName = "Artist 4", + imageUrl = "https://placehold.co/300x500/png", + title = "Artwork 4", + ), + UiArtwork( + id = 5, + artistName = "Artist 5", + imageUrl = "https://placehold.co/500x300/png", + title = "Artwork 5", + ), + UiArtwork( + id = 6, + artistName = "Artist 6", + imageUrl = "https://placehold.co/400x800/png", + title = "Artwork 6", + ), + ), + ) + } + } + } + + private fun navigateToArtworkDetail(artworkId: Int) { + viewModelScope.launch { + _uiEvent.send(ArtworksUiEvent.NavigateToArtworkDetail(artworkId)) + } + } + } diff --git a/presentation/src/main/kotlin/com/nexters/ziine/android/presentation/component/NetworkImage.kt b/presentation/src/main/kotlin/com/nexters/ziine/android/presentation/component/NetworkImage.kt new file mode 100644 index 0000000..4c7edab --- /dev/null +++ b/presentation/src/main/kotlin/com/nexters/ziine/android/presentation/component/NetworkImage.kt @@ -0,0 +1,79 @@ +package com.nexters.ziine.android.presentation.component + +import androidx.compose.foundation.Image +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.res.painterResource +import coil.compose.AsyncImage +import coil.request.ImageRequest +import com.nexters.ziine.android.presentation.R +import com.nexters.ziine.android.presentation.preview.ComponentPreview +import com.nexters.ziine.android.presentation.ui.theme.ZiineTheme + +@Composable +fun NetworkImage( + imageUrl: String?, + contentDescription: String?, + modifier: Modifier = Modifier, + loadingImage: Painter = painterResource(id = R.drawable.placeholder), + // failureImage: Painter = painterResource(id = R.drawable.placeholder), + contentScale: ContentScale = ContentScale.Crop, +) { + val context = LocalContext.current + +// if (LocalInspectionMode.current) { +// Image( +// painter = loadingImage, +// contentDescription = "Example Image Icon", +// modifier = modifier, +// ) +// } else { +// CoilImage( +// imageModel = { imageUrl }, +// modifier = modifier, +// component = rememberImageComponent { +// +CrossfadePlugin(duration = 500) +// // 사진이 어떻게 나오는지 확인하기 위해 개발용으로 넣어둠, API 연동 후 제거 예정 +// // +PlaceholderPlugin.Loading(loadingImage) +// +PlaceholderPlugin.Failure(failureImage) +// }, +// imageOptions = ImageOptions( +// contentScale = contentScale, +// alignment = Alignment.Center, +// contentDescription = contentDescription, +// ), +// ) +// } + if (LocalInspectionMode.current) { + Image( + painter = loadingImage, + contentDescription = "Example Image Icon", + modifier = modifier, + ) + } else { + AsyncImage( + model = ImageRequest.Builder(context) + .data(imageUrl) + .crossfade(true) + .build(), + contentDescription = contentDescription, + modifier = modifier, + contentScale = contentScale, + ) + } +} + +@ComponentPreview +@Composable +private fun NetworkImagePreview() { + ZiineTheme { + NetworkImage( + imageUrl = "", + contentDescription = "", + ) + } +} diff --git a/presentation/src/main/kotlin/com/nexters/ziine/android/presentation/preview/ArtworksPreviewParameterProvider.kt b/presentation/src/main/kotlin/com/nexters/ziine/android/presentation/preview/ArtworksPreviewParameterProvider.kt new file mode 100644 index 0000000..18157e3 --- /dev/null +++ b/presentation/src/main/kotlin/com/nexters/ziine/android/presentation/preview/ArtworksPreviewParameterProvider.kt @@ -0,0 +1,51 @@ +package com.nexters.ziine.android.presentation.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.nexters.ziine.android.presentation.artworks.model.UiArtwork +import com.nexters.ziine.android.presentation.artworks.viewmodel.ArtworksUiState +import kotlinx.collections.immutable.persistentListOf + +internal class ArtworksPreviewParameterProvider : PreviewParameterProvider { + override val values = sequenceOf( + ArtworksUiState( + artworks = persistentListOf( + UiArtwork( + id = 1, + artistName = "Artist 1", + imageUrl = "https://via.placeholder.com/150", + title = "Artwork 1", + ), + UiArtwork( + id = 2, + artistName = "Artist 2", + imageUrl = "https://via.placeholder.com/150", + title = "Artwork 2", + ), + UiArtwork( + id = 3, + artistName = "Artist 3", + imageUrl = "https://via.placeholder.com/150", + title = "Artwork 3", + ), + UiArtwork( + id = 4, + artistName = "Artist 4", + imageUrl = "https://via.placeholder.com/150", + title = "Artwork 4", + ), + UiArtwork( + id = 5, + artistName = "Artist 5", + imageUrl = "https://via.placeholder.com/150", + title = "Artwork 5", + ), + UiArtwork( + id = 6, + artistName = "Artist 6", + imageUrl = "https://via.placeholder.com/150", + title = "Artwork 6", + ), + ), + ), + ) +} diff --git a/presentation/src/main/res/drawable/placeholder.png b/presentation/src/main/res/drawable/placeholder.png new file mode 100644 index 0000000..d1a1872 Binary files /dev/null and b/presentation/src/main/res/drawable/placeholder.png differ