├── README.md ├── app ├── .DS_Store ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── .DS_Store │ ├── androidTest │ └── java │ │ └── com │ │ └── cleanarch │ │ └── errorhandling │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── cleanarch │ │ │ └── errorhandling │ │ │ ├── BookCard.kt │ │ │ ├── BooksList.kt │ │ │ ├── BooksScreen.kt │ │ │ ├── MainActivity.kt │ │ │ ├── common │ │ │ ├── ErrorCodes.kt │ │ │ ├── ErrorType.kt │ │ │ ├── Resource.kt │ │ │ └── extensions │ │ │ │ └── Throwable.kt │ │ │ ├── models │ │ │ └── Book.kt │ │ │ ├── network │ │ │ └── BooksRemoteDataSourceImpl.kt │ │ │ ├── repository │ │ │ ├── BooksRepository.kt │ │ │ └── BooksRepositoryImpl.kt │ │ │ ├── ui │ │ │ ├── common │ │ │ │ ├── ErrorText.kt │ │ │ │ ├── ErrorView.kt │ │ │ │ ├── LoadingView.kt │ │ │ │ └── UiDataState.kt │ │ │ ├── resources │ │ │ │ └── ErrorTypeConverter.kt │ │ │ └── theme │ │ │ │ ├── Color.kt │ │ │ │ ├── Theme.kt │ │ │ │ └── Type.kt │ │ │ └── viewModel │ │ │ └── BooksViewModel.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ └── test │ └── java │ └── com │ └── cleanarch │ └── errorhandling │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── local.properties └── settings.gradle /README.md: -------------------------------------------------------------------------------- 1 | # Clean Architecture Error Handling 2 | This project shows way about how to handle Errors occuring in Network and propogate them to the presentation / UI Layer. 3 | 4 | ## Medium 5 | This github repo is linked with medium Article wrote about this topic whose link is below 6 | - [Error Handling in Clean Architecture using Flow and Jetpack Compose](https://medium.com/@wunder.saqib/error-handling-in-clean-architecture-using-flow-and-jetpack-compose-b39c729a68eb) 7 | 8 | 9 | ## Frameworks 10 | - Kotlin Flows 11 | - Jetpack Compose 12 | 13 | 14 | ## Sources 15 | [Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) 16 | 17 | -------------------------------------------------------------------------------- /app/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saqib-github-commits/CleanArchErrorHandling/b4693f0a3b04610cdfee566b3e3954ee2d534ecf/app/.DS_Store -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'org.jetbrains.kotlin.android' 4 | } 5 | 6 | android { 7 | namespace 'com.cleanarch.errorhandling' 8 | compileSdk 33 9 | 10 | defaultConfig { 11 | applicationId "com.cleanarch.errorhandling" 12 | minSdk 24 13 | targetSdk 33 14 | versionCode 1 15 | versionName "1.0" 16 | 17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 18 | vectorDrawables { 19 | useSupportLibrary true 20 | } 21 | } 22 | 23 | buildTypes { 24 | release { 25 | minifyEnabled false 26 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 27 | } 28 | } 29 | compileOptions { 30 | sourceCompatibility JavaVersion.VERSION_1_8 31 | targetCompatibility JavaVersion.VERSION_1_8 32 | } 33 | kotlinOptions { 34 | jvmTarget = '1.8' 35 | } 36 | buildFeatures { 37 | compose true 38 | } 39 | composeOptions { 40 | kotlinCompilerExtensionVersion '1.3.2' 41 | } 42 | packagingOptions { 43 | resources { 44 | excludes += '/META-INF/{AL2.0,LGPL2.1}' 45 | } 46 | } 47 | } 48 | 49 | dependencies { 50 | 51 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4' 52 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4' 53 | 54 | implementation 'androidx.core:core-ktx:1.10.1' 55 | implementation platform('org.jetbrains.kotlin:kotlin-bom:1.8.0') 56 | 57 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1' 58 | implementation 'androidx.activity:activity-compose:1.7.2' 59 | implementation 'androidx.lifecycle:lifecycle-runtime-compose:2.6.1' 60 | 61 | implementation 'com.squareup.retrofit2:retrofit:2.9.0' 62 | implementation 'com.squareup.retrofit2:converter-moshi:2.9.0' 63 | 64 | implementation platform('androidx.compose:compose-bom:2022.10.00') 65 | implementation 'androidx.compose.ui:ui' 66 | implementation 'androidx.compose.ui:ui-graphics' 67 | implementation 'androidx.compose.ui:ui-tooling-preview' 68 | implementation 'androidx.compose.material3:material3' 69 | testImplementation 'junit:junit:4.13.2' 70 | androidTestImplementation 'androidx.test.ext:junit:1.1.5' 71 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' 72 | androidTestImplementation platform('androidx.compose:compose-bom:2022.10.00') 73 | androidTestImplementation 'androidx.compose.ui:ui-test-junit4' 74 | debugImplementation 'androidx.compose.ui:ui-tooling' 75 | debugImplementation 'androidx.compose.ui:ui-test-manifest' 76 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saqib-github-commits/CleanArchErrorHandling/b4693f0a3b04610cdfee566b3e3954ee2d534ecf/app/src/.DS_Store -------------------------------------------------------------------------------- /app/src/androidTest/java/com/cleanarch/errorhandling/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.cleanarch.errorhandling 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.cleanarch.errorhandling", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 15 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/cleanarch/errorhandling/BookCard.kt: -------------------------------------------------------------------------------- 1 | package com.cleanarch.errorhandling 2 | 3 | import androidx.compose.foundation.layout.Row 4 | import androidx.compose.foundation.layout.fillMaxWidth 5 | import androidx.compose.foundation.layout.height 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.material3.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.tooling.preview.Preview 12 | import androidx.compose.ui.unit.dp 13 | import com.cleanarch.errorhandling.models.Book 14 | 15 | 16 | @Composable 17 | fun BookCard(books: Book) { 18 | Row( 19 | modifier = Modifier 20 | .padding(8.dp) 21 | .fillMaxWidth() 22 | .height(50.dp), 23 | verticalAlignment = Alignment.CenterVertically 24 | ) { 25 | Text(text = books.title) 26 | } 27 | } 28 | 29 | 30 | 31 | @Preview 32 | @Composable 33 | fun PreviewBookCard() { 34 | BookCard(books = Book("title")) 35 | } -------------------------------------------------------------------------------- /app/src/main/java/com/cleanarch/errorhandling/BooksList.kt: -------------------------------------------------------------------------------- 1 | package com.cleanarch.errorhandling 2 | 3 | import androidx.compose.foundation.lazy.LazyColumn 4 | import androidx.compose.foundation.lazy.items 5 | import androidx.compose.runtime.Composable 6 | import com.cleanarch.errorhandling.models.Book 7 | 8 | @Composable 9 | fun BooksList(books: List) { 10 | LazyColumn { 11 | items(books) { BookCard(it) } 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/cleanarch/errorhandling/BooksScreen.kt: -------------------------------------------------------------------------------- 1 | package com.cleanarch.errorhandling 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.PaddingValues 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.height 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.lazy.LazyColumn 10 | import androidx.compose.foundation.lazy.items 11 | import androidx.compose.material3.CenterAlignedTopAppBar 12 | import androidx.compose.material3.ExperimentalMaterial3Api 13 | import androidx.compose.material3.Scaffold 14 | import androidx.compose.material3.Text 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.runtime.collectAsState 17 | import androidx.compose.runtime.getValue 18 | import androidx.compose.ui.Alignment 19 | import androidx.compose.ui.Modifier 20 | import androidx.compose.ui.tooling.preview.Preview 21 | import androidx.compose.ui.unit.dp 22 | import com.cleanarch.errorhandling.models.Book 23 | import com.cleanarch.errorhandling.ui.common.ErrorView 24 | import com.cleanarch.errorhandling.ui.common.LoadingView 25 | import com.cleanarch.errorhandling.ui.common.UiDataState 26 | import com.cleanarch.errorhandling.viewModel.BooksViewModel 27 | 28 | @Composable 29 | fun BooksScreen(viewModel: BooksViewModel = BooksViewModel()) { 30 | // state hoisting 31 | val uiDataState by viewModel.books.collectAsState() 32 | 33 | BooksScreen(uiDataState) 34 | } 35 | 36 | @OptIn(ExperimentalMaterial3Api::class) 37 | @Composable 38 | fun BooksScreen(uiDataState: UiDataState>) { 39 | Scaffold( 40 | topBar = { CenterAlignedTopAppBar(title = { Text(text = "Books")}) } 41 | ) { PaddingValues -> 42 | Column( 43 | modifier = Modifier.padding(PaddingValues) 44 | ) { 45 | when (uiDataState) { 46 | is UiDataState.Loading -> LoadingView() 47 | is UiDataState.Loaded -> BooksList(uiDataState.data) 48 | is UiDataState.Error -> ErrorView(uiDataState.error, {}) 49 | } 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /app/src/main/java/com/cleanarch/errorhandling/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.cleanarch.errorhandling 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.material3.Surface 9 | import androidx.compose.material3.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.tooling.preview.Preview 13 | import com.cleanarch.errorhandling.ui.theme.CleanArchErrorHandlingTheme 14 | 15 | class MainActivity : ComponentActivity() { 16 | override fun onCreate(savedInstanceState: Bundle?) { 17 | super.onCreate(savedInstanceState) 18 | setContent { 19 | CleanArchErrorHandlingTheme { 20 | // A surface container using the 'background' color from the theme 21 | Surface( 22 | modifier = Modifier.fillMaxSize(), 23 | color = MaterialTheme.colorScheme.background 24 | ) { 25 | BooksScreen() 26 | } 27 | } 28 | } 29 | } 30 | } 31 | 32 | @Composable 33 | fun Greeting(name: String, modifier: Modifier = Modifier) { 34 | Text( 35 | text = "Hello $name!", 36 | modifier = modifier 37 | ) 38 | } 39 | 40 | @Preview(showBackground = true) 41 | @Composable 42 | fun GreetingPreview() { 43 | CleanArchErrorHandlingTheme { 44 | Greeting("Android") 45 | } 46 | } -------------------------------------------------------------------------------- /app/src/main/java/com/cleanarch/errorhandling/common/ErrorCodes.kt: -------------------------------------------------------------------------------- 1 | package com.cleanarch.errorhandling.common 2 | 3 | object ErrorCodes { 4 | 5 | object Http { 6 | const val InternalServer = 501 7 | const val ServiceUnavailable = 503 8 | const val ResourceNotFound = 404 9 | } 10 | } -------------------------------------------------------------------------------- /app/src/main/java/com/cleanarch/errorhandling/common/ErrorType.kt: -------------------------------------------------------------------------------- 1 | package com.cleanarch.errorhandling.common 2 | 3 | sealed class ErrorType { 4 | 5 | sealed class Api: ErrorType() { 6 | 7 | object Network: Api() 8 | 9 | object ServiceUnavailable : Api() 10 | 11 | object NotFound : Api() 12 | 13 | object Server : Api() 14 | 15 | } 16 | 17 | object Unknown: ErrorType() 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/cleanarch/errorhandling/common/Resource.kt: -------------------------------------------------------------------------------- 1 | package com.cleanarch.errorhandling.common 2 | 3 | 4 | sealed class Resource { 5 | data class Success(val data: T): Resource() 6 | data class Error(val error: ErrorType): Resource() 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/cleanarch/errorhandling/common/extensions/Throwable.kt: -------------------------------------------------------------------------------- 1 | package com.cleanarch.errorhandling.common.extensions 2 | 3 | import com.cleanarch.errorhandling.common.ErrorCodes 4 | import com.cleanarch.errorhandling.common.ErrorType 5 | import retrofit2.HttpException 6 | import java.io.IOException 7 | 8 | fun Throwable.toErrorType() = when (this) { 9 | is IOException -> ErrorType.Api.Network 10 | is HttpException -> when (code()) { 11 | ErrorCodes.Http.ResourceNotFound -> ErrorType.Api.NotFound 12 | ErrorCodes.Http.InternalServer -> ErrorType.Api.Server 13 | ErrorCodes.Http.ServiceUnavailable -> ErrorType.Api.ServiceUnavailable 14 | else -> ErrorType.Unknown 15 | } 16 | else -> ErrorType.Unknown 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/cleanarch/errorhandling/models/Book.kt: -------------------------------------------------------------------------------- 1 | package com.cleanarch.errorhandling.models 2 | 3 | data class Book ( 4 | val title: String 5 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/cleanarch/errorhandling/network/BooksRemoteDataSourceImpl.kt: -------------------------------------------------------------------------------- 1 | package com.cleanarch.errorhandling.network 2 | 3 | import com.cleanarch.errorhandling.common.Resource 4 | import com.cleanarch.errorhandling.models.Book 5 | import kotlinx.coroutines.Dispatchers 6 | import kotlinx.coroutines.flow.Flow 7 | import kotlinx.coroutines.flow.flow 8 | import kotlinx.coroutines.flow.flowOn 9 | import java.io.IOException 10 | import kotlin.random.Random 11 | 12 | interface BooksRemoteDataSource { 13 | fun fetchBooks(): Flow>> 14 | } 15 | 16 | class BooksRemoteDataSourceImpl: BooksRemoteDataSource { 17 | 18 | override fun fetchBooks() = flow { 19 | when (Random.nextBoolean()) { 20 | true -> emit(Resource.Success(listOf(Book("title1"), Book("title2")))) 21 | false -> throw IOException() 22 | } 23 | }.flowOn(Dispatchers.IO) 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/cleanarch/errorhandling/repository/BooksRepository.kt: -------------------------------------------------------------------------------- 1 | package com.cleanarch.errorhandling.repository 2 | 3 | import com.cleanarch.errorhandling.common.Resource 4 | import com.cleanarch.errorhandling.models.Book 5 | import kotlinx.coroutines.flow.Flow 6 | 7 | interface BooksRepository { 8 | 9 | fun getBooks(): Flow>> 10 | 11 | } -------------------------------------------------------------------------------- /app/src/main/java/com/cleanarch/errorhandling/repository/BooksRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.cleanarch.errorhandling.repository 2 | 3 | import com.cleanarch.errorhandling.common.Resource 4 | import com.cleanarch.errorhandling.common.extensions.toErrorType 5 | import com.cleanarch.errorhandling.network.BooksRemoteDataSource 6 | import com.cleanarch.errorhandling.network.BooksRemoteDataSourceImpl 7 | import kotlinx.coroutines.flow.catch 8 | 9 | class BooksRepositoryImpl( 10 | private val booksRemoteDataSource: BooksRemoteDataSource = BooksRemoteDataSourceImpl() 11 | ) : BooksRepository { 12 | 13 | override fun getBooks()= booksRemoteDataSource.fetchBooks().catch { 14 | emit(Resource.Error(it.toErrorType())) 15 | } 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/cleanarch/errorhandling/ui/common/ErrorText.kt: -------------------------------------------------------------------------------- 1 | package com.cleanarch.errorhandling.ui.common 2 | 3 | import androidx.annotation.StringRes 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.res.stringResource 6 | 7 | sealed class ErrorText { 8 | class StringResource(@StringRes val id: Int): ErrorText() 9 | 10 | @Composable 11 | fun asString() = when(this) { 12 | is StringResource -> stringResource(id) 13 | } 14 | 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/cleanarch/errorhandling/ui/common/ErrorView.kt: -------------------------------------------------------------------------------- 1 | package com.cleanarch.errorhandling.ui.common 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Spacer 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.foundation.layout.height 8 | import androidx.compose.material3.Button 9 | import androidx.compose.material3.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.tooling.preview.Preview 14 | import androidx.compose.ui.unit.dp 15 | import com.cleanarch.errorhandling.R 16 | 17 | @Composable 18 | fun ErrorView(errorText: ErrorText, action: () -> Unit) { 19 | Column( 20 | modifier = Modifier.fillMaxSize(), 21 | verticalArrangement = Arrangement.Center, 22 | horizontalAlignment = Alignment.CenterHorizontally 23 | ) { 24 | Text(text = errorText.asString()) 25 | Spacer(modifier = Modifier.height(8.dp)) 26 | Button(onClick = { 27 | action() } 28 | ) { 29 | Text(text = "Retry") 30 | } 31 | } 32 | } 33 | 34 | @Composable 35 | @Preview 36 | fun PreviewErrorView() { 37 | ErrorView(ErrorText.StringResource(R.string.error_general)) { } 38 | } -------------------------------------------------------------------------------- /app/src/main/java/com/cleanarch/errorhandling/ui/common/LoadingView.kt: -------------------------------------------------------------------------------- 1 | package com.cleanarch.errorhandling.ui.common 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.material3.CircularProgressIndicator 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Alignment 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.tooling.preview.Preview 11 | import androidx.compose.ui.unit.dp 12 | 13 | @Composable 14 | fun LoadingView() { 15 | Column ( 16 | modifier = Modifier.fillMaxSize(), 17 | verticalArrangement = Arrangement.Center, 18 | horizontalAlignment = Alignment.CenterHorizontally 19 | ) { 20 | CircularProgressIndicator() 21 | } 22 | } 23 | 24 | @Composable 25 | @Preview(widthDp = 200, heightDp = 200) 26 | fun PreviewLoadingView() { 27 | LoadingView() 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/cleanarch/errorhandling/ui/common/UiDataState.kt: -------------------------------------------------------------------------------- 1 | package com.cleanarch.errorhandling.ui.common 2 | 3 | sealed class UiDataState { 4 | class Loading: UiDataState() 5 | data class Error(val error: ErrorText) : UiDataState() 6 | data class Loaded(val data: T): UiDataState() 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/cleanarch/errorhandling/ui/resources/ErrorTypeConverter.kt: -------------------------------------------------------------------------------- 1 | package com.cleanarch.errorhandling.ui.resources 2 | 3 | import com.cleanarch.errorhandling.R 4 | import com.cleanarch.errorhandling.common.ErrorType 5 | import com.cleanarch.errorhandling.ui.common.ErrorText 6 | 7 | interface ErrorTypeToErrorTextConverter { 8 | 9 | fun convert(errorType: ErrorType): ErrorText 10 | } 11 | 12 | class ErrorTypeToErrorTextConverterImpl : ErrorTypeToErrorTextConverter { 13 | 14 | override fun convert(errorType: ErrorType) = when (errorType) { 15 | ErrorType.Api.NotFound -> ErrorText.StringResource(R.string.error_resource_not_found) 16 | ErrorType.Api.ServiceUnavailable -> ErrorText.StringResource(R.string.error_service_unavailable) 17 | ErrorType.Api.Server -> ErrorText.StringResource(R.string.error_server) 18 | ErrorType.Api.Network -> ErrorText.StringResource(R.string.error_network_unavailable) 19 | ErrorType.Unknown -> ErrorText.StringResource(R.string.error_general) 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/java/com/cleanarch/errorhandling/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.cleanarch.errorhandling.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple80 = Color(0xFFD0BCFF) 6 | val PurpleGrey80 = Color(0xFFCCC2DC) 7 | val Pink80 = Color(0xFFEFB8C8) 8 | 9 | val Purple40 = Color(0xFF6650a4) 10 | val PurpleGrey40 = Color(0xFF625b71) 11 | val Pink40 = Color(0xFF7D5260) -------------------------------------------------------------------------------- /app/src/main/java/com/cleanarch/errorhandling/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.cleanarch.errorhandling.ui.theme 2 | 3 | import android.app.Activity 4 | import android.os.Build 5 | import androidx.compose.foundation.isSystemInDarkTheme 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.darkColorScheme 8 | import androidx.compose.material3.dynamicDarkColorScheme 9 | import androidx.compose.material3.dynamicLightColorScheme 10 | import androidx.compose.material3.lightColorScheme 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.SideEffect 13 | import androidx.compose.ui.graphics.toArgb 14 | import androidx.compose.ui.platform.LocalContext 15 | import androidx.compose.ui.platform.LocalView 16 | import androidx.core.view.WindowCompat 17 | 18 | private val DarkColorScheme = darkColorScheme( 19 | primary = Purple80, 20 | secondary = PurpleGrey80, 21 | tertiary = Pink80 22 | ) 23 | 24 | private val LightColorScheme = lightColorScheme( 25 | primary = Purple40, 26 | secondary = PurpleGrey40, 27 | tertiary = Pink40 28 | 29 | /* Other default colors to override 30 | background = Color(0xFFFFFBFE), 31 | surface = Color(0xFFFFFBFE), 32 | onPrimary = Color.White, 33 | onSecondary = Color.White, 34 | onTertiary = Color.White, 35 | onBackground = Color(0xFF1C1B1F), 36 | onSurface = Color(0xFF1C1B1F), 37 | */ 38 | ) 39 | 40 | @Composable 41 | fun CleanArchErrorHandlingTheme( 42 | darkTheme: Boolean = isSystemInDarkTheme(), 43 | // Dynamic color is available on Android 12+ 44 | dynamicColor: Boolean = true, 45 | content: @Composable () -> Unit 46 | ) { 47 | val colorScheme = when { 48 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 49 | val context = LocalContext.current 50 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 51 | } 52 | 53 | darkTheme -> DarkColorScheme 54 | else -> LightColorScheme 55 | } 56 | val view = LocalView.current 57 | if (!view.isInEditMode) { 58 | SideEffect { 59 | val window = (view.context as Activity).window 60 | window.statusBarColor = colorScheme.primary.toArgb() 61 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme 62 | } 63 | } 64 | 65 | MaterialTheme( 66 | colorScheme = colorScheme, 67 | typography = Typography, 68 | content = content 69 | ) 70 | } -------------------------------------------------------------------------------- /app/src/main/java/com/cleanarch/errorhandling/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.cleanarch.errorhandling.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | bodyLarge = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp, 15 | lineHeight = 24.sp, 16 | letterSpacing = 0.5.sp 17 | ) 18 | /* Other default text styles to override 19 | titleLarge = TextStyle( 20 | fontFamily = FontFamily.Default, 21 | fontWeight = FontWeight.Normal, 22 | fontSize = 22.sp, 23 | lineHeight = 28.sp, 24 | letterSpacing = 0.sp 25 | ), 26 | labelSmall = TextStyle( 27 | fontFamily = FontFamily.Default, 28 | fontWeight = FontWeight.Medium, 29 | fontSize = 11.sp, 30 | lineHeight = 16.sp, 31 | letterSpacing = 0.5.sp 32 | ) 33 | */ 34 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/cleanarch/errorhandling/viewModel/BooksViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.cleanarch.errorhandling.viewModel 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.cleanarch.errorhandling.common.Resource 6 | import com.cleanarch.errorhandling.models.Book 7 | import com.cleanarch.errorhandling.repository.BooksRepository 8 | import com.cleanarch.errorhandling.repository.BooksRepositoryImpl 9 | import com.cleanarch.errorhandling.ui.common.UiDataState 10 | import com.cleanarch.errorhandling.ui.resources.ErrorTypeToErrorTextConverter 11 | import com.cleanarch.errorhandling.ui.resources.ErrorTypeToErrorTextConverterImpl 12 | import kotlinx.coroutines.flow.MutableStateFlow 13 | import kotlinx.coroutines.flow.StateFlow 14 | import kotlinx.coroutines.flow.asStateFlow 15 | import kotlinx.coroutines.flow.catch 16 | import kotlinx.coroutines.launch 17 | 18 | class BooksViewModel ( 19 | private val booksRepository: BooksRepository = BooksRepositoryImpl(), 20 | private val errorTypeToErrorTextConverter: ErrorTypeToErrorTextConverter = ErrorTypeToErrorTextConverterImpl() 21 | ): ViewModel() { 22 | 23 | private val _books = MutableStateFlow>>(UiDataState.Loading()) 24 | val books: StateFlow>> = _books.asStateFlow() 25 | 26 | init { 27 | viewModelScope.launch { 28 | booksRepository.getBooks() 29 | .catch { } 30 | .collect { 31 | when (it) { 32 | is Resource.Success -> _books.value = UiDataState.Loaded(it.data) 33 | is Resource.Error -> _books.value = UiDataState.Error(errorTypeToErrorTextConverter.convert(it.error)) 34 | } 35 | } 36 | } 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saqib-github-commits/CleanArchErrorHandling/b4693f0a3b04610cdfee566b3e3954ee2d534ecf/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saqib-github-commits/CleanArchErrorHandling/b4693f0a3b04610cdfee566b3e3954ee2d534ecf/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saqib-github-commits/CleanArchErrorHandling/b4693f0a3b04610cdfee566b3e3954ee2d534ecf/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saqib-github-commits/CleanArchErrorHandling/b4693f0a3b04610cdfee566b3e3954ee2d534ecf/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saqib-github-commits/CleanArchErrorHandling/b4693f0a3b04610cdfee566b3e3954ee2d534ecf/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saqib-github-commits/CleanArchErrorHandling/b4693f0a3b04610cdfee566b3e3954ee2d534ecf/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saqib-github-commits/CleanArchErrorHandling/b4693f0a3b04610cdfee566b3e3954ee2d534ecf/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saqib-github-commits/CleanArchErrorHandling/b4693f0a3b04610cdfee566b3e3954ee2d534ecf/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saqib-github-commits/CleanArchErrorHandling/b4693f0a3b04610cdfee566b3e3954ee2d534ecf/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saqib-github-commits/CleanArchErrorHandling/b4693f0a3b04610cdfee566b3e3954ee2d534ecf/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | CleanArchErrorHandling 3 | You are not connected to internet, Please try again. 4 | Currently Service is not available, Please try again later. 5 | Required information is not available at the moment, Please try again later. 6 | There is some unknown error happened, if it happens again then please report, Please try again. 7 | There was some issue loading information, Please try again. 8 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |