This is a movie / tv series app using Offline-First / Single-Source Of Truth (SSOT) Architecture with Clean Architecture:
- JetPack Compose Android’s recommended modern toolkit for building native UI
- Compose Navigation that is using Type Safety
- Dagger/Hilt Dependency injection library
- Coil An image loading library for Android backed by Kotlin Coroutines
- Detekt A static code analysis tool for Kotlin
- Ktlint This plugin creates convenient tasks in your Gradle project that run ktlint checks or do code auto format.
- Flow
- Coroutines
- Dependency Management with Version Catalog
- Jacoco Plugin Generate a coverage report about tests. This article explain the configuration.
- GitHub Actions to build and check test coverage
- Shared element transition During transition from list (person or movie) to it details.
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
- Figma Inspiration
- Figma Empty State
- TMDB APIs This application uses TMDB and the TMDB APIs but is not endorsed, certified, or otherwise approved by TMDB."
- Now in Android GitHub with suggested architecture.
Presented by Philipp Lackner is this video
Error handling crossing layers, with Result interface defined in domain module as following:
sealed interface Result<out D, out E : RootError> {
data class Success<out D, out E : RootError>(
val data: D,
) : Result<D, E>
data class Error<out D, out E : RootError>(
val error: E,
) : Result<D, E>
data class Loading<out D, out E : RootError>(
val isLoading: Boolean,
) : Result<D, E>
}Defining Error interface that can be implemented in different layers like:
sealed interface Error- DataError, NetworkError enum that describes each errors and can be mapped and properly show to the user in presentation layer
sealed interface DataError : Error {
enum class Network : DataError {
BAD_REQUEST,
FORBIDDEN,
REQUEST_TIMEOUT,
INTERNAL_SERVER_ERROR,
SERVICE_UNAVAILABLE,
NO_INTERNET,
UNKNOWN,
}
}In Presentation layer, implement asUiText extension function for each new ErrorType and map it to the correct description, according the user language.
fun DataError.asUiText(): UiText =
when (this) {
DataError.Network.BAD_REQUEST ->
UiText.StringResource(
R.string.data_error_network_bad_request,
)
DataError.Network.FORBIDDEN ->
UiText.StringResource(
R.string.data_error_network_forbidden,
)
}Simplify the error handling in ViewModel
movieDetailsUseCase(movieId, language, countryCode)
.onEach { result ->
when (result) {
is Result.Error -> {
_movieDetailsState.update {
AppState(error = result.error.asUiText())
}
}
is Result.Loading -> {
_movieDetailsState.update { AppState(isLoading = result.isLoading) }
}
is Result.Success -> {
_movieDetailsState.update { AppState(data = result.data) }
}
}
}.launchIn(viewModelScope)










