├── .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 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
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 |
--------------------------------------------------------------------------------
/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 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/test/java/com/ydhnwb/cleanarchitectureexercise/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.ydhnwb.cleanarchitectureexercise
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | buildscript {
3 | ext.kotlin_version = "1.5.0"
4 |
5 | // http
6 | ext.retrofit_version = "2.9.0"
7 | ext.okhttp_logging_version = "4.9.0"
8 |
9 | // coroutine
10 | ext.coroutine_version = "1.4.1"
11 |
12 | // dagger hilt
13 | ext.dagger_hilt_version = '2.40.1'
14 | ext.dagger_hilt_viewmodel_version = "1.0.0-alpha03"
15 |
16 | // activiy ktx
17 | ext.activity_ktx_version = "1.2.3"
18 | ext.lifecycle_version = "2.4.0-alpha02"
19 |
20 |
21 | repositories {
22 | google()
23 | mavenCentral()
24 | }
25 | dependencies {
26 | classpath 'com.android.tools.build:gradle:7.2.1'
27 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
28 | classpath "com.google.dagger:hilt-android-gradle-plugin:$dagger_hilt_version"
29 |
30 |
31 | // NOTE: Do not place your application dependencies here; they belong
32 | // in the individual module build.gradle files
33 | }
34 | }
35 |
36 | allprojects {
37 | repositories {
38 | google()
39 | mavenCentral()
40 | jcenter() // Warning: this repository is going to shut down soon
41 | }
42 | }
43 |
44 | task clean(type: Delete) {
45 | delete rootProject.buildDir
46 | }
--------------------------------------------------------------------------------
/docs/Screenshot_1622220439.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mhasancse15/android-clean-architecture/5a9be4e32cf93457b69a7dd8ace90d06df3aa27a/docs/Screenshot_1622220439.png
--------------------------------------------------------------------------------
/docs/Screenshot_1622220448.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mhasancse15/android-clean-architecture/5a9be4e32cf93457b69a7dd8ace90d06df3aa27a/docs/Screenshot_1622220448.png
--------------------------------------------------------------------------------
/docs/Screenshot_1622220456.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mhasancse15/android-clean-architecture/5a9be4e32cf93457b69a7dd8ace90d06df3aa27a/docs/Screenshot_1622220456.png
--------------------------------------------------------------------------------
/docs/Screenshot_1622220461.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mhasancse15/android-clean-architecture/5a9be4e32cf93457b69a7dd8ace90d06df3aa27a/docs/Screenshot_1622220461.png
--------------------------------------------------------------------------------
/docs/Screenshot_1622220467.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mhasancse15/android-clean-architecture/5a9be4e32cf93457b69a7dd8ace90d06df3aa27a/docs/Screenshot_1622220467.png
--------------------------------------------------------------------------------
/docs/clean.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mhasancse15/android-clean-architecture/5a9be4e32cf93457b69a7dd8ace90d06df3aa27a/docs/clean.png
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | android.enableJetifier=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mhasancse15/android-clean-architecture/5a9be4e32cf93457b69a7dd8ace90d06df3aa27a/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri May 21 21:46:53 WIB 2021
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
8 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = "Clean Architecture Exercise"
2 | include ':app'
3 |
--------------------------------------------------------------------------------