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

[Session] 세션 상세 - UI 구현 #137

Merged
merged 6 commits into from
Jul 30, 2023
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
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
Expand All @@ -34,8 +33,6 @@ import com.droidknights.app2023.core.model.Session
import com.droidknights.app2023.core.model.Speaker
import com.droidknights.app2023.core.model.Tag
import kotlinx.datetime.LocalDateTime
import java.time.LocalTime
import java.time.format.DateTimeFormatter

@Composable
internal fun SessionCard(
Expand Down Expand Up @@ -71,11 +68,7 @@ internal fun SessionCard(

// 트랙
Spacer(modifier = Modifier.height(12.dp))
Row {
TrackChip(room = session.room)
Spacer(modifier = Modifier.width(8.dp))
TimeChip(LocalTime.of(16, 45))
}
SessionChips(session = session)

// 발표자
Spacer(modifier = Modifier.height(12.dp))
Expand Down Expand Up @@ -116,26 +109,6 @@ private fun CategoryChip() {
)
}

@Composable
private fun TrackChip(room: Room) {
TextChip(
text = stringResource(id = room.textRes),
containerColor = MaterialTheme.colorScheme.secondaryContainer,
labelColor = MaterialTheme.colorScheme.onSecondaryContainer,
)
}

@Composable
private fun TimeChip(time: LocalTime) {
val pattern = stringResource(id = R.string.session_time_fmt)
val formatter = remember { DateTimeFormatter.ofPattern(pattern) }
TextChip(
text = formatter.format(time),
containerColor = MaterialTheme.colorScheme.tertiaryContainer,
labelColor = MaterialTheme.colorScheme.onTertiaryContainer,
)
}

private val CardContentPadding =
PaddingValues(start = 24.dp, top = 16.dp, end = 24.dp, bottom = 24.dp)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.droidknights.app2023.feature.session

import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.droidknights.app2023.core.designsystem.component.TextChip
import com.droidknights.app2023.core.model.Room
import com.droidknights.app2023.core.model.Session
import kotlinx.datetime.toJavaLocalDateTime
import java.time.LocalTime
import java.time.format.DateTimeFormatter

@Composable
internal fun SessionChips(session: Session) {
Row {
TrackChip(room = session.room)
Spacer(modifier = Modifier.width(8.dp))
TimeChip(session.startTime.toJavaLocalDateTime().toLocalTime())
}
}

@Composable
private fun TrackChip(room: Room) {
TextChip(
text = stringResource(id = room.textRes),
containerColor = MaterialTheme.colorScheme.secondaryContainer,
labelColor = MaterialTheme.colorScheme.onSecondaryContainer,
)
}

@Composable
private fun TimeChip(time: LocalTime) {
val pattern = stringResource(id = R.string.session_time_fmt)
val formatter = remember { DateTimeFormatter.ofPattern(pattern) }
TextChip(
text = formatter.format(time),
containerColor = MaterialTheme.colorScheme.tertiaryContainer,
labelColor = MaterialTheme.colorScheme.onTertiaryContainer,
)
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,140 @@
package com.droidknights.app2023.feature.session

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.CircularProgressIndicator
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.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.droidknights.app2023.core.designsystem.component.KnightsTopAppBar
import com.droidknights.app2023.core.designsystem.component.NetworkImage
import com.droidknights.app2023.core.designsystem.component.TopAppBarNavigationType
import com.droidknights.app2023.core.designsystem.theme.KnightsTheme
import com.droidknights.app2023.core.designsystem.theme.surfaceDim
import com.droidknights.app2023.core.model.Session
import com.droidknights.app2023.core.model.Speaker

@Composable
internal fun SessionDetailScreen(
sessionId: String,
onBackClick: () -> Unit,
viewModel: SessionDetailViewModel = hiltViewModel(),
) {
// TODO : UI 구현
val sessionUiState by viewModel.sessionUiState.collectAsStateWithLifecycle()
Column(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.surfaceDim)
.systemBarsPadding(),
) {
SessionDetailTopAppBar(onBackClick = onBackClick)
SessionDetailContent(uiState = sessionUiState)
}

LaunchedEffect(sessionId) {
viewModel.fetchSession(sessionId)
}
}

@Composable
private fun SessionDetailTopAppBar(
onBackClick: () -> Unit,
) {
KnightsTopAppBar(
titleRes = R.string.session_detail_title,
navigationIconContentDescription = null,
navigationType = TopAppBarNavigationType.Back,
onNavigationClick = onBackClick,
)
}

@Composable
private fun SessionDetailContent(uiState: SessionDetailUiState) {
when (uiState) {
SessionDetailUiState.Loading -> SessionDetailLoading()
is SessionDetailUiState.Success -> SessionDetailContent(uiState.session)
}
}


@Composable
private fun SessionDetailLoading() {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
}

@Composable
private fun SessionDetailContent(session: Session) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp)
) {
SessionDetailTitle(title = session.title, modifier = Modifier.padding(top = 8.dp))
Spacer(modifier = Modifier.height(8.dp))
SessionChips(session = session)
Spacer(modifier = Modifier.height(40.dp))
SessionDetailSpeaker(session.speakers)
}
}

@Composable
private fun SessionDetailTitle(
title: String,
modifier: Modifier = Modifier,
) {
Text(
modifier = modifier.padding(end = 64.dp),
text = title,
style = KnightsTheme.typography.headlineMediumB,
color = MaterialTheme.colorScheme.onSurface,
)
}

@Composable
private fun SessionDetailSpeaker(
speakers: List<Speaker>,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
Text(
text = stringResource(id = R.string.session_detail_speaker),
style = KnightsTheme.typography.labelSmallM,
color = MaterialTheme.colorScheme.onSurface,
)
speakers.forEach { speaker ->
Text(
text = speaker.name,
style = KnightsTheme.typography.titleMediumB,
color = MaterialTheme.colorScheme.onSurface,
)
}
Spacer(Modifier.height(8.dp))
NetworkImage(
imageUrl = speakers.first().imageUrl,
modifier = Modifier
.size(108.dp)
.clip(CircleShape),
placeholder = painterResource(id = com.droidknights.app2023.core.designsystem.R.drawable.placeholder_speaker)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.droidknights.app2023.feature.session

import com.droidknights.app2023.core.model.Session

sealed interface SessionDetailUiState {

object Loading : SessionDetailUiState

data class Success(val session: Session) : SessionDetailUiState
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.droidknights.app2023.feature.session

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.droidknights.app2023.core.domain.usecase.GetSessionUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
class SessionDetailViewModel @Inject constructor(
private val getSessionUseCase: GetSessionUseCase,
) : ViewModel() {

private val _sessionUiState =
MutableStateFlow<SessionDetailUiState>(SessionDetailUiState.Loading)
val sessionUiState: StateFlow<SessionDetailUiState> = _sessionUiState

fun fetchSession(sessionId: String) {
viewModelScope.launch {
val session = getSessionUseCase(sessionId)
_sessionUiState.value = SessionDetailUiState.Success(session)
}
}
}
3 changes: 3 additions & 0 deletions feature/session/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,7 @@

<string name="session_category">카테고리</string>
<string name="session_time_fmt">HH:mm 발표</string>

<string name="session_detail_title">세션 상세 정보</string>
<string name="session_detail_speaker">발표자</string>
</resources>
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.droidknights.app2023.feature.session

import app.cash.turbine.test
import com.droidknights.app2023.core.domain.usecase.GetSessionUseCase
import com.droidknights.app2023.core.model.Level
import com.droidknights.app2023.core.model.Room
import com.droidknights.app2023.core.model.Session
import com.droidknights.app2023.core.testing.rule.MainDispatcherRule
import io.mockk.coEvery
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import kotlinx.datetime.LocalDateTime
import org.junit.Rule
import org.junit.Test
import kotlin.test.assertIs

class SessionDetailViewModelTest {
@get:Rule
val dispatcherRule = MainDispatcherRule()

private val getSessionUseCase: GetSessionUseCase = mockk()
private lateinit var viewModel: SessionDetailViewModel

private val fakeSession = Session(
id = "1",
title = "title",
content = emptyList(),
speakers = emptyList(),
level = Level.BASIC,
tags = emptyList(),
room = Room.TRACK1,
startTime = LocalDateTime(2023, 9, 12, 13, 0, 0),
endTime = LocalDateTime(2023, 9, 12, 13, 30, 0),
)

@Test
fun `세션 데이터를 확인할 수 있다`() = runTest {
// given
val sessionId = "1"
coEvery { getSessionUseCase(sessionId) } returns fakeSession
viewModel = SessionDetailViewModel(getSessionUseCase)

// when
viewModel.fetchSession(sessionId)

// then
viewModel.sessionUiState.test {
val actual = awaitItem()
assertIs<SessionDetailUiState.Success>(actual)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ internal class SessionViewModelTest {
private lateinit var viewModel: SessionViewModel

private val fakeSession = Session(
id = "1",
title = "title",
content = emptyList(),
speakers = emptyList(),
Expand Down