├── .gitignore ├── .idea ├── .gitignore ├── .name ├── compiler.xml ├── gradle.xml ├── jarRepositories.xml ├── misc.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── ydhnwb │ │ └── cleanarchitectureexercise │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── ydhnwb │ │ │ └── cleanarchitectureexercise │ │ │ ├── data │ │ │ ├── common │ │ │ │ ├── module │ │ │ │ │ ├── NetworkModule.kt │ │ │ │ │ └── SharedPrefModule.kt │ │ │ │ └── utils │ │ │ │ │ ├── RequestInterceptor.kt │ │ │ │ │ └── ResponseWrapper.kt │ │ │ ├── login │ │ │ │ ├── LoginModule.kt │ │ │ │ ├── remote │ │ │ │ │ ├── api │ │ │ │ │ │ └── LoginApi.kt │ │ │ │ │ └── dto │ │ │ │ │ │ ├── LoginRequest.kt │ │ │ │ │ │ └── LoginResponse.kt │ │ │ │ └── repository │ │ │ │ │ └── LoginRepositoryImpl.kt │ │ │ ├── product │ │ │ │ ├── ProductModule.kt │ │ │ │ ├── remote │ │ │ │ │ ├── api │ │ │ │ │ │ └── ProductApi.kt │ │ │ │ │ └── dto │ │ │ │ │ │ ├── ProductCreateRequest.kt │ │ │ │ │ │ ├── ProductResponse.kt │ │ │ │ │ │ ├── ProductUpdateRequest.kt │ │ │ │ │ │ └── ProductUserResponse.kt │ │ │ │ └── repository │ │ │ │ │ └── ProductRepositoryImpl.kt │ │ │ └── register │ │ │ │ ├── RegisterModule.kt │ │ │ │ ├── remote │ │ │ │ ├── api │ │ │ │ │ └── RegisterApi.kt │ │ │ │ └── dto │ │ │ │ │ ├── RegisterRequest.kt │ │ │ │ │ └── RegisterResponse.kt │ │ │ │ └── repository │ │ │ │ └── RegisterRepositoryImpl.kt │ │ │ ├── domain │ │ │ ├── common │ │ │ │ └── base │ │ │ │ │ └── BaseResult.kt │ │ │ ├── login │ │ │ │ ├── LoginRepository.kt │ │ │ │ ├── entity │ │ │ │ │ └── LoginEntity.kt │ │ │ │ └── usecase │ │ │ │ │ └── LoginUseCase.kt │ │ │ ├── product │ │ │ │ ├── ProductRepository.kt │ │ │ │ ├── entity │ │ │ │ │ ├── ProductEntity.kt │ │ │ │ │ └── ProductUserEntity.kt │ │ │ │ └── usecase │ │ │ │ │ ├── CreateProductUseCase.kt │ │ │ │ │ ├── DeleteProductByIdUseCase.kt │ │ │ │ │ ├── GetAllMyProductUseCase.kt │ │ │ │ │ ├── GetProductByIdUseCase.kt │ │ │ │ │ └── UpdateProductUseCase.kt │ │ │ └── register │ │ │ │ ├── RegisterRepository.kt │ │ │ │ ├── entity │ │ │ │ └── RegisterEntity.kt │ │ │ │ └── usecase │ │ │ │ └── RegisterUseCase.kt │ │ │ ├── infra │ │ │ └── utils │ │ │ │ └── SharedPrefs.kt │ │ │ └── presentation │ │ │ ├── App.kt │ │ │ ├── common │ │ │ └── extension │ │ │ │ ├── ContextExt.kt │ │ │ │ ├── StringExt.kt │ │ │ │ └── ViewExt.kt │ │ │ ├── login │ │ │ ├── LoginActivity.kt │ │ │ └── LoginViewModel.kt │ │ │ ├── main │ │ │ ├── MainActivity.kt │ │ │ ├── create_product │ │ │ │ ├── CreateMainFragment.kt │ │ │ │ └── CreateMainFragmentViewModel.kt │ │ │ ├── detail │ │ │ │ ├── DetailMainFragment.kt │ │ │ │ └── DetailMainFragmentViewModel.kt │ │ │ └── home │ │ │ │ ├── HomeMainFragment.kt │ │ │ │ ├── HomeMainProductAdapter.kt │ │ │ │ └── HomeMainViewModel.kt │ │ │ └── register │ │ │ ├── RegisterActivity.kt │ │ │ └── RegisterViewModel.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── ic_baseline_arrow_back_24.xml │ │ ├── ic_baseline_create_24.xml │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ ├── activity_login.xml │ │ ├── activity_main.xml │ │ ├── activity_register.xml │ │ ├── content_main.xml │ │ ├── fragment_main_create.xml │ │ ├── fragment_main_detail.xml │ │ ├── fragment_main_home.xml │ │ └── item_product.xml │ │ ├── menu │ │ └── menu_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── navigation │ │ └── nav_graph.xml │ │ ├── values-night │ │ └── themes.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── themes.xml │ └── test │ └── java │ └── com │ └── ydhnwb │ └── cleanarchitectureexercise │ └── ExampleUnitTest.kt ├── build.gradle ├── docs ├── Screenshot_1622220439.png ├── Screenshot_1622220448.png ├── Screenshot_1622220456.png ├── Screenshot_1622220461.png ├── Screenshot_1622220467.png └── clean.png ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | Clean Architecture Exercise -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 13 | 14 | 15 | 17 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # android-clean-architecture 2 | A clean architecture example. Using Kotlin Flow, Retrofit and Dagger Hilt, etc. 3 | 4 | ## Intro 5 | 6 | _Architecture_ means the overall design of the project. It’s the organization of the code into classes or files or components or modules. And it’s how all these groups of code relate to each other. The architecture defines where the application performs its core functionality and how that functionality interacts with things like the database and the user interface. 7 | 8 | _Clean architecture_ refers to organizing the project so that it’s easy to understand and easy to change as the project grows. This doesn’t happen by chance. It takes intentional planning. 9 | 10 | ## Reference 11 | - If you want a db cache using room db, [go here](https://github.com/ydhnwb/android-cache-example) 12 | - This android app using backend from [this repo](https://github.com/ydhnwb/golang_heroku) 13 | - If you want tutorial how to create this app step by step, go to this [youtube playlist](https://www.youtube.com/playlist?list=PLkVx132FdJZnNsBTJSr4Sc1oAwRFXl2G4). 14 | 15 | ## Screenshots 16 | 17 | 18 | 19 | 20 | 21 | ## Notes 22 | I created this app just for example how to implement clean architecture on android. _**It really biased to my preference and experience. Also, _clean architecture_ is not mandatory to do. If you feel that you/your team are taking so much benefit by implementing this design pattern, then go for it. Otherwise, don't use it or just modify to you/your team preferences.**_ 23 | This app have little bug. If you go from FragmentA to FragmentB, then go back, the fragmentA re render again. I Know how to fix it but I dont have time to do it. Search for "retain fragment jetpack navigation", use the newest solution 24 | 25 | 26 | ## Main Picture 27 | There are 3 layer in this app. 28 | | Presentation Layer | Domain Layer | Data Layer | 29 | | ----------------------- | --------------------- | ---------------------------------- | 30 | | your ui/view | entity | data source, dto | 31 | | your viewmodel | usecase | repository implementation | 32 | | probably your extension | repository interface | your library config(retrofit/room) | 33 | | etc... | etc.. | etc... | 34 | 35 | 36 | 37 | 38 | ## App Level Example 39 | 40 | | Presentation Layer | Something in Between | Domain Layer | Data Layer | Outer data layer | 41 | | ------------------------------ | --------------------- | --------------------------------- | ---------------------------------- | ---------------- | 42 | | LoginActivity & LoginViewModel | <- LoginUseCase -> | <- LoginRepository (interface) -> | <- LoginRepositoryImplementation | DataSource | 43 | 44 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | id 'kotlin-kapt' 5 | id 'dagger.hilt.android.plugin' 6 | } 7 | 8 | 9 | 10 | android { 11 | compileSdkVersion 30 12 | buildToolsVersion "30.0.3" 13 | 14 | defaultConfig { 15 | applicationId "com.ydhnwb.cleanarchitectureexercise" 16 | minSdkVersion 16 17 | targetSdkVersion 30 18 | versionCode 1 19 | versionName "1.0" 20 | multiDexEnabled true 21 | 22 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 23 | 24 | // default build config 25 | buildConfigField 'String', 'API_BASE_URL', '"https://golang-heroku.herokuapp.com/api/"' 26 | } 27 | 28 | buildTypes { 29 | release { 30 | minifyEnabled false 31 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 32 | } 33 | } 34 | compileOptions { 35 | sourceCompatibility JavaVersion.VERSION_1_8 36 | targetCompatibility JavaVersion.VERSION_1_8 37 | } 38 | kotlinOptions { 39 | jvmTarget = '1.8' 40 | } 41 | buildFeatures { 42 | viewBinding true 43 | } 44 | } 45 | 46 | dependencies { 47 | 48 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 49 | implementation 'androidx.core:core-ktx:1.5.0' 50 | implementation 'androidx.appcompat:appcompat:1.3.0' 51 | implementation 'com.google.android.material:material:1.3.0' 52 | implementation 'androidx.constraintlayout:constraintlayout:2.0.4' 53 | implementation 'androidx.navigation:navigation-fragment-ktx:2.4.0-alpha04' 54 | implementation 'androidx.navigation:navigation-ui-ktx:2.4.0-alpha04' 55 | testImplementation 'junit:junit:4.+' 56 | androidTestImplementation 'androidx.test.ext:junit:1.1.2' 57 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' 58 | implementation 'com.android.support:multidex:1.0.3' 59 | 60 | 61 | // http 62 | implementation "com.squareup.retrofit2:retrofit:$retrofit_version" 63 | implementation "com.squareup.retrofit2:converter-gson:$retrofit_version" 64 | implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_logging_version" 65 | 66 | // coroutine 67 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutine_version" 68 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutine_version" 69 | 70 | // dagger - Hilt 71 | implementation "com.google.dagger:hilt-android:$dagger_hilt_version" 72 | kapt "com.google.dagger:hilt-android-compiler:$dagger_hilt_version" 73 | implementation "androidx.hilt:hilt-lifecycle-viewmodel:$dagger_hilt_viewmodel_version" 74 | kapt "androidx.hilt:hilt-compiler:$dagger_hilt_viewmodel_version" 75 | 76 | // Activity KTX for viewModels() 77 | implementation "androidx.activity:activity-ktx:$activity_ktx_version" 78 | implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version") 79 | 80 | 81 | } -------------------------------------------------------------------------------- /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/androidTest/java/com/ydhnwb/cleanarchitectureexercise/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise 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.ydhnwb.cleanarchitectureexercise", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 15 | 18 | 19 | 20 | 23 | 24 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/data/common/module/NetworkModule.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.data.common.module 2 | 3 | import com.ydhnwb.cleanarchitectureexercise.BuildConfig 4 | import com.ydhnwb.cleanarchitectureexercise.data.common.utils.RequestInterceptor 5 | import com.ydhnwb.cleanarchitectureexercise.infra.utils.SharedPrefs 6 | import dagger.Module 7 | import dagger.Provides 8 | import dagger.hilt.InstallIn 9 | import dagger.hilt.components.SingletonComponent 10 | import okhttp3.OkHttpClient 11 | import retrofit2.Retrofit 12 | import retrofit2.converter.gson.GsonConverterFactory 13 | import java.util.concurrent.TimeUnit 14 | import javax.inject.Singleton 15 | 16 | 17 | @Module 18 | @InstallIn(SingletonComponent::class) 19 | object NetworkModule { 20 | 21 | @Singleton 22 | @Provides 23 | fun provideRetrofit(okHttp: OkHttpClient) : Retrofit { 24 | return Retrofit.Builder().apply { 25 | addConverterFactory(GsonConverterFactory.create()) 26 | client(okHttp) 27 | baseUrl(BuildConfig.API_BASE_URL) 28 | }.build() 29 | } 30 | 31 | @Singleton 32 | @Provides 33 | fun provideOkHttp(requestInterceptor: RequestInterceptor) : OkHttpClient { 34 | return OkHttpClient.Builder().apply { 35 | connectTimeout(60, TimeUnit.SECONDS) 36 | readTimeout(60, TimeUnit.SECONDS) 37 | writeTimeout(60, TimeUnit.SECONDS) 38 | addInterceptor(requestInterceptor) 39 | }.build() 40 | } 41 | 42 | @Provides 43 | fun provideRequestInterceptor(prefs: SharedPrefs) : RequestInterceptor { 44 | return RequestInterceptor(prefs) 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/data/common/module/SharedPrefModule.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.data.common.module 2 | 3 | import android.content.Context 4 | import com.ydhnwb.cleanarchitectureexercise.infra.utils.SharedPrefs 5 | import dagger.Module 6 | import dagger.Provides 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.android.components.ActivityComponent 9 | import dagger.hilt.android.qualifiers.ApplicationContext 10 | import dagger.hilt.components.SingletonComponent 11 | import javax.inject.Singleton 12 | 13 | @Module 14 | @InstallIn(SingletonComponent::class) 15 | object SharedPrefModule { 16 | 17 | @Provides 18 | fun provideSharedPref(@ApplicationContext context: Context) : SharedPrefs{ 19 | return SharedPrefs(context) 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/data/common/utils/RequestInterceptor.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.data.common.utils 2 | 3 | import com.ydhnwb.cleanarchitectureexercise.infra.utils.SharedPrefs 4 | import okhttp3.Interceptor 5 | import okhttp3.Response 6 | import javax.inject.Inject 7 | 8 | class RequestInterceptor constructor(private val pref: SharedPrefs) : Interceptor { 9 | override fun intercept(chain: Interceptor.Chain): Response { 10 | val token = pref.getToken() 11 | val newRequest = chain.request().newBuilder() 12 | .addHeader("Authorization", token) 13 | .build() 14 | return chain.proceed(newRequest) 15 | } 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/data/common/utils/ResponseWrapper.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.data.common.utils 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class WrappedListResponse ( 6 | var code: Int, 7 | @SerializedName("message") var message : String, 8 | @SerializedName("status") var status : Boolean, 9 | @SerializedName("errors") var errors : List? = null, 10 | @SerializedName("data") var data : List? = null 11 | ) 12 | 13 | 14 | data class WrappedResponse ( 15 | var code: Int, 16 | @SerializedName("message") var message : String, 17 | @SerializedName("status") var status : Boolean, 18 | @SerializedName("errors") var errors : List? = null, 19 | @SerializedName("data") var data : T? = null 20 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/data/login/LoginModule.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.data.login 2 | 3 | import com.ydhnwb.cleanarchitectureexercise.data.common.module.NetworkModule 4 | import com.ydhnwb.cleanarchitectureexercise.data.login.remote.api.LoginApi 5 | import com.ydhnwb.cleanarchitectureexercise.data.login.repository.LoginRepositoryImpl 6 | import com.ydhnwb.cleanarchitectureexercise.domain.login.LoginRepository 7 | import dagger.Module 8 | import dagger.Provides 9 | import dagger.hilt.InstallIn 10 | import dagger.hilt.components.SingletonComponent 11 | import retrofit2.Retrofit 12 | import javax.inject.Singleton 13 | 14 | @Module(includes = [NetworkModule::class]) 15 | @InstallIn(SingletonComponent::class) 16 | class LoginModule { 17 | 18 | @Singleton 19 | @Provides 20 | fun provideLoginApi(retrofit: Retrofit) : LoginApi { 21 | return retrofit.create(LoginApi::class.java) 22 | } 23 | 24 | @Singleton 25 | @Provides 26 | fun provideLoginRepository(loginApi: LoginApi) : LoginRepository { 27 | return LoginRepositoryImpl(loginApi) 28 | } 29 | 30 | 31 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/data/login/remote/api/LoginApi.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.data.login.remote.api 2 | 3 | import com.ydhnwb.cleanarchitectureexercise.data.common.utils.WrappedResponse 4 | import com.ydhnwb.cleanarchitectureexercise.data.login.remote.dto.LoginRequest 5 | import com.ydhnwb.cleanarchitectureexercise.data.login.remote.dto.LoginResponse 6 | import retrofit2.Response 7 | import retrofit2.http.Body 8 | import retrofit2.http.POST 9 | 10 | interface LoginApi { 11 | @POST("auth/login") 12 | suspend fun login(@Body loginRequest: LoginRequest) : Response> 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/data/login/remote/dto/LoginRequest.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.data.login.remote.dto 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class LoginRequest( 6 | @SerializedName("email") val email: String, 7 | @SerializedName("password") val password: String 8 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/data/login/remote/dto/LoginResponse.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.data.login.remote.dto 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class LoginResponse( 6 | @SerializedName("id") var id: Int? = null, 7 | @SerializedName("name") var name : String? = null, 8 | @SerializedName("email") var email : String? = null, 9 | @SerializedName("token") var token: String? = null 10 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/data/login/repository/LoginRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.data.login.repository 2 | 3 | import com.google.gson.Gson 4 | import com.google.gson.reflect.TypeToken 5 | import com.ydhnwb.cleanarchitectureexercise.data.common.utils.WrappedResponse 6 | import com.ydhnwb.cleanarchitectureexercise.data.login.remote.api.LoginApi 7 | import com.ydhnwb.cleanarchitectureexercise.data.login.remote.dto.LoginRequest 8 | import com.ydhnwb.cleanarchitectureexercise.data.login.remote.dto.LoginResponse 9 | import com.ydhnwb.cleanarchitectureexercise.domain.common.base.BaseResult 10 | import com.ydhnwb.cleanarchitectureexercise.domain.login.LoginRepository 11 | import com.ydhnwb.cleanarchitectureexercise.domain.login.entity.LoginEntity 12 | import kotlinx.coroutines.flow.Flow 13 | import kotlinx.coroutines.flow.flow 14 | import javax.inject.Inject 15 | 16 | 17 | class LoginRepositoryImpl @Inject constructor(private val loginApi: LoginApi) : LoginRepository { 18 | override suspend fun login(loginRequest: LoginRequest): Flow>> { 19 | return flow { 20 | val response = loginApi.login(loginRequest) 21 | if(response.isSuccessful){ 22 | val body = response.body()!! 23 | val loginEntity = LoginEntity(body.data?.id!!, body.data?.name!!, body.data?.email!!, body.data?.token!!) 24 | emit(BaseResult.Success(loginEntity)) 25 | }else{ 26 | val type = object : TypeToken>(){}.type 27 | val err : WrappedResponse = Gson().fromJson(response.errorBody()!!.charStream(), type) 28 | err.code = response.code() 29 | emit(BaseResult.Error(err)) 30 | } 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/data/product/ProductModule.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.data.product 2 | 3 | import com.ydhnwb.cleanarchitectureexercise.data.common.module.NetworkModule 4 | import com.ydhnwb.cleanarchitectureexercise.data.product.remote.api.ProductApi 5 | import com.ydhnwb.cleanarchitectureexercise.data.product.repository.ProductRepositoryImpl 6 | import com.ydhnwb.cleanarchitectureexercise.domain.product.ProductRepository 7 | import dagger.Module 8 | import dagger.Provides 9 | import dagger.hilt.InstallIn 10 | import dagger.hilt.components.SingletonComponent 11 | import retrofit2.Retrofit 12 | import javax.inject.Singleton 13 | 14 | 15 | @Module(includes = [NetworkModule::class]) 16 | @InstallIn(SingletonComponent::class) 17 | class ProductModule { 18 | @Singleton 19 | @Provides 20 | fun provideProductApi(retrofit: Retrofit) : ProductApi { 21 | return retrofit.create(ProductApi::class.java) 22 | } 23 | 24 | @Singleton 25 | @Provides 26 | fun provideProductRepository(productApi: ProductApi) : ProductRepository { 27 | return ProductRepositoryImpl(productApi) 28 | } 29 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/data/product/remote/api/ProductApi.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.data.product.remote.api 2 | 3 | import com.ydhnwb.cleanarchitectureexercise.data.common.utils.WrappedListResponse 4 | import com.ydhnwb.cleanarchitectureexercise.data.common.utils.WrappedResponse 5 | import com.ydhnwb.cleanarchitectureexercise.data.product.remote.dto.ProductCreateRequest 6 | import com.ydhnwb.cleanarchitectureexercise.data.product.remote.dto.ProductResponse 7 | import com.ydhnwb.cleanarchitectureexercise.data.product.remote.dto.ProductUpdateRequest 8 | import retrofit2.Response 9 | import retrofit2.http.* 10 | 11 | interface ProductApi { 12 | @GET("product/") 13 | suspend fun getAllMyProducts() : Response> 14 | 15 | @GET("product/{id}") 16 | suspend fun getProductById(@Path("id") id: String) : Response> 17 | 18 | @PUT("product/{id}") 19 | suspend fun updateProduct(@Body productUpdateRequest: ProductUpdateRequest, @Path("id") id: String) : Response> 20 | 21 | @DELETE("product/{id}") 22 | suspend fun deleteProduct(@Path("id") id: String) : Response> 23 | 24 | @POST("product/") 25 | suspend fun createProduct(@Body productCreateRequest: ProductCreateRequest) : Response> 26 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/data/product/remote/dto/ProductCreateRequest.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.data.product.remote.dto 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class ProductCreateRequest( 6 | @SerializedName("name") val productName: String, 7 | @SerializedName("price") val price : Int 8 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/data/product/remote/dto/ProductResponse.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.data.product.remote.dto 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class ProductResponse( 6 | @SerializedName("id") var id: Int, 7 | @SerializedName("product_name") var name: String, 8 | @SerializedName("price") var price: Int, 9 | @SerializedName("user") var user: ProductUserResponse 10 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/data/product/remote/dto/ProductUpdateRequest.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.data.product.remote.dto 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class ProductUpdateRequest( 6 | @SerializedName("name") val name: String, 7 | @SerializedName("price") val price: Int 8 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/data/product/remote/dto/ProductUserResponse.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.data.product.remote.dto 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class ProductUserResponse( 6 | @SerializedName("id") var id: Int, 7 | @SerializedName("name") var name: String, 8 | @SerializedName("email") var email: String 9 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/data/product/repository/ProductRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.data.product.repository 2 | 3 | import com.google.gson.Gson 4 | import com.google.gson.reflect.TypeToken 5 | import com.ydhnwb.cleanarchitectureexercise.data.common.utils.WrappedListResponse 6 | import com.ydhnwb.cleanarchitectureexercise.data.common.utils.WrappedResponse 7 | import com.ydhnwb.cleanarchitectureexercise.data.product.remote.api.ProductApi 8 | import com.ydhnwb.cleanarchitectureexercise.data.product.remote.dto.ProductCreateRequest 9 | import com.ydhnwb.cleanarchitectureexercise.data.product.remote.dto.ProductResponse 10 | import com.ydhnwb.cleanarchitectureexercise.data.product.remote.dto.ProductUpdateRequest 11 | import com.ydhnwb.cleanarchitectureexercise.domain.common.base.BaseResult 12 | import com.ydhnwb.cleanarchitectureexercise.domain.product.ProductRepository 13 | import com.ydhnwb.cleanarchitectureexercise.domain.product.entity.ProductEntity 14 | import com.ydhnwb.cleanarchitectureexercise.domain.product.entity.ProductUserEntity 15 | import kotlinx.coroutines.flow.Flow 16 | import kotlinx.coroutines.flow.flow 17 | import javax.inject.Inject 18 | 19 | class ProductRepositoryImpl @Inject constructor(private val productApi: ProductApi) : ProductRepository { 20 | override suspend fun getAllMyProducts(): Flow, WrappedListResponse>> { 21 | return flow { 22 | val response = productApi.getAllMyProducts() 23 | if (response.isSuccessful){ 24 | val body = response.body()!! 25 | val products = mutableListOf() 26 | var user: ProductUserEntity? 27 | body.data?.forEach { productResponse -> 28 | user = ProductUserEntity(productResponse.user.id, productResponse.user.name, productResponse.user.email) 29 | products.add(ProductEntity( 30 | productResponse.id, 31 | productResponse.name, 32 | productResponse.price, 33 | user!! 34 | )) 35 | } 36 | emit(BaseResult.Success(products)) 37 | }else{ 38 | val type = object : TypeToken>(){}.type 39 | val err = Gson().fromJson>(response.errorBody()!!.charStream(), type)!! 40 | err.code = response.code() 41 | emit(BaseResult.Error(err)) 42 | } 43 | } 44 | } 45 | 46 | override suspend fun getProductById(id: String): Flow>> { 47 | return flow { 48 | val response = productApi.getProductById(id) 49 | if(response.isSuccessful){ 50 | val body = response.body()!! 51 | val user = ProductUserEntity(body.data?.user?.id!!, body.data?.user?.name!!, body.data?.user?.email!!) 52 | val product = ProductEntity(body.data?.id!!, body.data?.name!!, body.data?.price!!, user) 53 | emit(BaseResult.Success(product)) 54 | }else{ 55 | val type = object : TypeToken>(){}.type 56 | val err = Gson().fromJson>(response.errorBody()!!.charStream(), type)!! 57 | err.code = response.code() 58 | emit(BaseResult.Error(err)) 59 | } 60 | } 61 | } 62 | 63 | override suspend fun updateProduct(productUpdateRequest: ProductUpdateRequest, id: String): Flow>> { 64 | return flow { 65 | val response = productApi.updateProduct(productUpdateRequest, id) 66 | if(response.isSuccessful){ 67 | val body = response.body()!! 68 | val user = ProductUserEntity(body.data?.user?.id!!, body.data?.user?.name!!, body.data?.user?.email!!) 69 | val product = ProductEntity(body.data?.id!!, body.data?.name!!, body.data?.price!!, user) 70 | emit(BaseResult.Success(product)) 71 | }else{ 72 | val type = object : TypeToken>(){}.type 73 | val err = Gson().fromJson>(response.errorBody()!!.charStream(), type)!! 74 | err.code = response.code() 75 | emit(BaseResult.Error(err)) 76 | } 77 | } 78 | } 79 | 80 | override suspend fun deleteProductById(id: String): Flow>> { 81 | return flow{ 82 | val response = productApi.deleteProduct(id) 83 | if(response.isSuccessful){ 84 | emit(BaseResult.Success(Unit)) 85 | }else{ 86 | val type = object : TypeToken>(){}.type 87 | val err = Gson().fromJson>(response.errorBody()!!.charStream(), type)!! 88 | err.code = response.code() 89 | emit(BaseResult.Error(err)) 90 | } 91 | } 92 | } 93 | 94 | override suspend fun createProduct(productCreateRequest: ProductCreateRequest): Flow>> { 95 | return flow { 96 | val response = productApi.createProduct(productCreateRequest) 97 | if(response.isSuccessful){ 98 | val body = response.body()!! 99 | val user = ProductUserEntity(body.data?.user?.id!!, body.data?.user?.name!!, body.data?.user?.email!!) 100 | val product = ProductEntity(body.data?.id!!, body.data?.name!!, body.data?.price!!, user) 101 | emit(BaseResult.Success(product)) 102 | }else{ 103 | val type = object : TypeToken>(){}.type 104 | val err = Gson().fromJson>(response.errorBody()!!.charStream(), type)!! 105 | err.code = response.code() 106 | emit(BaseResult.Error(err)) 107 | } 108 | } 109 | } 110 | 111 | 112 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/data/register/RegisterModule.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.data.register 2 | 3 | import com.ydhnwb.cleanarchitectureexercise.data.common.module.NetworkModule 4 | import com.ydhnwb.cleanarchitectureexercise.data.register.remote.api.RegisterApi 5 | import com.ydhnwb.cleanarchitectureexercise.data.register.repository.RegisterRepositoryImpl 6 | import com.ydhnwb.cleanarchitectureexercise.domain.register.RegisterRepository 7 | import dagger.Module 8 | import dagger.Provides 9 | import dagger.hilt.InstallIn 10 | import dagger.hilt.components.SingletonComponent 11 | import retrofit2.Retrofit 12 | import javax.inject.Singleton 13 | 14 | @Module(includes = [NetworkModule::class]) 15 | @InstallIn(SingletonComponent::class) 16 | class RegisterModule { 17 | @Singleton 18 | @Provides 19 | fun provideRegisterApi(retrofit: Retrofit) : RegisterApi { 20 | return retrofit.create(RegisterApi::class.java) 21 | } 22 | 23 | @Singleton 24 | @Provides 25 | fun provideRegisterRepository(registerApi: RegisterApi) : RegisterRepository { 26 | return RegisterRepositoryImpl(registerApi) 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/data/register/remote/api/RegisterApi.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.data.register.remote.api 2 | 3 | import com.ydhnwb.cleanarchitectureexercise.data.common.utils.WrappedResponse 4 | import com.ydhnwb.cleanarchitectureexercise.data.register.remote.dto.RegisterRequest 5 | import com.ydhnwb.cleanarchitectureexercise.data.register.remote.dto.RegisterResponse 6 | import kotlinx.coroutines.flow.Flow 7 | import retrofit2.Response 8 | import retrofit2.http.Body 9 | import retrofit2.http.POST 10 | 11 | interface RegisterApi { 12 | @POST("auth/register") 13 | suspend fun register(@Body registerRequest: RegisterRequest) : Response> 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/data/register/remote/dto/RegisterRequest.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.data.register.remote.dto 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class RegisterRequest( 6 | @SerializedName("name") val name: String, 7 | @SerializedName("email") val email: String, 8 | @SerializedName("password") val password: String 9 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/data/register/remote/dto/RegisterResponse.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.data.register.remote.dto 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class RegisterResponse ( 6 | @SerializedName("id") var id: Int? = null, 7 | @SerializedName("name") var name : String? = null, 8 | @SerializedName("email") var email : String? = null, 9 | @SerializedName("token") var token: String? = null 10 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/data/register/repository/RegisterRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.data.register.repository 2 | 3 | import com.google.gson.Gson 4 | import com.google.gson.reflect.TypeToken 5 | import com.ydhnwb.cleanarchitectureexercise.data.common.utils.WrappedResponse 6 | import com.ydhnwb.cleanarchitectureexercise.data.register.remote.api.RegisterApi 7 | import com.ydhnwb.cleanarchitectureexercise.data.register.remote.dto.RegisterRequest 8 | import com.ydhnwb.cleanarchitectureexercise.data.register.remote.dto.RegisterResponse 9 | import com.ydhnwb.cleanarchitectureexercise.domain.common.base.BaseResult 10 | import com.ydhnwb.cleanarchitectureexercise.domain.register.RegisterRepository 11 | import com.ydhnwb.cleanarchitectureexercise.domain.register.entity.RegisterEntity 12 | import kotlinx.coroutines.flow.Flow 13 | import kotlinx.coroutines.flow.flow 14 | import javax.inject.Inject 15 | 16 | class RegisterRepositoryImpl @Inject constructor(private val registerApi: RegisterApi) : RegisterRepository { 17 | override suspend fun register(registerRequest: RegisterRequest): Flow>> { 18 | return flow { 19 | val response = registerApi.register(registerRequest) 20 | if (response.isSuccessful){ 21 | val body = response.body()!! 22 | val registerEntity = RegisterEntity(body.data?.id!!, body.data?.name!!, body.data?.email!!, body.data?.token!!) 23 | emit(BaseResult.Success(registerEntity)) 24 | }else{ 25 | val type = object : TypeToken>(){}.type 26 | val err : WrappedResponse = Gson().fromJson(response.errorBody()!!.charStream(), type) 27 | err.code = response.code() 28 | emit(BaseResult.Error(err)) 29 | } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/domain/common/base/BaseResult.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.domain.common.base 2 | 3 | sealed class BaseResult { 4 | data class Success (val data : T) : BaseResult() 5 | data class Error (val rawResponse: U) : BaseResult() 6 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/domain/login/LoginRepository.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.domain.login 2 | 3 | import com.ydhnwb.cleanarchitectureexercise.data.common.utils.WrappedResponse 4 | import com.ydhnwb.cleanarchitectureexercise.data.login.remote.dto.LoginRequest 5 | import com.ydhnwb.cleanarchitectureexercise.data.login.remote.dto.LoginResponse 6 | import com.ydhnwb.cleanarchitectureexercise.domain.common.base.BaseResult 7 | import com.ydhnwb.cleanarchitectureexercise.domain.login.entity.LoginEntity 8 | import kotlinx.coroutines.flow.Flow 9 | 10 | interface LoginRepository { 11 | suspend fun login(loginRequest: LoginRequest) : Flow>> 12 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/domain/login/entity/LoginEntity.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.domain.login.entity 2 | 3 | data class LoginEntity( 4 | var id : Int, 5 | var name: String, 6 | var email: String, 7 | var token: String 8 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/domain/login/usecase/LoginUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.domain.login.usecase 2 | 3 | import com.ydhnwb.cleanarchitectureexercise.data.common.utils.WrappedResponse 4 | import com.ydhnwb.cleanarchitectureexercise.data.login.remote.dto.LoginRequest 5 | import com.ydhnwb.cleanarchitectureexercise.data.login.remote.dto.LoginResponse 6 | import com.ydhnwb.cleanarchitectureexercise.domain.common.base.BaseResult 7 | import com.ydhnwb.cleanarchitectureexercise.domain.login.LoginRepository 8 | import com.ydhnwb.cleanarchitectureexercise.domain.login.entity.LoginEntity 9 | import kotlinx.coroutines.flow.Flow 10 | import javax.inject.Inject 11 | 12 | class LoginUseCase @Inject constructor(private val loginRepository: LoginRepository) { 13 | suspend fun execute(loginRequest: LoginRequest): Flow>> { 14 | return loginRepository.login(loginRequest) 15 | } 16 | 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/domain/product/ProductRepository.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.domain.product 2 | 3 | import com.ydhnwb.cleanarchitectureexercise.data.common.utils.WrappedListResponse 4 | import com.ydhnwb.cleanarchitectureexercise.data.common.utils.WrappedResponse 5 | import com.ydhnwb.cleanarchitectureexercise.data.product.remote.dto.ProductCreateRequest 6 | import com.ydhnwb.cleanarchitectureexercise.data.product.remote.dto.ProductResponse 7 | import com.ydhnwb.cleanarchitectureexercise.data.product.remote.dto.ProductUpdateRequest 8 | import com.ydhnwb.cleanarchitectureexercise.domain.common.base.BaseResult 9 | import com.ydhnwb.cleanarchitectureexercise.domain.product.entity.ProductEntity 10 | import kotlinx.coroutines.flow.Flow 11 | 12 | interface ProductRepository { 13 | suspend fun getAllMyProducts() : Flow, WrappedListResponse>> 14 | suspend fun getProductById(id: String) : Flow>> 15 | suspend fun updateProduct(productUpdateRequest: ProductUpdateRequest, id: String) : Flow>> 16 | suspend fun deleteProductById(id: String) : Flow>> 17 | suspend fun createProduct(productCreateRequest: ProductCreateRequest) : Flow>> 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/domain/product/entity/ProductEntity.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.domain.product.entity 2 | 3 | data class ProductEntity( 4 | var id: Int, 5 | var name: String, 6 | var price: Int, 7 | var user: ProductUserEntity 8 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/domain/product/entity/ProductUserEntity.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.domain.product.entity 2 | 3 | data class ProductUserEntity( 4 | var id: Int, 5 | var name: String, 6 | var email: String 7 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/domain/product/usecase/CreateProductUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.domain.product.usecase 2 | 3 | import com.ydhnwb.cleanarchitectureexercise.data.common.utils.WrappedResponse 4 | import com.ydhnwb.cleanarchitectureexercise.data.product.remote.dto.ProductCreateRequest 5 | import com.ydhnwb.cleanarchitectureexercise.data.product.remote.dto.ProductResponse 6 | import com.ydhnwb.cleanarchitectureexercise.domain.common.base.BaseResult 7 | import com.ydhnwb.cleanarchitectureexercise.domain.product.ProductRepository 8 | import com.ydhnwb.cleanarchitectureexercise.domain.product.entity.ProductEntity 9 | import kotlinx.coroutines.flow.Flow 10 | import javax.inject.Inject 11 | 12 | class CreateProductUseCase @Inject constructor(private val productRepository: ProductRepository) { 13 | suspend fun invoke(productCreateRequest: ProductCreateRequest) : Flow>> { 14 | return productRepository.createProduct(productCreateRequest) 15 | } 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/domain/product/usecase/DeleteProductByIdUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.domain.product.usecase 2 | 3 | import com.ydhnwb.cleanarchitectureexercise.data.common.utils.WrappedResponse 4 | import com.ydhnwb.cleanarchitectureexercise.data.product.remote.dto.ProductResponse 5 | import com.ydhnwb.cleanarchitectureexercise.domain.common.base.BaseResult 6 | import com.ydhnwb.cleanarchitectureexercise.domain.product.ProductRepository 7 | import kotlinx.coroutines.flow.Flow 8 | import javax.inject.Inject 9 | 10 | class DeleteProductByIdUseCase @Inject constructor(private val productRepository: ProductRepository) { 11 | suspend fun invoke(id: String) : Flow>> { 12 | return productRepository.deleteProductById(id) 13 | } 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/domain/product/usecase/GetAllMyProductUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.domain.product.usecase 2 | 3 | import com.ydhnwb.cleanarchitectureexercise.data.common.utils.WrappedListResponse 4 | import com.ydhnwb.cleanarchitectureexercise.data.product.remote.dto.ProductResponse 5 | import com.ydhnwb.cleanarchitectureexercise.domain.common.base.BaseResult 6 | import com.ydhnwb.cleanarchitectureexercise.domain.product.ProductRepository 7 | import com.ydhnwb.cleanarchitectureexercise.domain.product.entity.ProductEntity 8 | import kotlinx.coroutines.flow.Flow 9 | import javax.inject.Inject 10 | 11 | class GetAllMyProductUseCase @Inject constructor(private val productRepository: ProductRepository) { 12 | suspend fun invoke() : Flow, WrappedListResponse>> { 13 | return productRepository.getAllMyProducts() 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/domain/product/usecase/GetProductByIdUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.domain.product.usecase 2 | 3 | import com.ydhnwb.cleanarchitectureexercise.data.common.utils.WrappedResponse 4 | import com.ydhnwb.cleanarchitectureexercise.data.product.remote.dto.ProductResponse 5 | import com.ydhnwb.cleanarchitectureexercise.domain.common.base.BaseResult 6 | import com.ydhnwb.cleanarchitectureexercise.domain.product.ProductRepository 7 | import com.ydhnwb.cleanarchitectureexercise.domain.product.entity.ProductEntity 8 | import kotlinx.coroutines.flow.Flow 9 | import javax.inject.Inject 10 | 11 | class GetProductByIdUseCase @Inject constructor(private val productRepository: ProductRepository) { 12 | suspend fun invoke(id: String) : Flow>> { 13 | return productRepository.getProductById(id) 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/domain/product/usecase/UpdateProductUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.domain.product.usecase 2 | 3 | import com.ydhnwb.cleanarchitectureexercise.data.common.utils.WrappedResponse 4 | import com.ydhnwb.cleanarchitectureexercise.data.product.remote.dto.ProductResponse 5 | import com.ydhnwb.cleanarchitectureexercise.data.product.remote.dto.ProductUpdateRequest 6 | import com.ydhnwb.cleanarchitectureexercise.domain.common.base.BaseResult 7 | import com.ydhnwb.cleanarchitectureexercise.domain.product.ProductRepository 8 | import com.ydhnwb.cleanarchitectureexercise.domain.product.entity.ProductEntity 9 | import kotlinx.coroutines.flow.Flow 10 | import javax.inject.Inject 11 | 12 | class UpdateProductUseCase @Inject constructor(private val productRepository: ProductRepository){ 13 | suspend fun invoke(productUpdateRequest: ProductUpdateRequest, id: String) : Flow>> { 14 | return productRepository.updateProduct(productUpdateRequest, id) 15 | } 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/domain/register/RegisterRepository.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.domain.register 2 | 3 | import com.ydhnwb.cleanarchitectureexercise.data.common.utils.WrappedResponse 4 | import com.ydhnwb.cleanarchitectureexercise.data.register.remote.dto.RegisterRequest 5 | import com.ydhnwb.cleanarchitectureexercise.data.register.remote.dto.RegisterResponse 6 | import com.ydhnwb.cleanarchitectureexercise.domain.common.base.BaseResult 7 | import com.ydhnwb.cleanarchitectureexercise.domain.register.entity.RegisterEntity 8 | import kotlinx.coroutines.flow.Flow 9 | 10 | interface RegisterRepository { 11 | suspend fun register(registerRequest: RegisterRequest) : Flow>> 12 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/domain/register/entity/RegisterEntity.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.domain.register.entity 2 | 3 | data class RegisterEntity( 4 | val id: Int, 5 | val name: String, 6 | val email: String, 7 | val token: String 8 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/domain/register/usecase/RegisterUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.domain.register.usecase 2 | 3 | import com.ydhnwb.cleanarchitectureexercise.data.common.utils.WrappedResponse 4 | import com.ydhnwb.cleanarchitectureexercise.data.register.remote.dto.RegisterRequest 5 | import com.ydhnwb.cleanarchitectureexercise.data.register.remote.dto.RegisterResponse 6 | import com.ydhnwb.cleanarchitectureexercise.domain.common.base.BaseResult 7 | import com.ydhnwb.cleanarchitectureexercise.domain.register.RegisterRepository 8 | import com.ydhnwb.cleanarchitectureexercise.domain.register.entity.RegisterEntity 9 | import kotlinx.coroutines.flow.Flow 10 | import javax.inject.Inject 11 | 12 | class RegisterUseCase @Inject constructor(private val registerRepository: RegisterRepository) { 13 | suspend fun invoke(registerRequest: RegisterRequest) : Flow>> { 14 | return registerRepository.register(registerRequest) 15 | } 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/infra/utils/SharedPrefs.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.infra.utils 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | 6 | @Suppress("UNCHECKED_CAST") 7 | class SharedPrefs (private val context: Context) { 8 | companion object { 9 | private const val PREF = "MyAppPrefName" 10 | private const val PREF_TOKEN = "user_token" 11 | } 12 | 13 | private val sharedPref: SharedPreferences = context.getSharedPreferences(PREF, Context.MODE_PRIVATE) 14 | 15 | 16 | fun saveToken(token: String){ 17 | put(PREF_TOKEN, token) 18 | } 19 | 20 | fun getToken() : String { 21 | return get(PREF_TOKEN, String::class.java) 22 | } 23 | 24 | private fun get(key: String, clazz: Class): T = 25 | when (clazz) { 26 | String::class.java -> sharedPref.getString(key, "") 27 | Boolean::class.java -> sharedPref.getBoolean(key, false) 28 | Float::class.java -> sharedPref.getFloat(key, -1f) 29 | Double::class.java -> sharedPref.getFloat(key, -1f) 30 | Int::class.java -> sharedPref.getInt(key, -1) 31 | Long::class.java -> sharedPref.getLong(key, -1L) 32 | else -> null 33 | } as T 34 | 35 | private fun put(key: String, data: T) { 36 | val editor = sharedPref.edit() 37 | when (data) { 38 | is String -> editor.putString(key, data) 39 | is Boolean -> editor.putBoolean(key, data) 40 | is Float -> editor.putFloat(key, data) 41 | is Double -> editor.putFloat(key, data.toFloat()) 42 | is Int -> editor.putInt(key, data) 43 | is Long -> editor.putLong(key, data) 44 | } 45 | editor.apply() 46 | } 47 | 48 | fun clear() { 49 | sharedPref.edit().run { 50 | remove(PREF_TOKEN) 51 | }.apply() 52 | } 53 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/presentation/App.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.presentation 2 | 3 | import android.app.Application 4 | import dagger.hilt.android.HiltAndroidApp 5 | 6 | @HiltAndroidApp 7 | class App : Application(){ 8 | override fun onCreate() { 9 | super.onCreate() 10 | 11 | } 12 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/presentation/common/extension/ContextExt.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.presentation.common.extension 2 | 3 | import android.content.Context 4 | import android.widget.Toast 5 | import androidx.appcompat.app.AlertDialog 6 | import com.ydhnwb.cleanarchitectureexercise.R 7 | 8 | fun Context.showToast(message: String){ 9 | Toast.makeText(this, message, Toast.LENGTH_LONG).show() 10 | } 11 | 12 | fun Context.showGenericAlertDialog(message: String){ 13 | AlertDialog.Builder(this).apply { 14 | setMessage(message) 15 | setPositiveButton(getString(R.string.button_text_ok)){ dialog, _ -> 16 | dialog.dismiss() 17 | } 18 | }.show() 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/presentation/common/extension/StringExt.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.presentation.common.extension 2 | 3 | import android.util.Patterns 4 | 5 | fun String.isEmail() : Boolean { 6 | return Patterns.EMAIL_ADDRESS.matcher(this).matches() 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/presentation/common/extension/ViewExt.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.presentation.common.extension 2 | 3 | import android.view.View 4 | 5 | fun View.gone(){ 6 | visibility = View.GONE 7 | } 8 | 9 | fun View.visible(){ 10 | visibility = View.VISIBLE 11 | } 12 | 13 | fun View.invisible(){ 14 | visibility = View.INVISIBLE 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/presentation/login/LoginActivity.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.presentation.login 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import androidx.activity.result.contract.ActivityResultContracts 6 | import androidx.activity.viewModels 7 | import androidx.appcompat.app.AppCompatActivity 8 | import androidx.lifecycle.Lifecycle 9 | import androidx.lifecycle.flowWithLifecycle 10 | import androidx.lifecycle.lifecycleScope 11 | import com.ydhnwb.cleanarchitectureexercise.R 12 | import com.ydhnwb.cleanarchitectureexercise.data.common.utils.WrappedResponse 13 | import com.ydhnwb.cleanarchitectureexercise.data.login.remote.dto.LoginRequest 14 | import com.ydhnwb.cleanarchitectureexercise.data.login.remote.dto.LoginResponse 15 | import com.ydhnwb.cleanarchitectureexercise.databinding.ActivityLoginBinding 16 | import com.ydhnwb.cleanarchitectureexercise.domain.login.entity.LoginEntity 17 | import com.ydhnwb.cleanarchitectureexercise.infra.utils.SharedPrefs 18 | import com.ydhnwb.cleanarchitectureexercise.presentation.common.extension.isEmail 19 | import com.ydhnwb.cleanarchitectureexercise.presentation.common.extension.showGenericAlertDialog 20 | import com.ydhnwb.cleanarchitectureexercise.presentation.common.extension.showToast 21 | import com.ydhnwb.cleanarchitectureexercise.presentation.main.MainActivity 22 | import com.ydhnwb.cleanarchitectureexercise.presentation.register.RegisterActivity 23 | import dagger.hilt.android.AndroidEntryPoint 24 | import kotlinx.coroutines.flow.launchIn 25 | import kotlinx.coroutines.flow.onEach 26 | import javax.inject.Inject 27 | 28 | @AndroidEntryPoint 29 | class LoginActivity : AppCompatActivity() { 30 | private lateinit var binding: ActivityLoginBinding 31 | 32 | private val viewModel: LoginViewModel by viewModels() 33 | 34 | private val openRegisterActivity = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> 35 | if (result.resultCode == RESULT_OK) { 36 | goToMainActivity() 37 | } 38 | } 39 | 40 | 41 | @Inject 42 | lateinit var sharedPrefs: SharedPrefs 43 | 44 | override fun onCreate(savedInstanceState: Bundle?) { 45 | super.onCreate(savedInstanceState) 46 | binding = ActivityLoginBinding.inflate(layoutInflater) 47 | setContentView(binding.root) 48 | login() 49 | goToRegisterActivity() 50 | observe() 51 | } 52 | 53 | private fun login(){ 54 | binding.loginButton.setOnClickListener { 55 | val email = binding.emailEditText.text.toString().trim() 56 | val password = binding.passwordEditText.text.toString().trim() 57 | if(validate(email, password)){ 58 | val loginRequest = LoginRequest(email, password) 59 | viewModel.login(loginRequest) 60 | } 61 | } 62 | } 63 | 64 | private fun validate(email: String, password: String) : Boolean{ 65 | resetAllInputError() 66 | if(!email.isEmail()){ 67 | setEmailError(getString(R.string.error_email_not_valid)) 68 | return false 69 | } 70 | 71 | if(password.length < 8){ 72 | setPasswordError(getString(R.string.error_password_not_valid)) 73 | return false 74 | } 75 | 76 | return true 77 | } 78 | 79 | private fun resetAllInputError(){ 80 | setEmailError(null) 81 | setPasswordError(null) 82 | } 83 | 84 | private fun setEmailError(e : String?){ 85 | binding.emailInput.error = e 86 | } 87 | 88 | private fun setPasswordError(e: String?){ 89 | binding.passwordInput.error = e 90 | } 91 | 92 | private fun observe(){ 93 | viewModel.mState 94 | .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED) 95 | .onEach { state -> handleStateChange(state) } 96 | .launchIn(lifecycleScope) 97 | } 98 | 99 | private fun handleStateChange(state: LoginActivityState){ 100 | when(state){ 101 | is LoginActivityState.Init -> Unit 102 | is LoginActivityState.ErrorLogin -> handleErrorLogin(state.rawResponse) 103 | is LoginActivityState.SuccessLogin -> handleSuccessLogin(state.loginEntity) 104 | is LoginActivityState.ShowToast -> showToast(state.message) 105 | is LoginActivityState.IsLoading -> handleLoading(state.isLoading) 106 | } 107 | } 108 | 109 | private fun handleErrorLogin(response: WrappedResponse){ 110 | showGenericAlertDialog(response.message) 111 | } 112 | 113 | private fun handleLoading(isLoading: Boolean){ 114 | binding.loginButton.isEnabled = !isLoading 115 | binding.registerButton.isEnabled = !isLoading 116 | binding.loadingProgressBar.isIndeterminate = isLoading 117 | if(!isLoading){ 118 | binding.loadingProgressBar.progress = 0 119 | } 120 | } 121 | 122 | private fun handleSuccessLogin(loginEntity: LoginEntity){ 123 | sharedPrefs.saveToken(loginEntity.token) 124 | showToast("Welcome ${loginEntity.name}") 125 | goToMainActivity() 126 | } 127 | 128 | private fun goToRegisterActivity(){ 129 | binding.registerButton.setOnClickListener { 130 | openRegisterActivity.launch(Intent(this@LoginActivity, RegisterActivity::class.java)) 131 | } 132 | } 133 | 134 | private fun goToMainActivity(){ 135 | startActivity(Intent(this@LoginActivity, MainActivity::class.java)) 136 | finish() 137 | } 138 | 139 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/presentation/login/LoginViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.presentation.login 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.ydhnwb.cleanarchitectureexercise.data.common.utils.WrappedResponse 6 | import com.ydhnwb.cleanarchitectureexercise.data.login.remote.dto.LoginRequest 7 | import com.ydhnwb.cleanarchitectureexercise.data.login.remote.dto.LoginResponse 8 | import com.ydhnwb.cleanarchitectureexercise.domain.common.base.BaseResult 9 | import com.ydhnwb.cleanarchitectureexercise.domain.login.entity.LoginEntity 10 | import com.ydhnwb.cleanarchitectureexercise.domain.login.usecase.LoginUseCase 11 | import dagger.hilt.android.lifecycle.HiltViewModel 12 | import kotlinx.coroutines.flow.* 13 | import kotlinx.coroutines.launch 14 | import javax.inject.Inject 15 | 16 | @HiltViewModel 17 | class LoginViewModel @Inject constructor(private val loginUseCase: LoginUseCase): ViewModel() { 18 | private val state = MutableStateFlow(LoginActivityState.Init) 19 | val mState: StateFlow get() = state 20 | 21 | 22 | private fun setLoading(){ 23 | state.value = LoginActivityState.IsLoading(true) 24 | } 25 | 26 | private fun hideLoading(){ 27 | state.value = LoginActivityState.IsLoading(false) 28 | } 29 | 30 | private fun showToast(message: String){ 31 | state.value = LoginActivityState.ShowToast(message) 32 | } 33 | 34 | 35 | fun login(loginRequest: LoginRequest){ 36 | viewModelScope.launch { 37 | loginUseCase.execute(loginRequest) 38 | .onStart { 39 | setLoading() 40 | } 41 | .catch { exception -> 42 | hideLoading() 43 | showToast(exception.message.toString()) 44 | } 45 | .collect { baseResult -> 46 | hideLoading() 47 | when(baseResult){ 48 | is BaseResult.Error -> state.value = LoginActivityState.ErrorLogin(baseResult.rawResponse) 49 | is BaseResult.Success -> state.value = LoginActivityState.SuccessLogin(baseResult.data) 50 | } 51 | } 52 | } 53 | } 54 | 55 | 56 | 57 | } 58 | 59 | sealed class LoginActivityState { 60 | object Init : LoginActivityState() 61 | data class IsLoading(val isLoading: Boolean) : LoginActivityState() 62 | data class ShowToast(val message: String) : LoginActivityState() 63 | data class SuccessLogin(val loginEntity: LoginEntity) : LoginActivityState() 64 | data class ErrorLogin(val rawResponse: WrappedResponse) : LoginActivityState() 65 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/presentation/main/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.presentation.main 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import com.google.android.material.snackbar.Snackbar 6 | import androidx.appcompat.app.AppCompatActivity 7 | import androidx.navigation.findNavController 8 | import androidx.navigation.ui.AppBarConfiguration 9 | import androidx.navigation.ui.navigateUp 10 | import androidx.navigation.ui.setupActionBarWithNavController 11 | import android.view.Menu 12 | import android.view.MenuItem 13 | import com.ydhnwb.cleanarchitectureexercise.R 14 | import com.ydhnwb.cleanarchitectureexercise.databinding.ActivityMainBinding 15 | import com.ydhnwb.cleanarchitectureexercise.infra.utils.SharedPrefs 16 | import com.ydhnwb.cleanarchitectureexercise.presentation.login.LoginActivity 17 | import dagger.hilt.android.AndroidEntryPoint 18 | import javax.inject.Inject 19 | 20 | 21 | @AndroidEntryPoint 22 | class MainActivity : AppCompatActivity() { 23 | 24 | private lateinit var appBarConfiguration: AppBarConfiguration 25 | private lateinit var binding: ActivityMainBinding 26 | 27 | @Inject 28 | lateinit var sharedPrefs: SharedPrefs 29 | 30 | override fun onCreate(savedInstanceState: Bundle?) { 31 | super.onCreate(savedInstanceState) 32 | 33 | binding = ActivityMainBinding.inflate(layoutInflater) 34 | setContentView(binding.root) 35 | 36 | setSupportActionBar(binding.toolbar) 37 | 38 | val navController = findNavController(R.id.nav_host_fragment_content_main) 39 | appBarConfiguration = AppBarConfiguration(navController.graph) 40 | setupActionBarWithNavController(navController, appBarConfiguration) 41 | 42 | } 43 | 44 | override fun onStart() { 45 | super.onStart() 46 | checkIsLoggedIn() 47 | } 48 | 49 | private fun checkIsLoggedIn(){ 50 | if (sharedPrefs.getToken().isEmpty()){ 51 | goToLoginActivity() 52 | } 53 | } 54 | 55 | private fun goToLoginActivity(){ 56 | startActivity(Intent(this@MainActivity, LoginActivity::class.java)) 57 | finish() 58 | } 59 | 60 | private fun signOut(){ 61 | sharedPrefs.clear() 62 | goToLoginActivity() 63 | } 64 | 65 | override fun onCreateOptionsMenu(menu: Menu): Boolean { 66 | menuInflater.inflate(R.menu.menu_main, menu) 67 | return true 68 | } 69 | 70 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 71 | return when (item.itemId) { 72 | R.id.action_sign_out -> { 73 | signOut() 74 | return true 75 | } 76 | else -> super.onOptionsItemSelected(item) 77 | } 78 | } 79 | 80 | override fun onSupportNavigateUp(): Boolean { 81 | val navController = findNavController(R.id.nav_host_fragment_content_main) 82 | return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp() 83 | } 84 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/presentation/main/create_product/CreateMainFragment.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.presentation.main.create_product 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import androidx.fragment.app.Fragment 6 | import androidx.fragment.app.setFragmentResult 7 | import androidx.fragment.app.viewModels 8 | import androidx.lifecycle.Lifecycle 9 | import androidx.lifecycle.flowWithLifecycle 10 | import androidx.lifecycle.lifecycleScope 11 | import androidx.navigation.fragment.findNavController 12 | import com.ydhnwb.cleanarchitectureexercise.R 13 | import com.ydhnwb.cleanarchitectureexercise.data.product.remote.dto.ProductCreateRequest 14 | import com.ydhnwb.cleanarchitectureexercise.databinding.FragmentMainCreateBinding 15 | import com.ydhnwb.cleanarchitectureexercise.presentation.common.extension.showToast 16 | import dagger.hilt.android.AndroidEntryPoint 17 | import kotlinx.coroutines.flow.launchIn 18 | import kotlinx.coroutines.flow.onEach 19 | 20 | @AndroidEntryPoint 21 | class CreateMainFragment : Fragment(R.layout.fragment_main_create){ 22 | private var _binding : FragmentMainCreateBinding? = null 23 | private val binding get() = _binding!! 24 | 25 | private val viewModel : CreateMainFragmentViewModel by viewModels() 26 | 27 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 28 | super.onViewCreated(view, savedInstanceState) 29 | _binding = FragmentMainCreateBinding.bind(view) 30 | observe() 31 | createProduct() 32 | } 33 | 34 | private fun setResultOkToPreviousFragment(){ 35 | val r = Bundle().apply { 36 | putBoolean("success_create", true) 37 | } 38 | setFragmentResult("success_create", r) 39 | } 40 | 41 | private fun observe(){ 42 | viewModel.mState.flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED) 43 | .onEach { state -> handleState(state) } 44 | .launchIn(viewLifecycleOwner.lifecycleScope) 45 | } 46 | 47 | private fun handleState(state: CreateMainFragmentState){ 48 | when(state){ 49 | is CreateMainFragmentState.IsLoading -> handleLoading(state.isLoading) 50 | is CreateMainFragmentState.SuccessCreate -> { 51 | setResultOkToPreviousFragment() 52 | findNavController().navigateUp() 53 | } 54 | is CreateMainFragmentState.ShowToast -> requireActivity().showToast(state.message) 55 | is CreateMainFragmentState.Init -> Unit 56 | } 57 | } 58 | 59 | private fun createProduct(){ 60 | binding.saveButton.setOnClickListener { 61 | val name = binding.productNameEditText.text.toString().trim() 62 | val price = binding.productPriceEditText.text.toString().trim() 63 | if(validate(name, price)){ 64 | viewModel.createProduct(ProductCreateRequest(name, price.toInt())) 65 | } 66 | } 67 | } 68 | 69 | private fun validate(name: String, price: String) : Boolean { 70 | resetAllError() 71 | 72 | if(name.isEmpty()){ 73 | setProductNameError(getString(R.string.error_product_name_not_valid)) 74 | return false 75 | } 76 | 77 | if(price.toIntOrNull() == null){ 78 | setProductPriceError(getString(R.string.error_price_not_valid)) 79 | return false 80 | } 81 | 82 | return true 83 | } 84 | 85 | private fun handleLoading(isLoading: Boolean) { 86 | binding.saveButton.isEnabled = !isLoading 87 | } 88 | 89 | private fun setProductNameError(e: String?){ 90 | binding.productNameInput.error = e 91 | } 92 | 93 | private fun setProductPriceError(e: String?){ 94 | binding.productPriceInput.error = e 95 | } 96 | 97 | private fun resetAllError(){ 98 | setProductNameError(null) 99 | setProductPriceError(null) 100 | } 101 | 102 | 103 | override fun onDestroy() { 104 | super.onDestroy() 105 | _binding = null 106 | } 107 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/presentation/main/create_product/CreateMainFragmentViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.presentation.main.create_product 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.ydhnwb.cleanarchitectureexercise.data.product.remote.dto.ProductCreateRequest 6 | import com.ydhnwb.cleanarchitectureexercise.domain.common.base.BaseResult 7 | import com.ydhnwb.cleanarchitectureexercise.domain.product.usecase.CreateProductUseCase 8 | import dagger.hilt.android.lifecycle.HiltViewModel 9 | import kotlinx.coroutines.flow.* 10 | import kotlinx.coroutines.launch 11 | import javax.inject.Inject 12 | 13 | @HiltViewModel 14 | class CreateMainFragmentViewModel @Inject constructor( 15 | private val createProductUseCase: CreateProductUseCase 16 | ) : ViewModel() { 17 | private val state = MutableStateFlow(CreateMainFragmentState.Init) 18 | val mState: StateFlow get() = state 19 | 20 | private fun setLoading(){ 21 | state.value = CreateMainFragmentState.IsLoading(true) 22 | } 23 | 24 | private fun hideLoading(){ 25 | state.value = CreateMainFragmentState.IsLoading(false) 26 | } 27 | 28 | private fun showToast(message: String){ 29 | state.value = CreateMainFragmentState.ShowToast(message) 30 | } 31 | 32 | private fun successCreate(){ 33 | state.value = CreateMainFragmentState.SuccessCreate 34 | } 35 | 36 | fun createProduct(productCreateRequest: ProductCreateRequest){ 37 | viewModelScope.launch { 38 | createProductUseCase.invoke(productCreateRequest) 39 | .onStart { 40 | setLoading() 41 | } 42 | .catch { exception -> 43 | hideLoading() 44 | showToast(exception.stackTraceToString()) 45 | } 46 | .collect { result -> 47 | hideLoading() 48 | when(result){ 49 | is BaseResult.Success -> successCreate() 50 | is BaseResult.Error -> showToast(result.rawResponse.message) 51 | } 52 | } 53 | } 54 | } 55 | } 56 | 57 | sealed class CreateMainFragmentState { 58 | object Init: CreateMainFragmentState() 59 | object SuccessCreate : CreateMainFragmentState() 60 | data class IsLoading(val isLoading: Boolean) : CreateMainFragmentState() 61 | data class ShowToast(val message: String) : CreateMainFragmentState() 62 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/presentation/main/detail/DetailMainFragment.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.presentation.main.detail 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import androidx.fragment.app.Fragment 6 | import androidx.fragment.app.viewModels 7 | import androidx.lifecycle.Lifecycle 8 | import androidx.lifecycle.flowWithLifecycle 9 | import androidx.lifecycle.lifecycleScope 10 | import androidx.navigation.fragment.findNavController 11 | import com.ydhnwb.cleanarchitectureexercise.R 12 | import com.ydhnwb.cleanarchitectureexercise.data.product.remote.dto.ProductUpdateRequest 13 | import com.ydhnwb.cleanarchitectureexercise.databinding.FragmentMainDetailBinding 14 | import com.ydhnwb.cleanarchitectureexercise.domain.product.entity.ProductEntity 15 | import com.ydhnwb.cleanarchitectureexercise.presentation.common.extension.showToast 16 | import dagger.hilt.android.AndroidEntryPoint 17 | import kotlinx.coroutines.flow.launchIn 18 | import kotlinx.coroutines.flow.onEach 19 | 20 | @AndroidEntryPoint 21 | class DetailMainFragment : Fragment(R.layout.fragment_main_detail) { 22 | 23 | private var _binding: FragmentMainDetailBinding? = null 24 | private val binding get() = _binding!! 25 | 26 | private val viewModel: DetailMainFragmentViewModel by viewModels() 27 | 28 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 29 | super.onViewCreated(view, savedInstanceState) 30 | _binding = FragmentMainDetailBinding.bind(view) 31 | observe() 32 | update() 33 | delete() 34 | fetchCurrentProduct() 35 | } 36 | 37 | private fun fetchCurrentProduct(){ 38 | val id = arguments?.getInt("product_id") ?: 0 39 | if (id != 0){ 40 | viewModel.getProductById(id.toString()) 41 | } 42 | } 43 | 44 | private fun observe(){ 45 | observeState() 46 | observeProduct() 47 | } 48 | 49 | private fun update(){ 50 | binding.updateButton.setOnClickListener { 51 | val name = binding.productNameEditText.text.toString().trim() 52 | val price = binding.productPriceEditText.text.toString().trim() 53 | val id = arguments?.getInt("product_id") ?: 0 54 | if(validate(name, price)){ 55 | viewModel.update(ProductUpdateRequest(name, price.toInt()), id.toString()) 56 | } 57 | } 58 | } 59 | 60 | private fun delete(){ 61 | binding.deleteButton.setOnClickListener { 62 | val id = arguments?.getInt("product_id") ?: 0 63 | viewModel.delete(id.toString()) 64 | } 65 | } 66 | 67 | private fun observeState(){ 68 | viewModel.state.flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED) 69 | .onEach { state -> handleState(state) } 70 | .launchIn(viewLifecycleOwner.lifecycleScope) 71 | } 72 | 73 | private fun observeProduct(){ 74 | viewModel.product.flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED) 75 | .onEach { product -> 76 | product?.let { handleProduct(it) } 77 | } 78 | .launchIn(viewLifecycleOwner.lifecycleScope) 79 | } 80 | 81 | private fun handleState(state: DetailMainFragmentState){ 82 | when(state){ 83 | is DetailMainFragmentState.SuccessUpdate -> findNavController().navigate(R.id.action_update_to_home) 84 | is DetailMainFragmentState.SuccessDelete -> findNavController().navigate(R.id.action_update_to_home) 85 | is DetailMainFragmentState.Init -> Unit 86 | is DetailMainFragmentState.ShowToast -> requireActivity().showToast(state.message) 87 | is DetailMainFragmentState.IsLoading -> handleLoading(state.isLoading) 88 | } 89 | } 90 | 91 | private fun handleLoading(isLoading: Boolean){ 92 | binding.deleteButton.isEnabled = !isLoading 93 | binding.updateButton.isEnabled = !isLoading 94 | } 95 | 96 | private fun handleProduct(productEntity: ProductEntity){ 97 | binding.productNameEditText.setText(productEntity.name) 98 | binding.productPriceEditText.setText(productEntity.price.toString()) 99 | } 100 | 101 | private fun resetAllError(){ 102 | setProductNameError(null) 103 | setPriceError(null) 104 | } 105 | 106 | private fun setProductNameError(e: String?){ 107 | binding.productNameInput.error = e 108 | } 109 | 110 | private fun setPriceError(e: String?){ 111 | binding.productPriceInput.error = e 112 | } 113 | 114 | private fun validate(name: String, price: String) : Boolean{ 115 | resetAllError() 116 | 117 | if(name.isEmpty()){ 118 | setProductNameError(getString(R.string.error_product_name_not_valid)) 119 | return false 120 | } 121 | 122 | val _price = price.toIntOrNull() 123 | if(_price == null){ 124 | setPriceError(getString(R.string.error_price_not_valid)) 125 | return false 126 | } 127 | 128 | return true 129 | } 130 | 131 | override fun onDestroyView() { 132 | super.onDestroyView() 133 | _binding = null 134 | } 135 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/presentation/main/detail/DetailMainFragmentViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.presentation.main.detail 2 | 3 | import android.util.Log 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import com.ydhnwb.cleanarchitectureexercise.data.product.remote.dto.ProductUpdateRequest 7 | import com.ydhnwb.cleanarchitectureexercise.domain.common.base.BaseResult 8 | import com.ydhnwb.cleanarchitectureexercise.domain.product.entity.ProductEntity 9 | import com.ydhnwb.cleanarchitectureexercise.domain.product.usecase.DeleteProductByIdUseCase 10 | import com.ydhnwb.cleanarchitectureexercise.domain.product.usecase.GetProductByIdUseCase 11 | import com.ydhnwb.cleanarchitectureexercise.domain.product.usecase.UpdateProductUseCase 12 | import dagger.hilt.android.lifecycle.HiltViewModel 13 | import kotlinx.coroutines.flow.* 14 | import kotlinx.coroutines.launch 15 | import javax.inject.Inject 16 | 17 | @HiltViewModel 18 | class DetailMainFragmentViewModel @Inject constructor( 19 | private val getProductByIdUseCase: GetProductByIdUseCase, 20 | private val updateProductUseCase: UpdateProductUseCase, 21 | private val deleteProductByIdUseCase: DeleteProductByIdUseCase 22 | ) : ViewModel() { 23 | private val _state = MutableStateFlow(DetailMainFragmentState.Init) 24 | val state : StateFlow get() = _state 25 | 26 | private val _product = MutableStateFlow(null) 27 | val product : StateFlow get() = _product 28 | 29 | private fun setLoading(){ 30 | _state.value = DetailMainFragmentState.IsLoading(true) 31 | } 32 | 33 | private fun hideLoading(){ 34 | _state.value = DetailMainFragmentState.IsLoading(false) 35 | } 36 | 37 | private fun showToast(message: String){ 38 | _state.value = DetailMainFragmentState.ShowToast(message) 39 | } 40 | 41 | fun getProductById(id: String){ 42 | viewModelScope.launch { 43 | getProductByIdUseCase.invoke(id) 44 | .onStart { 45 | setLoading() 46 | } 47 | .catch { exception -> 48 | hideLoading() 49 | showToast(exception.stackTraceToString()) 50 | } 51 | .collect { result -> 52 | hideLoading() 53 | when(result){ 54 | is BaseResult.Success -> { 55 | _product.value = result.data 56 | } 57 | is BaseResult.Error -> { 58 | showToast(result.rawResponse.message) 59 | } 60 | } 61 | } 62 | } 63 | } 64 | 65 | fun update(productUpdateRequest: ProductUpdateRequest, productId: String){ 66 | viewModelScope.launch { 67 | updateProductUseCase.invoke(productUpdateRequest, productId) 68 | .onStart { 69 | setLoading() 70 | } 71 | .catch { exception -> 72 | hideLoading() 73 | showToast(exception.stackTraceToString()) 74 | } 75 | .collect { result -> 76 | hideLoading() 77 | when(result){ 78 | is BaseResult.Success -> { 79 | _state.value = DetailMainFragmentState.SuccessUpdate 80 | } 81 | is BaseResult.Error -> { 82 | Log.e("DetailMainFragmentVM", result.rawResponse.errors!![0]) 83 | showToast(result.rawResponse.message) 84 | } 85 | } 86 | } 87 | } 88 | } 89 | 90 | fun delete(productId : String) { 91 | viewModelScope.launch { 92 | deleteProductByIdUseCase.invoke(productId) 93 | .onStart { 94 | setLoading() 95 | } 96 | .catch { exception -> 97 | Log.e("DetailMainViewModel", exception.stackTraceToString()) 98 | showToast(exception.stackTraceToString()) 99 | } 100 | .collect { result -> 101 | hideLoading() 102 | when(result){ 103 | is BaseResult.Success -> { 104 | _state.value = DetailMainFragmentState.SuccessDelete 105 | } 106 | is BaseResult.Error -> { 107 | showToast(result.rawResponse.message) 108 | } 109 | } 110 | } 111 | } 112 | } 113 | 114 | } 115 | 116 | sealed class DetailMainFragmentState { 117 | object Init : DetailMainFragmentState() 118 | object SuccessUpdate : DetailMainFragmentState() 119 | object SuccessDelete : DetailMainFragmentState() 120 | data class IsLoading(val isLoading: Boolean) : DetailMainFragmentState() 121 | data class ShowToast(val message : String) : DetailMainFragmentState() 122 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/presentation/main/home/HomeMainFragment.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.presentation.main.home 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import androidx.activity.result.contract.ActivityResultContracts 6 | import androidx.appcompat.app.AppCompatActivity 7 | import androidx.core.os.bundleOf 8 | import androidx.fragment.app.Fragment 9 | import androidx.fragment.app.setFragmentResultListener 10 | import androidx.fragment.app.viewModels 11 | import androidx.lifecycle.Lifecycle 12 | import androidx.lifecycle.flowWithLifecycle 13 | import androidx.lifecycle.lifecycleScope 14 | import androidx.navigation.fragment.findNavController 15 | import androidx.recyclerview.widget.LinearLayoutManager 16 | import com.ydhnwb.cleanarchitectureexercise.R 17 | import com.ydhnwb.cleanarchitectureexercise.databinding.FragmentMainHomeBinding 18 | import com.ydhnwb.cleanarchitectureexercise.domain.product.entity.ProductEntity 19 | import com.ydhnwb.cleanarchitectureexercise.presentation.common.extension.gone 20 | import com.ydhnwb.cleanarchitectureexercise.presentation.common.extension.showToast 21 | import com.ydhnwb.cleanarchitectureexercise.presentation.common.extension.visible 22 | import dagger.hilt.android.AndroidEntryPoint 23 | import kotlinx.coroutines.flow.launchIn 24 | import kotlinx.coroutines.flow.onEach 25 | 26 | @AndroidEntryPoint 27 | class HomeMainFragment : Fragment(R.layout.fragment_main_home) { 28 | 29 | private var _binding: FragmentMainHomeBinding? = null 30 | private val binding get() = _binding!! 31 | 32 | private val viewModel: HomeMainViewModel by viewModels() 33 | 34 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 35 | super.onViewCreated(view, savedInstanceState) 36 | _binding = FragmentMainHomeBinding.bind(view) 37 | setupRecyclerView() 38 | observe() 39 | goToCreateProductPage() 40 | setFragmentResultListener("success_create"){ requestKey, bundle -> 41 | if(bundle.getBoolean("success_create")){ 42 | viewModel.fetchAllMyProducts() 43 | } 44 | } 45 | } 46 | 47 | private fun goToCreateProductPage(){ 48 | binding.createFab.setOnClickListener { 49 | findNavController().navigate(R.id.action_home_to_create) 50 | } 51 | } 52 | 53 | private fun setupRecyclerView(){ 54 | val mAdapter = HomeMainProductAdapter(mutableListOf()) 55 | mAdapter.setItemTapListener(object : HomeMainProductAdapter.OnItemTap{ 56 | override fun onTap(product: ProductEntity) { 57 | val b = bundleOf("product_id" to product.id) 58 | findNavController().navigate(R.id.action_home_to_detail, b) 59 | } 60 | }) 61 | 62 | binding.productsRecyclerView.apply { 63 | adapter = mAdapter 64 | layoutManager = LinearLayoutManager(requireActivity()) 65 | } 66 | } 67 | 68 | private fun fetchProducts(){ 69 | viewModel.fetchAllMyProducts() 70 | } 71 | 72 | private fun observeState(){ 73 | viewModel.mState 74 | .flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED) 75 | .onEach { state -> 76 | handleState(state) 77 | } 78 | .launchIn(viewLifecycleOwner.lifecycleScope) 79 | } 80 | 81 | private fun observeProducts(){ 82 | viewModel.mProducts 83 | .flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED) 84 | .onEach { products -> 85 | handleProducts(products) 86 | } 87 | .launchIn(viewLifecycleOwner.lifecycleScope) 88 | } 89 | 90 | private fun observe(){ 91 | observeState() 92 | observeProducts() 93 | } 94 | 95 | private fun handleProducts(products: List){ 96 | binding.productsRecyclerView.adapter?.let { 97 | if(it is HomeMainProductAdapter){ 98 | it.updateList(products) 99 | } 100 | } 101 | } 102 | 103 | private fun handleState(state: HomeMainFragmentState){ 104 | when(state){ 105 | is HomeMainFragmentState.IsLoading -> handleLoading(state.isLoading) 106 | is HomeMainFragmentState.ShowToast -> requireActivity().showToast(state.message) 107 | is HomeMainFragmentState.Init -> Unit 108 | } 109 | } 110 | 111 | private fun handleLoading(isLoading: Boolean){ 112 | if(isLoading){ 113 | binding.loadingProgressBar.visible() 114 | }else{ 115 | binding.loadingProgressBar.gone() 116 | } 117 | } 118 | 119 | override fun onDestroyView() { 120 | super.onDestroyView() 121 | _binding = null 122 | } 123 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/presentation/main/home/HomeMainProductAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.presentation.main.home 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.recyclerview.widget.RecyclerView 6 | import com.ydhnwb.cleanarchitectureexercise.databinding.ItemProductBinding 7 | import com.ydhnwb.cleanarchitectureexercise.domain.product.entity.ProductEntity 8 | 9 | class HomeMainProductAdapter(private val products: MutableList) : RecyclerView.Adapter(){ 10 | interface OnItemTap { 11 | fun onTap(product: ProductEntity) 12 | } 13 | 14 | fun setItemTapListener(l: OnItemTap){ 15 | onTapListener = l 16 | } 17 | 18 | private var onTapListener: OnItemTap? = null 19 | 20 | fun updateList(mProducts: List){ 21 | products.clear() 22 | products.addAll(mProducts) 23 | notifyDataSetChanged() 24 | } 25 | 26 | inner class ViewHolder(private val itemBinding: ItemProductBinding) : RecyclerView.ViewHolder(itemBinding.root){ 27 | fun bind(product: ProductEntity){ 28 | itemBinding.productNameTextView.text = product.name 29 | itemBinding.root.setOnClickListener { 30 | onTapListener?.onTap(product) 31 | } 32 | } 33 | } 34 | 35 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 36 | val view = ItemProductBinding.inflate(LayoutInflater.from(parent.context), parent, false) 37 | return ViewHolder(view) 38 | } 39 | 40 | override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bind(products[position]) 41 | 42 | override fun getItemCount() = products.size 43 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/presentation/main/home/HomeMainViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.presentation.main.home 2 | 3 | import android.util.Log 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import com.ydhnwb.cleanarchitectureexercise.domain.common.base.BaseResult 7 | import com.ydhnwb.cleanarchitectureexercise.domain.product.entity.ProductEntity 8 | import com.ydhnwb.cleanarchitectureexercise.domain.product.usecase.GetAllMyProductUseCase 9 | import dagger.hilt.android.lifecycle.HiltViewModel 10 | import kotlinx.coroutines.flow.* 11 | import kotlinx.coroutines.launch 12 | import javax.inject.Inject 13 | 14 | @HiltViewModel 15 | class HomeMainViewModel @Inject constructor(private val getAllMyProductUseCase: GetAllMyProductUseCase) : ViewModel(){ 16 | private val state = MutableStateFlow(HomeMainFragmentState.Init) 17 | val mState: StateFlow get() = state 18 | 19 | private val products = MutableStateFlow>(mutableListOf()) 20 | val mProducts: StateFlow> get() = products 21 | 22 | init { 23 | fetchAllMyProducts() 24 | } 25 | 26 | 27 | private fun setLoading(){ 28 | state.value = HomeMainFragmentState.IsLoading(true) 29 | } 30 | 31 | private fun hideLoading(){ 32 | state.value = HomeMainFragmentState.IsLoading(false) 33 | } 34 | 35 | private fun showToast(message: String){ 36 | state.value = HomeMainFragmentState.ShowToast(message) 37 | } 38 | 39 | fun fetchAllMyProducts(){ 40 | viewModelScope.launch { 41 | getAllMyProductUseCase.invoke() 42 | .onStart { 43 | setLoading() 44 | } 45 | .catch { exception -> 46 | hideLoading() 47 | showToast(exception.message.toString()) 48 | } 49 | .collect { result -> 50 | hideLoading() 51 | when(result){ 52 | is BaseResult.Success -> { 53 | products.value = result.data 54 | } 55 | is BaseResult.Error -> { 56 | showToast(result.rawResponse.message) 57 | } 58 | } 59 | } 60 | } 61 | } 62 | 63 | } 64 | 65 | sealed class HomeMainFragmentState { 66 | object Init : HomeMainFragmentState() 67 | data class IsLoading(val isLoading: Boolean) : HomeMainFragmentState() 68 | data class ShowToast(val message : String) : HomeMainFragmentState() 69 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/presentation/register/RegisterActivity.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.presentation.register 2 | 3 | import android.os.Bundle 4 | import androidx.activity.viewModels 5 | import androidx.appcompat.app.AppCompatActivity 6 | import androidx.lifecycle.Lifecycle 7 | import androidx.lifecycle.flowWithLifecycle 8 | import androidx.lifecycle.lifecycleScope 9 | import com.ydhnwb.cleanarchitectureexercise.R 10 | import com.ydhnwb.cleanarchitectureexercise.data.common.utils.WrappedResponse 11 | import com.ydhnwb.cleanarchitectureexercise.data.register.remote.dto.RegisterRequest 12 | import com.ydhnwb.cleanarchitectureexercise.data.register.remote.dto.RegisterResponse 13 | import com.ydhnwb.cleanarchitectureexercise.databinding.ActivityRegisterBinding 14 | import com.ydhnwb.cleanarchitectureexercise.domain.register.entity.RegisterEntity 15 | import com.ydhnwb.cleanarchitectureexercise.infra.utils.SharedPrefs 16 | import com.ydhnwb.cleanarchitectureexercise.presentation.common.extension.isEmail 17 | import com.ydhnwb.cleanarchitectureexercise.presentation.common.extension.showGenericAlertDialog 18 | import com.ydhnwb.cleanarchitectureexercise.presentation.common.extension.showToast 19 | import dagger.hilt.android.AndroidEntryPoint 20 | import kotlinx.coroutines.flow.launchIn 21 | import kotlinx.coroutines.flow.onEach 22 | import javax.inject.Inject 23 | 24 | @AndroidEntryPoint 25 | class RegisterActivity : AppCompatActivity() { 26 | 27 | private lateinit var binding : ActivityRegisterBinding 28 | private val viewModel: RegisterViewModel by viewModels() 29 | @Inject 30 | lateinit var sharedPrefs: SharedPrefs 31 | 32 | 33 | 34 | override fun onCreate(savedInstanceState: Bundle?) { 35 | super.onCreate(savedInstanceState) 36 | binding = ActivityRegisterBinding.inflate(layoutInflater) 37 | setContentView(binding.root) 38 | back() 39 | register() 40 | observe() 41 | } 42 | 43 | private fun register(){ 44 | binding.registerButton.setOnClickListener { 45 | val name = binding.nameEditText.text.toString().trim() 46 | val email = binding.emailEditText.text.toString().trim() 47 | val password = binding.passwordEditText.text.toString().trim() 48 | if(validate(name, email, password)){ 49 | viewModel.register(RegisterRequest(name, email, password)) 50 | } 51 | } 52 | } 53 | 54 | private fun validate(name: String, email: String, password: String) : Boolean{ 55 | resetAllInputError() 56 | 57 | if(name.isEmpty()){ 58 | setNameError(getString(R.string.error_name_not_valid)) 59 | return false 60 | } 61 | 62 | if(!email.isEmail()){ 63 | setEmailError(getString(R.string.error_email_not_valid)) 64 | return false 65 | } 66 | 67 | if(password.length < 8){ 68 | setPasswordError(getString(R.string.error_password_not_valid)) 69 | return false 70 | } 71 | 72 | return true 73 | } 74 | 75 | private fun resetAllInputError(){ 76 | setNameError(null) 77 | setEmailError(null) 78 | setPasswordError(null) 79 | } 80 | 81 | private fun setNameError(e: String?){ 82 | binding.nameInput.error = e 83 | } 84 | 85 | private fun setEmailError(e: String?){ 86 | binding.emailInput.error = e 87 | } 88 | 89 | private fun setPasswordError(e: String?){ 90 | binding.passwordInput.error = e 91 | } 92 | 93 | private fun back(){ 94 | binding.backButton.setOnClickListener { 95 | finish() 96 | } 97 | } 98 | 99 | private fun handleStateChange(state: RegisterActivityState){ 100 | when(state){ 101 | is RegisterActivityState.ShowToast -> showToast(state.message) 102 | is RegisterActivityState.IsLoading -> handleLoading(state.isLoading) 103 | is RegisterActivityState.SuccessRegister -> handleSuccessRegister(state.registerEntity) 104 | is RegisterActivityState.ErrorRegister -> handleErrorRegister(state.rawResponse) 105 | is RegisterActivityState.Init -> Unit 106 | } 107 | } 108 | 109 | private fun observe(){ 110 | viewModel.mState 111 | .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED) 112 | .onEach { state -> handleStateChange(state) } 113 | .launchIn(lifecycleScope) 114 | } 115 | 116 | private fun handleSuccessRegister(registerEntity: RegisterEntity){ 117 | sharedPrefs.saveToken(registerEntity.token) 118 | setResult(RESULT_OK) 119 | finish() 120 | } 121 | 122 | private fun handleErrorRegister(httpResponse: WrappedResponse){ 123 | showGenericAlertDialog(httpResponse.message) 124 | } 125 | 126 | private fun handleLoading(isLoading: Boolean){ 127 | binding.registerButton.isEnabled = !isLoading 128 | binding.backButton.isEnabled = !isLoading 129 | binding.loadingProgressBar.isIndeterminate = isLoading 130 | if(!isLoading){ 131 | binding.loadingProgressBar.progress = 0 132 | } 133 | } 134 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ydhnwb/cleanarchitectureexercise/presentation/register/RegisterViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.ydhnwb.cleanarchitectureexercise.presentation.register 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.ydhnwb.cleanarchitectureexercise.data.common.utils.WrappedResponse 6 | import com.ydhnwb.cleanarchitectureexercise.data.register.remote.dto.RegisterRequest 7 | import com.ydhnwb.cleanarchitectureexercise.data.register.remote.dto.RegisterResponse 8 | import com.ydhnwb.cleanarchitectureexercise.domain.common.base.BaseResult 9 | import com.ydhnwb.cleanarchitectureexercise.domain.register.entity.RegisterEntity 10 | import com.ydhnwb.cleanarchitectureexercise.domain.register.usecase.RegisterUseCase 11 | import dagger.hilt.android.lifecycle.HiltViewModel 12 | import kotlinx.coroutines.flow.* 13 | import kotlinx.coroutines.launch 14 | import javax.inject.Inject 15 | 16 | @HiltViewModel 17 | class RegisterViewModel @Inject constructor(private val registerUseCase: RegisterUseCase) : ViewModel() { 18 | private val state = MutableStateFlow(RegisterActivityState.Init) 19 | val mState: StateFlow get() = state 20 | 21 | private fun setLoading(){ 22 | state.value = RegisterActivityState.IsLoading(true) 23 | } 24 | 25 | private fun hideLoading(){ 26 | state.value = RegisterActivityState.IsLoading(false) 27 | } 28 | 29 | private fun showToast(message: String){ 30 | state.value = RegisterActivityState.ShowToast(message) 31 | } 32 | 33 | private fun successRegister(registerEntity: RegisterEntity){ 34 | state.value = RegisterActivityState.SuccessRegister(registerEntity) 35 | } 36 | 37 | private fun failedRegister(rawResponse: WrappedResponse){ 38 | state.value = RegisterActivityState.ErrorRegister(rawResponse) 39 | } 40 | 41 | fun register(registerRequest: RegisterRequest){ 42 | viewModelScope.launch { 43 | registerUseCase.invoke(registerRequest) 44 | .onStart { 45 | setLoading() 46 | } 47 | .catch { exception -> 48 | showToast(exception.message.toString()) 49 | hideLoading() 50 | } 51 | .collect { result -> 52 | hideLoading() 53 | when(result){ 54 | is BaseResult.Success -> successRegister(result.data) 55 | is BaseResult.Error -> failedRegister(result.rawResponse) 56 | } 57 | } 58 | } 59 | } 60 | } 61 | 62 | sealed class RegisterActivityState { 63 | object Init : RegisterActivityState() 64 | data class IsLoading(val isLoading: Boolean) : RegisterActivityState() 65 | data class ShowToast(val message: String) : RegisterActivityState() 66 | data class SuccessRegister(val registerEntity: RegisterEntity) : RegisterActivityState() 67 | data class ErrorRegister(val rawResponse: WrappedResponse) : RegisterActivityState() 68 | } -------------------------------------------------------------------------------- /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_baseline_arrow_back_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_create_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /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/layout/activity_login.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 16 | 17 | 26 | 27 | 32 | 33 | 40 | 41 | 47 | 52 | 53 | 54 | 61 | 67 | 68 | 69 | 74 | 75 | 76 | 77 | 78 | 79 | 88 | 89 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_register.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 15 | 16 | 25 | 26 | 33 | 34 | 39 | 40 | 46 | 47 | 53 | 58 | 59 | 60 | 66 | 72 | 73 | 74 | 75 | 82 | 88 | 89 | 90 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /app/src/main/res/layout/content_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_main_create.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 12 | 13 | 19 | 24 | 25 | 26 | 27 | 33 | 39 | 40 | 41 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_main_detail.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 15 | 16 | 17 | 23 | 28 | 29 | 30 | 36 | 42 | 43 | 44 | 49 | 50 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_main_home.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 14 | 23 | 24 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_product.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 15 | 16 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 | 5 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhasancse15/android-clean-architecture/5a9be4e32cf93457b69a7dd8ace90d06df3aa27a/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhasancse15/android-clean-architecture/5a9be4e32cf93457b69a7dd8ace90d06df3aa27a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhasancse15/android-clean-architecture/5a9be4e32cf93457b69a7dd8ace90d06df3aa27a/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhasancse15/android-clean-architecture/5a9be4e32cf93457b69a7dd8ace90d06df3aa27a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhasancse15/android-clean-architecture/5a9be4e32cf93457b69a7dd8ace90d06df3aa27a/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhasancse15/android-clean-architecture/5a9be4e32cf93457b69a7dd8ace90d06df3aa27a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhasancse15/android-clean-architecture/5a9be4e32cf93457b69a7dd8ace90d06df3aa27a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhasancse15/android-clean-architecture/5a9be4e32cf93457b69a7dd8ace90d06df3aa27a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhasancse15/android-clean-architecture/5a9be4e32cf93457b69a7dd8ace90d06df3aa27a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhasancse15/android-clean-architecture/5a9be4e32cf93457b69a7dd8ace90d06df3aa27a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/navigation/nav_graph.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 17 | 18 | 21 | 22 | 27 | 28 | 34 | 35 | 36 | 41 | 42 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | #F44336 11 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 16dp 3 | 16dp 4 | 28sp 5 | 16dp 6 | 32dp 7 | 12dp 8 | 8dp 9 | 12sp 10 | 2dp 11 | 8dp 12 | 8dp 13 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Clean Architecture Exercise 3 | Settings 4 | 5 | First Fragment 6 | Second Fragment 7 | Next 8 | Previous 9 | 10 | Hello first fragment 11 | Hello second fragment. Arg: %1$s 12 | Sign in 13 | Login to your account to access all the feature. 14 | Enter your email 15 | Password 16 | Sign in 17 | I don\'t have an account 18 | Ok 19 | Email is not valid 20 | Password is not valid 21 | Sign up 22 | Create an account to get access to all the feature 23 | Back 24 | Your full name 25 | Create account 26 | Name is not valid 27 | Primary 28 | Detail 29 | Product name 30 | Price 31 | Update 32 | Delete 33 | Product name is not valid 34 | Price is not valid 35 | Your email 36 | Password 37 | Create new product 38 | Save 39 | Create 40 | Sign out 41 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 | 21 | 22 |