Skip to content

Commit

Permalink
detail page implemented (#18)
Browse files Browse the repository at this point in the history
  • Loading branch information
VictorHVS authored Aug 3, 2023
1 parent a4302ec commit 7450eb9
Show file tree
Hide file tree
Showing 15 changed files with 301 additions and 15 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Study app that displays the characters and episodes of Rick and Morty using Jetp
- [x] Unit Tests
- [x] Sonar (https://sonarcloud.io/project/overview?id=VictorHVS_rick-n-morty)
- [x] CodeCov (https://codecov.io/gh/VictorHVS/rick-n-morty)
- [x] UX/UI using [Figma] (https://www.figma.com/file/quqLCyNbZniCM58U78lQ5g/RNM-UNIVERSE-PROJECT?type=design&node-id=54300%3A24571&mode=design&t=xgYm75a5iwueubPG-1), Material 3 and Generated images using Bing Image
- [x] UX/UI using [Figma](https://www.figma.com/file/quqLCyNbZniCM58U78lQ5g/%5BM3%5D-RNM-UNIVERSE-PROJECT?type=design&node-id=54313-25258&mode=design&t=VYaJlO1ELop5IFBA-4), Material 3 and Generated images using Bing Image
- [x] Hilt Setup
- [x] Retrofit Setup
- [ ] Room Setup
Expand Down
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ dependencies {

implementation(platform(libs.compose.bom))
implementation(libs.compose.paging3)
implementation(libs.compose.material.iconsExtended)
implementation(libs.ui)
implementation(libs.ui.graphics)
implementation(libs.ui.tooling.preview)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.victorhvs.rnm.data.datasources.remote

import com.victorhvs.rnm.data.models.Character
import com.victorhvs.rnm.data.models.Episode
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query

interface RNMService {
Expand All @@ -12,6 +14,16 @@ interface RNMService {
@Query("page") page: Int = 1
): ApiResponse<Character>

@GET("character/{id}")
suspend fun getCharacterById(
@Path("id") id: Int
): Character

@GET("episode/{ids}")
suspend fun getEpisodes(
@Path("ids") ids: List<Int>
): List<Episode>

companion object {
const val BASE_URL = "https://rickandmortyapi.com/api/"
const val TIMEOUT_IN_SECONDS = 15L
Expand Down
12 changes: 12 additions & 0 deletions app/src/main/java/com/victorhvs/rnm/data/di/NetworkModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import com.victorhvs.rnm.core.DispatcherProviderImpl
import com.victorhvs.rnm.data.datasources.remote.RNMService
import com.victorhvs.rnm.data.repositories.CharacterRepository
import com.victorhvs.rnm.data.repositories.CharacterRepositoryImpl
import com.victorhvs.rnm.data.repositories.EpisodesRepository
import com.victorhvs.rnm.data.repositories.EpisodesRepositoryImpl
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
Expand Down Expand Up @@ -74,4 +76,14 @@ object NetworkModule {
dispatcher = dispacher,
rnmService = rnmService,
)

@Provides
@Singleton
fun provideEpisodeRepository(
rnmService: RNMService,
dispacher: DispatcherProvider
): EpisodesRepository = EpisodesRepositoryImpl(
dispatcher = dispacher,
rnmService = rnmService,
)
}
4 changes: 3 additions & 1 deletion app/src/main/java/com/victorhvs/rnm/data/models/Episode.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.victorhvs.rnm.data.models

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class Episode(
@SerialName("id") val id: String,
@SerialName("id") val id: Int,
@SerialName("created") val created: String = "",
@SerialName("air_date") val airDate: String = "--/--/----",
@SerialName("characters") val characters: List<String> = emptyList(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import androidx.paging.PagingData
import com.victorhvs.rnm.data.models.Character
import kotlinx.coroutines.flow.Flow

fun interface CharacterRepository {
interface CharacterRepository {
fun searchCharacter(query: String): Flow<PagingData<Character>>

suspend fun getCharacter(id: Int): Character

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.victorhvs.rnm.data.datasources.pagingsource.SearchCharacterPagingSour
import com.victorhvs.rnm.data.datasources.remote.RNMService
import com.victorhvs.rnm.data.models.Character
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext
import javax.inject.Inject

class CharacterRepositoryImpl @Inject constructor(
Expand All @@ -28,6 +29,12 @@ class CharacterRepositoryImpl @Inject constructor(
).flow
}

override suspend fun getCharacter(id: Int): Character {
return withContext(dispatcher.io()) {
rnmService.getCharacterById(id = id)
}
}

companion object {
const val CHARS_PER_PAGE = 20
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.victorhvs.rnm.data.repositories

import com.victorhvs.rnm.data.models.Episode

interface EpisodesRepository {

suspend fun getEpisodes(ids: List<Int>): List<Episode>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.victorhvs.rnm.data.repositories

import com.victorhvs.rnm.core.DispatcherProvider
import com.victorhvs.rnm.data.datasources.remote.RNMService
import com.victorhvs.rnm.data.models.Episode
import kotlinx.coroutines.withContext
import javax.inject.Inject

class EpisodesRepositoryImpl @Inject constructor(
private val rnmService: RNMService,
private val dispatcher: DispatcherProvider
) : EpisodesRepository {
override suspend fun getEpisodes(ids: List<Int>): List<Episode> {
return withContext(dispatcher.io()) {
rnmService.getEpisodes(ids = ids)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.victorhvs.rnm.presentation.components
import android.content.res.Configuration
import androidx.compose.material3.Card
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
Expand All @@ -29,12 +30,13 @@ fun EpisodeHorizontalCard(
.semantics { testTagsAsResourceId = true }
) {
ListItem(
colors = ListItemDefaults.colors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
headlineContent = {
Text(
text = name,
maxLines = 1,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface
color = MaterialTheme.colorScheme.onSurfaceVariant
)
},
overlineContent = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package com.victorhvs.rnm.presentation.screens.detail

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Badge
import androidx.compose.material.icons.outlined.FilterFrames
import androidx.compose.material.icons.outlined.TheaterComedy
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SuggestionChip
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import com.victorhvs.rnm.R
import com.victorhvs.rnm.data.models.Character
import com.victorhvs.rnm.data.models.Episode
import com.victorhvs.rnm.presentation.components.EpisodeHorizontalCard
import com.victorhvs.rnm.presentation.components.ProgressBar
import com.victorhvs.rnm.presentation.components.RNMImage
import com.victorhvs.rnm.presentation.components.SectionTitle
import com.victorhvs.rnm.presentation.theme.spacing

@Composable
fun DetailScreen(
viewModel: DetailViewModel = hiltViewModel()
) {
val uiState = viewModel.state.collectAsState()

when (val state = uiState.value) {
is DetailViewModel.UiState.Error -> state.e.printStackTrace()
DetailViewModel.UiState.Loading -> ProgressBar()
is DetailViewModel.UiState.Success -> DetailContent(
character = state.charInfo,
episodes = state.episodes
)
}
}

@Composable
fun DetailContent(
character: Character,
episodes: List<Episode>
) {
LazyColumn(
modifier = Modifier,
verticalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.small),
) {
item {
CharInfoHeader(
modifier = Modifier.padding(top = MaterialTheme.spacing.small),
character = character
)
}

if (episodes.isNotEmpty()) {
item {
SectionTitle(
modifier = Modifier
.padding(horizontal = MaterialTheme.spacing.medium)
.fillMaxWidth(),
title = R.string.episodes
)
}

items(episodes) { episode ->
EpisodeHorizontalCard(
modifier = Modifier.padding(horizontal = MaterialTheme.spacing.medium),
position = episode.id,
name = episode.name,
episode = episode.episode,
airDate = episode.airDate
)
}
}
}
}

@Composable
fun CharInfoHeader(
modifier: Modifier = Modifier,
character: Character
) {
Column(
modifier = modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
RNMImage(
imageUrl = character.image,
contentDescription = character.name
)
Text(
text = character.name,
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onSurface
)
Row(
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.small),
) {
SuggestionChip(
onClick = { },
label = { Text(text = character.status) },
icon = {
Icon(
imageVector = Icons.Outlined.Badge,
contentDescription = character.status,
)
}
)
SuggestionChip(
onClick = { },
label = { Text(text = character.species) },
icon = {
Icon(
imageVector = Icons.Outlined.FilterFrames,
contentDescription = character.status,
)
}
)
SuggestionChip(
onClick = { },
label = {
Text(
text = stringResource(
id = R.string.n_episodes,
character.episode.size
)
)
},
icon = {
Icon(
imageVector = Icons.Outlined.TheaterComedy,
contentDescription = character.status,
)
}
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.victorhvs.rnm.presentation.screens.detail

import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.victorhvs.rnm.data.models.Character
import com.victorhvs.rnm.data.models.Episode
import com.victorhvs.rnm.data.repositories.CharacterRepository
import com.victorhvs.rnm.data.repositories.EpisodesRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
class DetailViewModel @Inject constructor(
private val charRepository: CharacterRepository,
private val epiRepository: EpisodesRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() {

private val _state = MutableStateFlow<UiState>(UiState.Loading)
val state: StateFlow<UiState> =
_state.asStateFlow()

init {
viewModelScope.launch {
runCatching {
// val charId = savedStateHandle.get<Int>("1")
val charId = 1
val charResult = charRepository.getCharacter(id = charId)
.also { char ->
_state.update {
UiState.Success(
charInfo = char
)
}
}
epiRepository.getEpisodes(ids = charResult.episode.map {
it.substringAfterLast("/").toInt()
}).also { episodes ->
_state.update {
UiState.Success(
charInfo = charResult,
episodes = episodes
)
}
}
}.onFailure { throwable ->
_state.update {
UiState.Error(
e = throwable
)
}
}
}
}

sealed class UiState {
data class Success(
val charInfo: Character,
val episodes: List<Episode> = emptyList()
) : UiState()

data object Loading : UiState()
data class Error(val e: Throwable) : UiState()
}
}
Loading

0 comments on commit 7450eb9

Please sign in to comment.