Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEAT][#6] Home 화면 Style API 연결 #83

Merged
merged 2 commits into from
Feb 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions feature/home/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

plugins {
alias(libs.plugins.ilab.android.feature)
alias(libs.plugins.ilab.android.retrofit)
}

android {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
Expand All @@ -53,10 +55,14 @@ import com.nexters.ilab.android.core.designsystem.theme.Subtitle1
import com.nexters.ilab.android.core.designsystem.theme.Subtitle2
import com.nexters.ilab.android.core.designsystem.theme.Title1
import com.nexters.ilab.android.core.designsystem.theme.Title2
import com.nexters.ilab.android.core.domain.entity.ProfileEntity
import com.nexters.ilab.android.core.domain.entity.StyleEntity
import com.nexters.ilab.core.ui.ComponentPreview
import com.nexters.ilab.core.ui.component.BackgroundImage
import com.nexters.ilab.core.ui.component.ILabButton
import com.nexters.ilab.core.ui.component.ILabDialog
import com.nexters.ilab.core.ui.component.ILabTopAppBar
import com.nexters.ilab.core.ui.component.LoadingIndicator
import com.nexters.ilab.core.ui.component.NetworkImage
import com.nexters.ilab.core.ui.component.PagerIndicator
import com.nexters.ilab.core.ui.component.TopAppBarNavigationType
Expand All @@ -79,6 +85,9 @@ internal fun HomeRoute(
onGenerateImgBtnClick = onGenerateImgBtnClick,
openProfileImageDialog = viewModel::openProfileImageDialog,
dismissProfileImageDialog = viewModel::dismissProfileImageDialog,
getStyleList = viewModel::getStyleList,
dismissNetworkErrorDialog = viewModel::dismissNetworkErrorDialog,
setSelectedStyleImage = viewModel::setSelectedStyleImage,
)
}

Expand All @@ -90,16 +99,31 @@ internal fun HomeScreen(
onGenerateImgBtnClick: () -> Unit,
openProfileImageDialog: (Int) -> Unit,
dismissProfileImageDialog: () -> Unit,
getStyleList: () -> Unit,
dismissNetworkErrorDialog: () -> Unit,
setSelectedStyleImage: (Int) -> Unit,
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(bottom = padding.calculateBottomPadding()),
horizontalAlignment = Alignment.CenterHorizontally,
) {
if (uiState.isLoading) {
LoadingIndicator(modifier = Modifier.fillMaxSize())
}
if (uiState.isNetworkErrorDialogVisible) {
NetworkErrorDialog(
onRetryClick = {
dismissNetworkErrorDialog()
getStyleList()
// todo: getProfileList
},
)
}
if (uiState.isProfileImageDialogVisible) {
ProfileImageDialog(
profileImage = uiState.profileImageList[uiState.selectedIndex],
profileImage = uiState.selectedProfileEntity,
onCloseClick = dismissProfileImageDialog,
onGenerateImgBtnClickClick = {
dismissProfileImageDialog()
Expand All @@ -113,6 +137,7 @@ internal fun HomeScreen(
profileImageList = uiState.profileImageList,
onGenerateImgBtnClick = onGenerateImgBtnClick,
openProfileImageDialog = openProfileImageDialog,
setSelectedStyleImage = setSelectedStyleImage,
)
}
}
Expand All @@ -134,10 +159,11 @@ internal fun HomeTopAppBar(onSettingClick: () -> Unit) {

@Composable
internal fun HomeContent(
styleImageList: List<ProfileImage>,
profileImageList: List<ProfileImage>,
styleImageList: List<StyleEntity>,
profileImageList: List<ProfileEntity>,
onGenerateImgBtnClick: () -> Unit,
openProfileImageDialog: (Int) -> Unit,
setSelectedStyleImage: (Int) -> Unit,
) {
val configuration = LocalConfiguration.current
val imgSize = (configuration.screenWidthDp - 52)
Expand All @@ -152,6 +178,7 @@ internal fun HomeContent(
HomeKeywordView(
styleImageList = styleImageList,
onGenerateImgBtnClick = onGenerateImgBtnClick,
setSelectedStyleImage = setSelectedStyleImage,
)
}

Expand All @@ -178,12 +205,19 @@ internal fun HomeContent(
@OptIn(ExperimentalFoundationApi::class)
@Composable
internal fun HomeKeywordView(
styleImageList: List<ProfileImage>,
styleImageList: List<StyleEntity>,
onGenerateImgBtnClick: () -> Unit,
setSelectedStyleImage: (Int) -> Unit,
) {
val pageCount = styleImageList.size
val pagerState = rememberPagerState(pageCount = { pageCount })

LaunchedEffect(pagerState) {
snapshotFlow { pagerState.currentPage }.collect { page ->
setSelectedStyleImage(page)
}
}

Box(modifier = Modifier.fillMaxSize()) {
BackgroundImage(
resId = R.drawable.bg_home_screen,
Expand All @@ -208,13 +242,13 @@ internal fun HomeKeywordView(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = "#" + styleImageList[page].profileKeyword,
text = "#" + styleImageList[page].name,
textAlign = TextAlign.Center,
style = Title1,
)
Spacer(modifier = Modifier.height(20.dp))
NetworkImage(
imageUrl = styleImageList[page].profileImage,
imageUrl = styleImageList[page].defaultImageUrl,
contentDescription = "Style Image Example ${page + 1}",
modifier = Modifier
.clip(RoundedCornerShape(topStart = 999.dp, topEnd = 999.dp))
Expand Down Expand Up @@ -258,7 +292,7 @@ internal fun HomeKeywordView(

@Composable
internal fun KeywordSampleImageItem(
profileImage: ProfileImage,
profileImage: ProfileEntity,
imageRatio: Dp,
startDp: Dp,
endDp: Dp,
Expand All @@ -273,12 +307,12 @@ internal fun KeywordSampleImageItem(
contentAlignment = Alignment.Center,
) {
NetworkImage(
imageUrl = profileImage.profileImage,
imageUrl = profileImage.imageUrl,
contentDescription = "Profile Image",
modifier = Modifier.fillMaxSize(),
)
Text(
text = "#" + profileImage.profileKeyword,
text = "#" + profileImage.name,
style = Subtitle1,
color = Color.White,
modifier = Modifier
Expand All @@ -291,7 +325,7 @@ internal fun KeywordSampleImageItem(
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun ProfileImageDialog(
profileImage: ProfileImage,
profileImage: ProfileEntity,
onCloseClick: () -> Unit,
onGenerateImgBtnClickClick: () -> Unit,
) {
Expand All @@ -303,7 +337,7 @@ internal fun ProfileImageDialog(
) {
Box(modifier = Modifier.fillMaxSize()) {
NetworkImage(
imageUrl = profileImage.profileImage,
imageUrl = profileImage.imageUrl,
contentDescription = "Profile Image",
modifier = Modifier.fillMaxSize(),
)
Expand All @@ -330,7 +364,7 @@ internal fun ProfileImageDialog(
Column {
Spacer(modifier = Modifier.weight(1f))
Text(
text = "#" + profileImage.profileKeyword,
text = "#" + profileImage.name,
color = Color.White,
style = Title1,
modifier = Modifier.fillMaxWidth(),
Expand All @@ -357,6 +391,23 @@ internal fun ProfileImageDialog(
}
}

@Composable
internal fun NetworkErrorDialog(
onRetryClick: () -> Unit,
) {
ILabDialog(
titleResId = R.string.network_error_title,
iconResId = R.drawable.ic_network_error,
iconDescription = "Network Error Icon",
firstDescriptionResId = R.string.network_error_description1,
secondDescriptionResId = R.string.network_error_description2,
confirmTextResId = R.string.network_error_confirm,
cancelTextResId = null,
onCancelClick = {},
onConfirmClick = onRetryClick,
)
}

@Preview(showBackground = true)
@Composable
internal fun previewHomeScreen() {
Expand All @@ -367,14 +418,17 @@ internal fun previewHomeScreen() {
onGenerateImgBtnClick = {},
openProfileImageDialog = {},
dismissProfileImageDialog = {},
getStyleList = {},
dismissNetworkErrorDialog = {},
setSelectedStyleImage = {},
)
}

@ComponentPreview
@Composable
fun ProfileImageDialogPreview() {
ProfileImageDialog(
profileImage = ProfileImage(),
profileImage = ProfileEntity("", "", ""),
onCloseClick = {},
onGenerateImgBtnClickClick = {},
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package com.nexters.ilab.android.feature.home

data class ProfileImage(
val profileImage: String = "",
val profileKeyword: String = "",
)
import com.nexters.ilab.android.core.domain.entity.ProfileEntity
import com.nexters.ilab.android.core.domain.entity.StyleEntity

data class HomeState(
val isLoading: Boolean = false,
val styleImageList: List<ProfileImage> = emptyList(),
val profileImageList: List<ProfileImage> = emptyList(),
val styleImageList: List<StyleEntity> = emptyList(),
val profileImageList: List<ProfileEntity> = emptyList(),
val isProfileImageDialogVisible: Boolean = false,
val selectedIndex: Int = 0,
val isNetworkErrorDialogVisible: Boolean = false,
val selectedProfileEntity: ProfileEntity = ProfileEntity("", "", ""),
val selectedStyleEntity: StyleEntity = StyleEntity(0, "", ""),
)
Original file line number Diff line number Diff line change
@@ -1,47 +1,86 @@
package com.nexters.ilab.android.feature.home

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.nexters.ilab.android.core.domain.entity.ProfileEntity
import com.nexters.ilab.android.core.domain.repository.StyleRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import org.orbitmvi.orbit.ContainerHost
import org.orbitmvi.orbit.syntax.simple.intent
import org.orbitmvi.orbit.syntax.simple.reduce
import org.orbitmvi.orbit.viewmodel.container
import retrofit2.HttpException
import timber.log.Timber
import javax.inject.Inject

@HiltViewModel
class HomeViewModel @Inject constructor() : ViewModel(), ContainerHost<HomeState, HomeSideEffect> {
class HomeViewModel @Inject constructor(
private val styleRepository: StyleRepository,
) : ViewModel(), ContainerHost<HomeState, HomeSideEffect> {
override val container = container<HomeState, HomeSideEffect>(HomeState())

// for test
val dummyStyleImageList: List<ProfileImage> = listOf(
ProfileImage("https://picsum.photos/200/266", "몽환적인"),
ProfileImage("https://picsum.photos/200/266", "자연적인"),
ProfileImage("https://picsum.photos/200/266", "스케치"),
ProfileImage("https://picsum.photos/200/266", "고독한"),
)

val dummyProfileImageList: List<ProfileImage> = listOf(
ProfileImage("https://picsum.photos/162/336", "몽환적인"),
ProfileImage("https://picsum.photos/162/336", "자연적인"),
ProfileImage("https://picsum.photos/162/336", "몽환적인"),
ProfileImage("https://picsum.photos/162/336", "자연적인"),
ProfileImage("https://picsum.photos/162/336", "몽환적인"),
ProfileImage("https://picsum.photos/162/336", "자연적인"),
ProfileImage("https://picsum.photos/162/336", "몽환적인"),
ProfileImage("https://picsum.photos/162/336", "자연적인"),
ProfileImage("https://picsum.photos/162/336", "몽환적인"),
ProfileImage("https://picsum.photos/162/336", "자연적인"),
ProfileImage("https://picsum.photos/162/336", "몽환적인"),
ProfileImage("https://picsum.photos/162/336", "자연적인"),
val dummyProfileImageList: List<ProfileEntity> = listOf(
ProfileEntity("1", "https://picsum.photos/162/336", "느와르"),
ProfileEntity("2", "https://picsum.photos/162/336", "경성"),
ProfileEntity("3", "https://picsum.photos/162/336", "일본애니"),
ProfileEntity("4", "https://picsum.photos/162/336", "반고흐"),
ProfileEntity("5", "https://picsum.photos/162/336", "고독한"),
ProfileEntity("6", "https://picsum.photos/162/336", "몽환적인"),
ProfileEntity("7", "https://picsum.photos/162/336", "몽환적인"),
ProfileEntity("8", "https://picsum.photos/162/336", "몽환적인"),
ProfileEntity("9", "https://picsum.photos/162/336", "몽환적인"),
ProfileEntity("10", "https://picsum.photos/162/336", "몽환적인"),
ProfileEntity("11", "https://picsum.photos/162/336", "몽환적인"),
ProfileEntity("12", "https://picsum.photos/162/336", "몽환적인"),
)

init {
getStyleList()
// todo: getProfileList
intent {
reduce {
state.copy(styleImageList = dummyStyleImageList)
state.copy(profileImageList = dummyProfileImageList)
}
}
}

fun getStyleList() = intent {
viewModelScope.launch {
reduce {
state.copy(profileImageList = dummyProfileImageList)
state.copy(isLoading = true)
}
styleRepository.getStyleList()
.onSuccess { styleImageList ->
Timber.d("$styleImageList")
val endIndex = if (styleImageList.size < 4) styleImageList.size - 1 else 3
reduce {
state.copy(styleImageList = styleImageList.shuffled().slice(0..endIndex))
}
}
.onFailure { exception ->
when (exception) {
is HttpException -> {
if (exception.code() != 200) {
openNetworkErrorDialog()
}
}
else -> {
Timber.e(exception)
}
}
}
reduce {
state.copy(isLoading = false)
}
}
}

fun setSelectedStyleImage(index: Int) = intent {
if (index in 0..<state.styleImageList.size) {
reduce {
state.copy(selectedStyleEntity = state.styleImageList[index])
}
}
}
Expand All @@ -50,8 +89,10 @@ class HomeViewModel @Inject constructor() : ViewModel(), ContainerHost<HomeState
reduce {
state.copy(isProfileImageDialogVisible = true)
}
reduce {
state.copy(selectedIndex = index)
if (index in 0..<state.profileImageList.size) {
reduce {
state.copy(selectedProfileEntity = state.profileImageList[index])
}
}
}

Expand All @@ -60,4 +101,16 @@ class HomeViewModel @Inject constructor() : ViewModel(), ContainerHost<HomeState
state.copy(isProfileImageDialogVisible = false)
}
}

private fun openNetworkErrorDialog() = intent {
reduce {
state.copy(isNetworkErrorDialogVisible = true)
}
}

fun dismissNetworkErrorDialog() = intent {
reduce {
state.copy(isNetworkErrorDialogVisible = false)
}
}
}
Loading