├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── architecture │ │ └── clean │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ ├── Robboto.TTF │ │ ├── Roboto-Light.ttf │ │ ├── Roboto-Medium.ttf │ │ └── Roboto_Italic.TTF │ ├── java │ │ └── com │ │ │ └── architecture │ │ │ └── clean │ │ │ ├── core │ │ │ ├── App.kt │ │ │ ├── Config.kt │ │ │ └── FragmentFactory.kt │ │ │ ├── data │ │ │ ├── mapper │ │ │ │ └── CloudErrorMapper.kt │ │ │ ├── repository │ │ │ │ └── AppRepoImp.kt │ │ │ ├── restful │ │ │ │ └── ApiService.kt │ │ │ └── source │ │ │ │ ├── cloud │ │ │ │ ├── BaseCloudRepository.kt │ │ │ │ └── CloudRepository.kt │ │ │ │ └── db │ │ │ │ ├── AppDatabase.kt │ │ │ │ └── FoodDao.kt │ │ │ ├── di │ │ │ ├── builder │ │ │ │ ├── ActivityBuilder.kt │ │ │ │ ├── AppViewModelBuilder.kt │ │ │ │ ├── MainActivityProviders.kt │ │ │ │ ├── RepositoryBuilder.kt │ │ │ │ └── ViewModelBuilder.kt │ │ │ ├── component │ │ │ │ └── CoreComponent.kt │ │ │ ├── module │ │ │ │ ├── ContextModule.kt │ │ │ │ ├── DataBaseModule.kt │ │ │ │ └── NetworkModule.kt │ │ │ └── qualifier │ │ │ │ └── ViewModelKey.kt │ │ │ ├── domain │ │ │ ├── mapper │ │ │ │ └── DomainErrorUtil.kt │ │ │ ├── model │ │ │ │ ├── Food.kt │ │ │ │ ├── FoodDto.kt │ │ │ │ └── response │ │ │ │ │ ├── DomainErrorException.kt │ │ │ │ │ ├── ErrorModel.kt │ │ │ │ │ ├── ErrorStatus.kt │ │ │ │ │ └── UseCaseResponse.kt │ │ │ ├── repository │ │ │ │ └── AppRepository.kt │ │ │ └── usecase │ │ │ │ ├── GetHomeUseCase.kt │ │ │ │ └── base │ │ │ │ ├── SingleUseCase.kt │ │ │ │ └── UseCase.kt │ │ │ └── ui │ │ │ ├── CustomBindingAdapter.kt │ │ │ ├── DataBindingViewHolder.kt │ │ │ ├── ViewModelFactory.kt │ │ │ ├── base │ │ │ └── BaseViewModel.kt │ │ │ ├── detail │ │ │ └── DetailActivity.kt │ │ │ └── home │ │ │ ├── HomeFragment.kt │ │ │ ├── HomeViewModel.kt │ │ │ ├── MainActivity.kt │ │ │ ├── adapter │ │ │ └── HomeAdapter.kt │ │ │ └── callback │ │ │ └── HomeCallBack.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── ic_account_circle_black_24dp.xml │ │ ├── ic_arrow_back_black_24dp.xml │ │ ├── ic_favorite_border_black_24dp.xml │ │ ├── ic_favorite_red_24dp.xml │ │ ├── ic_launcher_background.xml │ │ └── pic.png │ │ ├── layout │ │ ├── activity_detail.xml │ │ ├── activity_login.xml │ │ ├── activity_main.xml │ │ ├── food_item_row.xml │ │ ├── fragment_home.xml │ │ ├── nav_header_main.xml │ │ └── toolbar_main.xml │ │ ├── menu │ │ ├── activity_main_drawer.xml │ │ └── menu_detail.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 │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── architecture │ └── clean │ ├── ExampleUnitTest.kt │ ├── RxSchedulersOverrideRule.java │ └── domain │ ├── SingleUseCaseTest.kt │ └── usecase │ └── GetHomeUseCaseTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── screenshots ├── Screenshot_20181010-162706.png └── Screenshot_20181010-162713.png └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the ART/Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | out/ 15 | 16 | # Gradle files 17 | .gradle/ 18 | build/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | 23 | # Proguard folder generated by Eclipse 24 | proguard/ 25 | 26 | # Log Files 27 | *.log 28 | 29 | # Android Studio Navigation editor temp files 30 | .navigation/ 31 | 32 | # Android Studio captures folder 33 | captures/ 34 | 35 | # IntelliJ 36 | *.iml 37 | .idea/ 38 | 39 | # Keystore files 40 | # Uncomment the following line if you do not want to check your keystore files in. 41 | #*.jks 42 | 43 | # External native build folder generated in Android Studio 2.2 and later 44 | .externalNativeBuild 45 | 46 | # Google Services (e.g. APIs or Firebase) 47 | google-services.json 48 | 49 | # Freeline 50 | freeline.py 51 | freeline/ 52 | freeline_project_description.json 53 | 54 | # fastlane 55 | fastlane/report.xml 56 | fastlane/Preview.html 57 | fastlane/screenshots 58 | fastlane/test_output 59 | fastlane/readme.md 60 | 61 | *.DS_Store 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android-Clean-Architecture 2 | Example project how to design Android application with clean architecture, MVVM, RXJava and Dagger2, create your next project to be more scalable and testable. 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | apply plugin: 'kotlin-kapt' 5 | 6 | android { 7 | compileSdkVersion 28 8 | defaultConfig { 9 | applicationId "com.cooking.app" 10 | minSdkVersion 17 11 | targetSdkVersion 28 12 | versionCode 111 13 | versionName "1.1.1" 14 | multiDexEnabled true 15 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 16 | } 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | dataBinding { 24 | enabled = true 25 | } 26 | compileOptions { 27 | targetCompatibility 1.8 28 | sourceCompatibility 1.8 29 | } 30 | } 31 | 32 | dependencies { 33 | implementation fileTree(dir: 'libs', include: ['*.jar']) 34 | implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 35 | implementation "com.android.support:appcompat-v7:$support_version" 36 | implementation "com.android.support:design:$support_version" 37 | implementation "com.android.support:cardview-v7:$support_version" 38 | implementation "com.android.support:recyclerview-v7:$support_version" 39 | implementation "com.squareup.picasso:picasso:$picasso_version" 40 | implementation 'com.squareup.retrofit2:retrofit:2.4.0' 41 | implementation 'com.android.support:multidex:1.0.3' 42 | implementation 'com.squareup.okhttp3:okhttp:3.10.0' 43 | implementation 'com.squareup.retrofit2:converter-gson:2.3.0' 44 | implementation 'com.google.code.gson:gson:2.8.2' 45 | implementation 'com.squareup.okhttp3:logging-interceptor:3.9.1' 46 | implementation "android.arch.lifecycle:extensions:$arch_version" 47 | kapt "android.arch.lifecycle:compiler:$arch_version" 48 | implementation "android.arch.lifecycle:reactivestreams:$arch_version" 49 | implementation "android.arch.lifecycle:common-java8:$arch_version" 50 | 51 | implementation "io.reactivex.rxjava2:rxjava:$rx_version" 52 | implementation "io.reactivex.rxjava2:rxandroid:$rx_android_version" 53 | implementation "com.squareup.retrofit2:adapter-rxjava2:$rx_adapter_version" 54 | 55 | implementation 'uk.co.chrisjenx:calligraphy:2.3.0' 56 | 57 | implementation "com.google.dagger:dagger:$daggerVersion" 58 | implementation "com.google.dagger:dagger-android:$daggerVersion" 59 | implementation "com.google.dagger:dagger-android-support:$daggerVersion" 60 | kapt "com.google.dagger:dagger-compiler:$daggerVersion" 61 | kapt "com.google.dagger:dagger-android-processor:$daggerVersion" 62 | kapt "com.google.dagger:dagger-android-support:$daggerVersion" 63 | 64 | implementation "android.arch.persistence.room:runtime:$roomVersion" 65 | kapt "android.arch.persistence.room:compiler:$roomVersion" 66 | implementation "android.arch.persistence.room:rxjava2:$roomVersion" 67 | 68 | testImplementation 'junit:junit:4.12' 69 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 70 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 71 | testImplementation 'org.mockito:mockito-inline:2.11.0' 72 | implementation "android.arch.core:core-testing:$arch_version" 73 | implementation "com.nhaarman:mockito-kotlin-kt1.1:$mockitoKotlinVersion" 74 | 75 | } 76 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/architecture/clean/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.architecture.clean 2 | 3 | import android.support.test.InstrumentationRegistry 4 | import android.support.test.runner.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.getTargetContext() 22 | assertEquals("com.architecture.clean", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/assets/Robboto.TTF: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samira-badamestani/android-clean-architecture/ea99a098327390a89d3fbb11ba4b9cd74f698f4b/app/src/main/assets/Robboto.TTF -------------------------------------------------------------------------------- /app/src/main/assets/Roboto-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samira-badamestani/android-clean-architecture/ea99a098327390a89d3fbb11ba4b9cd74f698f4b/app/src/main/assets/Roboto-Light.ttf -------------------------------------------------------------------------------- /app/src/main/assets/Roboto-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samira-badamestani/android-clean-architecture/ea99a098327390a89d3fbb11ba4b9cd74f698f4b/app/src/main/assets/Roboto-Medium.ttf -------------------------------------------------------------------------------- /app/src/main/assets/Roboto_Italic.TTF: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samira-badamestani/android-clean-architecture/ea99a098327390a89d3fbb11ba4b9cd74f698f4b/app/src/main/assets/Roboto_Italic.TTF -------------------------------------------------------------------------------- /app/src/main/java/com/architecture/clean/core/App.kt: -------------------------------------------------------------------------------- 1 | package com.architecture.clean.core 2 | 3 | 4 | import android.content.Context 5 | import android.support.multidex.MultiDex 6 | import com.architecture.clean.R 7 | import com.architecture.clean.di.component.DaggerCoreComponent 8 | import dagger.android.AndroidInjector 9 | import dagger.android.support.DaggerApplication 10 | import uk.co.chrisjenx.calligraphy.CalligraphyConfig 11 | 12 | class App : DaggerApplication() { 13 | override fun applicationInjector(): AndroidInjector { 14 | return DaggerCoreComponent 15 | .builder() 16 | .application(this) 17 | .build() 18 | } 19 | 20 | override fun onCreate() { 21 | super.onCreate() 22 | CalligraphyConfig.initDefault(CalligraphyConfig.Builder() 23 | .setDefaultFontPath("fonts/Robboto.TTF") 24 | .setFontAttrId(R.attr.fontPath) 25 | .build() 26 | ) 27 | } 28 | override fun attachBaseContext(base: Context?) { 29 | super.attachBaseContext(base) 30 | MultiDex.install(this) 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/architecture/clean/core/Config.kt: -------------------------------------------------------------------------------- 1 | package com.architecture.clean.core 2 | 3 | object Config { 4 | var HOST = "http://www.recipepuppy.com/" 5 | } -------------------------------------------------------------------------------- /app/src/main/java/com/architecture/clean/core/FragmentFactory.kt: -------------------------------------------------------------------------------- 1 | package com.architecture.clean.core 2 | 3 | import android.support.v4.app.FragmentManager 4 | import com.architecture.clean.ui.home.HomeFragment 5 | 6 | object FragmentFactory{ 7 | 8 | fun getHomeFragment(supportFragmentManager: FragmentManager): HomeFragment { 9 | var fragment = supportFragmentManager.findFragmentByTag(HomeFragment.FRAGMENT_NAME) 10 | if (fragment == null) { 11 | fragment = HomeFragment() 12 | } 13 | return fragment as HomeFragment 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/architecture/clean/data/mapper/CloudErrorMapper.kt: -------------------------------------------------------------------------------- 1 | package com.architecture.clean.data.mapper 2 | 3 | import android.util.Log 4 | import com.architecture.clean.domain.model.response.DomainErrorException 5 | import com.architecture.clean.domain.model.response.ErrorModel 6 | import com.architecture.clean.domain.model.response.ErrorStatus 7 | import com.google.gson.Gson 8 | import com.google.gson.JsonObject 9 | import okhttp3.ResponseBody 10 | import retrofit2.HttpException 11 | import java.io.IOException 12 | import java.net.SocketTimeoutException 13 | import javax.inject.Inject 14 | 15 | class CloudErrorMapper @Inject constructor(private val gson: Gson) { 16 | 17 | fun mapToDomainErrorException(throwable: Throwable?): ErrorModel { 18 | val errorModel: ErrorModel? = when (throwable) { 19 | 20 | // if throwable is an instance of HttpException 21 | // then attempt to parse error data from response body 22 | is HttpException -> { 23 | // handle UNAUTHORIZED situation (when token expired) 24 | if (throwable.code() == 401) { 25 | ErrorModel(ErrorStatus.UNAUTHORIZED) 26 | } else { 27 | getHttpError(throwable.response().errorBody()) 28 | } 29 | } 30 | 31 | // handle api call timeout error 32 | is SocketTimeoutException -> { 33 | ErrorModel(ErrorStatus.TIMEOUT) 34 | } 35 | 36 | // handle connection error 37 | is IOException -> { 38 | ErrorModel(ErrorStatus.NO_CONNECTION) 39 | } 40 | else -> null 41 | } 42 | return errorModel!! 43 | } 44 | 45 | /** 46 | * attempts to parse http response body and get error data from it 47 | * 48 | * @param body retrofit response body 49 | * @return returns an instance of [ErrorModel] with parsed data or NOT_DEFINED status 50 | */ 51 | private fun getHttpError(body: ResponseBody?): ErrorModel { 52 | return try { 53 | // use response body to get error detail 54 | val result = body?.string() 55 | Log.d("getHttpError", "getErrorMessage() called with: errorBody = [$result]") 56 | val json = Gson().fromJson(result, JsonObject::class.java) 57 | ErrorModel( json.toString(), 400, ErrorStatus.BAD_RESPONSE) 58 | } catch (e: Throwable) { 59 | e.printStackTrace() 60 | ErrorModel(ErrorStatus.NOT_DEFINED) 61 | } 62 | 63 | } 64 | } -------------------------------------------------------------------------------- /app/src/main/java/com/architecture/clean/data/repository/AppRepoImp.kt: -------------------------------------------------------------------------------- 1 | package com.architecture.clean.data.repository 2 | 3 | import com.architecture.clean.data.source.cloud.BaseCloudRepository 4 | import com.architecture.clean.data.source.db.FoodDao 5 | import com.architecture.clean.domain.model.FoodDto 6 | import com.architecture.clean.domain.repository.AppRepository 7 | import io.reactivex.Single 8 | import javax.inject.Inject 9 | 10 | class AppRepoImp @Inject constructor( 11 | private val cloudRepository: BaseCloudRepository, 12 | private val foodDao: FoodDao 13 | ): AppRepository { 14 | override fun getHome(): Single { 15 | return cloudRepository 16 | .getHome().map(this::saveFoods) 17 | } 18 | 19 | private fun saveFoods(foodDto: FoodDto):FoodDto{ 20 | if (foodDto.results.size>0){ 21 | for (food in foodDto.results){ 22 | foodDao.insertFood(food) 23 | } 24 | } 25 | return foodDto 26 | } 27 | } -------------------------------------------------------------------------------- /app/src/main/java/com/architecture/clean/data/restful/ApiService.kt: -------------------------------------------------------------------------------- 1 | package com.architecture.clean.data.restful 2 | 3 | import com.architecture.clean.domain.model.FoodDto 4 | import io.reactivex.Single 5 | import retrofit2.http.GET 6 | 7 | interface ApiService { 8 | 9 | @GET("api/") 10 | fun getHome( 11 | ): Single 12 | 13 | 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/architecture/clean/data/source/cloud/BaseCloudRepository.kt: -------------------------------------------------------------------------------- 1 | package com.architecture.clean.data.source.cloud 2 | 3 | import com.architecture.clean.domain.model.FoodDto 4 | import io.reactivex.Single 5 | 6 | interface BaseCloudRepository { 7 | fun getHome(): Single 8 | } -------------------------------------------------------------------------------- /app/src/main/java/com/architecture/clean/data/source/cloud/CloudRepository.kt: -------------------------------------------------------------------------------- 1 | package com.architecture.clean.data.source.cloud 2 | 3 | import com.architecture.clean.data.restful.ApiService 4 | import com.architecture.clean.domain.model.FoodDto 5 | import io.reactivex.Single 6 | 7 | class CloudRepository(private val apIs: ApiService) : BaseCloudRepository { 8 | override fun getHome(): Single { 9 | return apIs.getHome() 10 | } 11 | 12 | 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/architecture/clean/data/source/db/AppDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.architecture.clean.data.source.db 2 | 3 | import android.arch.persistence.room.Database 4 | import android.arch.persistence.room.RoomDatabase 5 | import com.architecture.clean.domain.model.Food 6 | 7 | @Database(entities = [Food::class], version = AppDatabase.VERSION) 8 | abstract class AppDatabase : RoomDatabase() { 9 | companion object { 10 | const val DB_NAME = "food.db" 11 | const val VERSION = 1 12 | } 13 | abstract fun foodDao(): FoodDao 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/architecture/clean/data/source/db/FoodDao.kt: -------------------------------------------------------------------------------- 1 | package com.architecture.clean.data.source.db 2 | 3 | import android.arch.persistence.room.* 4 | import com.architecture.clean.domain.model.Food 5 | 6 | 7 | @Dao 8 | interface FoodDao { 9 | 10 | @Insert(onConflict = OnConflictStrategy.REPLACE) 11 | fun insertFood(food: Food): Long 12 | 13 | @Delete 14 | fun deleteFood(food: Food): Int 15 | 16 | @Query("SELECT * from Food") 17 | fun selectAllFoods(): MutableList 18 | 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/architecture/clean/di/builder/ActivityBuilder.kt: -------------------------------------------------------------------------------- 1 | package com.architecture.clean.di.builder 2 | 3 | import com.architecture.clean.ui.detail.DetailActivity 4 | import com.architecture.clean.ui.home.MainActivity 5 | import dagger.Module 6 | import dagger.android.ContributesAndroidInjector 7 | 8 | @Module 9 | abstract class ActivityBuilder { 10 | 11 | @ContributesAndroidInjector(modules = [MainActivityProviders::class]) 12 | abstract fun bindMainActivity(): MainActivity 13 | @ContributesAndroidInjector() 14 | abstract fun bindDetailActivity(): DetailActivity 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/architecture/clean/di/builder/AppViewModelBuilder.kt: -------------------------------------------------------------------------------- 1 | package com.architecture.clean.di.builder 2 | 3 | import android.arch.lifecycle.ViewModel 4 | import com.architecture.clean.di.qualifier.ViewModelKey 5 | import com.architecture.clean.ui.home.HomeViewModel 6 | import dagger.Binds 7 | import dagger.Module 8 | import dagger.multibindings.IntoMap 9 | 10 | @Module 11 | abstract class AppViewModelBuilder { 12 | @Binds 13 | @IntoMap 14 | @ViewModelKey(HomeViewModel::class) 15 | abstract fun bindHomeViewModel(homeViewModel: HomeViewModel): ViewModel 16 | 17 | 18 | 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/architecture/clean/di/builder/MainActivityProviders.kt: -------------------------------------------------------------------------------- 1 | package com.architecture.clean.di.builder 2 | 3 | import com.architecture.clean.ui.home.HomeFragment 4 | import dagger.Module 5 | import dagger.android.ContributesAndroidInjector 6 | 7 | @Module 8 | abstract class MainActivityProviders{ 9 | @ContributesAndroidInjector 10 | abstract fun provideHomeFragment(): HomeFragment 11 | 12 | } -------------------------------------------------------------------------------- /app/src/main/java/com/architecture/clean/di/builder/RepositoryBuilder.kt: -------------------------------------------------------------------------------- 1 | package com.architecture.clean.di.builder 2 | 3 | import com.architecture.clean.data.repository.AppRepoImp 4 | import com.architecture.clean.domain.repository.AppRepository 5 | import dagger.Binds 6 | import dagger.Module 7 | 8 | @Module 9 | abstract class RepositoryBuilder { 10 | @Binds 11 | abstract fun bindsMovieRepository(repoImp: AppRepoImp): AppRepository 12 | } -------------------------------------------------------------------------------- /app/src/main/java/com/architecture/clean/di/builder/ViewModelBuilder.kt: -------------------------------------------------------------------------------- 1 | package com.architecture.clean.di.builder 2 | 3 | import android.arch.lifecycle.ViewModelProvider 4 | import com.architecture.clean.ui.ViewModelFactory 5 | import dagger.Binds 6 | import dagger.Module 7 | 8 | @Module(includes = [ 9 | (RepositoryBuilder::class), 10 | (AppViewModelBuilder::class) 11 | ]) 12 | abstract class ViewModelBuilder { 13 | 14 | @Binds 15 | abstract fun bindViewModelFactory(viewModelFactory: ViewModelFactory): ViewModelProvider.Factory 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/architecture/clean/di/component/CoreComponent.kt: -------------------------------------------------------------------------------- 1 | package com.architecture.clean.di.component 2 | 3 | import android.app.Application 4 | import com.architecture.clean.core.App 5 | import com.architecture.clean.di.builder.ActivityBuilder 6 | import com.architecture.clean.di.module.ContextModule 7 | import com.architecture.clean.di.module.DataBaseModule 8 | import com.architecture.clean.di.module.NetworkModule 9 | import dagger.BindsInstance 10 | import dagger.Component 11 | import dagger.android.AndroidInjector 12 | import dagger.android.support.AndroidSupportInjectionModule 13 | import javax.inject.Singleton 14 | 15 | @Singleton 16 | @Component(modules = [AndroidSupportInjectionModule::class, NetworkModule::class, ActivityBuilder::class, ContextModule::class,DataBaseModule::class]) 17 | interface CoreComponent : AndroidInjector { 18 | 19 | @Component.Builder 20 | interface Builder { 21 | @BindsInstance 22 | fun application(application: Application): Builder 23 | 24 | fun build(): CoreComponent 25 | } 26 | 27 | 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/architecture/clean/di/module/ContextModule.kt: -------------------------------------------------------------------------------- 1 | package com.architecture.clean.di.module 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import com.architecture.clean.di.builder.ViewModelBuilder 6 | import dagger.Module 7 | import dagger.Provides 8 | import javax.inject.Singleton 9 | 10 | @Module(includes = [ViewModelBuilder::class]) 11 | class ContextModule { 12 | 13 | @Provides 14 | @Singleton 15 | fun provideContext(application: Application): Context { 16 | return application 17 | } 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/architecture/clean/di/module/DataBaseModule.kt: -------------------------------------------------------------------------------- 1 | package com.architecture.clean.di.module 2 | 3 | import android.app.Application 4 | import android.arch.persistence.room.Room 5 | import com.architecture.clean.data.source.db.AppDatabase 6 | import com.architecture.clean.data.source.db.FoodDao 7 | import dagger.Module 8 | import dagger.Provides 9 | import javax.inject.Singleton 10 | 11 | @Module 12 | class DataBaseModule { 13 | 14 | @Provides 15 | @Singleton 16 | fun provideRoomDatabase(application: Application): AppDatabase { 17 | return Room 18 | .databaseBuilder(application, AppDatabase::class.java, AppDatabase.DB_NAME) 19 | .fallbackToDestructiveMigration() 20 | .build() 21 | } 22 | 23 | @Provides 24 | fun provideUserDao(appDataBase: AppDatabase): FoodDao { 25 | return appDataBase.foodDao() 26 | } 27 | } -------------------------------------------------------------------------------- /app/src/main/java/com/architecture/clean/di/module/NetworkModule.kt: -------------------------------------------------------------------------------- 1 | package com.architecture.clean.di.module 2 | 3 | import com.architecture.clean.core.Config 4 | import com.architecture.clean.data.restful.ApiService 5 | import com.architecture.clean.data.source.cloud.BaseCloudRepository 6 | import com.architecture.clean.data.source.cloud.CloudRepository 7 | import com.google.gson.Gson 8 | import dagger.Module 9 | import dagger.Provides 10 | import okhttp3.OkHttpClient 11 | import okhttp3.logging.HttpLoggingInterceptor 12 | import retrofit2.Retrofit 13 | import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory 14 | import retrofit2.converter.gson.GsonConverterFactory 15 | import java.util.concurrent.TimeUnit 16 | import javax.inject.Singleton 17 | 18 | @Module 19 | class NetworkModule { 20 | 21 | @Provides 22 | @Singleton 23 | fun providesRetrofit( 24 | gsonConverterFactory: GsonConverterFactory, 25 | rxJava2CallAdapterFactory: RxJava2CallAdapterFactory, 26 | okHttpClient: OkHttpClient 27 | ): Retrofit { 28 | return Retrofit.Builder().baseUrl(Config.HOST) 29 | .addConverterFactory(gsonConverterFactory) 30 | .addCallAdapterFactory(rxJava2CallAdapterFactory) 31 | .client(okHttpClient) 32 | .build() 33 | } 34 | 35 | @Provides 36 | @Singleton 37 | fun providesOkHttpClient(): OkHttpClient { 38 | val client = OkHttpClient.Builder() 39 | // .cache(cache) 40 | .connectTimeout(60, TimeUnit.SECONDS) 41 | .writeTimeout(60, TimeUnit.SECONDS) 42 | .readTimeout(60, TimeUnit.SECONDS) 43 | val interceptor = HttpLoggingInterceptor() 44 | interceptor.level = HttpLoggingInterceptor.Level.BODY 45 | client.addNetworkInterceptor(interceptor) 46 | return client.build() 47 | } 48 | 49 | 50 | @Provides 51 | @Singleton 52 | fun providesGson(): Gson { 53 | return Gson() 54 | } 55 | 56 | @Provides 57 | @Singleton 58 | fun providesGsonConverterFactory(): GsonConverterFactory { 59 | return GsonConverterFactory.create() 60 | } 61 | 62 | @Provides 63 | @Singleton 64 | fun providesRxJavaCallAdapterFactory(): RxJava2CallAdapterFactory { 65 | return RxJava2CallAdapterFactory.create() 66 | } 67 | 68 | @Singleton 69 | @Provides 70 | fun provideService( retrofit: Retrofit): ApiService { 71 | return retrofit.create(ApiService::class.java) 72 | } 73 | 74 | 75 | @Provides 76 | fun provideCloudRepository(apIs: ApiService): BaseCloudRepository { 77 | return CloudRepository(apIs) 78 | } 79 | 80 | 81 | } -------------------------------------------------------------------------------- /app/src/main/java/com/architecture/clean/di/qualifier/ViewModelKey.kt: -------------------------------------------------------------------------------- 1 | package com.architecture.clean.di.qualifier 2 | 3 | import android.arch.lifecycle.ViewModel 4 | import dagger.MapKey 5 | import kotlin.reflect.KClass 6 | 7 | @MustBeDocumented 8 | @Target( 9 | AnnotationTarget.FUNCTION, 10 | AnnotationTarget.PROPERTY_GETTER, 11 | AnnotationTarget.PROPERTY_SETTER 12 | ) 13 | @Retention(AnnotationRetention.RUNTIME) 14 | @MapKey 15 | annotation class ViewModelKey(val value: KClass) -------------------------------------------------------------------------------- /app/src/main/java/com/architecture/clean/domain/mapper/DomainErrorUtil.kt: -------------------------------------------------------------------------------- 1 | package com.architecture.clean.domain.mapper 2 | 3 | import com.architecture.clean.domain.model.response.DomainErrorException 4 | import com.architecture.clean.domain.model.response.ErrorModel 5 | import com.architecture.clean.domain.model.response.ErrorStatus 6 | import com.google.gson.Gson 7 | import okhttp3.ResponseBody 8 | import javax.inject.Inject 9 | 10 | /** 11 | * A util class that generate an instance of [ErrorModel] with happened [Throwable] 12 | * @param gson an instance of [Gson] to parse error body from [ResponseBody] 13 | */ 14 | class DomainErrorUtil @Inject constructor(val gson: Gson) { 15 | 16 | /** 17 | * Generate an instance of [ErrorModel] with happened [Throwable] 18 | * @param t happened [Throwable] 19 | * 20 | * @return rentuns an instance of [ErrorModel] 21 | */ 22 | fun getErrorModel(t: Throwable?): ErrorModel { 23 | if (t is DomainErrorException) 24 | return t.errorModel 25 | 26 | // if response was successful but no data received 27 | if (t is NullPointerException) { 28 | return ErrorModel(ErrorStatus.EMPTY_RESPONSE) 29 | } 30 | 31 | // something happened that we are not make our self ready for it 32 | return ErrorModel(ErrorStatus.NOT_DEFINED) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/architecture/clean/domain/model/Food.kt: -------------------------------------------------------------------------------- 1 | package com.architecture.clean.domain.model 2 | 3 | 4 | import android.arch.persistence.room.Entity 5 | import android.arch.persistence.room.PrimaryKey 6 | import android.os.Parcel 7 | import android.os.Parcelable 8 | import com.google.gson.annotations.SerializedName 9 | 10 | 11 | @Entity 12 | data class Food( 13 | @PrimaryKey(autoGenerate = true) 14 | @SerializedName("id") var id: Int, 15 | @SerializedName("title") var title: String, 16 | @SerializedName("href") var href: String, 17 | @SerializedName("ingredients") var ingredients: String, 18 | @SerializedName("thumbnail") var thumbnail: String 19 | ) : Parcelable { 20 | constructor(source: Parcel) : this( 21 | source.readInt(), 22 | source.readString(), 23 | source.readString(), 24 | source.readString(), 25 | source.readString() 26 | ) 27 | 28 | override fun describeContents() = 0 29 | 30 | override fun writeToParcel(dest: Parcel, flags: Int) = with(dest) { 31 | writeInt(id) 32 | writeString(title) 33 | writeString(href) 34 | writeString(ingredients) 35 | writeString(thumbnail) 36 | } 37 | 38 | companion object { 39 | @JvmField 40 | val CREATOR: Parcelable.Creator = object : Parcelable.Creator { 41 | override fun createFromParcel(source: Parcel): Food = Food(source) 42 | override fun newArray(size: Int): Array = arrayOfNulls(size) 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /app/src/main/java/com/architecture/clean/domain/model/FoodDto.kt: -------------------------------------------------------------------------------- 1 | package com.architecture.clean.domain.model 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class FoodDto( 6 | @SerializedName("results") val results: ArrayList 7 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/architecture/clean/domain/model/response/DomainErrorException.kt: -------------------------------------------------------------------------------- 1 | package com.architecture.clean.domain.model.response 2 | 3 | 4 | 5 | class DomainErrorException(val errorModel: ErrorModel): Throwable() { 6 | } -------------------------------------------------------------------------------- /app/src/main/java/com/architecture/clean/domain/model/response/ErrorModel.kt: -------------------------------------------------------------------------------- 1 | package com.architecture.clean.domain.model.response 2 | 3 | /** 4 | * Default error model that comes from server if something goes wrong with a repository call 5 | */ 6 | data class ErrorModel( 7 | val message: String?, 8 | val code: Int?, 9 | @Transient var errorStatus: ErrorStatus 10 | ) { 11 | constructor(errorStatus: ErrorStatus) : this(null, null, errorStatus) 12 | } -------------------------------------------------------------------------------- /app/src/main/java/com/architecture/clean/domain/model/response/ErrorStatus.kt: -------------------------------------------------------------------------------- 1 | package com.architecture.clean.domain.model.response 2 | 3 | /** 4 | * various error status to know what happened if something goes wrong with a repository call 5 | */ 6 | enum class ErrorStatus { 7 | /** 8 | * error in connecting to repository (Server or Database) 9 | */ 10 | NO_CONNECTION, 11 | /** 12 | * error in getting value (Json Error, Server Error, etc) 13 | */ 14 | BAD_RESPONSE, 15 | /** 16 | * Time out error 17 | */ 18 | TIMEOUT, 19 | /** 20 | * no data available in repository 21 | */ 22 | EMPTY_RESPONSE, 23 | /** 24 | * an unexpected error 25 | */ 26 | NOT_DEFINED, 27 | /** 28 | * bad credential 29 | */ 30 | UNAUTHORIZED 31 | } -------------------------------------------------------------------------------- /app/src/main/java/com/architecture/clean/domain/model/response/UseCaseResponse.kt: -------------------------------------------------------------------------------- 1 | package com.architecture.clean.domain.model.response 2 | 3 | 4 | 5 | /** 6 | * base sealed class for handling UseCase responses in [BaseUseCase] 7 | * @see [BaseUseCase] 8 | */ 9 | sealed class UseCaseResponse 10 | 11 | /** 12 | * Wrapper for success response of repository calls 13 | */ 14 | data class SuccessResponse(val value: T): UseCaseResponse() 15 | 16 | /** 17 | * Wrapper for error response of repository calls 18 | */ 19 | data class ErrorResponse(val error: ErrorModel): UseCaseResponse() -------------------------------------------------------------------------------- /app/src/main/java/com/architecture/clean/domain/repository/AppRepository.kt: -------------------------------------------------------------------------------- 1 | package com.architecture.clean.domain.repository 2 | 3 | import com.architecture.clean.domain.model.FoodDto 4 | import io.reactivex.Single 5 | 6 | interface AppRepository{ 7 | fun getHome(): Single 8 | } -------------------------------------------------------------------------------- /app/src/main/java/com/architecture/clean/domain/usecase/GetHomeUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.architecture.clean.domain.usecase 2 | 3 | import com.architecture.clean.data.mapper.CloudErrorMapper 4 | import com.architecture.clean.domain.mapper.DomainErrorUtil 5 | import com.architecture.clean.domain.model.FoodDto 6 | import com.architecture.clean.domain.repository.AppRepository 7 | import com.architecture.clean.domain.usecase.base.SingleUseCase 8 | import io.reactivex.Single 9 | import javax.inject.Inject 10 | 11 | class GetHomeUseCase @Inject constructor( 12 | errorUtil: CloudErrorMapper, 13 | private val appRepository: AppRepository 14 | ) : SingleUseCase(errorUtil) { 15 | 16 | override fun execute(): Single { 17 | return appRepository.getHome() 18 | } 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/architecture/clean/domain/usecase/base/SingleUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.architecture.clean.domain.usecase.base 2 | 3 | import com.architecture.clean.data.mapper.CloudErrorMapper 4 | import com.architecture.clean.domain.mapper.DomainErrorUtil 5 | import com.architecture.clean.domain.model.response.* 6 | import io.reactivex.Single 7 | import io.reactivex.android.schedulers.AndroidSchedulers 8 | import io.reactivex.disposables.CompositeDisposable 9 | import io.reactivex.disposables.Disposable 10 | import io.reactivex.schedulers.Schedulers 11 | 12 | abstract class SingleUseCase( val errorUtil: CloudErrorMapper) : UseCase>() { 13 | 14 | fun execute( 15 | compositeDisposable: CompositeDisposable, 16 | onResponse: (UseCaseResponse) -> Unit, 17 | onTokenExpire: (() -> Unit)? = null 18 | ): Disposable { 19 | return this.execute() 20 | .subscribeOn(Schedulers.io()) 21 | .observeOn(AndroidSchedulers.mainThread()) 22 | .subscribe( 23 | { 24 | onResponse(SuccessResponse(it)) 25 | }, 26 | { 27 | val error : ErrorModel = errorUtil.mapToDomainErrorException(it) 28 | 29 | if (error.errorStatus == ErrorStatus.UNAUTHORIZED) { 30 | onTokenExpire?.invoke() 31 | } 32 | onResponse(ErrorResponse(error)) 33 | 34 | }).also { compositeDisposable.add(it) } 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /app/src/main/java/com/architecture/clean/domain/usecase/base/UseCase.kt: -------------------------------------------------------------------------------- 1 | package com.architecture.clean.domain.usecase.base 2 | 3 | abstract class UseCase { 4 | 5 | abstract fun execute(): T 6 | 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/architecture/clean/ui/CustomBindingAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.architecture.clean.ui 2 | 3 | import android.databinding.BindingAdapter 4 | import android.widget.ImageView 5 | import com.squareup.picasso.Picasso 6 | 7 | object CustomBindingAdapter{ 8 | 9 | @JvmStatic 10 | @BindingAdapter("bind:image_url") 11 | fun loadImage(imageView: ImageView, url: String) { 12 | Picasso.with(imageView.context).load(url).into(imageView) 13 | } 14 | 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/architecture/clean/ui/DataBindingViewHolder.kt: -------------------------------------------------------------------------------- 1 | package com.architecture.clean.ui 2 | 3 | import android.databinding.DataBindingUtil 4 | import android.databinding.ViewDataBinding 5 | import android.support.annotation.IdRes 6 | import android.support.annotation.NonNull 7 | import android.support.v7.widget.RecyclerView 8 | import android.view.LayoutInflater 9 | import android.view.ViewGroup 10 | 11 | 12 | abstract class DataBindingViewHolder 13 | 14 | ( val dataBinding: ViewDataBinding) 15 | 16 | : RecyclerView.ViewHolder(dataBinding.root) { 17 | 18 | constructor(@NonNull inflater: LayoutInflater, @IdRes layoutId: Int, 19 | @NonNull parent: ViewGroup, @NonNull attachToParent: Boolean) : 20 | this(DataBindingUtil.inflate(inflater, layoutId, parent, attachToParent)) 21 | 22 | 23 | abstract fun onBind(t: T) 24 | } 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/architecture/clean/ui/ViewModelFactory.kt: -------------------------------------------------------------------------------- 1 | package com.architecture.clean.ui 2 | 3 | import android.arch.lifecycle.ViewModel 4 | import android.arch.lifecycle.ViewModelProvider 5 | import javax.inject.Inject 6 | import javax.inject.Provider 7 | import javax.inject.Singleton 8 | 9 | @Singleton 10 | class ViewModelFactory 11 | @Inject constructor(private val creators: Map, @JvmSuppressWildcards Provider>) : ViewModelProvider.Factory { 12 | 13 | override fun create(modelClass: Class): T { 14 | // get the ViewModel provider based on given class 15 | val creator = creators[modelClass] ?: creators.entries.firstOrNull { 16 | modelClass.isAssignableFrom(it.key) 17 | }?.value ?: throw IllegalArgumentException("Unknown model class $modelClass") 18 | try { 19 | @Suppress("UNCHECKED_CAST") 20 | return creator.get() as T 21 | } catch (e: Exception) { 22 | throw RuntimeException(e) 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/main/java/com/architecture/clean/ui/base/BaseViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.architecture.clean.ui.base 2 | 3 | 4 | import android.arch.lifecycle.LifecycleObserver 5 | import android.arch.lifecycle.ViewModel 6 | import io.reactivex.disposables.CompositeDisposable 7 | 8 | abstract class BaseViewModel 9 | : ViewModel(), LifecycleObserver { 10 | 11 | val compositeDisposable: CompositeDisposable = CompositeDisposable() 12 | 13 | 14 | override fun onCleared() { 15 | super.onCleared() 16 | compositeDisposable.clear() 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/architecture/clean/ui/detail/DetailActivity.kt: -------------------------------------------------------------------------------- 1 | package com.architecture.clean.ui.detail 2 | 3 | import android.databinding.DataBindingUtil 4 | import android.os.Build 5 | import android.os.Bundle 6 | import android.support.annotation.RequiresApi 7 | import android.support.v4.content.ContextCompat 8 | import android.util.Log 9 | import android.view.WindowManager 10 | import com.architecture.clean.R 11 | import com.architecture.clean.databinding.ActivityDetailBinding 12 | import com.architecture.clean.domain.model.Food 13 | import dagger.android.support.DaggerAppCompatActivity 14 | import kotlinx.android.synthetic.main.activity_detail.* 15 | 16 | class DetailActivity : DaggerAppCompatActivity() { 17 | private val TAG = DetailActivity::class.java.simpleName 18 | 19 | @RequiresApi(Build.VERSION_CODES.LOLLIPOP) 20 | override fun onCreate(savedInstanceState: Bundle?) { 21 | super.onCreate(savedInstanceState) 22 | val food: Food= intent.getParcelableExtra("food") 23 | val binding : ActivityDetailBinding = DataBindingUtil.setContentView(this, R.layout.activity_detail) 24 | binding.item=food 25 | this.window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN) 26 | back.setOnClickListener { 27 | supportFinishAfterTransition() 28 | } 29 | fab.setOnClickListener{ 30 | Log.d(TAG,fab.tag.toString()) 31 | when(fab.tag.toString()){ 32 | 33 | getString(R.string.not_clicked) -> { 34 | fab.setImageDrawable(ContextCompat.getDrawable(applicationContext,R.drawable.ic_favorite_red_24dp)) 35 | fab.tag=getString(R.string.clicked) 36 | } 37 | getString(R.string.clicked) -> { 38 | fab.setImageDrawable(ContextCompat.getDrawable(applicationContext,R.drawable.ic_favorite_border_black_24dp)) 39 | fab.tag=getString(R.string.not_clicked) 40 | } 41 | 42 | } 43 | 44 | } 45 | } 46 | 47 | override fun onBackPressed() { 48 | supportFinishAfterTransition() 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/java/com/architecture/clean/ui/home/HomeFragment.kt: -------------------------------------------------------------------------------- 1 | package com.architecture.clean.ui.home 2 | 3 | import android.app.ActivityOptions 4 | import android.arch.lifecycle.Observer 5 | import android.arch.lifecycle.ViewModelProvider 6 | import android.arch.lifecycle.ViewModelProviders 7 | import android.content.Intent 8 | import android.databinding.DataBindingUtil 9 | import android.os.Bundle 10 | import android.support.v7.widget.LinearLayoutManager 11 | import android.util.Log 12 | import android.view.LayoutInflater 13 | import android.view.View 14 | import android.view.ViewGroup 15 | import android.widget.Toast 16 | import com.architecture.clean.R 17 | import com.architecture.clean.databinding.FragmentHomeBinding 18 | import com.architecture.clean.domain.model.Food 19 | import com.architecture.clean.domain.model.FoodDto 20 | import com.architecture.clean.ui.detail.DetailActivity 21 | import com.architecture.clean.ui.home.adapter.HomeAdapter 22 | import com.architecture.clean.ui.home.callback.HomeCallBack 23 | import dagger.android.support.DaggerFragment 24 | import kotlinx.android.synthetic.main.fragment_home.* 25 | import javax.inject.Inject 26 | import kotlinx.android.synthetic.main.food_item_row.view.* 27 | import android.util.Pair as UtilPair 28 | 29 | 30 | class HomeFragment: DaggerFragment(), HomeCallBack { 31 | private val TAG: String = HomeFragment::class.java.simpleName 32 | companion object { 33 | val FRAGMENT_NAME: String = HomeFragment::class.java.name 34 | } 35 | @Inject 36 | lateinit var viewModelFactory: ViewModelProvider.Factory 37 | private val viewModel: HomeViewModel by lazy { ViewModelProviders.of(this,viewModelFactory).get(HomeViewModel::class.java) } 38 | val adapter : HomeAdapter by lazy { HomeAdapter(arrayListOf(),this) } 39 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 40 | val binding : FragmentHomeBinding= DataBindingUtil.inflate(inflater, R.layout.fragment_home, container, false) 41 | binding.setLifecycleOwner(this) 42 | binding.viewModel=viewModel 43 | lifecycle.addObserver(viewModel) 44 | return binding.root 45 | } 46 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 47 | super.onViewCreated(view, savedInstanceState) 48 | with(viewModel) { 49 | homeData.observe(this@HomeFragment, Observer { 50 | initView(it) 51 | } 52 | ) 53 | error.observe(this@HomeFragment, Observer { 54 | progressBar_home.visibility= View.GONE 55 | Toast.makeText(context, "${it?.message}", Toast.LENGTH_LONG).show() 56 | }) 57 | } 58 | } 59 | 60 | private fun initView(it: FoodDto?) { 61 | rv_main_home.layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) 62 | rv_main_home.adapter = adapter 63 | progressBar_home.visibility=View.GONE 64 | if (it!!.results.isNotEmpty()) { 65 | adapter.clear() 66 | adapter.add(it.results) 67 | 68 | }else{ 69 | Toast.makeText(context, context?.getString(R.string.empty_list), android.widget.Toast.LENGTH_LONG).show() 70 | } 71 | } 72 | 73 | override fun itemClick(item: Food) { 74 | val intent = Intent(activity,DetailActivity::class.java) 75 | intent.putExtra("food",item) 76 | val p1 = UtilPair.create(rv_main_home.img_icon, "image") 77 | val p2 = UtilPair.create(rv_main_home.header, "profile") 78 | val options = ActivityOptions.makeSceneTransitionAnimation(activity, 79 | p1,p2) 80 | 81 | 82 | startActivity(intent, options.toBundle()) 83 | } 84 | } -------------------------------------------------------------------------------- /app/src/main/java/com/architecture/clean/ui/home/HomeViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.architecture.clean.ui.home 2 | 3 | import android.arch.lifecycle.MutableLiveData 4 | import android.util.Log 5 | import com.architecture.clean.domain.model.FoodDto 6 | import com.architecture.clean.domain.model.response.ErrorModel 7 | import com.architecture.clean.domain.model.response.ErrorResponse 8 | import com.architecture.clean.domain.model.response.SuccessResponse 9 | import com.architecture.clean.domain.model.response.UseCaseResponse 10 | import com.architecture.clean.domain.usecase.GetHomeUseCase 11 | import com.architecture.clean.ui.base.BaseViewModel 12 | import javax.inject.Inject 13 | 14 | class HomeViewModel @Inject constructor(private val getHomeUseCase: GetHomeUseCase) : BaseViewModel() { 15 | private val TAG = HomeViewModel::class.java.simpleName 16 | val homeData: MutableLiveData by lazy { MutableLiveData() } 17 | val error : MutableLiveData by lazy { MutableLiveData() } 18 | init { 19 | getHomeUseCase.execute(compositeDisposable, this::getHomeResponse) 20 | } 21 | public fun getHomeResponse(response: UseCaseResponse) { 22 | Log.d(TAG, "getHomeResponse() called with: response = [$response]") 23 | when (response) { 24 | is SuccessResponse -> homeData.value = response.value 25 | is ErrorResponse -> error.value=response.error 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/architecture/clean/ui/home/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.architecture.clean.ui.home 2 | 3 | 4 | import android.os.Bundle 5 | import android.support.v7.widget.Toolbar 6 | import com.architecture.clean.R 7 | import com.architecture.clean.core.FragmentFactory 8 | import dagger.android.support.DaggerAppCompatActivity 9 | import kotlinx.android.synthetic.main.toolbar_main.* 10 | 11 | class MainActivity : DaggerAppCompatActivity() { 12 | private val TAG = MainActivity::class.java.simpleName 13 | 14 | override fun onCreate(savedInstanceState: Bundle?) { 15 | super.onCreate(savedInstanceState) 16 | setContentView(R.layout.activity_main) 17 | val toolbar: Toolbar by lazy { toolbar_main_activity } 18 | setSupportActionBar(toolbar) 19 | supportActionBar!!.setDisplayShowTitleEnabled(false) 20 | showHomeFragment() 21 | } 22 | 23 | private fun showHomeFragment() { 24 | val fragmentTransaction = supportFragmentManager.beginTransaction() 25 | .replace(R.id.container, 26 | FragmentFactory.getHomeFragment(supportFragmentManager), 27 | HomeFragment.FRAGMENT_NAME) 28 | fragmentTransaction.addToBackStack(HomeFragment.FRAGMENT_NAME) 29 | fragmentTransaction.commit() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/architecture/clean/ui/home/adapter/HomeAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.architecture.clean.ui.home.adapter 2 | 3 | import android.databinding.ViewDataBinding 4 | import android.support.v7.widget.RecyclerView 5 | import android.view.LayoutInflater 6 | import android.view.ViewGroup 7 | import com.architecture.clean.BR.click 8 | import com.architecture.clean.BR.item 9 | import com.architecture.clean.databinding.FoodItemRowBinding 10 | import com.architecture.clean.domain.model.Food 11 | import com.architecture.clean.ui.DataBindingViewHolder 12 | import com.architecture.clean.ui.home.callback.HomeCallBack 13 | 14 | class HomeAdapter( 15 | private var items: ArrayList = arrayListOf(), 16 | private val callback: HomeCallBack 17 | ) : RecyclerView.Adapter() { 18 | override fun getItemCount(): Int = items.size 19 | 20 | override fun onBindViewHolder(holder: SimpleHolder, position: Int) { 21 | holder.onBind(items[position]) 22 | } 23 | 24 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SimpleHolder { 25 | val binding = FoodItemRowBinding.inflate(LayoutInflater.from(parent.context),parent,false) 26 | return SimpleHolder(binding) 27 | } 28 | 29 | inner class SimpleHolder(dataBinding: ViewDataBinding) 30 | : DataBindingViewHolder(dataBinding) { 31 | override fun onBind(t: Food): Unit = with(t) { 32 | dataBinding.setVariable(item,t) 33 | dataBinding.setVariable(click,callback) 34 | } 35 | } 36 | 37 | fun add(list: ArrayList) { 38 | items.addAll(list) 39 | notifyDataSetChanged() 40 | } 41 | 42 | fun clear() { 43 | items.clear() 44 | notifyDataSetChanged() 45 | } 46 | } -------------------------------------------------------------------------------- /app/src/main/java/com/architecture/clean/ui/home/callback/HomeCallBack.kt: -------------------------------------------------------------------------------- 1 | package com.architecture.clean.ui.home.callback 2 | 3 | import com.architecture.clean.domain.model.Food 4 | 5 | interface HomeCallBack{ 6 | fun itemClick(item: Food) 7 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_account_circle_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_back_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_favorite_border_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_favorite_red_24dp.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/drawable/pic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samira-badamestani/android-clean-architecture/ea99a098327390a89d3fbb11ba4b9cd74f698f4b/app/src/main/res/drawable/pic.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_detail.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 17 | 18 | 24 | 25 | 33 | 36 | 43 | 51 | 52 | 53 | 54 | 55 | 56 | 61 | 67 | 73 | 83 | 94 | 106 | 112 | 113 | 114 | 115 | 116 | 117 | 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_login.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 12 | 21 | 22 | 28 | 42 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 13 | 14 | 19 | 20 | 21 | 22 | 23 | 24 | 28 | 29 | 33 | 34 | 35 | 36 | 37 | 44 | 45 | -------------------------------------------------------------------------------- /app/src/main/res/layout/food_item_row.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 10 | 13 | 14 | 23 | 26 | 32 | 39 | 49 | 60 | 72 | 73 | 81 | 82 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_home.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 13 | 20 | 21 | 30 | 31 | 35 | 36 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /app/src/main/res/layout/nav_header_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/layout/toolbar_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/menu/activity_main_drawer.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_detail.xml: -------------------------------------------------------------------------------- 1 | 5 | 10 | 11 | -------------------------------------------------------------------------------- /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/samira-badamestani/android-clean-architecture/ea99a098327390a89d3fbb11ba4b9cd74f698f4b/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samira-badamestani/android-clean-architecture/ea99a098327390a89d3fbb11ba4b9cd74f698f4b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samira-badamestani/android-clean-architecture/ea99a098327390a89d3fbb11ba4b9cd74f698f4b/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samira-badamestani/android-clean-architecture/ea99a098327390a89d3fbb11ba4b9cd74f698f4b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samira-badamestani/android-clean-architecture/ea99a098327390a89d3fbb11ba4b9cd74f698f4b/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samira-badamestani/android-clean-architecture/ea99a098327390a89d3fbb11ba4b9cd74f698f4b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samira-badamestani/android-clean-architecture/ea99a098327390a89d3fbb11ba4b9cd74f698f4b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samira-badamestani/android-clean-architecture/ea99a098327390a89d3fbb11ba4b9cd74f698f4b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samira-badamestani/android-clean-architecture/ea99a098327390a89d3fbb11ba4b9cd74f698f4b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samira-badamestani/android-clean-architecture/ea99a098327390a89d3fbb11ba4b9cd74f698f4b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #ccdd79 4 | #CAD966 5 | #73830a 6 | #fff 7 | #FAF8F8 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 180dp 3 | 16dp 4 | 16dp 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Food 3 | Please wait... 4 | Health 5 | Sign in 6 | Start 7 | Alicia Scott 8 | Today 03:14PM 9 | oops, List is empty. 10 | DetailActivity 11 | 12 | "Material is the metaphor.\n\n" 13 | 14 | "A material metaphor is the unifying theory of a rationalized space and a system of motion." 15 | "The material is grounded in tactile reality, inspired by the study of paper and ink, yet " 16 | "technologically advanced and open to imagination and magic.\n" 17 | "Surfaces and edges of the material provide visual cues that are grounded in reality. The " 18 | "use of familiar tactile attributes helps users quickly understand affordances. Yet the " 19 | "flexibility of the material creates new affordances that supercede those in the physical " 20 | "world, without breaking the rules of physics.\n" 21 | "The fundamentals of light, surface, and movement are key to conveying how objects move, " 22 | "interact, and exist in space and in relation to each other. Realistic lighting shows " 23 | "seams, divides space, and indicates moving parts.\n\n" 24 | 25 | Settings 26 | not_clicked 27 | clicked 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 |