├── .gitignore ├── .idea ├── assetWizardSettings.xml ├── caches │ └── gradle_models.ser ├── codeStyles │ └── Project.xml ├── compiler.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── jarRepositories.xml ├── kotlinScripting.xml ├── misc.xml ├── runConfigurations.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── ic_launcher-web.png │ ├── ic_launcher_app-web.png │ ├── java │ └── com │ │ └── example │ │ └── eziketobenna │ │ └── bakingapp │ │ ├── BakingApplication.kt │ │ ├── di │ │ ├── AppComponent.kt │ │ └── NavigationModule.kt │ │ ├── navigation │ │ ├── NavigationDispatcher.kt │ │ └── NavigationDispatcherImpl.kt │ │ └── ui │ │ └── MainActivity.kt │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ ├── ic_baseline_arrow_back_24.xml │ └── ic_launcher_background.xml │ ├── font │ └── googlesans.ttf │ ├── layout │ └── activity_main.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── navigation │ └── nav_main.xml │ ├── values-xlarge │ └── resources.xml │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle.kts ├── buildSrc ├── build.gradle.kts └── src │ └── main │ └── kotlin │ ├── AppDependencies.kt │ ├── BuildType.kt │ ├── Extensions.kt │ └── plugin │ ├── kotlin-library.gradle.kts │ └── spotless.gradle.kts ├── common └── views │ ├── .gitignore │ ├── build.gradle.kts │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── example │ │ └── eziketobenna │ │ └── bakingapp │ │ └── views │ │ └── SimpleEmptyStateView.kt │ └── res │ ├── drawable │ └── ic_error_page_2.xml │ ├── layout │ └── simple_empty_state_view_layout.xml │ └── values │ ├── attrs.xml │ ├── strings.xml │ └── styles.xml ├── core ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── example │ │ └── eziketobenna │ │ └── bakingapp │ │ └── core │ │ ├── di │ │ ├── component │ │ │ └── CoreComponent.kt │ │ ├── mapkeys │ │ │ └── ViewModelKey.kt │ │ ├── module │ │ │ ├── DataModule.kt │ │ │ ├── ExecutorModule.kt │ │ │ ├── FactoriesModule.kt │ │ │ ├── ImageLoaderModule.kt │ │ │ └── RemoteModule.kt │ │ └── scope │ │ │ └── FeatureScope.kt │ │ ├── executor │ │ └── PostExecutionThreadImpl.kt │ │ ├── ext │ │ ├── Extensions.kt │ │ ├── FlowExt.kt │ │ └── ViewExt.kt │ │ ├── factory │ │ └── ViewModelFactory.kt │ │ ├── imageLoader │ │ ├── ImageLoader.kt │ │ └── ImageLoaderImpl.kt │ │ └── viewBinding │ │ └── ViewBinding.kt │ └── res │ └── drawable │ └── cheese_cake.jpg ├── features ├── recipes │ ├── model │ │ ├── .gitignore │ │ ├── build.gradle.kts │ │ ├── consumer-rules.pro │ │ ├── proguard-rules.pro │ │ └── src │ │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ └── java │ │ │ └── com │ │ │ └── example │ │ │ └── eziketobenna │ │ │ └── bakingapp │ │ │ └── model │ │ │ ├── IngredientModel.kt │ │ │ ├── RecipeModel.kt │ │ │ ├── StepInfoModel.kt │ │ │ ├── StepModel.kt │ │ │ └── mapper │ │ │ ├── IngredientModelMapper.kt │ │ │ ├── RecipeModelMapper.kt │ │ │ └── StepModelMapper.kt │ ├── recipe │ │ ├── .gitignore │ │ ├── build.gradle.kts │ │ └── src │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── eziketobenna │ │ │ │ │ └── bakingapp │ │ │ │ │ └── recipe │ │ │ │ │ ├── Extensions.kt │ │ │ │ │ ├── di │ │ │ │ │ ├── Injector.kt │ │ │ │ │ ├── component │ │ │ │ │ │ └── RecipeComponent.kt │ │ │ │ │ └── module │ │ │ │ │ │ ├── PresentationModule.kt │ │ │ │ │ │ └── ViewModelModule.kt │ │ │ │ │ ├── presentation │ │ │ │ │ ├── Alias.kt │ │ │ │ │ ├── RecipeViewModel.kt │ │ │ │ │ ├── RecipeViewStateReducer.kt │ │ │ │ │ ├── mvi │ │ │ │ │ │ ├── RecipeViewAction.kt │ │ │ │ │ │ ├── RecipeViewIntent.kt │ │ │ │ │ │ ├── RecipeViewResult.kt │ │ │ │ │ │ └── RecipeViewState.kt │ │ │ │ │ └── processor │ │ │ │ │ │ ├── RecipeActionProcessor.kt │ │ │ │ │ │ └── RecipeViewIntentProcessor.kt │ │ │ │ │ └── ui │ │ │ │ │ ├── RecipeAdapter.kt │ │ │ │ │ └── RecipeFragment.kt │ │ │ └── res │ │ │ │ ├── drawable │ │ │ │ ├── error.jpg │ │ │ │ └── ic_empty.xml │ │ │ │ ├── layout-land │ │ │ │ └── step_detail.xml │ │ │ │ ├── layout │ │ │ │ ├── content_main.xml │ │ │ │ ├── fragment_recipe.xml │ │ │ │ ├── recipe_placeholder_item.xml │ │ │ │ └── step_detail.xml │ │ │ │ ├── values-sw600dp │ │ │ │ └── resources.xml │ │ │ │ └── values │ │ │ │ ├── colors.xml │ │ │ │ ├── dimens.xml │ │ │ │ ├── resources.xml │ │ │ │ ├── strings.xml │ │ │ │ └── styles.xml │ │ │ └── test │ │ │ └── java │ │ │ └── com │ │ │ └── example │ │ │ └── eziketobenna │ │ │ └── bakingapp │ │ │ └── recipe │ │ │ └── presentation │ │ │ ├── RecipeViewStateReducerTest.kt │ │ │ ├── data │ │ │ └── DummyData.kt │ │ │ ├── executor │ │ │ └── TestPostExecutionThread.kt │ │ │ ├── fake │ │ │ ├── FakeActionProcessor.kt │ │ │ └── FakeRecipeRepository.kt │ │ │ └── processor │ │ │ ├── RecipeActionProcessorTest.kt │ │ │ └── RecipeViewIntentProcessorTest.kt │ ├── recipeDetail │ │ ├── .gitignore │ │ ├── build.gradle.kts │ │ └── src │ │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── eziketobenna │ │ │ │ └── bakingapp │ │ │ │ └── recipedetail │ │ │ │ ├── di │ │ │ │ ├── Injector.kt │ │ │ │ ├── component │ │ │ │ │ └── RecipeDetailComponent.kt │ │ │ │ └── module │ │ │ │ │ ├── PresentationModule.kt │ │ │ │ │ └── ViewModelModule.kt │ │ │ │ ├── model │ │ │ │ ├── IngredientDetailMapper.kt │ │ │ │ ├── RecipeDetailModel.kt │ │ │ │ └── StepDetailMapper.kt │ │ │ │ ├── presentation │ │ │ │ ├── Alias.kt │ │ │ │ ├── RecipeDetailViewAction.kt │ │ │ │ ├── RecipeDetailViewIntent.kt │ │ │ │ ├── RecipeDetailViewModel.kt │ │ │ │ ├── RecipeDetailViewResult.kt │ │ │ │ ├── RecipeDetailViewState.kt │ │ │ │ ├── RecipeDetailViewStateReducer.kt │ │ │ │ ├── factory │ │ │ │ │ └── RecipeDetailModelFactory.kt │ │ │ │ └── processor │ │ │ │ │ ├── RecipeDetailActionProcessor.kt │ │ │ │ │ └── RecipeDetailIntentProcessor.kt │ │ │ │ └── ui │ │ │ │ ├── RecipeDetailFragment.kt │ │ │ │ └── adapter │ │ │ │ ├── HeaderViewHolder.kt │ │ │ │ ├── IngredientStepAdapter.kt │ │ │ │ ├── IngredientViewHolder.kt │ │ │ │ ├── RecyclerViewExt.kt │ │ │ │ └── StepViewHolder.kt │ │ │ └── res │ │ │ ├── drawable │ │ │ └── ic_play_arrow_black_24dp.xml │ │ │ ├── layout │ │ │ ├── fragment_recipe_detail.xml │ │ │ ├── ingredient_list_content.xml │ │ │ ├── item_header_layout.xml │ │ │ └── step_list_content.xml │ │ │ └── values │ │ │ └── strings.xml │ └── stepDetail │ │ ├── .gitignore │ │ ├── build.gradle.kts │ │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── com │ │ │ └── example │ │ │ └── eziketobenna │ │ │ └── bakingapp │ │ │ └── stepdetail │ │ │ ├── di │ │ │ ├── Injector.kt │ │ │ ├── component │ │ │ │ └── StepDetailComponent.kt │ │ │ └── module │ │ │ │ ├── PresentationModule.kt │ │ │ │ └── ViewModelModule.kt │ │ │ ├── presentation │ │ │ ├── Alias.kt │ │ │ ├── StepDetailViewAction.kt │ │ │ ├── StepDetailViewIntent.kt │ │ │ ├── StepDetailViewModel.kt │ │ │ ├── StepDetailViewResult.kt │ │ │ ├── StepDetailViewState.kt │ │ │ ├── StepDetailViewStateReducer.kt │ │ │ ├── factory │ │ │ │ └── StepDetailViewStateFactory.kt │ │ │ └── processor │ │ │ │ ├── StepDetailActionProcessor.kt │ │ │ │ └── StepDetailIntentProcessor.kt │ │ │ └── ui │ │ │ └── StepDetailFragment.kt │ │ └── res │ │ ├── layout │ │ └── fragment_step_detail.xml │ │ └── values │ │ └── strings.xml └── videoPlayer │ ├── .gitignore │ ├── build.gradle.kts │ └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── example │ │ └── eziketobenna │ │ └── bakingapp │ │ └── videoplayer │ │ ├── VideoPlayer.kt │ │ ├── VideoPlayerComponent.kt │ │ └── VideoPlayerState.kt │ └── res │ ├── layout │ └── video_player.xml │ └── values │ └── dimens.xml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── libraries ├── data │ ├── .gitignore │ ├── build.gradle.kts │ └── src │ │ ├── main │ │ └── java │ │ │ └── com │ │ │ └── example │ │ │ └── eziketobenna │ │ │ └── bakingapp │ │ │ └── data │ │ │ ├── contract │ │ │ └── RecipeRemote.kt │ │ │ ├── mapper │ │ │ ├── IngredientEntityMapper.kt │ │ │ ├── RecipeEntityMapper.kt │ │ │ ├── StepEntityMapper.kt │ │ │ └── base │ │ │ │ └── EntityMapper.kt │ │ │ ├── model │ │ │ ├── IngredientEntity.kt │ │ │ ├── RecipeEntity.kt │ │ │ └── StepEntity.kt │ │ │ └── repository │ │ │ └── RecipeRepositoryImpl.kt │ │ └── test │ │ └── java │ │ └── com │ │ └── example │ │ └── eziketobenna │ │ └── bakingapp │ │ └── data │ │ ├── fake │ │ ├── DummyData.kt │ │ └── FakeRecipeRemoteImpl.kt │ │ ├── mapper │ │ ├── IngredientEntityMapperTest.kt │ │ ├── RecipeEntityMapperTest.kt │ │ └── StepEntityMapperTest.kt │ │ └── repository │ │ └── RecipeRepositoryImplTest.kt ├── domain │ ├── .gitignore │ ├── build.gradle.kts │ └── src │ │ ├── main │ │ └── java │ │ │ └── com │ │ │ └── example │ │ │ └── eziketobenna │ │ │ └── bakingapp │ │ │ └── domain │ │ │ ├── exception │ │ │ └── NoParamsException.kt │ │ │ ├── executor │ │ │ └── PostExecutionThread.kt │ │ │ ├── model │ │ │ ├── Ingredient.kt │ │ │ ├── Recipe.kt │ │ │ └── Step.kt │ │ │ ├── repository │ │ │ └── RecipeRepository.kt │ │ │ └── usecase │ │ │ ├── FetchRecipes.kt │ │ │ └── base │ │ │ └── FlowUseCase.kt │ │ └── test │ │ └── java │ │ └── com │ │ └── example │ │ └── eziketobenna │ │ └── bakingapp │ │ └── domain │ │ ├── data │ │ └── DummyData.kt │ │ ├── executor │ │ └── TestPostExecutionThread.kt │ │ ├── fake │ │ ├── FakeRecipeRepository.kt │ │ └── FakeUseCases.kt │ │ └── usecase │ │ ├── FetchRecipesTest.kt │ │ └── base │ │ └── FlowUseCaseTest.kt └── remote │ ├── .gitignore │ ├── build.gradle.kts │ └── src │ ├── main │ └── java │ │ └── com │ │ └── example │ │ └── eziketobenna │ │ └── bakingapp │ │ └── remote │ │ ├── ApiService.kt │ │ ├── ApiServiceFactory.kt │ │ ├── impl │ │ └── RecipeRemoteImpl.kt │ │ ├── mapper │ │ ├── IngredientRemoteMapper.kt │ │ ├── RecipeRemoteMapper.kt │ │ ├── StepRemoteMapper.kt │ │ └── base │ │ │ └── RemoteModelMapper.kt │ │ └── model │ │ ├── IngredientRemoteModel.kt │ │ ├── RecipeRemoteModel.kt │ │ └── StepRemoteModel.kt │ └── test │ ├── java │ └── com │ │ └── example │ │ └── eziketobenna │ │ └── bakingapp │ │ └── remote │ │ ├── impl │ │ └── RecipeRemoteTest.kt │ │ ├── mapper │ │ ├── IngredientRemoteMapperTest.kt │ │ ├── RecipeRemoteMapperTest.kt │ │ └── StepRemoteMapperTest.kt │ │ └── utils │ │ ├── DummyData.kt │ │ ├── Helpers.kt │ │ └── RecipeRequestDispatcher.kt │ └── resources │ └── response │ └── recipe_response.json ├── presentation ├── .gitignore ├── build.gradle.kts └── src │ ├── main │ └── java │ │ └── com │ │ └── example │ │ └── eziketobenna │ │ └── bakingapp │ │ └── presentation │ │ ├── event │ │ ├── SingleEvent.kt │ │ └── ViewEvent.kt │ │ ├── mapper │ │ └── ModelMapper.kt │ │ └── mvi │ │ ├── ActionProcessor.kt │ │ ├── IntentProcessor.kt │ │ ├── MVIPresenter.kt │ │ ├── MVIView.kt │ │ ├── ViewAction.kt │ │ ├── ViewIntent.kt │ │ ├── ViewResult.kt │ │ ├── ViewState.kt │ │ └── ViewStateReducer.kt │ └── test │ └── java │ └── com │ └── example │ └── eziketobenna │ └── bakingapp │ └── presentation │ └── SingleEventTest.kt └── settings.gradle.kts /.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 | /.idea 18 | .gradle/ 19 | build/ 20 | 21 | # Local configuration file (sdk path, etc) 22 | local.properties 23 | 24 | # Proguard folder generated by Eclipse 25 | proguard/ 26 | 27 | # Log Files 28 | *.log 29 | 30 | # Android Studio Navigation editor temp files 31 | .navigation/ 32 | 33 | # Android Studio captures folder 34 | captures/ 35 | 36 | # Intellij 37 | *.iml 38 | .idea/workspace.xml 39 | .idea/tasks.xml 40 | .idea/gradle.xml 41 | .idea/dictionaries 42 | .idea/libraries 43 | 44 | # Keystore files 45 | *.jks 46 | 47 | # External native build folder generated in Android Studio 2.2 and later 48 | .externalNativeBuild 49 | 50 | # Google Services (e.g. APIs or Firebase) 51 | google-services.json 52 | 53 | # Freeline 54 | freeline.py 55 | freeline/ 56 | freeline_project_description.json -------------------------------------------------------------------------------- /.idea/caches/gradle_models.ser: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Baking-App-Kotlin/9d2684ac2fcf8efb182bbd7e16a8c16152bc9ea7/.idea/caches/gradle_models.ser -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 36 | 37 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | -------------------------------------------------------------------------------- /.idea/kotlinScripting.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BakingApp 2 | The Baking App displays a list of recipes, ingredients required to make it, and steps plus video tutorials on how to make the recipe. 3 | 4 | #### This is a rewrite of the Baking App project from the Udacity Android Nanodegree Programme 5 | 6 | ## Features 7 | * Kotlin Coroutines with Flow (State Flow) 8 | * Clean Architecture with MVI (Uni-directional data flow) 9 | * Jetpack Navigation for DFMs 10 | * Dynamic Feature Modules 11 | * Video streaming with Exoplayer 12 | * Dagger Hilt 13 | * Kotlin Gradle DSL 14 | 15 | 16 | ## Libraries 17 | * [FlowBinding](https://github.com/ReactiveCircus/FlowBinding) 18 | * [Moshi](https://github.com/square/moshi) 19 | * [Kotlin Coroutines](https://github.com/Kotlin/kotlinx.coroutines) 20 | * [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel) 21 | * [DFM navigation](https://developer.android.com/guide/navigation/navigation-dynamic) 22 | * [Coil](https://github.com/coil-kt/coil) 23 | * [ShimmerLayout](https://github.com/facebook/shimmer-android) 24 | * [Exoplayer](https://github.com/google/ExoPlayer) 25 | * [Truth](https://github.com/google/truth) 26 | * [Dagger Hilt](https://dagger.dev/hilt) 27 | * [Kotlin Gradle DSL](https://guides.gradle.org/migrating-build-logic-from-groovy-to-kotlin) 28 | 29 |

Screenshots

30 |

31 | 32 | 33 |
34 | 35 | ## Author 36 | Ezike Tobenna 37 | 38 | ## License 39 | This project is licensed under the Apache License 2.0 - See: http://www.apache.org/licenses/LICENSE-2.0.txt 40 | 41 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /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.kts. 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/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Baking-App-Kotlin/9d2684ac2fcf8efb182bbd7e16a8c16152bc9ea7/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /app/src/main/ic_launcher_app-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Baking-App-Kotlin/9d2684ac2fcf8efb182bbd7e16a8c16152bc9ea7/app/src/main/ic_launcher_app-web.png -------------------------------------------------------------------------------- /app/src/main/java/com/example/eziketobenna/bakingapp/BakingApplication.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp 2 | 3 | import android.app.Application 4 | import dagger.hilt.android.HiltAndroidApp 5 | 6 | @HiltAndroidApp 7 | class BakingApplication : Application() 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/eziketobenna/bakingapp/di/AppComponent.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.di 2 | 3 | import com.example.eziketobenna.bakingapp.navigation.NavigationDispatcher 4 | import dagger.hilt.EntryPoint 5 | import dagger.hilt.InstallIn 6 | import dagger.hilt.android.components.ActivityComponent 7 | 8 | @EntryPoint 9 | @InstallIn(ActivityComponent::class) 10 | interface AppComponent { 11 | val navigationDispatcher: NavigationDispatcher 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/eziketobenna/bakingapp/di/NavigationModule.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.di 2 | 3 | import android.app.Activity 4 | import androidx.navigation.NavController 5 | import androidx.navigation.findNavController 6 | import com.example.eziketobenna.bakingapp.R 7 | import com.example.eziketobenna.bakingapp.navigation.NavigationDispatcher 8 | import com.example.eziketobenna.bakingapp.navigation.NavigationDispatcherImpl 9 | import dagger.Binds 10 | import dagger.Module 11 | import dagger.Provides 12 | import dagger.hilt.InstallIn 13 | import dagger.hilt.android.components.ActivityComponent 14 | import dagger.hilt.android.scopes.ActivityScoped 15 | 16 | @InstallIn(ActivityComponent::class) 17 | @Module 18 | interface NavigationModule { 19 | 20 | @get:[Binds ActivityScoped] 21 | val NavigationDispatcherImpl.navigationDispatcher: NavigationDispatcher 22 | 23 | companion object { 24 | @[Provides ActivityScoped] 25 | fun provideNavController(activity: Activity): NavController = 26 | activity.findNavController(R.id.mainNavHostFragment) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/eziketobenna/bakingapp/navigation/NavigationDispatcher.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.navigation 2 | 3 | import com.example.eziketobenna.bakingapp.model.RecipeModel 4 | import com.example.eziketobenna.bakingapp.model.StepInfoModel 5 | 6 | interface NavigationDispatcher { 7 | fun openRecipeDetail(model: RecipeModel) 8 | fun openStepDetail(stepInfoModel: StepInfoModel) 9 | fun goBack() 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/eziketobenna/bakingapp/navigation/NavigationDispatcherImpl.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.navigation 2 | 3 | import androidx.core.os.bundleOf 4 | import androidx.navigation.NavController 5 | import com.example.eziketobenna.bakingapp.R 6 | import com.example.eziketobenna.bakingapp.model.RecipeModel 7 | import com.example.eziketobenna.bakingapp.model.StepInfoModel 8 | import javax.inject.Inject 9 | 10 | class NavigationDispatcherImpl @Inject constructor( 11 | private val navController: NavController 12 | ) : NavigationDispatcher { 13 | 14 | override fun openRecipeDetail(model: RecipeModel) { 15 | navController.navigate( 16 | R.id.recipeDetailFragment, bundleOf(RECIPE_ARG to model) 17 | ) 18 | } 19 | 20 | override fun openStepDetail(stepInfoModel: StepInfoModel) { 21 | navController.navigate( 22 | R.id.stepDetailFragment, bundleOf(STEP_INFO_ARG to stepInfoModel) 23 | ) 24 | } 25 | 26 | override fun goBack() { 27 | navController.navigateUp() 28 | } 29 | 30 | companion object { 31 | const val RECIPE_ARG: String = "recipe" 32 | const val STEP_INFO_ARG: String = "stepInfo" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/eziketobenna/bakingapp/ui/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.ui 2 | 3 | import android.os.Bundle 4 | import android.view.MenuItem 5 | import androidx.appcompat.app.AppCompatActivity 6 | import androidx.navigation.NavController 7 | import androidx.navigation.ui.AppBarConfiguration 8 | import androidx.navigation.ui.NavigationUI 9 | import com.example.eziketobenna.bakingapp.databinding.ActivityMainBinding 10 | import dagger.hilt.android.AndroidEntryPoint 11 | import javax.inject.Inject 12 | import javax.inject.Provider 13 | 14 | @AndroidEntryPoint 15 | class MainActivity : AppCompatActivity() { 16 | 17 | /** 18 | * Using [Provider] to enable lazy retrieval of the [NavController] 19 | * The id used to get the [NavController] instance will be looked up 20 | * before the call to setContentView cos in the [Hilt_MainActivity], 21 | * inject() is called before super.onCreate(), which is normal. 22 | * 23 | * Using [Provider] or [Lazy] will prevent it from throwing an error, 24 | * since they offer lazy retrieval and or initialization. 25 | */ 26 | @Inject 27 | lateinit var _navController: Provider 28 | 29 | private val navController: NavController 30 | get() = _navController.get() 31 | 32 | override fun onCreate(savedInstanceState: Bundle?) { 33 | super.onCreate(savedInstanceState) 34 | val binding: ActivityMainBinding = ActivityMainBinding.inflate(layoutInflater) 35 | setContentView(binding.root) 36 | 37 | setSupportActionBar(binding.toolbar) 38 | 39 | NavigationUI.setupActionBarWithNavController( 40 | this, 41 | navController, 42 | AppBarConfiguration(navController.graph) 43 | ) 44 | } 45 | 46 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 47 | if (item.itemId == android.R.id.home) { 48 | onBackPressed() 49 | return true 50 | } 51 | return super.onOptionsItemSelected(item) 52 | } 53 | 54 | override fun onBackPressed() { 55 | navController.navigateUp() 56 | } 57 | 58 | override fun onSupportNavigateUp(): Boolean { 59 | return navController.navigateUp() 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /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_baseline_arrow_back_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/font/googlesans.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Baking-App-Kotlin/9d2684ac2fcf8efb182bbd7e16a8c16152bc9ea7/app/src/main/res/font/googlesans.ttf -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 17 | 18 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Baking-App-Kotlin/9d2684ac2fcf8efb182bbd7e16a8c16152bc9ea7/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Baking-App-Kotlin/9d2684ac2fcf8efb182bbd7e16a8c16152bc9ea7/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Baking-App-Kotlin/9d2684ac2fcf8efb182bbd7e16a8c16152bc9ea7/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Baking-App-Kotlin/9d2684ac2fcf8efb182bbd7e16a8c16152bc9ea7/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Baking-App-Kotlin/9d2684ac2fcf8efb182bbd7e16a8c16152bc9ea7/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Baking-App-Kotlin/9d2684ac2fcf8efb182bbd7e16a8c16152bc9ea7/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Baking-App-Kotlin/9d2684ac2fcf8efb182bbd7e16a8c16152bc9ea7/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Baking-App-Kotlin/9d2684ac2fcf8efb182bbd7e16a8c16152bc9ea7/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Baking-App-Kotlin/9d2684ac2fcf8efb182bbd7e16a8c16152bc9ea7/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Baking-App-Kotlin/9d2684ac2fcf8efb182bbd7e16a8c16152bc9ea7/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Baking-App-Kotlin/9d2684ac2fcf8efb182bbd7e16a8c16152bc9ea7/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Baking-App-Kotlin/9d2684ac2fcf8efb182bbd7e16a8c16152bc9ea7/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Baking-App-Kotlin/9d2684ac2fcf8efb182bbd7e16a8c16152bc9ea7/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Baking-App-Kotlin/9d2684ac2fcf8efb182bbd7e16a8c16152bc9ea7/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Baking-App-Kotlin/9d2684ac2fcf8efb182bbd7e16a8c16152bc9ea7/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/navigation/nav_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 14 | 21 | 22 | 23 | 29 | 30 | 37 | 38 | 42 | 43 | 44 | 45 | 51 | 52 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /app/src/main/res/values-xlarge/resources.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #ffffff 4 | #cfd8dc 5 | #42a5f5 6 | #29302e 7 | #a6dbddde 8 | #bdbdbd 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 32dp 4 | 30dp 5 | 28dp 6 | 24dp 7 | 22dp 8 | 20dp 9 | 18dp 10 | 16dp 11 | 12dp 12 | 8dp 13 | 4dp 14 | 2dp 15 | 48dp 16 | 1dp 17 | 6dp 18 | 200dp 19 | 240dp 20 | 16dp 21 | 200dp 22 | 250dp 23 | 16dp 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Baking App 3 | Recipe Detail 4 | Step Detail 5 | Video Player 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 6 | 7 | 10 | 11 | 14 | 15 | 22 | 23 | -------------------------------------------------------------------------------- /core/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import Dependencies.Coroutines 2 | import Dependencies.DI 3 | import Dependencies.Kotlin 4 | import Dependencies.Network 5 | import Dependencies.View 6 | import ProjectLib.data 7 | import ProjectLib.domain 8 | import ProjectLib.remote 9 | 10 | plugins { 11 | androidLibrary 12 | kotlin(kotlinAndroid) 13 | kotlin(kotlinKapt) 14 | daggerHilt 15 | } 16 | 17 | android { 18 | compileSdkVersion(Config.Version.compileSdkVersion) 19 | defaultConfig { 20 | minSdkVersion(Config.Version.minSdkVersion) 21 | targetSdkVersion(Config.Version.targetSdkVersion) 22 | } 23 | 24 | @Suppress("UnstableApiUsage") 25 | compileOptions { 26 | sourceCompatibility = JavaVersion.VERSION_1_8 27 | targetCompatibility = JavaVersion.VERSION_1_8 28 | } 29 | 30 | kotlinOptions { 31 | jvmTarget = JavaVersion.VERSION_1_8.toString() 32 | } 33 | 34 | buildTypes { 35 | named(BuildType.DEBUG) { 36 | isMinifyEnabled = BuildTypeDebug.isMinifyEnabled 37 | versionNameSuffix = BuildTypeDebug.versionNameSuffix 38 | } 39 | } 40 | } 41 | 42 | dependencies { 43 | implementation(project(domain)) 44 | implementation(project(data)) 45 | implementation(project(remote)) 46 | 47 | implementation(Kotlin.stdlib) 48 | implementation(DI.daggerHiltAndroid) 49 | implementation(Network.moshi) 50 | implementation(Coroutines.core) 51 | implementation(View.coil) 52 | implementation(View.fragment) 53 | implementation(View.appCompat) 54 | 55 | kapt(DI.AnnotationProcessor.daggerHiltAndroid) 56 | } 57 | -------------------------------------------------------------------------------- /core/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Baking-App-Kotlin/9d2684ac2fcf8efb182bbd7e16a8c16152bc9ea7/core/consumer-rules.pro -------------------------------------------------------------------------------- /core/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.kts. 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 -------------------------------------------------------------------------------- /core/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /core/src/main/java/com/example/eziketobenna/bakingapp/core/di/component/CoreComponent.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.core.di.component 2 | 3 | import com.example.eziketobenna.bakingapp.core.imageLoader.ImageLoader 4 | import com.example.eziketobenna.bakingapp.domain.executor.PostExecutionThread 5 | import com.example.eziketobenna.bakingapp.domain.repository.RecipeRepository 6 | import dagger.hilt.EntryPoint 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.android.components.ApplicationComponent 9 | 10 | @EntryPoint 11 | @InstallIn(ApplicationComponent::class) 12 | interface CoreComponent { 13 | val imageLoader: ImageLoader 14 | val recipeRepository: RecipeRepository 15 | val postExecutionThread: PostExecutionThread 16 | } 17 | -------------------------------------------------------------------------------- /core/src/main/java/com/example/eziketobenna/bakingapp/core/di/mapkeys/ViewModelKey.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.core.di.mapkeys 2 | 3 | import androidx.lifecycle.ViewModel 4 | import dagger.MapKey 5 | import kotlin.reflect.KClass 6 | 7 | @Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, 8 | AnnotationTarget.PROPERTY_SETTER) 9 | @Retention(AnnotationRetention.RUNTIME) 10 | @MapKey 11 | annotation class ViewModelKey(val value: KClass) 12 | -------------------------------------------------------------------------------- /core/src/main/java/com/example/eziketobenna/bakingapp/core/di/module/DataModule.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.core.di.module 2 | 3 | import com.example.eziketobenna.bakingapp.data.repository.RecipeRepositoryImpl 4 | import com.example.eziketobenna.bakingapp.domain.repository.RecipeRepository 5 | import dagger.Binds 6 | import dagger.Module 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.android.components.ApplicationComponent 9 | import javax.inject.Singleton 10 | 11 | @InstallIn(ApplicationComponent::class) 12 | @Module 13 | interface DataModule { 14 | 15 | @get:[Binds Singleton] 16 | val RecipeRepositoryImpl.recipeRepository: RecipeRepository 17 | } 18 | -------------------------------------------------------------------------------- /core/src/main/java/com/example/eziketobenna/bakingapp/core/di/module/ExecutorModule.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.core.di.module 2 | 3 | import com.example.eziketobenna.bakingapp.core.executor.PostExecutionThreadImpl 4 | import com.example.eziketobenna.bakingapp.domain.executor.PostExecutionThread 5 | import dagger.Binds 6 | import dagger.Module 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.android.components.ApplicationComponent 9 | import javax.inject.Singleton 10 | 11 | @InstallIn(ApplicationComponent::class) 12 | @Module 13 | interface ExecutorModule { 14 | 15 | @get:[Binds Singleton] 16 | val PostExecutionThreadImpl.postExecutionThread: PostExecutionThread 17 | } 18 | -------------------------------------------------------------------------------- /core/src/main/java/com/example/eziketobenna/bakingapp/core/di/module/FactoriesModule.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.core.di.module 2 | 3 | import androidx.lifecycle.ViewModelProvider 4 | import com.example.eziketobenna.bakingapp.core.di.scope.FeatureScope 5 | import com.example.eziketobenna.bakingapp.core.factory.ViewModelFactory 6 | import dagger.Binds 7 | import dagger.Module 8 | import dagger.hilt.InstallIn 9 | import dagger.hilt.android.components.ApplicationComponent 10 | 11 | @InstallIn(ApplicationComponent::class) 12 | @Module 13 | interface FactoriesModule { 14 | 15 | @get:[FeatureScope Binds] 16 | val ViewModelFactory.viewModelFactory: ViewModelProvider.Factory 17 | } 18 | -------------------------------------------------------------------------------- /core/src/main/java/com/example/eziketobenna/bakingapp/core/di/module/ImageLoaderModule.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.core.di.module 2 | 3 | import com.example.eziketobenna.bakingapp.core.imageLoader.ImageLoader 4 | import com.example.eziketobenna.bakingapp.core.imageLoader.ImageLoaderImpl 5 | import dagger.Binds 6 | import dagger.Module 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.android.components.ApplicationComponent 9 | import javax.inject.Singleton 10 | 11 | @InstallIn(ApplicationComponent::class) 12 | @Module 13 | interface ImageLoaderModule { 14 | 15 | @get:[Binds Singleton] 16 | val ImageLoaderImpl.imageLoader: ImageLoader 17 | } 18 | -------------------------------------------------------------------------------- /core/src/main/java/com/example/eziketobenna/bakingapp/core/di/module/RemoteModule.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.core.di.module 2 | 3 | import com.example.eziketobenna.bakingapp.core.BuildConfig 4 | import com.example.eziketobenna.bakingapp.data.contract.RecipeRemote 5 | import com.example.eziketobenna.bakingapp.remote.ApiService 6 | import com.example.eziketobenna.bakingapp.remote.ApiServiceFactory 7 | import com.example.eziketobenna.bakingapp.remote.impl.RecipeRemoteImpl 8 | import com.squareup.moshi.Moshi 9 | import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory 10 | import dagger.Binds 11 | import dagger.Module 12 | import dagger.Provides 13 | import dagger.hilt.InstallIn 14 | import dagger.hilt.android.components.ApplicationComponent 15 | import javax.inject.Singleton 16 | 17 | @InstallIn(ApplicationComponent::class) 18 | @Module 19 | interface RemoteModule { 20 | 21 | @get:[Binds Singleton] 22 | val RecipeRemoteImpl.bindRemote: RecipeRemote 23 | 24 | companion object { 25 | @get:[Provides Singleton] 26 | val provideMoshi: Moshi 27 | get() = Moshi.Builder() 28 | .add(KotlinJsonAdapterFactory()).build() 29 | 30 | @[Provides Singleton] 31 | fun provideApiService(moshi: Moshi): ApiService = 32 | ApiServiceFactory.makeAPiService(BuildConfig.DEBUG, moshi) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /core/src/main/java/com/example/eziketobenna/bakingapp/core/di/scope/FeatureScope.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.core.di.scope 2 | 3 | import javax.inject.Scope 4 | 5 | @Scope 6 | @kotlin.annotation.Retention(AnnotationRetention.RUNTIME) 7 | annotation class FeatureScope 8 | -------------------------------------------------------------------------------- /core/src/main/java/com/example/eziketobenna/bakingapp/core/executor/PostExecutionThreadImpl.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.core.executor 2 | 3 | import com.example.eziketobenna.bakingapp.domain.executor.PostExecutionThread 4 | import javax.inject.Inject 5 | import kotlinx.coroutines.CoroutineDispatcher 6 | import kotlinx.coroutines.Dispatchers 7 | 8 | class PostExecutionThreadImpl @Inject constructor() : PostExecutionThread { 9 | override val main: CoroutineDispatcher = Dispatchers.Main 10 | override val io: CoroutineDispatcher = Dispatchers.IO 11 | override val default: CoroutineDispatcher = Dispatchers.Default 12 | } 13 | -------------------------------------------------------------------------------- /core/src/main/java/com/example/eziketobenna/bakingapp/core/ext/Extensions.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.core.ext 2 | 3 | val Throwable.errorMessage: String 4 | get() = message ?: localizedMessage ?: "An error occurred 😩" 5 | 6 | inline fun String.notEmpty(action: (String) -> Unit) { 7 | if (this.isNotEmpty()) { 8 | action(this) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /core/src/main/java/com/example/eziketobenna/bakingapp/core/ext/FlowExt.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.core.ext 2 | 3 | import androidx.lifecycle.LifecycleOwner 4 | import androidx.lifecycle.lifecycleScope 5 | import kotlinx.coroutines.CancellationException 6 | import kotlinx.coroutines.channels.SendChannel 7 | import kotlinx.coroutines.flow.Flow 8 | import kotlinx.coroutines.flow.launchIn 9 | import kotlinx.coroutines.flow.onEach 10 | 11 | inline fun Flow.observe( 12 | lifecycleOwner: LifecycleOwner, 13 | crossinline action: (R) -> Unit 14 | ) { 15 | this.onEach { 16 | action(it) 17 | }.launchIn(lifecycleOwner.lifecycleScope) 18 | } 19 | 20 | fun SendChannel.safeOffer(value: E): Boolean = !isClosedForSend && try { 21 | offer(value) 22 | } catch (e: CancellationException) { 23 | false 24 | } 25 | -------------------------------------------------------------------------------- /core/src/main/java/com/example/eziketobenna/bakingapp/core/ext/ViewExt.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.core.ext 2 | 3 | import android.content.Context 4 | import android.graphics.drawable.Drawable 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import androidx.activity.OnBackPressedCallback 9 | import androidx.activity.addCallback 10 | import androidx.annotation.DrawableRes 11 | import androidx.appcompat.app.AppCompatActivity 12 | import androidx.core.content.ContextCompat 13 | import androidx.fragment.app.Fragment 14 | 15 | fun ViewGroup.inflate(layout: Int): View { 16 | val layoutInflater: LayoutInflater = 17 | context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater 18 | return layoutInflater.inflate(layout, this, false) 19 | } 20 | 21 | fun Fragment.getDrawable(@DrawableRes id: Int): Drawable? { 22 | return ContextCompat.getDrawable( 23 | requireContext(), 24 | id 25 | ) 26 | } 27 | 28 | fun Fragment.onBackPress(onBackPressed: OnBackPressedCallback.() -> Unit) { 29 | requireActivity().onBackPressedDispatcher.addCallback( 30 | viewLifecycleOwner, 31 | onBackPressed = onBackPressed 32 | ) 33 | } 34 | 35 | val Fragment.actionBar: androidx.appcompat.app.ActionBar? 36 | get() = (requireActivity() as AppCompatActivity).supportActionBar 37 | 38 | var androidx.appcompat.app.ActionBar.visible: Boolean 39 | set(value) = if (value) this.show() else this.hide() 40 | get() = this.isShowing 41 | -------------------------------------------------------------------------------- /core/src/main/java/com/example/eziketobenna/bakingapp/core/factory/ViewModelFactory.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.core.factory 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.ViewModelProvider 5 | import javax.inject.Inject 6 | import javax.inject.Provider 7 | 8 | @Suppress("UNCHECKED_CAST") 9 | class ViewModelFactory @Inject constructor( 10 | private val creators: Map, @JvmSuppressWildcards Provider> 11 | ) : ViewModelProvider.Factory { 12 | 13 | override fun create(modelClass: Class): T { 14 | val found: Map.Entry, Provider>? = 15 | creators.entries.find { modelClass.isAssignableFrom(it.key) } 16 | val creator: Provider = found?.value 17 | ?: throw IllegalArgumentException("Unknown model class $modelClass") 18 | try { 19 | return creator.get() as T 20 | } catch (e: Exception) { 21 | throw RuntimeException(e) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /core/src/main/java/com/example/eziketobenna/bakingapp/core/imageLoader/ImageLoader.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.core.imageLoader 2 | 3 | import android.widget.ImageView 4 | 5 | interface ImageLoader { 6 | fun loadImage(view: ImageView, url: String) 7 | } 8 | -------------------------------------------------------------------------------- /core/src/main/java/com/example/eziketobenna/bakingapp/core/imageLoader/ImageLoaderImpl.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.core.imageLoader 2 | 3 | import android.widget.ImageView 4 | import coil.api.load 5 | import coil.size.ViewSizeResolver 6 | import javax.inject.Inject 7 | import javax.inject.Singleton 8 | 9 | @Singleton 10 | class ImageLoaderImpl @Inject constructor() : ImageLoader { 11 | override fun loadImage(view: ImageView, url: String) { 12 | view.load(url) { 13 | crossfade(true) 14 | size(ViewSizeResolver(view, false)) 15 | listener(onError = { _, throwable -> 16 | throwable.printStackTrace() 17 | }) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /core/src/main/java/com/example/eziketobenna/bakingapp/core/viewBinding/ViewBinding.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.core.viewBinding 2 | 3 | import android.view.View 4 | import androidx.fragment.app.Fragment 5 | import androidx.lifecycle.DefaultLifecycleObserver 6 | import androidx.lifecycle.Lifecycle 7 | import androidx.lifecycle.LifecycleOwner 8 | import androidx.viewbinding.ViewBinding 9 | import kotlin.properties.ReadOnlyProperty 10 | import kotlin.reflect.KProperty 11 | 12 | /** 13 | * A lazy property that gets cleaned up when the fragment's view is destroyed. 14 | * Accessing this variable while the fragment's view is destroyed will throw an [IllegalStateException]. 15 | */ 16 | class ViewBindingDelegate( 17 | private val fragment: Fragment, 18 | private val viewBindingFactory: (View) -> T 19 | ) : ReadOnlyProperty, DefaultLifecycleObserver { 20 | 21 | init { 22 | fragment.lifecycle.addObserver(this) 23 | } 24 | 25 | private var _value: T? = null 26 | 27 | private val viewLifecycleObserver: DefaultLifecycleObserver = 28 | object : DefaultLifecycleObserver { 29 | override fun onDestroy(owner: LifecycleOwner) { 30 | disposeValue() 31 | } 32 | } 33 | 34 | private fun disposeValue() { 35 | _value = null 36 | } 37 | 38 | override fun onCreate(owner: LifecycleOwner) { 39 | fragment.viewLifecycleOwnerLiveData.observe(fragment) { viewLifecycleOwner -> 40 | viewLifecycleOwner?.lifecycle?.addObserver(viewLifecycleObserver) 41 | } 42 | } 43 | 44 | override fun getValue(thisRef: Fragment, property: KProperty<*>): T { 45 | val binding: T? = _value 46 | if (binding != null) { 47 | return binding 48 | } 49 | 50 | val lifecycle: Lifecycle = fragment.viewLifecycleOwner.lifecycle 51 | if (!lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) { 52 | throw IllegalStateException( 53 | "Should not attempt to get bindings when " + 54 | "Fragment views are destroyed." 55 | ) 56 | } 57 | 58 | return viewBindingFactory(thisRef.requireView()).also { 59 | _value = it 60 | } 61 | } 62 | } 63 | 64 | fun Fragment.viewBinding(viewBindingFactory: (View) -> T): ViewBindingDelegate = 65 | ViewBindingDelegate( 66 | fragment = this, 67 | viewBindingFactory = viewBindingFactory 68 | ) 69 | -------------------------------------------------------------------------------- /core/src/main/res/drawable/cheese_cake.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Baking-App-Kotlin/9d2684ac2fcf8efb182bbd7e16a8c16152bc9ea7/core/src/main/res/drawable/cheese_cake.jpg -------------------------------------------------------------------------------- /features/recipes/model/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /features/recipes/model/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import Dependencies.DI 2 | import Dependencies.Kotlin 3 | import ProjectLib.domain 4 | import ProjectLib.presentation 5 | 6 | plugins { 7 | androidLibrary 8 | kotlin(kotlinAndroid) 9 | kotlin(kotlinAndroidExtension) 10 | } 11 | 12 | android { 13 | compileSdkVersion(Config.Version.compileSdkVersion) 14 | defaultConfig { 15 | minSdkVersion(Config.Version.minSdkVersion) 16 | targetSdkVersion(Config.Version.targetSdkVersion) 17 | } 18 | 19 | @Suppress("UnstableApiUsage") 20 | compileOptions { 21 | sourceCompatibility = JavaVersion.VERSION_1_8 22 | targetCompatibility = JavaVersion.VERSION_1_8 23 | } 24 | 25 | kotlinOptions { 26 | jvmTarget = JavaVersion.VERSION_1_8.toString() 27 | } 28 | 29 | buildTypes { 30 | named(BuildType.DEBUG) { 31 | isMinifyEnabled = BuildTypeDebug.isMinifyEnabled 32 | versionNameSuffix = BuildTypeDebug.versionNameSuffix 33 | } 34 | } 35 | } 36 | 37 | dependencies { 38 | implementation(project(domain)) 39 | implementation(project(presentation)) 40 | implementation(Kotlin.stdlib) 41 | implementation(DI.javaxInject) 42 | } 43 | -------------------------------------------------------------------------------- /features/recipes/model/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Baking-App-Kotlin/9d2684ac2fcf8efb182bbd7e16a8c16152bc9ea7/features/recipes/model/consumer-rules.pro -------------------------------------------------------------------------------- /features/recipes/model/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.kts. 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 -------------------------------------------------------------------------------- /features/recipes/model/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /features/recipes/model/src/main/java/com/example/eziketobenna/bakingapp/model/IngredientModel.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.model 2 | 3 | import android.os.Parcelable 4 | import kotlinx.android.parcel.Parcelize 5 | 6 | @Parcelize 7 | data class IngredientModel( 8 | val quantity: Double, 9 | val measure: String, 10 | val ingredient: String 11 | ) : Parcelable 12 | -------------------------------------------------------------------------------- /features/recipes/model/src/main/java/com/example/eziketobenna/bakingapp/model/RecipeModel.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.model 2 | 3 | import android.os.Parcelable 4 | import kotlinx.android.parcel.Parcelize 5 | 6 | @Parcelize 7 | data class RecipeModel( 8 | val id: Int, 9 | val name: String, 10 | val image: String, 11 | val servings: Int, 12 | val ingredients: List, 13 | val steps: List 14 | ) : Parcelable 15 | -------------------------------------------------------------------------------- /features/recipes/model/src/main/java/com/example/eziketobenna/bakingapp/model/StepInfoModel.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.model 2 | 3 | import android.os.Parcelable 4 | import kotlinx.android.parcel.Parcelize 5 | 6 | @Parcelize 7 | data class StepInfoModel( 8 | val steps: List, 9 | val step: StepModel 10 | ) : Parcelable 11 | -------------------------------------------------------------------------------- /features/recipes/model/src/main/java/com/example/eziketobenna/bakingapp/model/StepModel.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.model 2 | 3 | import android.os.Parcelable 4 | import kotlinx.android.parcel.Parcelize 5 | 6 | @Parcelize 7 | data class StepModel( 8 | val id: Int, 9 | val videoURL: String, 10 | val description: String, 11 | val shortDescription: String, 12 | val thumbnailURL: String 13 | ) : Parcelable 14 | -------------------------------------------------------------------------------- /features/recipes/model/src/main/java/com/example/eziketobenna/bakingapp/model/mapper/IngredientModelMapper.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.model.mapper 2 | 3 | import com.example.eziketobenna.bakingapp.domain.model.Ingredient 4 | import com.example.eziketobenna.bakingapp.model.IngredientModel 5 | import com.example.eziketobenna.bakingapp.presentation.mapper.ModelMapper 6 | import javax.inject.Inject 7 | 8 | class IngredientModelMapper @Inject constructor() : ModelMapper { 9 | 10 | override fun mapToModel(domain: Ingredient): IngredientModel { 11 | return IngredientModel( 12 | domain.quantity, 13 | domain.measure, 14 | domain.ingredient 15 | ) 16 | } 17 | 18 | override fun mapToDomain(model: IngredientModel): Ingredient { 19 | return Ingredient( 20 | model.quantity, 21 | model.measure, 22 | model.ingredient 23 | ) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /features/recipes/model/src/main/java/com/example/eziketobenna/bakingapp/model/mapper/RecipeModelMapper.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.model.mapper 2 | 3 | import com.example.eziketobenna.bakingapp.domain.model.Recipe 4 | import com.example.eziketobenna.bakingapp.model.RecipeModel 5 | import com.example.eziketobenna.bakingapp.presentation.mapper.ModelMapper 6 | import javax.inject.Inject 7 | 8 | class RecipeModelMapper @Inject constructor( 9 | private val stepModelMapper: StepModelMapper, 10 | private val ingredientModelMapper: IngredientModelMapper 11 | ) : ModelMapper { 12 | 13 | override fun mapToModel(domain: Recipe): RecipeModel { 14 | return RecipeModel( 15 | domain.id, 16 | domain.name, 17 | domain.image, 18 | domain.servings, 19 | ingredientModelMapper.mapToModelList(domain.ingredients), 20 | stepModelMapper.mapToModelList(domain.steps) 21 | ) 22 | } 23 | 24 | override fun mapToDomain(model: RecipeModel): Recipe { 25 | return Recipe( 26 | model.id, 27 | model.name, 28 | model.image, 29 | model.servings, 30 | ingredientModelMapper.mapToDomainList(model.ingredients), 31 | stepModelMapper.mapToDomainList(model.steps) 32 | ) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /features/recipes/model/src/main/java/com/example/eziketobenna/bakingapp/model/mapper/StepModelMapper.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.model.mapper 2 | 3 | import com.example.eziketobenna.bakingapp.domain.model.Step 4 | import com.example.eziketobenna.bakingapp.model.StepModel 5 | import com.example.eziketobenna.bakingapp.presentation.mapper.ModelMapper 6 | import javax.inject.Inject 7 | 8 | class StepModelMapper @Inject constructor() : ModelMapper { 9 | 10 | override fun mapToModel(domain: Step): StepModel { 11 | return StepModel( 12 | domain.id, 13 | domain.videoURL, 14 | domain.description, 15 | domain.shortDescription, 16 | domain.thumbnailURL 17 | ) 18 | } 19 | 20 | override fun mapToDomain(model: StepModel): Step { 21 | return Step( 22 | model.id, 23 | model.videoURL, 24 | model.description, 25 | model.shortDescription, 26 | model.thumbnailURL 27 | ) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /features/recipes/recipe/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /features/recipes/recipe/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import Dependencies.AndroidX 2 | import Dependencies.Coroutines 3 | import Dependencies.DI 4 | import Dependencies.FlowBinding 5 | import Dependencies.Kotlin 6 | import Dependencies.Test 7 | import Dependencies.View 8 | import ProjectLib.app 9 | import ProjectLib.core 10 | import ProjectLib.domain 11 | import ProjectLib.presentation 12 | import ProjectLib.recipeDetail 13 | import ProjectLib.recipeModel 14 | import ProjectLib.views 15 | 16 | plugins { 17 | dynamicFeature 18 | kotlin(kotlinAndroid) 19 | kotlin(kotlinKapt) 20 | daggerHilt 21 | } 22 | 23 | android { 24 | compileSdkVersion(Config.Version.compileSdkVersion) 25 | defaultConfig { 26 | minSdkVersion(Config.Version.minSdkVersion) 27 | targetSdkVersion(Config.Version.targetSdkVersion) 28 | } 29 | 30 | @Suppress("UnstableApiUsage") 31 | compileOptions { 32 | sourceCompatibility = JavaVersion.VERSION_1_8 33 | targetCompatibility = JavaVersion.VERSION_1_8 34 | } 35 | 36 | kotlinOptions { 37 | jvmTarget = JavaVersion.VERSION_1_8.toString() 38 | } 39 | 40 | buildTypes { 41 | named(BuildType.DEBUG) { 42 | isMinifyEnabled = BuildTypeDebug.isMinifyEnabled 43 | versionNameSuffix = BuildTypeDebug.versionNameSuffix 44 | } 45 | } 46 | } 47 | 48 | dependencies { 49 | implementation(project(app)) 50 | implementation(project(core)) 51 | implementation(project(presentation)) 52 | implementation(project(recipeModel)) 53 | implementation(project(recipeDetail)) 54 | implementation(project(domain)) 55 | implementation(project(views)) 56 | 57 | implementAll(View.components) 58 | implementation(View.recyclerView) 59 | implementation(View.shimmerLayout) 60 | implementation(View.swipeRefreshLayout) 61 | 62 | implementation(FlowBinding.swipeRefresh) 63 | implementation(DI.daggerHiltAndroid) 64 | 65 | implementation(Kotlin.stdlib) 66 | implementAll(AndroidX.components) 67 | implementAll(Coroutines.components) 68 | 69 | kapt(DI.AnnotationProcessor.daggerHiltAndroid) 70 | 71 | testImplementation(Test.junit) 72 | testImplementation(Test.truth) 73 | testImplementation(Test.coroutinesTest) 74 | } 75 | -------------------------------------------------------------------------------- /features/recipes/recipe/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /features/recipes/recipe/src/main/java/com/example/eziketobenna/bakingapp/recipe/Extensions.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.recipe 2 | 3 | import com.example.eziketobenna.bakingapp.core.ext.safeOffer 4 | import com.example.eziketobenna.bakingapp.views.SimpleEmptyStateView 5 | import kotlinx.coroutines.channels.awaitClose 6 | import kotlinx.coroutines.flow.Flow 7 | import kotlinx.coroutines.flow.callbackFlow 8 | import kotlinx.coroutines.flow.conflate 9 | import kotlinx.coroutines.flow.debounce 10 | 11 | //region FlowBinding extensions 12 | val SimpleEmptyStateView.clicks: Flow 13 | get() = callbackFlow { 14 | val listener: () -> Unit = { 15 | safeOffer(Unit) 16 | Unit 17 | } 18 | buttonClickListener = listener 19 | awaitClose { buttonClickListener = null } 20 | }.conflate().debounce(200) 21 | //endregion 22 | -------------------------------------------------------------------------------- /features/recipes/recipe/src/main/java/com/example/eziketobenna/bakingapp/recipe/di/Injector.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.recipe.di 2 | 3 | import com.example.eziketobenna.bakingapp.core.di.component.CoreComponent 4 | import com.example.eziketobenna.bakingapp.di.AppComponent 5 | import com.example.eziketobenna.bakingapp.recipe.di.component.DaggerRecipeComponent 6 | import com.example.eziketobenna.bakingapp.recipe.ui.RecipeFragment 7 | import dagger.hilt.android.EntryPointAccessors 8 | 9 | internal fun inject(fragment: RecipeFragment) { 10 | DaggerRecipeComponent 11 | .factory() 12 | .create(coreComponent(fragment), appComponent(fragment)) 13 | .inject(fragment) 14 | } 15 | 16 | private fun coreComponent(fragment: RecipeFragment): CoreComponent = 17 | EntryPointAccessors.fromApplication( 18 | fragment.requireContext().applicationContext, 19 | CoreComponent::class.java 20 | ) 21 | 22 | private fun appComponent(fragment: RecipeFragment): AppComponent = 23 | EntryPointAccessors.fromActivity( 24 | fragment.requireActivity(), 25 | AppComponent::class.java 26 | ) 27 | -------------------------------------------------------------------------------- /features/recipes/recipe/src/main/java/com/example/eziketobenna/bakingapp/recipe/di/component/RecipeComponent.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.recipe.di.component 2 | 3 | import com.example.eziketobenna.bakingapp.core.di.component.CoreComponent 4 | import com.example.eziketobenna.bakingapp.core.di.module.FactoriesModule 5 | import com.example.eziketobenna.bakingapp.core.di.scope.FeatureScope 6 | import com.example.eziketobenna.bakingapp.di.AppComponent 7 | import com.example.eziketobenna.bakingapp.recipe.di.module.PresentationModule 8 | import com.example.eziketobenna.bakingapp.recipe.di.module.ViewModelModule 9 | import com.example.eziketobenna.bakingapp.recipe.ui.RecipeFragment 10 | import dagger.Component 11 | 12 | @FeatureScope 13 | @Component( 14 | dependencies = [CoreComponent::class, AppComponent::class], 15 | modules = [FactoriesModule::class, ViewModelModule::class, PresentationModule::class] 16 | ) 17 | interface RecipeComponent { 18 | 19 | fun inject(recipeFragment: RecipeFragment) 20 | 21 | @Component.Factory 22 | interface Factory { 23 | fun create( 24 | coreComponent: CoreComponent, 25 | appComponent: AppComponent 26 | ): RecipeComponent 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /features/recipes/recipe/src/main/java/com/example/eziketobenna/bakingapp/recipe/di/module/PresentationModule.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.recipe.di.module 2 | 3 | import com.example.eziketobenna.bakingapp.core.di.scope.FeatureScope 4 | import com.example.eziketobenna.bakingapp.recipe.presentation.RecipeIntentProcessor 5 | import com.example.eziketobenna.bakingapp.recipe.presentation.RecipeStateReducer 6 | import com.example.eziketobenna.bakingapp.recipe.presentation.RecipeViewActionProcessor 7 | import com.example.eziketobenna.bakingapp.recipe.presentation.RecipeViewStateReducer 8 | import com.example.eziketobenna.bakingapp.recipe.presentation.processor.RecipeActionProcessor 9 | import com.example.eziketobenna.bakingapp.recipe.presentation.processor.RecipeViewIntentProcessor 10 | import dagger.Binds 11 | import dagger.Module 12 | import dagger.hilt.migration.DisableInstallInCheck 13 | 14 | @DisableInstallInCheck 15 | @Module 16 | interface PresentationModule { 17 | 18 | @get:[Binds FeatureScope] 19 | val RecipeActionProcessor.actionProcessor: RecipeViewActionProcessor 20 | 21 | @get:[Binds FeatureScope] 22 | val RecipeViewIntentProcessor.intentProcessor: RecipeIntentProcessor 23 | 24 | @get:[Binds FeatureScope] 25 | val RecipeViewStateReducer.reducer: RecipeStateReducer 26 | } 27 | -------------------------------------------------------------------------------- /features/recipes/recipe/src/main/java/com/example/eziketobenna/bakingapp/recipe/di/module/ViewModelModule.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.recipe.di.module 2 | 3 | import androidx.lifecycle.ViewModel 4 | import com.example.eziketobenna.bakingapp.core.di.mapkeys.ViewModelKey 5 | import com.example.eziketobenna.bakingapp.recipe.presentation.RecipeViewModel 6 | import dagger.Binds 7 | import dagger.Module 8 | import dagger.hilt.migration.DisableInstallInCheck 9 | import dagger.multibindings.IntoMap 10 | 11 | @DisableInstallInCheck 12 | @Module 13 | interface ViewModelModule { 14 | 15 | @get:[Binds IntoMap ViewModelKey(RecipeViewModel::class)] 16 | val RecipeViewModel.recipeViewModel: ViewModel 17 | } 18 | -------------------------------------------------------------------------------- /features/recipes/recipe/src/main/java/com/example/eziketobenna/bakingapp/recipe/presentation/Alias.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.recipe.presentation 2 | 3 | import com.example.eziketobenna.bakingapp.presentation.mvi.ActionProcessor 4 | import com.example.eziketobenna.bakingapp.presentation.mvi.IntentProcessor 5 | import com.example.eziketobenna.bakingapp.presentation.mvi.ViewStateReducer 6 | import com.example.eziketobenna.bakingapp.recipe.presentation.mvi.RecipeViewAction 7 | import com.example.eziketobenna.bakingapp.recipe.presentation.mvi.RecipeViewIntent 8 | import com.example.eziketobenna.bakingapp.recipe.presentation.mvi.RecipeViewResult 9 | import com.example.eziketobenna.bakingapp.recipe.presentation.mvi.RecipeViewState 10 | 11 | typealias RecipeViewActionProcessor = 12 | @JvmSuppressWildcards ActionProcessor 13 | 14 | typealias RecipeStateReducer = 15 | @JvmSuppressWildcards ViewStateReducer 16 | 17 | typealias RecipeIntentProcessor = 18 | @JvmSuppressWildcards IntentProcessor 19 | -------------------------------------------------------------------------------- /features/recipes/recipe/src/main/java/com/example/eziketobenna/bakingapp/recipe/presentation/mvi/RecipeViewAction.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.recipe.presentation.mvi 2 | 3 | import com.example.eziketobenna.bakingapp.presentation.mvi.ViewAction 4 | 5 | sealed class RecipeViewAction : ViewAction { 6 | object LoadInitialAction : RecipeViewAction() 7 | object RetryFetchAction : RecipeViewAction() 8 | object RefreshRecipesAction : RecipeViewAction() 9 | } 10 | -------------------------------------------------------------------------------- /features/recipes/recipe/src/main/java/com/example/eziketobenna/bakingapp/recipe/presentation/mvi/RecipeViewIntent.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.recipe.presentation.mvi 2 | 3 | import com.example.eziketobenna.bakingapp.presentation.mvi.ViewIntent 4 | 5 | sealed class RecipeViewIntent : ViewIntent { 6 | object LoadInitialViewIntent : RecipeViewIntent() 7 | object RecipeRetryViewIntent : RecipeViewIntent() 8 | object RecipeRefreshViewIntent : RecipeViewIntent() 9 | } 10 | -------------------------------------------------------------------------------- /features/recipes/recipe/src/main/java/com/example/eziketobenna/bakingapp/recipe/presentation/mvi/RecipeViewResult.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.recipe.presentation.mvi 2 | 3 | import com.example.eziketobenna.bakingapp.domain.model.Recipe 4 | import com.example.eziketobenna.bakingapp.presentation.mvi.ViewResult 5 | 6 | sealed class RecipeViewResult : ViewResult { 7 | 8 | sealed class LoadInitialResult : RecipeViewResult() { 9 | data class Loaded(val recipes: List) : LoadInitialResult() 10 | object Loading : LoadInitialResult() 11 | object Empty : LoadInitialResult() 12 | data class Error(val cause: Throwable) : LoadInitialResult() 13 | } 14 | 15 | sealed class RetryFetchResult : RecipeViewResult() { 16 | data class Loaded(val recipes: List) : RetryFetchResult() 17 | object Loading : RetryFetchResult() 18 | object Empty : RetryFetchResult() 19 | data class Error(val cause: Throwable) : RetryFetchResult() 20 | } 21 | 22 | sealed class RefreshRecipesResult : RecipeViewResult() { 23 | data class Loaded(val recipes: List) : RefreshRecipesResult() 24 | object Refreshing : RefreshRecipesResult() 25 | object Empty : RefreshRecipesResult() 26 | data class Error(val cause: Throwable) : RefreshRecipesResult() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /features/recipes/recipe/src/main/java/com/example/eziketobenna/bakingapp/recipe/presentation/processor/RecipeViewIntentProcessor.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.recipe.presentation.processor 2 | 3 | import com.example.eziketobenna.bakingapp.core.di.scope.FeatureScope 4 | import com.example.eziketobenna.bakingapp.presentation.mvi.IntentProcessor 5 | import com.example.eziketobenna.bakingapp.recipe.presentation.mvi.RecipeViewAction 6 | import com.example.eziketobenna.bakingapp.recipe.presentation.mvi.RecipeViewAction.LoadInitialAction 7 | import com.example.eziketobenna.bakingapp.recipe.presentation.mvi.RecipeViewAction.RefreshRecipesAction 8 | import com.example.eziketobenna.bakingapp.recipe.presentation.mvi.RecipeViewAction.RetryFetchAction 9 | import com.example.eziketobenna.bakingapp.recipe.presentation.mvi.RecipeViewIntent 10 | import com.example.eziketobenna.bakingapp.recipe.presentation.mvi.RecipeViewIntent.LoadInitialViewIntent 11 | import com.example.eziketobenna.bakingapp.recipe.presentation.mvi.RecipeViewIntent.RecipeRefreshViewIntent 12 | import com.example.eziketobenna.bakingapp.recipe.presentation.mvi.RecipeViewIntent.RecipeRetryViewIntent 13 | import javax.inject.Inject 14 | 15 | @FeatureScope 16 | class RecipeViewIntentProcessor @Inject constructor() : 17 | IntentProcessor { 18 | 19 | override fun intentToAction(intent: RecipeViewIntent): RecipeViewAction { 20 | return when (intent) { 21 | LoadInitialViewIntent -> LoadInitialAction 22 | RecipeRetryViewIntent -> RetryFetchAction 23 | RecipeRefreshViewIntent -> RefreshRecipesAction 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /features/recipes/recipe/src/main/res/drawable/error.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Baking-App-Kotlin/9d2684ac2fcf8efb182bbd7e16a8c16152bc9ea7/features/recipes/recipe/src/main/res/drawable/error.jpg -------------------------------------------------------------------------------- /features/recipes/recipe/src/main/res/layout-land/step_detail.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 16 | 17 | 26 | 27 | 41 | 42 | -------------------------------------------------------------------------------- /features/recipes/recipe/src/main/res/layout/content_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 18 | 19 | 34 | 35 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /features/recipes/recipe/src/main/res/layout/fragment_recipe.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 14 | 15 | 21 | 22 | 23 | 24 | 25 | 26 | 34 | 35 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /features/recipes/recipe/src/main/res/values-sw600dp/resources.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | 5 | -------------------------------------------------------------------------------- /features/recipes/recipe/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #ffffff 4 | #cfd8dc 5 | #42a5f5 6 | #29302e 7 | #a6dbddde 8 | #bdbdbd 9 | 10 | -------------------------------------------------------------------------------- /features/recipes/recipe/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 32dp 4 | 30dp 5 | 28dp 6 | 24dp 7 | 22dp 8 | 20dp 9 | 18dp 10 | 16dp 11 | 12dp 12 | 8dp 13 | 4dp 14 | 2dp 15 | 48dp 16 | 1dp 17 | 6dp 18 | 200dp 19 | 240dp 20 | 16dp 21 | 200dp 22 | 250dp 23 | 16dp 24 | 25 | 29 | 8dp 30 | 31 | -------------------------------------------------------------------------------- /features/recipes/recipe/src/main/res/values/resources.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | false 4 | 5 | -------------------------------------------------------------------------------- /features/recipes/recipe/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Baking App 3 | Food image 4 | Check your internet connection 5 | Retry 6 | Steps 7 | Step Detail 8 | Recipe data unavailable 9 | Ingredients 10 | NEXT 11 | PREV 12 | Ingredients and Steps 13 | To be displayed if there\'s no video 14 | EXAMPLE 15 | Add widget 16 | No data available 17 | Show in widget 18 | Step Detail 19 | Finish 20 | You must implement the OnStepClickListener interface 21 | Step Description 22 | %1$s will be shown in the widget 23 | Recipes 24 | No recipes available at the moment 25 | An error occurred 26 | 27 | "https://cdn.pixabay.com/photo/2018/08/30/10/30/plum-cake-3641849_1280.jpg" 28 | "https://bakingamoment.com/wp-content/uploads/2016/10/IMG_8205-brownie-recipe-720x720.jpg" 29 | "https://cdn.pixabay.com/photo/2016/11/29/11/38/blur-1869227_1280.jpg" 30 | "https://cdn.pixabay.com/photo/2016/08/08/16/20/cheesecake-1578691_1280.jpg" 31 | 32 | 33 | -------------------------------------------------------------------------------- /features/recipes/recipe/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | -------------------------------------------------------------------------------- /features/recipes/recipe/src/test/java/com/example/eziketobenna/bakingapp/recipe/presentation/data/DummyData.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.recipe.presentation.data 2 | 3 | import com.example.eziketobenna.bakingapp.domain.model.Ingredient 4 | import com.example.eziketobenna.bakingapp.domain.model.Recipe 5 | import com.example.eziketobenna.bakingapp.domain.model.Step 6 | import com.example.eziketobenna.bakingapp.model.RecipeModel 7 | import com.example.eziketobenna.bakingapp.model.mapper.RecipeModelMapper 8 | 9 | internal object DummyData { 10 | 11 | val recipe = Recipe( 12 | id = 3, 13 | name = "Burritos", 14 | image = "imgurl.com", 15 | servings = 1, 16 | ingredients = listOf(ingredient), 17 | steps = listOf(step) 18 | ) 19 | 20 | private val ingredient: Ingredient 21 | get() = Ingredient( 22 | quantity = 1.4, 23 | measure = "3", 24 | ingredient = "salt" 25 | ) 26 | 27 | private val step: Step 28 | get() = Step( 29 | id = 1, 30 | description = "pour stuff", 31 | shortDescription = "pour", 32 | videoURL = "url.com", 33 | thumbnailURL = "thumb.com" 34 | ) 35 | 36 | fun recipeModelList(recipeModelMapper: RecipeModelMapper): List = 37 | recipeModelMapper.mapToModelList(listOf(recipe)) 38 | } 39 | -------------------------------------------------------------------------------- /features/recipes/recipe/src/test/java/com/example/eziketobenna/bakingapp/recipe/presentation/executor/TestPostExecutionThread.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.recipe.presentation.executor 2 | 3 | import com.example.eziketobenna.bakingapp.domain.executor.PostExecutionThread 4 | import kotlinx.coroutines.CoroutineDispatcher 5 | import kotlinx.coroutines.test.TestCoroutineDispatcher 6 | 7 | class TestPostExecutionThread : PostExecutionThread { 8 | 9 | override val main: CoroutineDispatcher 10 | get() = TestCoroutineDispatcher() 11 | 12 | override val io: CoroutineDispatcher 13 | get() = TestCoroutineDispatcher() 14 | 15 | override val default: CoroutineDispatcher 16 | get() = TestCoroutineDispatcher() 17 | } 18 | -------------------------------------------------------------------------------- /features/recipes/recipe/src/test/java/com/example/eziketobenna/bakingapp/recipe/presentation/fake/FakeRecipeRepository.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.recipe.presentation.fake 2 | 3 | import com.example.eziketobenna.bakingapp.domain.model.Recipe 4 | import com.example.eziketobenna.bakingapp.domain.repository.RecipeRepository 5 | import com.example.eziketobenna.bakingapp.recipe.presentation.data.DummyData 6 | import java.net.SocketTimeoutException 7 | import kotlinx.coroutines.flow.Flow 8 | import kotlinx.coroutines.flow.flow 9 | import kotlinx.coroutines.flow.flowOf 10 | 11 | class FakeRecipeRepository : RecipeRepository { 12 | 13 | override fun fetchRecipes(): Flow> { 14 | return flowOf(listOf(DummyData.recipe)) 15 | } 16 | } 17 | 18 | class FakeRecipeRepositoryEmpty : RecipeRepository { 19 | 20 | override fun fetchRecipes(): Flow> { 21 | return flowOf(listOf()) 22 | } 23 | } 24 | 25 | class FakeRecipeRepositoryError : RecipeRepository { 26 | 27 | companion object { 28 | const val ERROR_MSG: String = "No data" 29 | } 30 | 31 | override fun fetchRecipes(): Flow> { 32 | return flow { throw SocketTimeoutException(ERROR_MSG) } 33 | } 34 | } 35 | 36 | fun makeFakeRecipeRepository(type: RepoType): RecipeRepository { 37 | return when (type) { 38 | RepoType.DATA -> FakeRecipeRepository() 39 | RepoType.EMPTY -> FakeRecipeRepositoryEmpty() 40 | RepoType.ERROR -> FakeRecipeRepositoryError() 41 | } 42 | } 43 | 44 | enum class RepoType { 45 | DATA, 46 | EMPTY, 47 | ERROR 48 | } 49 | -------------------------------------------------------------------------------- /features/recipes/recipe/src/test/java/com/example/eziketobenna/bakingapp/recipe/presentation/processor/RecipeViewIntentProcessorTest.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.recipe.presentation.processor 2 | 3 | import com.example.eziketobenna.bakingapp.recipe.presentation.mvi.RecipeViewAction 4 | import com.example.eziketobenna.bakingapp.recipe.presentation.mvi.RecipeViewAction.LoadInitialAction 5 | import com.example.eziketobenna.bakingapp.recipe.presentation.mvi.RecipeViewAction.RefreshRecipesAction 6 | import com.example.eziketobenna.bakingapp.recipe.presentation.mvi.RecipeViewAction.RetryFetchAction 7 | import com.example.eziketobenna.bakingapp.recipe.presentation.mvi.RecipeViewIntent.LoadInitialViewIntent 8 | import com.example.eziketobenna.bakingapp.recipe.presentation.mvi.RecipeViewIntent.RecipeRefreshViewIntent 9 | import com.example.eziketobenna.bakingapp.recipe.presentation.mvi.RecipeViewIntent.RecipeRetryViewIntent 10 | import com.google.common.truth.Truth.assertThat 11 | import org.junit.Test 12 | 13 | class RecipeViewIntentProcessorTest { 14 | 15 | private val recipeViewIntentProcessor = RecipeViewIntentProcessor() 16 | 17 | @Test 18 | fun `check that LoadInitialViewIntent is mapped to LoadInitialAction`() { 19 | val action: RecipeViewAction = 20 | recipeViewIntentProcessor.intentToAction(LoadInitialViewIntent) 21 | assertThat(action).isInstanceOf(LoadInitialAction::class.java) 22 | } 23 | 24 | @Test 25 | fun `check that RecipeRetryViewIntent is mapped to RetryFetchAction`() { 26 | val action: RecipeViewAction = 27 | recipeViewIntentProcessor.intentToAction(RecipeRetryViewIntent) 28 | assertThat(action).isInstanceOf(RetryFetchAction::class.java) 29 | } 30 | 31 | @Test 32 | fun `check that RecipeRefreshViewIntent is mapped to RefreshRecipesAction`() { 33 | val action: RecipeViewAction = 34 | recipeViewIntentProcessor.intentToAction(RecipeRefreshViewIntent) 35 | assertThat(action).isInstanceOf(RefreshRecipesAction::class.java) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /features/recipes/recipeDetail/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /features/recipes/recipeDetail/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import Dependencies.AndroidX 2 | import Dependencies.Coroutines 3 | import Dependencies.DI 4 | import Dependencies.FlowBinding 5 | import Dependencies.Kotlin 6 | import Dependencies.View 7 | import ProjectLib.app 8 | import ProjectLib.core 9 | import ProjectLib.domain 10 | import ProjectLib.presentation 11 | import ProjectLib.recipeModel 12 | import ProjectLib.stepDetail 13 | 14 | plugins { 15 | dynamicFeature 16 | kotlin(kotlinAndroid) 17 | kotlin(kotlinAndroidExtension) 18 | kotlin(kotlinKapt) 19 | daggerHilt 20 | } 21 | 22 | android { 23 | compileSdkVersion(Config.Version.compileSdkVersion) 24 | defaultConfig { 25 | minSdkVersion(Config.Version.minSdkVersion) 26 | targetSdkVersion(Config.Version.targetSdkVersion) 27 | } 28 | 29 | @Suppress("UnstableApiUsage") 30 | compileOptions { 31 | sourceCompatibility = JavaVersion.VERSION_1_8 32 | targetCompatibility = JavaVersion.VERSION_1_8 33 | } 34 | 35 | kotlinOptions { 36 | jvmTarget = JavaVersion.VERSION_1_8.toString() 37 | } 38 | 39 | buildTypes { 40 | named(BuildType.DEBUG) { 41 | isMinifyEnabled = BuildTypeDebug.isMinifyEnabled 42 | versionNameSuffix = BuildTypeDebug.versionNameSuffix 43 | } 44 | } 45 | } 46 | 47 | dependencies { 48 | implementation(project(app)) 49 | implementation(project(core)) 50 | implementation(project(presentation)) 51 | implementation(project(recipeModel)) 52 | implementation(project(stepDetail)) 53 | implementation(project(domain)) 54 | 55 | implementAll(View.components) 56 | implementation(View.recyclerView) 57 | implementation(FlowBinding.lifecycle) 58 | 59 | implementation(DI.daggerHiltAndroid) 60 | 61 | implementation(Kotlin.stdlib) 62 | implementAll(AndroidX.components) 63 | implementAll(Coroutines.components) 64 | 65 | kapt(DI.AnnotationProcessor.daggerHiltAndroid) 66 | } 67 | -------------------------------------------------------------------------------- /features/recipes/recipeDetail/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /features/recipes/recipeDetail/src/main/java/com/example/eziketobenna/bakingapp/recipedetail/di/Injector.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.recipedetail.di 2 | 3 | import com.example.eziketobenna.bakingapp.di.AppComponent 4 | import com.example.eziketobenna.bakingapp.recipedetail.di.component.DaggerRecipeDetailComponent 5 | import com.example.eziketobenna.bakingapp.recipedetail.ui.RecipeDetailFragment 6 | import dagger.hilt.android.EntryPointAccessors 7 | 8 | internal fun inject(fragment: RecipeDetailFragment) { 9 | DaggerRecipeDetailComponent 10 | .factory() 11 | .create(appComponent(fragment)) 12 | .inject(fragment) 13 | } 14 | 15 | private fun appComponent(fragment: RecipeDetailFragment): AppComponent = 16 | EntryPointAccessors.fromActivity( 17 | fragment.requireActivity(), 18 | AppComponent::class.java 19 | ) 20 | -------------------------------------------------------------------------------- /features/recipes/recipeDetail/src/main/java/com/example/eziketobenna/bakingapp/recipedetail/di/component/RecipeDetailComponent.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.recipedetail.di.component 2 | 3 | import com.example.eziketobenna.bakingapp.core.di.module.FactoriesModule 4 | import com.example.eziketobenna.bakingapp.core.di.scope.FeatureScope 5 | import com.example.eziketobenna.bakingapp.di.AppComponent 6 | import com.example.eziketobenna.bakingapp.recipedetail.di.module.PresentationModule 7 | import com.example.eziketobenna.bakingapp.recipedetail.di.module.ViewModelModule 8 | import com.example.eziketobenna.bakingapp.recipedetail.ui.RecipeDetailFragment 9 | import dagger.Component 10 | 11 | @FeatureScope 12 | @Component( 13 | dependencies = [AppComponent::class], 14 | modules = [FactoriesModule::class, ViewModelModule::class, 15 | PresentationModule::class] 16 | ) 17 | interface RecipeDetailComponent { 18 | 19 | fun inject(fragment: RecipeDetailFragment) 20 | 21 | @Component.Factory 22 | interface Factory { 23 | fun create(appComponent: AppComponent): RecipeDetailComponent 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /features/recipes/recipeDetail/src/main/java/com/example/eziketobenna/bakingapp/recipedetail/di/module/PresentationModule.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.recipedetail.di.module 2 | 3 | import com.example.eziketobenna.bakingapp.core.di.scope.FeatureScope 4 | import com.example.eziketobenna.bakingapp.recipedetail.presentation.DetailActionProcessor 5 | import com.example.eziketobenna.bakingapp.recipedetail.presentation.DetailIntentProcessor 6 | import com.example.eziketobenna.bakingapp.recipedetail.presentation.DetailViewStateReducer 7 | import com.example.eziketobenna.bakingapp.recipedetail.presentation.RecipeDetailViewStateReducer 8 | import com.example.eziketobenna.bakingapp.recipedetail.presentation.processor.RecipeDetailActionProcessor 9 | import com.example.eziketobenna.bakingapp.recipedetail.presentation.processor.RecipeDetailIntentProcessor 10 | import dagger.Binds 11 | import dagger.Module 12 | import dagger.hilt.migration.DisableInstallInCheck 13 | 14 | @DisableInstallInCheck 15 | @Module 16 | interface PresentationModule { 17 | 18 | @get:[Binds FeatureScope] 19 | val RecipeDetailActionProcessor.actionProcessor: DetailActionProcessor 20 | 21 | @get:[Binds FeatureScope] 22 | val RecipeDetailIntentProcessor.intentProcessor: DetailIntentProcessor 23 | 24 | @get:[Binds FeatureScope] 25 | val RecipeDetailViewStateReducer.reducer: DetailViewStateReducer 26 | } 27 | -------------------------------------------------------------------------------- /features/recipes/recipeDetail/src/main/java/com/example/eziketobenna/bakingapp/recipedetail/di/module/ViewModelModule.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.recipedetail.di.module 2 | 3 | import androidx.lifecycle.ViewModel 4 | import com.example.eziketobenna.bakingapp.core.di.mapkeys.ViewModelKey 5 | import com.example.eziketobenna.bakingapp.recipedetail.presentation.RecipeDetailViewModel 6 | import dagger.Binds 7 | import dagger.Module 8 | import dagger.hilt.migration.DisableInstallInCheck 9 | import dagger.multibindings.IntoMap 10 | 11 | @DisableInstallInCheck 12 | @Module 13 | interface ViewModelModule { 14 | 15 | @get:[Binds IntoMap ViewModelKey(RecipeDetailViewModel::class)] 16 | val RecipeDetailViewModel.recipeDetailViewModel: ViewModel 17 | } 18 | -------------------------------------------------------------------------------- /features/recipes/recipeDetail/src/main/java/com/example/eziketobenna/bakingapp/recipedetail/model/IngredientDetailMapper.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.recipedetail.model 2 | 3 | import com.example.eziketobenna.bakingapp.domain.model.Ingredient 4 | import com.example.eziketobenna.bakingapp.presentation.mapper.ModelMapper 5 | import javax.inject.Inject 6 | 7 | class IngredientDetailMapper @Inject constructor() : 8 | ModelMapper { 9 | 10 | override fun mapToModel(domain: Ingredient): IngredientDetailItem { 11 | return IngredientDetailItem( 12 | domain.quantity, 13 | domain.measure, 14 | domain.ingredient 15 | ) 16 | } 17 | 18 | override fun mapToDomain(model: IngredientDetailItem): Ingredient { 19 | return Ingredient( 20 | model.quantity, 21 | model.measure, 22 | model.ingredient 23 | ) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /features/recipes/recipeDetail/src/main/java/com/example/eziketobenna/bakingapp/recipedetail/model/RecipeDetailModel.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.recipedetail.model 2 | 3 | import androidx.annotation.StringRes 4 | 5 | sealed class RecipeDetailModel 6 | 7 | data class IngredientDetailItem( 8 | val quantity: Double, 9 | val measure: String, 10 | val ingredient: String 11 | ) : RecipeDetailModel() 12 | 13 | data class StepDetailItem( 14 | val id: Int, 15 | val videoURL: String, 16 | val description: String, 17 | val shortDescription: String, 18 | val thumbnailURL: String 19 | ) : RecipeDetailModel() 20 | 21 | data class HeaderItem(@StringRes val header: Int) : RecipeDetailModel() 22 | -------------------------------------------------------------------------------- /features/recipes/recipeDetail/src/main/java/com/example/eziketobenna/bakingapp/recipedetail/model/StepDetailMapper.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.recipedetail.model 2 | 3 | import com.example.eziketobenna.bakingapp.domain.model.Step 4 | import com.example.eziketobenna.bakingapp.presentation.mapper.ModelMapper 5 | import javax.inject.Inject 6 | 7 | class StepDetailMapper @Inject constructor() : ModelMapper { 8 | 9 | override fun mapToModel(domain: Step): StepDetailItem { 10 | return StepDetailItem( 11 | domain.id, 12 | domain.videoURL, 13 | domain.description, 14 | domain.shortDescription, 15 | domain.thumbnailURL 16 | ) 17 | } 18 | 19 | override fun mapToDomain(model: StepDetailItem): Step { 20 | return Step( 21 | model.id, 22 | model.videoURL, 23 | model.description, 24 | model.shortDescription, 25 | model.thumbnailURL 26 | ) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /features/recipes/recipeDetail/src/main/java/com/example/eziketobenna/bakingapp/recipedetail/presentation/Alias.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.recipedetail.presentation 2 | 3 | import com.example.eziketobenna.bakingapp.presentation.mvi.ActionProcessor 4 | import com.example.eziketobenna.bakingapp.presentation.mvi.IntentProcessor 5 | import com.example.eziketobenna.bakingapp.presentation.mvi.ViewStateReducer 6 | 7 | typealias DetailActionProcessor = 8 | @JvmSuppressWildcards ActionProcessor 9 | 10 | typealias DetailIntentProcessor = 11 | @JvmSuppressWildcards IntentProcessor 12 | 13 | typealias DetailViewStateReducer = 14 | @JvmSuppressWildcards ViewStateReducer 15 | -------------------------------------------------------------------------------- /features/recipes/recipeDetail/src/main/java/com/example/eziketobenna/bakingapp/recipedetail/presentation/RecipeDetailViewAction.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.recipedetail.presentation 2 | 3 | import com.example.eziketobenna.bakingapp.domain.model.Ingredient 4 | import com.example.eziketobenna.bakingapp.domain.model.Step 5 | import com.example.eziketobenna.bakingapp.presentation.mvi.ViewAction 6 | 7 | sealed class RecipeDetailViewAction : ViewAction { 8 | object Idle : RecipeDetailViewAction() 9 | data class LoadRecipeDetailAction( 10 | val ingredients: List, 11 | val steps: List 12 | ) : RecipeDetailViewAction() 13 | 14 | data class OpenStepInfoViewAction( 15 | val step: Step, 16 | val steps: List 17 | ) : RecipeDetailViewAction() 18 | } 19 | -------------------------------------------------------------------------------- /features/recipes/recipeDetail/src/main/java/com/example/eziketobenna/bakingapp/recipedetail/presentation/RecipeDetailViewIntent.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.recipedetail.presentation 2 | 3 | import com.example.eziketobenna.bakingapp.model.RecipeModel 4 | import com.example.eziketobenna.bakingapp.model.StepModel 5 | import com.example.eziketobenna.bakingapp.presentation.mvi.ViewIntent 6 | import com.example.eziketobenna.bakingapp.recipedetail.model.StepDetailItem 7 | 8 | sealed class RecipeDetailViewIntent : ViewIntent { 9 | data class LoadRecipeDetailIntent( 10 | val recipe: RecipeModel 11 | ) : RecipeDetailViewIntent() 12 | 13 | data class OpenStepInfoViewIntent( 14 | val stepDetailItem: StepDetailItem, 15 | val steps: List 16 | ) : RecipeDetailViewIntent() 17 | } 18 | -------------------------------------------------------------------------------- /features/recipes/recipeDetail/src/main/java/com/example/eziketobenna/bakingapp/recipedetail/presentation/RecipeDetailViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.recipedetail.presentation 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.example.eziketobenna.bakingapp.navigation.NavigationDispatcher 6 | import com.example.eziketobenna.bakingapp.presentation.mvi.MVIPresenter 7 | import javax.inject.Inject 8 | import kotlinx.coroutines.flow.Flow 9 | import kotlinx.coroutines.flow.MutableStateFlow 10 | import kotlinx.coroutines.flow.StateFlow 11 | import kotlinx.coroutines.flow.distinctUntilChanged 12 | import kotlinx.coroutines.flow.flatMapMerge 13 | import kotlinx.coroutines.flow.launchIn 14 | import kotlinx.coroutines.flow.onEach 15 | import kotlinx.coroutines.flow.scan 16 | 17 | class RecipeDetailViewModel @Inject constructor( 18 | private val recipeDetailActionProcessor: DetailActionProcessor, 19 | private val recipeDetailIntentProcessor: DetailIntentProcessor, 20 | private val recipeDetailStateReducer: DetailViewStateReducer, 21 | private val navigationDispatcher: NavigationDispatcher 22 | ) : ViewModel(), MVIPresenter, 23 | NavigationDispatcher by navigationDispatcher { 24 | 25 | private val actionsFlow: MutableStateFlow = 26 | MutableStateFlow(RecipeDetailViewAction.Idle) 27 | 28 | private val recipeDetailViewState: MutableStateFlow = 29 | MutableStateFlow(RecipeDetailViewState.Idle) 30 | 31 | override val viewState: StateFlow 32 | get() = recipeDetailViewState 33 | 34 | init { 35 | processActions() 36 | } 37 | 38 | override fun processIntent(intents: Flow) { 39 | intents.onEach { 40 | actionsFlow.value = recipeDetailIntentProcessor.intentToAction(it) 41 | }.launchIn(viewModelScope) 42 | } 43 | 44 | private fun processActions() { 45 | actionsFlow 46 | .flatMapMerge { 47 | recipeDetailActionProcessor.actionToResult(it) 48 | } 49 | .scan(RecipeDetailViewState.Idle) { previous: RecipeDetailViewState, result -> 50 | recipeDetailStateReducer.reduce(previous, result) 51 | }.distinctUntilChanged() 52 | .onEach { viewState -> 53 | recipeDetailViewState.value = viewState 54 | }.launchIn(viewModelScope) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /features/recipes/recipeDetail/src/main/java/com/example/eziketobenna/bakingapp/recipedetail/presentation/RecipeDetailViewResult.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.recipedetail.presentation 2 | 3 | import com.example.eziketobenna.bakingapp.domain.model.Ingredient 4 | import com.example.eziketobenna.bakingapp.domain.model.Step 5 | import com.example.eziketobenna.bakingapp.presentation.mvi.ViewResult 6 | 7 | sealed class RecipeDetailViewResult : ViewResult { 8 | object IdleResult : RecipeDetailViewResult() 9 | data class LoadedData(val ingredients: List, val steps: List) : 10 | RecipeDetailViewResult() 11 | 12 | data class OpenStepInfo(val step: Step, val steps: List) : 13 | RecipeDetailViewResult() 14 | } 15 | -------------------------------------------------------------------------------- /features/recipes/recipeDetail/src/main/java/com/example/eziketobenna/bakingapp/recipedetail/presentation/RecipeDetailViewState.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.recipedetail.presentation 2 | 3 | import com.example.eziketobenna.bakingapp.model.StepInfoModel 4 | import com.example.eziketobenna.bakingapp.presentation.event.ViewEvent 5 | import com.example.eziketobenna.bakingapp.presentation.mvi.ViewState 6 | import com.example.eziketobenna.bakingapp.recipedetail.model.RecipeDetailModel 7 | 8 | sealed class RecipeDetailViewState : ViewState { 9 | object Idle : RecipeDetailViewState() 10 | data class Success(val model: List) : RecipeDetailViewState() 11 | data class NavigateToStepInfo(val openStepInfoEvent: ViewEvent) : RecipeDetailViewState() 12 | } 13 | -------------------------------------------------------------------------------- /features/recipes/recipeDetail/src/main/java/com/example/eziketobenna/bakingapp/recipedetail/presentation/RecipeDetailViewStateReducer.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.recipedetail.presentation 2 | 3 | import com.example.eziketobenna.bakingapp.core.di.scope.FeatureScope 4 | import com.example.eziketobenna.bakingapp.presentation.event.ViewEvent 5 | import com.example.eziketobenna.bakingapp.presentation.mvi.ViewStateReducer 6 | import com.example.eziketobenna.bakingapp.recipedetail.presentation.RecipeDetailViewResult.LoadedData 7 | import com.example.eziketobenna.bakingapp.recipedetail.presentation.RecipeDetailViewResult.OpenStepInfo 8 | import com.example.eziketobenna.bakingapp.recipedetail.presentation.factory.RecipeDetailModelFactory 9 | import javax.inject.Inject 10 | 11 | @FeatureScope 12 | class RecipeDetailViewStateReducer @Inject constructor( 13 | private val recipeDetailModelFactory: RecipeDetailModelFactory 14 | ) : ViewStateReducer { 15 | 16 | override fun reduce( 17 | previous: RecipeDetailViewState, 18 | result: RecipeDetailViewResult 19 | ): RecipeDetailViewState { 20 | 21 | return when (result) { 22 | RecipeDetailViewResult.IdleResult -> RecipeDetailViewState.Idle 23 | is LoadedData -> handleLoadDataResult(result) 24 | is OpenStepInfo -> navigateToStepInfo(result) 25 | } 26 | } 27 | 28 | private fun navigateToStepInfo(result: OpenStepInfo): RecipeDetailViewState.NavigateToStepInfo { 29 | return RecipeDetailViewState.NavigateToStepInfo( 30 | ViewEvent(recipeDetailModelFactory.createStepInfoModel(result.step, result.steps)) 31 | ) 32 | } 33 | 34 | private fun handleLoadDataResult(result: LoadedData): RecipeDetailViewState { 35 | return RecipeDetailViewState.Success( 36 | recipeDetailModelFactory.makeRecipeDetailModelList(result.ingredients, result.steps) 37 | ) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /features/recipes/recipeDetail/src/main/java/com/example/eziketobenna/bakingapp/recipedetail/presentation/factory/RecipeDetailModelFactory.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.recipedetail.presentation.factory 2 | 3 | import com.example.eziketobenna.bakingapp.core.di.scope.FeatureScope 4 | import com.example.eziketobenna.bakingapp.domain.model.Ingredient 5 | import com.example.eziketobenna.bakingapp.domain.model.Step 6 | import com.example.eziketobenna.bakingapp.model.StepInfoModel 7 | import com.example.eziketobenna.bakingapp.model.mapper.StepModelMapper 8 | import com.example.eziketobenna.bakingapp.recipedetail.R 9 | import com.example.eziketobenna.bakingapp.recipedetail.model.HeaderItem 10 | import com.example.eziketobenna.bakingapp.recipedetail.model.IngredientDetailMapper 11 | import com.example.eziketobenna.bakingapp.recipedetail.model.RecipeDetailModel 12 | import com.example.eziketobenna.bakingapp.recipedetail.model.StepDetailMapper 13 | import javax.inject.Inject 14 | 15 | @[FeatureScope OptIn(ExperimentalStdlibApi::class)] 16 | class RecipeDetailModelFactory @Inject constructor( 17 | private val stepDetailMapper: StepDetailMapper, 18 | private val ingredientDetailMapper: IngredientDetailMapper, 19 | private val stepModelMapper: StepModelMapper 20 | ) { 21 | 22 | fun createStepInfoModel(step: Step, steps: List): StepInfoModel = StepInfoModel( 23 | step = stepModelMapper.mapToModel(step), 24 | steps = stepModelMapper.mapToModelList(steps) 25 | ) 26 | 27 | fun makeRecipeDetailModelList( 28 | ingredients: List, 29 | steps: List 30 | ): List = buildList { 31 | add(HeaderItem(R.string.ingredients)) 32 | addAll(ingredientDetailMapper.mapToModelList(ingredients)) 33 | add(HeaderItem(R.string.steps)) 34 | addAll(stepDetailMapper.mapToModelList(steps)) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /features/recipes/recipeDetail/src/main/java/com/example/eziketobenna/bakingapp/recipedetail/presentation/processor/RecipeDetailActionProcessor.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.recipedetail.presentation.processor 2 | 3 | import com.example.eziketobenna.bakingapp.core.di.scope.FeatureScope 4 | import com.example.eziketobenna.bakingapp.presentation.mvi.ActionProcessor 5 | import com.example.eziketobenna.bakingapp.recipedetail.presentation.RecipeDetailViewAction 6 | import com.example.eziketobenna.bakingapp.recipedetail.presentation.RecipeDetailViewAction.LoadRecipeDetailAction 7 | import com.example.eziketobenna.bakingapp.recipedetail.presentation.RecipeDetailViewAction.OpenStepInfoViewAction 8 | import com.example.eziketobenna.bakingapp.recipedetail.presentation.RecipeDetailViewResult 9 | import com.example.eziketobenna.bakingapp.recipedetail.presentation.RecipeDetailViewResult.OpenStepInfo 10 | import javax.inject.Inject 11 | import kotlinx.coroutines.flow.Flow 12 | import kotlinx.coroutines.flow.flowOf 13 | 14 | @FeatureScope 15 | class RecipeDetailActionProcessor @Inject constructor() : 16 | ActionProcessor { 17 | 18 | override fun actionToResult(viewAction: RecipeDetailViewAction): Flow { 19 | return when (viewAction) { 20 | RecipeDetailViewAction.Idle -> flowOf( 21 | RecipeDetailViewResult.IdleResult 22 | ) 23 | is LoadRecipeDetailAction -> handleLoadRecipeDetailAction(viewAction) 24 | is OpenStepInfoViewAction -> openStepInfoResult(viewAction) 25 | } 26 | } 27 | 28 | private fun openStepInfoResult(viewAction: OpenStepInfoViewAction): Flow = 29 | flowOf(OpenStepInfo(viewAction.step, viewAction.steps)) 30 | 31 | private fun handleLoadRecipeDetailAction( 32 | viewAction: LoadRecipeDetailAction 33 | ): Flow { 34 | return flowOf( 35 | RecipeDetailViewResult.LoadedData( 36 | viewAction.ingredients, 37 | viewAction.steps 38 | ) 39 | ) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /features/recipes/recipeDetail/src/main/java/com/example/eziketobenna/bakingapp/recipedetail/presentation/processor/RecipeDetailIntentProcessor.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.recipedetail.presentation.processor 2 | 3 | import com.example.eziketobenna.bakingapp.core.di.scope.FeatureScope 4 | import com.example.eziketobenna.bakingapp.model.mapper.IngredientModelMapper 5 | import com.example.eziketobenna.bakingapp.model.mapper.StepModelMapper 6 | import com.example.eziketobenna.bakingapp.presentation.mvi.IntentProcessor 7 | import com.example.eziketobenna.bakingapp.recipedetail.model.StepDetailMapper 8 | import com.example.eziketobenna.bakingapp.recipedetail.presentation.RecipeDetailViewAction 9 | import com.example.eziketobenna.bakingapp.recipedetail.presentation.RecipeDetailViewAction.LoadRecipeDetailAction 10 | import com.example.eziketobenna.bakingapp.recipedetail.presentation.RecipeDetailViewAction.OpenStepInfoViewAction 11 | import com.example.eziketobenna.bakingapp.recipedetail.presentation.RecipeDetailViewIntent 12 | import com.example.eziketobenna.bakingapp.recipedetail.presentation.RecipeDetailViewIntent.LoadRecipeDetailIntent 13 | import com.example.eziketobenna.bakingapp.recipedetail.presentation.RecipeDetailViewIntent.OpenStepInfoViewIntent 14 | import javax.inject.Inject 15 | 16 | @FeatureScope 17 | class RecipeDetailIntentProcessor @Inject constructor( 18 | private val stepModelMapper: StepModelMapper, 19 | private val ingredientModelMapper: IngredientModelMapper, 20 | private val stepDetailMapper: StepDetailMapper 21 | ) : IntentProcessor { 22 | 23 | override fun intentToAction(intent: RecipeDetailViewIntent): RecipeDetailViewAction { 24 | return when (intent) { 25 | is LoadRecipeDetailIntent -> loadRecipeDetailAction(intent) 26 | is OpenStepInfoViewIntent -> openStepInfoViewAction(intent) 27 | } 28 | } 29 | 30 | private fun openStepInfoViewAction(intent: OpenStepInfoViewIntent): OpenStepInfoViewAction { 31 | return OpenStepInfoViewAction( 32 | step = stepDetailMapper.mapToDomain(intent.stepDetailItem), 33 | steps = stepModelMapper.mapToDomainList(intent.steps) 34 | ) 35 | } 36 | 37 | private fun loadRecipeDetailAction(intent: LoadRecipeDetailIntent): LoadRecipeDetailAction = 38 | LoadRecipeDetailAction( 39 | ingredientModelMapper.mapToDomainList(intent.recipe.ingredients), 40 | stepModelMapper.mapToDomainList(intent.recipe.steps) 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /features/recipes/recipeDetail/src/main/java/com/example/eziketobenna/bakingapp/recipedetail/ui/adapter/HeaderViewHolder.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.recipedetail.ui.adapter 2 | 3 | import androidx.recyclerview.widget.RecyclerView 4 | import com.example.eziketobenna.bakingapp.recipedetail.databinding.ItemHeaderLayoutBinding 5 | import com.example.eziketobenna.bakingapp.recipedetail.model.HeaderItem 6 | 7 | class HeaderViewHolder(private val binding: ItemHeaderLayoutBinding) : 8 | RecyclerView.ViewHolder(binding.root) { 9 | 10 | fun bind(headerItem: HeaderItem) { 11 | binding.header.run { 12 | text = context.getString(headerItem.header) 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /features/recipes/recipeDetail/src/main/java/com/example/eziketobenna/bakingapp/recipedetail/ui/adapter/IngredientViewHolder.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.recipedetail.ui.adapter 2 | 3 | import androidx.recyclerview.widget.RecyclerView 4 | import com.example.eziketobenna.bakingapp.recipedetail.databinding.IngredientListContentBinding 5 | import com.example.eziketobenna.bakingapp.recipedetail.model.IngredientDetailItem 6 | 7 | class IngredientViewHolder(private val binding: IngredientListContentBinding) : 8 | RecyclerView.ViewHolder(binding.root) { 9 | 10 | fun bind(model: IngredientDetailItem) { 11 | binding.ingredient.text = model.ingredient 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /features/recipes/recipeDetail/src/main/java/com/example/eziketobenna/bakingapp/recipedetail/ui/adapter/RecyclerViewExt.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.recipedetail.ui.adapter 2 | 3 | import com.example.eziketobenna.bakingapp.core.ext.safeOffer 4 | import com.example.eziketobenna.bakingapp.recipedetail.model.StepDetailItem 5 | import kotlinx.coroutines.channels.awaitClose 6 | import kotlinx.coroutines.flow.Flow 7 | import kotlinx.coroutines.flow.callbackFlow 8 | import kotlinx.coroutines.flow.conflate 9 | import kotlinx.coroutines.flow.debounce 10 | 11 | val IngredientStepAdapter.stepClicks: Flow 12 | get() = callbackFlow { 13 | val listener: StepClickListener = { step: StepDetailItem -> 14 | safeOffer(step) 15 | Unit 16 | } 17 | stepClickListener = listener 18 | awaitClose { stepClickListener = null } 19 | }.conflate().debounce(200) 20 | -------------------------------------------------------------------------------- /features/recipes/recipeDetail/src/main/java/com/example/eziketobenna/bakingapp/recipedetail/ui/adapter/StepViewHolder.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.recipedetail.ui.adapter 2 | 3 | import androidx.recyclerview.widget.RecyclerView 4 | import com.example.eziketobenna.bakingapp.recipedetail.databinding.StepListContentBinding 5 | import com.example.eziketobenna.bakingapp.recipedetail.model.StepDetailItem 6 | 7 | class StepViewHolder(private val binding: StepListContentBinding) : 8 | RecyclerView.ViewHolder(binding.root) { 9 | 10 | fun bind( 11 | stepDetailItem: StepDetailItem, 12 | stepClickListener: StepClickListener? 13 | ) { 14 | binding.shortDescription.text = stepDetailItem.shortDescription 15 | binding.root.setOnClickListener { 16 | stepClickListener?.invoke(stepDetailItem) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /features/recipes/recipeDetail/src/main/res/drawable/ic_play_arrow_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /features/recipes/recipeDetail/src/main/res/layout/fragment_recipe_detail.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /features/recipes/recipeDetail/src/main/res/layout/ingredient_list_content.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | -------------------------------------------------------------------------------- /features/recipes/recipeDetail/src/main/res/layout/item_header_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | -------------------------------------------------------------------------------- /features/recipes/recipeDetail/src/main/res/layout/step_list_content.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 17 | 18 | 26 | 27 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /features/recipes/recipeDetail/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Ingredient and Steps 4 | Ingredients 5 | Steps 6 | playback icon 7 | 8 | -------------------------------------------------------------------------------- /features/recipes/stepDetail/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /features/recipes/stepDetail/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import Dependencies.AndroidX 2 | import Dependencies.Coroutines 3 | import Dependencies.DI 4 | import Dependencies.FlowBinding 5 | import Dependencies.Kotlin 6 | import Dependencies.View 7 | import ProjectLib.app 8 | import ProjectLib.core 9 | import ProjectLib.domain 10 | import ProjectLib.presentation 11 | import ProjectLib.recipeModel 12 | import ProjectLib.videoPlayer 13 | 14 | plugins { 15 | dynamicFeature 16 | kotlin(kotlinAndroid) 17 | kotlin(kotlinAndroidExtension) 18 | kotlin(kotlinKapt) 19 | daggerHilt 20 | } 21 | 22 | android { 23 | compileSdkVersion(Config.Version.compileSdkVersion) 24 | defaultConfig { 25 | minSdkVersion(Config.Version.minSdkVersion) 26 | targetSdkVersion(Config.Version.targetSdkVersion) 27 | } 28 | 29 | @Suppress("UnstableApiUsage") 30 | compileOptions { 31 | sourceCompatibility = JavaVersion.VERSION_1_8 32 | targetCompatibility = JavaVersion.VERSION_1_8 33 | } 34 | 35 | kotlinOptions { 36 | jvmTarget = JavaVersion.VERSION_1_8.toString() 37 | } 38 | 39 | buildTypes { 40 | named(BuildType.DEBUG) { 41 | isMinifyEnabled = BuildTypeDebug.isMinifyEnabled 42 | versionNameSuffix = BuildTypeDebug.versionNameSuffix 43 | } 44 | } 45 | } 46 | 47 | dependencies { 48 | implementation(project(app)) 49 | implementation(project(core)) 50 | implementation(project(presentation)) 51 | implementation(project(videoPlayer)) 52 | implementation(project(recipeModel)) 53 | implementation(project(domain)) 54 | 55 | implementation(FlowBinding.android) 56 | 57 | implementAll(View.components) 58 | implementation(DI.daggerHiltAndroid) 59 | implementation(Kotlin.stdlib) 60 | implementAll(AndroidX.components) 61 | implementAll(Coroutines.components) 62 | 63 | kapt(DI.AnnotationProcessor.daggerHiltAndroid) 64 | } 65 | -------------------------------------------------------------------------------- /features/recipes/stepDetail/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /features/recipes/stepDetail/src/main/java/com/example/eziketobenna/bakingapp/stepdetail/di/Injector.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.stepdetail.di 2 | 3 | import com.example.eziketobenna.bakingapp.di.AppComponent 4 | import com.example.eziketobenna.bakingapp.stepdetail.di.component.DaggerStepDetailComponent 5 | import com.example.eziketobenna.bakingapp.stepdetail.ui.StepDetailFragment 6 | import dagger.hilt.android.EntryPointAccessors 7 | 8 | fun inject(fragment: StepDetailFragment) { 9 | DaggerStepDetailComponent 10 | .factory() 11 | .create(appComponent(fragment)) 12 | .inject(fragment) 13 | } 14 | 15 | private fun appComponent(fragment: StepDetailFragment): AppComponent = 16 | EntryPointAccessors.fromActivity( 17 | fragment.requireActivity(), 18 | AppComponent::class.java 19 | ) 20 | -------------------------------------------------------------------------------- /features/recipes/stepDetail/src/main/java/com/example/eziketobenna/bakingapp/stepdetail/di/component/StepDetailComponent.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.stepdetail.di.component 2 | 3 | import com.example.eziketobenna.bakingapp.core.di.module.FactoriesModule 4 | import com.example.eziketobenna.bakingapp.core.di.scope.FeatureScope 5 | import com.example.eziketobenna.bakingapp.di.AppComponent 6 | import com.example.eziketobenna.bakingapp.stepdetail.di.module.PresentationModule 7 | import com.example.eziketobenna.bakingapp.stepdetail.di.module.ViewModelModule 8 | import com.example.eziketobenna.bakingapp.stepdetail.ui.StepDetailFragment 9 | import dagger.Component 10 | 11 | @FeatureScope 12 | @Component( 13 | dependencies = [AppComponent::class], 14 | modules = [FactoriesModule::class, ViewModelModule::class, PresentationModule::class] 15 | ) 16 | interface StepDetailComponent { 17 | 18 | fun inject(fragment: StepDetailFragment) 19 | 20 | @Component.Factory 21 | interface Factory { 22 | fun create(appComponent: AppComponent): StepDetailComponent 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /features/recipes/stepDetail/src/main/java/com/example/eziketobenna/bakingapp/stepdetail/di/module/PresentationModule.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.stepdetail.di.module 2 | 3 | import com.example.eziketobenna.bakingapp.core.di.scope.FeatureScope 4 | import com.example.eziketobenna.bakingapp.stepdetail.presentation.StepActionProcessor 5 | import com.example.eziketobenna.bakingapp.stepdetail.presentation.StepDetailViewStateReducer 6 | import com.example.eziketobenna.bakingapp.stepdetail.presentation.StepIntentProcessor 7 | import com.example.eziketobenna.bakingapp.stepdetail.presentation.StepViewStateReducer 8 | import com.example.eziketobenna.bakingapp.stepdetail.presentation.processor.StepDetailActionProcessor 9 | import com.example.eziketobenna.bakingapp.stepdetail.presentation.processor.StepDetailIntentProcessor 10 | import dagger.Binds 11 | import dagger.Module 12 | import dagger.hilt.migration.DisableInstallInCheck 13 | 14 | @DisableInstallInCheck 15 | @Module 16 | interface PresentationModule { 17 | 18 | @get:[Binds FeatureScope] 19 | val StepDetailActionProcessor.actionProcessor: StepActionProcessor 20 | 21 | @get:[Binds FeatureScope] 22 | val StepDetailIntentProcessor.intentProcessor: StepIntentProcessor 23 | 24 | @get:[Binds FeatureScope] 25 | val StepDetailViewStateReducer.reducer: StepViewStateReducer 26 | } 27 | -------------------------------------------------------------------------------- /features/recipes/stepDetail/src/main/java/com/example/eziketobenna/bakingapp/stepdetail/di/module/ViewModelModule.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.stepdetail.di.module 2 | 3 | import androidx.lifecycle.ViewModel 4 | import com.example.eziketobenna.bakingapp.core.di.mapkeys.ViewModelKey 5 | import com.example.eziketobenna.bakingapp.stepdetail.presentation.StepDetailViewModel 6 | import dagger.Binds 7 | import dagger.Module 8 | import dagger.hilt.migration.DisableInstallInCheck 9 | import dagger.multibindings.IntoMap 10 | 11 | @DisableInstallInCheck 12 | @Module 13 | interface ViewModelModule { 14 | 15 | @get:[Binds IntoMap ViewModelKey(StepDetailViewModel::class)] 16 | val StepDetailViewModel.stepDetailViewModel: ViewModel 17 | } 18 | -------------------------------------------------------------------------------- /features/recipes/stepDetail/src/main/java/com/example/eziketobenna/bakingapp/stepdetail/presentation/Alias.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.stepdetail.presentation 2 | 3 | import com.example.eziketobenna.bakingapp.presentation.mvi.ActionProcessor 4 | import com.example.eziketobenna.bakingapp.presentation.mvi.IntentProcessor 5 | import com.example.eziketobenna.bakingapp.presentation.mvi.ViewStateReducer 6 | 7 | typealias StepIntentProcessor = 8 | @JvmSuppressWildcards IntentProcessor 9 | 10 | typealias StepActionProcessor = 11 | @JvmSuppressWildcards ActionProcessor 12 | 13 | typealias StepViewStateReducer = 14 | @JvmSuppressWildcards ViewStateReducer 15 | -------------------------------------------------------------------------------- /features/recipes/stepDetail/src/main/java/com/example/eziketobenna/bakingapp/stepdetail/presentation/StepDetailViewAction.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.stepdetail.presentation 2 | 3 | import com.example.eziketobenna.bakingapp.domain.model.Step 4 | import com.example.eziketobenna.bakingapp.presentation.mvi.ViewAction 5 | 6 | sealed class StepDetailViewAction : ViewAction { 7 | object Idle : StepDetailViewAction() 8 | data class LoadInitialViewAction(val index: Int, val steps: List, val step: Step) : 9 | StepDetailViewAction() 10 | 11 | data class GoToNextStepViewAction(val steps: List) : StepDetailViewAction() 12 | data class GoToPreviousStepViewAction(val steps: List) : StepDetailViewAction() 13 | } 14 | -------------------------------------------------------------------------------- /features/recipes/stepDetail/src/main/java/com/example/eziketobenna/bakingapp/stepdetail/presentation/StepDetailViewIntent.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.stepdetail.presentation 2 | 3 | import com.example.eziketobenna.bakingapp.model.StepInfoModel 4 | import com.example.eziketobenna.bakingapp.model.StepModel 5 | import com.example.eziketobenna.bakingapp.presentation.mvi.ViewIntent 6 | 7 | sealed class StepDetailViewIntent : ViewIntent { 8 | data class LoadInitialViewIntent(val stepInfoModel: StepInfoModel) : StepDetailViewIntent() 9 | data class GoToNextStepViewIntent(val steps: List) : StepDetailViewIntent() 10 | data class GoToPreviousStepViewIntent(val steps: List) : StepDetailViewIntent() 11 | } 12 | -------------------------------------------------------------------------------- /features/recipes/stepDetail/src/main/java/com/example/eziketobenna/bakingapp/stepdetail/presentation/StepDetailViewResult.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.stepdetail.presentation 2 | 3 | import com.example.eziketobenna.bakingapp.domain.model.Step 4 | import com.example.eziketobenna.bakingapp.presentation.mvi.ViewResult 5 | 6 | sealed class StepDetailViewResult : ViewResult { 7 | object IdleResult : StepDetailViewResult() 8 | data class LoadedInitialResult(val stepIndex: Int, val steps: List, val step: Step) : 9 | StepDetailViewResult() 10 | data class GoToNextStepViewResult(val steps: List) : StepDetailViewResult() 11 | data class GoToPreviousStepViewResult(val steps: List) : StepDetailViewResult() 12 | } 13 | -------------------------------------------------------------------------------- /features/recipes/stepDetail/src/main/java/com/example/eziketobenna/bakingapp/stepdetail/presentation/StepDetailViewState.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.stepdetail.presentation 2 | 3 | import com.example.eziketobenna.bakingapp.presentation.event.ViewEvent 4 | import com.example.eziketobenna.bakingapp.presentation.mvi.ViewState 5 | 6 | sealed class StepDetailViewState : ViewState { 7 | object Idle : StepDetailViewState() 8 | data class Loaded( 9 | val stepDescription: String, 10 | val videoUrl: String, 11 | val stepIndex: Int, 12 | val totalSteps: Int, 13 | val currentPosition: Int, 14 | val showPrev: Boolean, 15 | val showNext: Boolean, 16 | val showVideo: Boolean 17 | ) : StepDetailViewState() { 18 | val progressText: String 19 | get() = "$currentPosition/$totalSteps" 20 | } 21 | 22 | data class FinishEvent(val closeEvent: ViewEvent) : StepDetailViewState() 23 | } 24 | -------------------------------------------------------------------------------- /features/recipes/stepDetail/src/main/java/com/example/eziketobenna/bakingapp/stepdetail/presentation/processor/StepDetailActionProcessor.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.stepdetail.presentation.processor 2 | 3 | import com.example.eziketobenna.bakingapp.core.di.scope.FeatureScope 4 | import com.example.eziketobenna.bakingapp.presentation.mvi.ActionProcessor 5 | import com.example.eziketobenna.bakingapp.stepdetail.presentation.StepDetailViewAction 6 | import com.example.eziketobenna.bakingapp.stepdetail.presentation.StepDetailViewAction.GoToNextStepViewAction 7 | import com.example.eziketobenna.bakingapp.stepdetail.presentation.StepDetailViewAction.GoToPreviousStepViewAction 8 | import com.example.eziketobenna.bakingapp.stepdetail.presentation.StepDetailViewAction.LoadInitialViewAction 9 | import com.example.eziketobenna.bakingapp.stepdetail.presentation.StepDetailViewResult 10 | import com.example.eziketobenna.bakingapp.stepdetail.presentation.StepDetailViewResult.GoToNextStepViewResult 11 | import com.example.eziketobenna.bakingapp.stepdetail.presentation.StepDetailViewResult.GoToPreviousStepViewResult 12 | import com.example.eziketobenna.bakingapp.stepdetail.presentation.StepDetailViewResult.LoadedInitialResult 13 | import javax.inject.Inject 14 | import kotlinx.coroutines.flow.Flow 15 | import kotlinx.coroutines.flow.flowOf 16 | 17 | @FeatureScope 18 | class StepDetailActionProcessor @Inject constructor() : 19 | ActionProcessor { 20 | 21 | override fun actionToResult(viewAction: StepDetailViewAction): Flow { 22 | return when (viewAction) { 23 | StepDetailViewAction.Idle -> flowOf(StepDetailViewResult.IdleResult) 24 | is LoadInitialViewAction -> loadedInitialResult(viewAction) 25 | is GoToNextStepViewAction -> flowOf(GoToNextStepViewResult(viewAction.steps)) 26 | is GoToPreviousStepViewAction -> flowOf(GoToPreviousStepViewResult(viewAction.steps)) 27 | } 28 | } 29 | 30 | private fun loadedInitialResult(viewAction: LoadInitialViewAction): Flow = 31 | flowOf( 32 | LoadedInitialResult( 33 | viewAction.index, 34 | viewAction.steps, 35 | viewAction.step 36 | ) 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /features/recipes/stepDetail/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | PREV 4 | NEXT 5 | Placeholdder 6 | Step description: 7 | FINISH 8 | 9 | -------------------------------------------------------------------------------- /features/videoPlayer/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /features/videoPlayer/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import Dependencies.AndroidX 2 | import Dependencies.Kotlin 3 | import Dependencies.View.appCompat 4 | import Dependencies.View.constraintLayout 5 | import Dependencies.View.exoPlayerCore 6 | import Dependencies.View.exoPlayerUI 7 | import ProjectLib.app 8 | 9 | plugins { 10 | dynamicFeature 11 | kotlin(kotlinAndroid) 12 | kotlin(kotlinAndroidExtension) 13 | } 14 | 15 | android { 16 | compileSdkVersion(Config.Version.compileSdkVersion) 17 | defaultConfig { 18 | minSdkVersion(Config.Version.minSdkVersion) 19 | targetSdkVersion(Config.Version.targetSdkVersion) 20 | } 21 | 22 | @Suppress("UnstableApiUsage") 23 | compileOptions { 24 | sourceCompatibility = JavaVersion.VERSION_1_8 25 | targetCompatibility = JavaVersion.VERSION_1_8 26 | } 27 | 28 | kotlinOptions { 29 | jvmTarget = JavaVersion.VERSION_1_8.toString() 30 | } 31 | 32 | buildTypes { 33 | named(BuildType.DEBUG) { 34 | isMinifyEnabled = BuildTypeDebug.isMinifyEnabled 35 | versionNameSuffix = BuildTypeDebug.versionNameSuffix 36 | } 37 | } 38 | } 39 | 40 | dependencies { 41 | implementation(project(app)) 42 | 43 | implementation(appCompat) 44 | implementation(exoPlayerCore) 45 | implementation(exoPlayerUI) 46 | implementation(constraintLayout) 47 | implementation(AndroidX.lifeCycleCommon) 48 | 49 | implementation(Kotlin.stdlib) 50 | } 51 | -------------------------------------------------------------------------------- /features/videoPlayer/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /features/videoPlayer/src/main/java/com/example/eziketobenna/bakingapp/videoplayer/VideoPlayerState.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.videoplayer 2 | 3 | import android.os.Parcelable 4 | import com.google.android.exoplayer2.C 5 | import kotlinx.android.parcel.Parcelize 6 | 7 | @Parcelize 8 | data class VideoPlayerState( 9 | var playWhenReady: Boolean = true, 10 | var currentWindow: Int = C.INDEX_UNSET, 11 | var playBackPosition: Long = 0, 12 | var videoUrl: String? = null 13 | ) : Parcelable 14 | -------------------------------------------------------------------------------- /features/videoPlayer/src/main/res/layout/video_player.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /features/videoPlayer/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 5 | -------------------------------------------------------------------------------- /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=-Xmx2g 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 | android.useAndroidX=true 15 | android.enableJetifier=true 16 | android.defaults.buildfeatures.viewbinding=true 17 | 18 | 19 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Baking-App-Kotlin/9d2684ac2fcf8efb182bbd7e16a8c16152bc9ea7/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Jun 10 21:21:24 WAT 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-rc-1-all.zip 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /libraries/data/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /libraries/data/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import Dependencies.Test 2 | import ProjectLib.domain 3 | 4 | plugins { 5 | kotlinLibrary 6 | } 7 | 8 | dependencies { 9 | implementation(project(domain)) 10 | testImplementation(Test.junit) 11 | testImplementation(Test.truth) 12 | testImplementation(Test.coroutinesTest) 13 | } 14 | -------------------------------------------------------------------------------- /libraries/data/src/main/java/com/example/eziketobenna/bakingapp/data/contract/RecipeRemote.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.data.contract 2 | 3 | import com.example.eziketobenna.bakingapp.data.model.RecipeEntity 4 | 5 | interface RecipeRemote { 6 | suspend fun fetchRecipes(): List 7 | } 8 | -------------------------------------------------------------------------------- /libraries/data/src/main/java/com/example/eziketobenna/bakingapp/data/mapper/IngredientEntityMapper.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.data.mapper 2 | 3 | import com.example.eziketobenna.bakingapp.data.mapper.base.EntityMapper 4 | import com.example.eziketobenna.bakingapp.data.model.IngredientEntity 5 | import com.example.eziketobenna.bakingapp.domain.model.Ingredient 6 | import javax.inject.Inject 7 | 8 | class IngredientEntityMapper @Inject constructor() : EntityMapper { 9 | 10 | override fun mapFromEntity(entity: IngredientEntity): Ingredient { 11 | return Ingredient( 12 | entity.quantity, 13 | entity.measure, 14 | entity.ingredient 15 | ) 16 | } 17 | 18 | override fun mapToEntity(domain: Ingredient): IngredientEntity { 19 | return IngredientEntity( 20 | domain.quantity, 21 | domain.measure, 22 | domain.ingredient 23 | ) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /libraries/data/src/main/java/com/example/eziketobenna/bakingapp/data/mapper/RecipeEntityMapper.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.data.mapper 2 | 3 | import com.example.eziketobenna.bakingapp.data.mapper.base.EntityMapper 4 | import com.example.eziketobenna.bakingapp.data.model.RecipeEntity 5 | import com.example.eziketobenna.bakingapp.domain.model.Recipe 6 | import javax.inject.Inject 7 | 8 | class RecipeEntityMapper @Inject constructor( 9 | private val ingredientMapper: IngredientEntityMapper, 10 | private val stepMapper: StepEntityMapper 11 | ) : EntityMapper { 12 | 13 | override fun mapFromEntity(entity: RecipeEntity): Recipe { 14 | return Recipe( 15 | entity.id, 16 | entity.name, 17 | entity.image, 18 | entity.servings, 19 | ingredientMapper.mapFromEntityList(entity.ingredients), 20 | stepMapper.mapFromEntityList(entity.steps) 21 | ) 22 | } 23 | 24 | override fun mapToEntity(domain: Recipe): RecipeEntity { 25 | return RecipeEntity( 26 | domain.id, 27 | domain.name, 28 | domain.image, 29 | domain.servings, 30 | ingredientMapper.mapFromDomainList(domain.ingredients), 31 | stepMapper.mapFromDomainList(domain.steps) 32 | ) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /libraries/data/src/main/java/com/example/eziketobenna/bakingapp/data/mapper/StepEntityMapper.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.data.mapper 2 | 3 | import com.example.eziketobenna.bakingapp.data.mapper.base.EntityMapper 4 | import com.example.eziketobenna.bakingapp.data.model.StepEntity 5 | import com.example.eziketobenna.bakingapp.domain.model.Step 6 | import javax.inject.Inject 7 | 8 | class StepEntityMapper @Inject constructor() : EntityMapper { 9 | 10 | override fun mapFromEntity(entity: StepEntity): Step { 11 | return Step( 12 | entity.id, 13 | entity.videoURL, 14 | entity.description, 15 | entity.shortDescription, 16 | entity.thumbnailURL 17 | ) 18 | } 19 | 20 | override fun mapToEntity(domain: Step): StepEntity { 21 | return StepEntity( 22 | domain.id, 23 | domain.videoURL, 24 | domain.description, 25 | domain.shortDescription, 26 | domain.thumbnailURL 27 | ) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /libraries/data/src/main/java/com/example/eziketobenna/bakingapp/data/mapper/base/EntityMapper.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.data.mapper.base 2 | 3 | interface EntityMapper { 4 | 5 | fun mapFromEntity(entity: E): D 6 | 7 | fun mapToEntity(domain: D): E 8 | 9 | fun mapFromEntityList(entities: List): List { 10 | return entities.mapTo(mutableListOf(), ::mapFromEntity) 11 | } 12 | 13 | fun mapFromDomainList(domainModels: List): List { 14 | return domainModels.mapTo(mutableListOf(), ::mapToEntity) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /libraries/data/src/main/java/com/example/eziketobenna/bakingapp/data/model/IngredientEntity.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.data.model 2 | 3 | data class IngredientEntity( 4 | val quantity: Double, 5 | val measure: String, 6 | val ingredient: String 7 | ) 8 | -------------------------------------------------------------------------------- /libraries/data/src/main/java/com/example/eziketobenna/bakingapp/data/model/RecipeEntity.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.data.model 2 | 3 | data class RecipeEntity( 4 | val id: Int, 5 | val name: String, 6 | val image: String, 7 | val servings: Int, 8 | val ingredients: List, 9 | val steps: List 10 | ) 11 | -------------------------------------------------------------------------------- /libraries/data/src/main/java/com/example/eziketobenna/bakingapp/data/model/StepEntity.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.data.model 2 | 3 | data class StepEntity( 4 | val id: Int, 5 | val videoURL: String, 6 | val description: String, 7 | val shortDescription: String, 8 | val thumbnailURL: String 9 | ) 10 | -------------------------------------------------------------------------------- /libraries/data/src/main/java/com/example/eziketobenna/bakingapp/data/repository/RecipeRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.data.repository 2 | 3 | import com.example.eziketobenna.bakingapp.data.contract.RecipeRemote 4 | import com.example.eziketobenna.bakingapp.data.mapper.RecipeEntityMapper 5 | import com.example.eziketobenna.bakingapp.data.model.RecipeEntity 6 | import com.example.eziketobenna.bakingapp.domain.model.Recipe 7 | import com.example.eziketobenna.bakingapp.domain.repository.RecipeRepository 8 | import javax.inject.Inject 9 | import kotlinx.coroutines.flow.Flow 10 | import kotlinx.coroutines.flow.flow 11 | 12 | class RecipeRepositoryImpl @Inject constructor( 13 | private val recipeRemote: RecipeRemote, 14 | private val recipeMapper: RecipeEntityMapper 15 | ) : RecipeRepository { 16 | 17 | override fun fetchRecipes(): Flow> { 18 | return flow { 19 | val recipes: List = recipeRemote.fetchRecipes() 20 | emit(recipeMapper.mapFromEntityList(recipes)) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /libraries/data/src/test/java/com/example/eziketobenna/bakingapp/data/fake/DummyData.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.data.fake 2 | 3 | import com.example.eziketobenna.bakingapp.data.model.IngredientEntity 4 | import com.example.eziketobenna.bakingapp.data.model.RecipeEntity 5 | import com.example.eziketobenna.bakingapp.data.model.StepEntity 6 | import com.example.eziketobenna.bakingapp.domain.model.Ingredient 7 | import com.example.eziketobenna.bakingapp.domain.model.Recipe 8 | import com.example.eziketobenna.bakingapp.domain.model.Step 9 | 10 | internal object DummyData { 11 | val recipeEntity = RecipeEntity( 12 | id = 1, 13 | name = "Eba", 14 | image = "imgurl.com", 15 | servings = 3, 16 | ingredients = listOf(ingredientEntity), 17 | steps = listOf(stepEntity) 18 | ) 19 | 20 | val ingredientEntity: IngredientEntity 21 | get() = IngredientEntity( 22 | quantity = 4.4, 23 | measure = "2", 24 | ingredient = "pepper" 25 | ) 26 | val stepEntity: StepEntity 27 | get() = StepEntity( 28 | id = 1, 29 | description = "pour stuff", 30 | shortDescription = "pour", 31 | videoURL = "url.com", 32 | thumbnailURL = "thumb.com" 33 | ) 34 | 35 | val recipe = Recipe( 36 | id = 1, 37 | name = "Eba", 38 | image = "imgurl.com", 39 | servings = 3, 40 | ingredients = listOf(ingredient), 41 | steps = listOf(step) 42 | ) 43 | 44 | val ingredient: Ingredient 45 | get() = Ingredient( 46 | quantity = 4.4, 47 | measure = "2", 48 | ingredient = "pepper" 49 | ) 50 | val step: Step 51 | get() = Step( 52 | id = 1, 53 | description = "pour stuff", 54 | shortDescription = "pour", 55 | videoURL = "url.com", 56 | thumbnailURL = "thumb.com" 57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /libraries/data/src/test/java/com/example/eziketobenna/bakingapp/data/fake/FakeRecipeRemoteImpl.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.data.fake 2 | 3 | import com.example.eziketobenna.bakingapp.data.contract.RecipeRemote 4 | import com.example.eziketobenna.bakingapp.data.model.RecipeEntity 5 | 6 | internal class FakeRecipeRemoteImpl : RecipeRemote { 7 | 8 | override suspend fun fetchRecipes(): List { 9 | return listOf(DummyData.recipeEntity) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /libraries/data/src/test/java/com/example/eziketobenna/bakingapp/data/mapper/IngredientEntityMapperTest.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.data.mapper 2 | 3 | import com.example.eziketobenna.bakingapp.data.fake.DummyData 4 | import com.example.eziketobenna.bakingapp.data.model.IngredientEntity 5 | import com.example.eziketobenna.bakingapp.domain.model.Ingredient 6 | import com.google.common.truth.Truth.assertThat 7 | import org.junit.Test 8 | 9 | class IngredientEntityMapperTest { 10 | 11 | private val ingredientEntityMapper = IngredientEntityMapper() 12 | 13 | @Test 14 | fun `check that mapFromEntity maps data correctly`() { 15 | val ingredientEntity: IngredientEntity = DummyData.ingredientEntity 16 | val ingredientDomain: Ingredient = ingredientEntityMapper.mapFromEntity(ingredientEntity) 17 | assertThat(ingredientEntity.ingredient).isEqualTo(ingredientDomain.ingredient) 18 | assertThat(ingredientEntity.quantity).isEqualTo(ingredientDomain.quantity) 19 | assertThat(ingredientEntity.measure).isEqualTo(ingredientDomain.measure) 20 | } 21 | 22 | @Test 23 | fun `check that mapToEntity maps data correctly`() { 24 | val ingredientDomain: Ingredient = DummyData.ingredient 25 | val ingredientEntity: IngredientEntity = ingredientEntityMapper.mapToEntity(ingredientDomain) 26 | assertThat(ingredientDomain.ingredient).isEqualTo(ingredientEntity.ingredient) 27 | assertThat(ingredientDomain.quantity).isEqualTo(ingredientEntity.quantity) 28 | assertThat(ingredientDomain.measure).isEqualTo(ingredientEntity.measure) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /libraries/data/src/test/java/com/example/eziketobenna/bakingapp/data/mapper/StepEntityMapperTest.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.data.mapper 2 | 3 | import com.example.eziketobenna.bakingapp.data.fake.DummyData 4 | import com.example.eziketobenna.bakingapp.data.model.StepEntity 5 | import com.example.eziketobenna.bakingapp.domain.model.Step 6 | import com.google.common.truth.Truth.assertThat 7 | import org.junit.Test 8 | 9 | class StepEntityMapperTest { 10 | 11 | private val stepEntityMapper = StepEntityMapper() 12 | 13 | @Test 14 | fun `check that mapFromEntity maps data correctly`() { 15 | val stepEntity: StepEntity = DummyData.stepEntity 16 | val stepDomain: Step = stepEntityMapper.mapFromEntity(stepEntity) 17 | assertThat(stepEntity.id).isEqualTo(stepDomain.id) 18 | assertThat(stepEntity.shortDescription).isEqualTo(stepDomain.shortDescription) 19 | assertThat(stepEntity.description).isEqualTo(stepDomain.description) 20 | assertThat(stepEntity.videoURL).isEqualTo(stepDomain.videoURL) 21 | assertThat(stepEntity.thumbnailURL).isEqualTo(stepDomain.thumbnailURL) 22 | } 23 | 24 | @Test 25 | fun `check that mapToEntity maps data correctly`() { 26 | val stepDomain: Step = DummyData.step 27 | val stepEntity: StepEntity = stepEntityMapper.mapToEntity(stepDomain) 28 | assertThat(stepDomain.id).isEqualTo(stepEntity.id) 29 | assertThat(stepDomain.shortDescription).isEqualTo(stepEntity.shortDescription) 30 | assertThat(stepDomain.description).isEqualTo(stepEntity.description) 31 | assertThat(stepDomain.videoURL).isEqualTo(stepEntity.videoURL) 32 | assertThat(stepDomain.thumbnailURL).isEqualTo(stepEntity.thumbnailURL) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /libraries/data/src/test/java/com/example/eziketobenna/bakingapp/data/repository/RecipeRepositoryImplTest.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.data.repository 2 | 3 | import com.example.eziketobenna.bakingapp.data.fake.DummyData 4 | import com.example.eziketobenna.bakingapp.data.fake.FakeRecipeRemoteImpl 5 | import com.example.eziketobenna.bakingapp.data.mapper.IngredientEntityMapper 6 | import com.example.eziketobenna.bakingapp.data.mapper.RecipeEntityMapper 7 | import com.example.eziketobenna.bakingapp.data.mapper.StepEntityMapper 8 | import com.example.eziketobenna.bakingapp.domain.model.Recipe 9 | import com.google.common.truth.Truth.assertThat 10 | import kotlinx.coroutines.flow.first 11 | import kotlinx.coroutines.test.runBlockingTest 12 | import org.junit.Test 13 | 14 | class RecipeRepositoryImplTest { 15 | 16 | private val recipeEntityMapper = 17 | RecipeEntityMapper(IngredientEntityMapper(), StepEntityMapper()) 18 | private val recipeRepository = 19 | RecipeRepositoryImpl(FakeRecipeRemoteImpl(), recipeEntityMapper) 20 | 21 | @Test 22 | fun `check that fetchRecipes returns data`() = runBlockingTest { 23 | val recipes: List = recipeRepository.fetchRecipes().first() 24 | assertThat(recipes).isNotEmpty() 25 | } 26 | 27 | @Test 28 | fun `check that FetchRecipes returns correct data`() = runBlockingTest { 29 | testData { recipe -> 30 | assertThat(recipe.name).isEqualTo(DummyData.recipe.name) 31 | assertThat(recipe.id).isEqualTo(DummyData.recipe.id) 32 | assertThat(recipe.image).isEqualTo(DummyData.recipe.image) 33 | assertThat(recipe.servings).isEqualTo(DummyData.recipe.servings) 34 | } 35 | } 36 | 37 | @Test 38 | fun `check that FetchRecipes returns correct ingredient list`() = runBlockingTest { 39 | testData { recipe -> 40 | assertThat(recipe.ingredients.size).isAtLeast(1) 41 | } 42 | } 43 | 44 | @Test 45 | fun `check that FetchRecipes returns correct steps list`() = runBlockingTest { 46 | testData { recipe -> 47 | assertThat(recipe.steps.size).isAtLeast(1) 48 | } 49 | } 50 | 51 | private suspend fun testData(recipe: (Recipe) -> Unit) { 52 | val recipes: List = recipeRepository.fetchRecipes().first() 53 | recipe(recipes.first()) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /libraries/domain/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /libraries/domain/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import Dependencies.Test 2 | 3 | plugins { 4 | kotlinLibrary 5 | } 6 | 7 | dependencies { 8 | testImplementation(Test.junit) 9 | testImplementation(Test.truth) 10 | testImplementation(Test.coroutinesTest) 11 | } 12 | -------------------------------------------------------------------------------- /libraries/domain/src/main/java/com/example/eziketobenna/bakingapp/domain/exception/NoParamsException.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.domain.exception 2 | 3 | import kotlin.contracts.ExperimentalContracts 4 | import kotlin.contracts.contract 5 | 6 | internal class NoParamsException(errorMessage: String = "Your params cannot be null for this use case") : 7 | IllegalArgumentException(errorMessage) 8 | 9 | @OptIn(ExperimentalContracts::class) 10 | fun requireParams(value: T?): T { 11 | contract { 12 | returns() implies (value != null) 13 | } 14 | 15 | if (value == null) { 16 | throw NoParamsException() 17 | } else { 18 | return value 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /libraries/domain/src/main/java/com/example/eziketobenna/bakingapp/domain/executor/PostExecutionThread.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.domain.executor 2 | 3 | import kotlinx.coroutines.CoroutineDispatcher 4 | 5 | interface PostExecutionThread { 6 | val main: CoroutineDispatcher 7 | val io: CoroutineDispatcher 8 | val default: CoroutineDispatcher 9 | } 10 | -------------------------------------------------------------------------------- /libraries/domain/src/main/java/com/example/eziketobenna/bakingapp/domain/model/Ingredient.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.domain.model 2 | 3 | data class Ingredient( 4 | val quantity: Double, 5 | val measure: String, 6 | val ingredient: String 7 | ) 8 | -------------------------------------------------------------------------------- /libraries/domain/src/main/java/com/example/eziketobenna/bakingapp/domain/model/Recipe.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.domain.model 2 | 3 | data class Recipe( 4 | val id: Int, 5 | val name: String, 6 | val image: String, 7 | val servings: Int, 8 | val ingredients: List, 9 | val steps: List 10 | ) 11 | -------------------------------------------------------------------------------- /libraries/domain/src/main/java/com/example/eziketobenna/bakingapp/domain/model/Step.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.domain.model 2 | 3 | data class Step( 4 | val id: Int, 5 | val videoURL: String, 6 | val description: String, 7 | val shortDescription: String, 8 | val thumbnailURL: String 9 | ) 10 | -------------------------------------------------------------------------------- /libraries/domain/src/main/java/com/example/eziketobenna/bakingapp/domain/repository/RecipeRepository.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.domain.repository 2 | 3 | import com.example.eziketobenna.bakingapp.domain.model.Recipe 4 | import kotlinx.coroutines.flow.Flow 5 | 6 | interface RecipeRepository { 7 | fun fetchRecipes(): Flow> 8 | } 9 | -------------------------------------------------------------------------------- /libraries/domain/src/main/java/com/example/eziketobenna/bakingapp/domain/usecase/FetchRecipes.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.domain.usecase 2 | 3 | import com.example.eziketobenna.bakingapp.domain.executor.PostExecutionThread 4 | import com.example.eziketobenna.bakingapp.domain.model.Recipe 5 | import com.example.eziketobenna.bakingapp.domain.repository.RecipeRepository 6 | import com.example.eziketobenna.bakingapp.domain.usecase.base.FlowUseCase 7 | import javax.inject.Inject 8 | import kotlinx.coroutines.flow.Flow 9 | 10 | class FetchRecipes @Inject constructor( 11 | private val recipeRepository: RecipeRepository, 12 | postExecutionThread: PostExecutionThread 13 | ) : FlowUseCase>(postExecutionThread) { 14 | 15 | override fun execute(params: Unit?): Flow> { 16 | return recipeRepository.fetchRecipes() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /libraries/domain/src/main/java/com/example/eziketobenna/bakingapp/domain/usecase/base/FlowUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.domain.usecase.base 2 | 3 | import com.example.eziketobenna.bakingapp.domain.executor.PostExecutionThread 4 | import kotlinx.coroutines.flow.Flow 5 | import kotlinx.coroutines.flow.flowOn 6 | 7 | abstract class FlowUseCase( 8 | private val postExecutionThread: PostExecutionThread 9 | ) { 10 | 11 | /** 12 | * Function which builds Flow instance based on given arguments 13 | * @param params initial use case arguments 14 | */ 15 | abstract fun execute(params: Params? = null): Flow 16 | 17 | operator fun invoke(params: Params? = null): Flow = execute(params) 18 | .flowOn(postExecutionThread.io) 19 | } 20 | -------------------------------------------------------------------------------- /libraries/domain/src/test/java/com/example/eziketobenna/bakingapp/domain/data/DummyData.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.domain.data 2 | 3 | import com.example.eziketobenna.bakingapp.domain.model.Ingredient 4 | import com.example.eziketobenna.bakingapp.domain.model.Recipe 5 | import com.example.eziketobenna.bakingapp.domain.model.Step 6 | 7 | internal object DummyData { 8 | val recipe = Recipe( 9 | id = 3, 10 | name = "Burritos", 11 | image = "imgurl.com", 12 | servings = 1, 13 | ingredients = listOf(ingredient), 14 | steps = listOf(step) 15 | ) 16 | 17 | val ingredient: Ingredient 18 | get() = Ingredient( 19 | quantity = 1.4, 20 | measure = "3", 21 | ingredient = "salt" 22 | ) 23 | val step: Step 24 | get() = Step( 25 | id = 1, 26 | description = "pour stuff", 27 | shortDescription = "pour", 28 | videoURL = "url.com", 29 | thumbnailURL = "thumb.com" 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /libraries/domain/src/test/java/com/example/eziketobenna/bakingapp/domain/executor/TestPostExecutionThread.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.domain.executor 2 | 3 | import kotlinx.coroutines.CoroutineDispatcher 4 | import kotlinx.coroutines.test.TestCoroutineDispatcher 5 | 6 | internal class TestPostExecutionThread : PostExecutionThread { 7 | 8 | override val main: CoroutineDispatcher 9 | get() = TestCoroutineDispatcher() 10 | 11 | override val io: CoroutineDispatcher 12 | get() = TestCoroutineDispatcher() 13 | 14 | override val default: CoroutineDispatcher 15 | get() = TestCoroutineDispatcher() 16 | } 17 | -------------------------------------------------------------------------------- /libraries/domain/src/test/java/com/example/eziketobenna/bakingapp/domain/fake/FakeRecipeRepository.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.domain.fake 2 | 3 | import com.example.eziketobenna.bakingapp.domain.data.DummyData 4 | import com.example.eziketobenna.bakingapp.domain.model.Recipe 5 | import com.example.eziketobenna.bakingapp.domain.repository.RecipeRepository 6 | import kotlinx.coroutines.flow.Flow 7 | import kotlinx.coroutines.flow.flowOf 8 | 9 | internal class FakeRecipeRepository : RecipeRepository { 10 | 11 | override fun fetchRecipes(): Flow> { 12 | return flowOf(listOf(DummyData.recipe)) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /libraries/domain/src/test/java/com/example/eziketobenna/bakingapp/domain/fake/FakeUseCases.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.domain.fake 2 | 3 | import com.example.eziketobenna.bakingapp.domain.exception.requireParams 4 | import com.example.eziketobenna.bakingapp.domain.executor.PostExecutionThread 5 | import com.example.eziketobenna.bakingapp.domain.usecase.base.FlowUseCase 6 | import java.net.SocketTimeoutException 7 | import kotlinx.coroutines.flow.Flow 8 | import kotlinx.coroutines.flow.flowOf 9 | 10 | class ExceptionUseCase(postExecutionThread: PostExecutionThread) : 11 | FlowUseCase(postExecutionThread) { 12 | 13 | override fun execute(params: Unit?): Flow { 14 | throw SocketTimeoutException("No network") 15 | } 16 | } 17 | 18 | class ParamUseCase(postExecutionThread: PostExecutionThread) : 19 | FlowUseCase(postExecutionThread) { 20 | 21 | override fun execute(params: String?): Flow { 22 | requireParams(params) 23 | return flowOf(params) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /libraries/domain/src/test/java/com/example/eziketobenna/bakingapp/domain/usecase/FetchRecipesTest.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.domain.usecase 2 | 3 | import com.example.eziketobenna.bakingapp.domain.data.DummyData 4 | import com.example.eziketobenna.bakingapp.domain.executor.TestPostExecutionThread 5 | import com.example.eziketobenna.bakingapp.domain.fake.FakeRecipeRepository 6 | import com.example.eziketobenna.bakingapp.domain.model.Recipe 7 | import com.google.common.truth.Truth.assertThat 8 | import kotlinx.coroutines.flow.first 9 | import kotlinx.coroutines.test.runBlockingTest 10 | import org.junit.Test 11 | 12 | class FetchRecipesTest { 13 | 14 | private val fetchRecipesUseCase = 15 | FetchRecipes(FakeRecipeRepository(), TestPostExecutionThread()) 16 | 17 | @Test 18 | fun `check that calling fetchRecipes returns recipe list`() = runBlockingTest { 19 | val recipes: List = fetchRecipesUseCase().first() 20 | assertThat(recipes.size).isAtLeast(1) 21 | } 22 | 23 | @Test 24 | fun `check that calling fetchRecipes returns correct data`() = runBlockingTest { 25 | val recipes: List = fetchRecipesUseCase().first() 26 | val recipe: Recipe = recipes.first() 27 | assertThat(recipe.id).isEqualTo(DummyData.recipe.id) 28 | assertThat(recipe.image).isEqualTo(DummyData.recipe.image) 29 | assertThat(recipe.servings).isEqualTo(DummyData.recipe.servings) 30 | assertThat(recipe.name).isEqualTo(DummyData.recipe.name) 31 | assertThat(recipe.ingredients).isEqualTo(listOf(DummyData.ingredient)) 32 | assertThat(recipe.steps).isEqualTo(listOf(DummyData.step)) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /libraries/domain/src/test/java/com/example/eziketobenna/bakingapp/domain/usecase/base/FlowUseCaseTest.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.domain.usecase.base 2 | 3 | import com.example.eziketobenna.bakingapp.domain.exception.NoParamsException 4 | import com.example.eziketobenna.bakingapp.domain.executor.TestPostExecutionThread 5 | import com.example.eziketobenna.bakingapp.domain.fake.ExceptionUseCase 6 | import com.example.eziketobenna.bakingapp.domain.fake.ParamUseCase 7 | import com.google.common.truth.Truth.assertThat 8 | import java.net.SocketTimeoutException 9 | import kotlinx.coroutines.flow.first 10 | import kotlinx.coroutines.test.runBlockingTest 11 | import org.junit.Test 12 | 13 | class FlowUseCaseTest { 14 | 15 | @Test(expected = SocketTimeoutException::class) 16 | fun `check that ExceptionUseCase throws exception`() = runBlockingTest { 17 | ExceptionUseCase(TestPostExecutionThread())() 18 | } 19 | 20 | @Test(expected = NoParamsException::class) 21 | fun `check that calling ParamUseCase without params throws exception`() = runBlockingTest { 22 | ParamUseCase(TestPostExecutionThread())() 23 | } 24 | 25 | @Test 26 | fun `check that ParamsUseCase returns correct data`() = runBlockingTest { 27 | val param = "Correct data" 28 | val useCase = ParamUseCase(TestPostExecutionThread()) 29 | val result: String = useCase(param).first() 30 | assertThat(result).isEqualTo(param) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /libraries/remote/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /libraries/remote/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import Dependencies.Network 2 | import Dependencies.Test 3 | import ProjectLib.data 4 | 5 | plugins { 6 | kotlinLibrary 7 | kotlin(kotlinKapt) 8 | } 9 | 10 | dependencies { 11 | implementation(project(data)) 12 | implementAll(Network.components) 13 | 14 | testImplementation(Test.junit) 15 | testImplementation(Test.truth) 16 | testImplementation(Test.mockWebServer) 17 | 18 | kapt(Network.AnnotationProcessor.moshi) 19 | } 20 | -------------------------------------------------------------------------------- /libraries/remote/src/main/java/com/example/eziketobenna/bakingapp/remote/ApiService.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.remote 2 | 3 | import com.example.eziketobenna.bakingapp.remote.model.RecipeRemoteModel 4 | import retrofit2.http.GET 5 | 6 | interface ApiService { 7 | @GET("baking.json") 8 | suspend fun fetchRecipes(): List 9 | } 10 | -------------------------------------------------------------------------------- /libraries/remote/src/main/java/com/example/eziketobenna/bakingapp/remote/ApiServiceFactory.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.remote 2 | 3 | import com.squareup.moshi.Moshi 4 | import java.util.concurrent.TimeUnit 5 | import okhttp3.OkHttpClient 6 | import okhttp3.logging.HttpLoggingInterceptor 7 | import retrofit2.Retrofit 8 | import retrofit2.converter.moshi.MoshiConverterFactory 9 | 10 | object ApiServiceFactory { 11 | 12 | private const val BASE_URL: String = "https://d17h27t6h515a5.cloudfront.net/topher/2017/May/59121517_baking/" 13 | 14 | fun makeAPiService(isDebug: Boolean, moshi: Moshi): ApiService { 15 | val okHttpClient: OkHttpClient = makeOkHttpClient( 16 | makeLoggingInterceptor((isDebug)) 17 | ) 18 | return makeAPiService(okHttpClient, moshi) 19 | } 20 | 21 | private fun makeAPiService(okHttpClient: OkHttpClient, moshi: Moshi): ApiService { 22 | val retrofit: Retrofit = Retrofit.Builder() 23 | .baseUrl(BASE_URL) 24 | .client(okHttpClient) 25 | .addConverterFactory(MoshiConverterFactory.create(moshi)) 26 | .build() 27 | return retrofit.create(ApiService::class.java) 28 | } 29 | 30 | private fun makeOkHttpClient(httpLoggingInterceptor: HttpLoggingInterceptor): OkHttpClient { 31 | return OkHttpClient.Builder() 32 | .addInterceptor(httpLoggingInterceptor) 33 | .connectTimeout(30, TimeUnit.SECONDS) 34 | .readTimeout(30, TimeUnit.SECONDS) 35 | .build() 36 | } 37 | 38 | private fun makeLoggingInterceptor(isDebug: Boolean): HttpLoggingInterceptor { 39 | val logging = HttpLoggingInterceptor() 40 | logging.level = if (isDebug) { 41 | HttpLoggingInterceptor.Level.BODY 42 | } else { 43 | HttpLoggingInterceptor.Level.NONE 44 | } 45 | return logging 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /libraries/remote/src/main/java/com/example/eziketobenna/bakingapp/remote/impl/RecipeRemoteImpl.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.remote.impl 2 | 3 | import com.example.eziketobenna.bakingapp.data.contract.RecipeRemote 4 | import com.example.eziketobenna.bakingapp.data.model.RecipeEntity 5 | import com.example.eziketobenna.bakingapp.remote.ApiService 6 | import com.example.eziketobenna.bakingapp.remote.mapper.RecipeRemoteMapper 7 | import com.example.eziketobenna.bakingapp.remote.model.RecipeRemoteModel 8 | import javax.inject.Inject 9 | 10 | class RecipeRemoteImpl @Inject constructor( 11 | private val apiService: ApiService, 12 | private val recipeRemoteMapper: RecipeRemoteMapper 13 | ) : RecipeRemote { 14 | 15 | override suspend fun fetchRecipes(): List { 16 | val recipes: List = apiService.fetchRecipes() 17 | return recipeRemoteMapper.mapModelList(recipes) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /libraries/remote/src/main/java/com/example/eziketobenna/bakingapp/remote/mapper/IngredientRemoteMapper.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.remote.mapper 2 | 3 | import com.example.eziketobenna.bakingapp.data.model.IngredientEntity 4 | import com.example.eziketobenna.bakingapp.remote.mapper.base.RemoteModelMapper 5 | import com.example.eziketobenna.bakingapp.remote.model.IngredientRemoteModel 6 | import javax.inject.Inject 7 | 8 | class IngredientRemoteMapper @Inject constructor() : 9 | RemoteModelMapper { 10 | 11 | override fun mapFromModel(model: IngredientRemoteModel): IngredientEntity { 12 | return IngredientEntity( 13 | model.quantity, 14 | model.measure, 15 | model.ingredient 16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /libraries/remote/src/main/java/com/example/eziketobenna/bakingapp/remote/mapper/RecipeRemoteMapper.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.remote.mapper 2 | 3 | import com.example.eziketobenna.bakingapp.data.model.RecipeEntity 4 | import com.example.eziketobenna.bakingapp.remote.mapper.base.RemoteModelMapper 5 | import com.example.eziketobenna.bakingapp.remote.model.RecipeRemoteModel 6 | import javax.inject.Inject 7 | 8 | class RecipeRemoteMapper @Inject constructor( 9 | private val ingredientRemoteMapper: IngredientRemoteMapper, 10 | private val stepRemoteMapper: StepRemoteMapper 11 | ) : RemoteModelMapper { 12 | 13 | override fun mapFromModel(model: RecipeRemoteModel): RecipeEntity { 14 | return RecipeEntity( 15 | model.id, 16 | model.name, 17 | model.image, 18 | model.servings, 19 | ingredientRemoteMapper.mapModelList(model.ingredients), 20 | stepRemoteMapper.mapModelList(model.steps) 21 | ) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /libraries/remote/src/main/java/com/example/eziketobenna/bakingapp/remote/mapper/StepRemoteMapper.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.remote.mapper 2 | 3 | import com.example.eziketobenna.bakingapp.data.model.StepEntity 4 | import com.example.eziketobenna.bakingapp.remote.mapper.base.RemoteModelMapper 5 | import com.example.eziketobenna.bakingapp.remote.model.StepRemoteModel 6 | import javax.inject.Inject 7 | 8 | class StepRemoteMapper @Inject constructor() : RemoteModelMapper { 9 | 10 | override fun mapFromModel(model: StepRemoteModel): StepEntity { 11 | return StepEntity( 12 | model.id, 13 | model.videoURL, 14 | model.description, 15 | model.shortDescription, 16 | model.thumbnailURL 17 | ) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /libraries/remote/src/main/java/com/example/eziketobenna/bakingapp/remote/mapper/base/RemoteModelMapper.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.remote.mapper.base 2 | 3 | interface RemoteModelMapper { 4 | 5 | fun mapFromModel(model: M): E 6 | 7 | fun mapModelList(models: List): List { 8 | return models.mapTo(mutableListOf(), ::mapFromModel) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /libraries/remote/src/main/java/com/example/eziketobenna/bakingapp/remote/model/IngredientRemoteModel.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.remote.model 2 | 3 | data class IngredientRemoteModel( 4 | val quantity: Double, 5 | val measure: String, 6 | val ingredient: String 7 | ) 8 | -------------------------------------------------------------------------------- /libraries/remote/src/main/java/com/example/eziketobenna/bakingapp/remote/model/RecipeRemoteModel.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.remote.model 2 | 3 | data class RecipeRemoteModel( 4 | val id: Int, 5 | val name: String, 6 | val image: String, 7 | val servings: Int, 8 | val ingredients: List, 9 | val steps: List 10 | ) 11 | -------------------------------------------------------------------------------- /libraries/remote/src/main/java/com/example/eziketobenna/bakingapp/remote/model/StepRemoteModel.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.remote.model 2 | 3 | data class StepRemoteModel( 4 | val id: Int, 5 | val videoURL: String, 6 | val description: String, 7 | val shortDescription: String, 8 | val thumbnailURL: String 9 | ) 10 | -------------------------------------------------------------------------------- /libraries/remote/src/test/java/com/example/eziketobenna/bakingapp/remote/impl/RecipeRemoteTest.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.remote.impl 2 | 3 | import com.example.eziketobenna.bakingapp.data.contract.RecipeRemote 4 | import com.example.eziketobenna.bakingapp.data.model.RecipeEntity 5 | import com.example.eziketobenna.bakingapp.remote.mapper.IngredientRemoteMapper 6 | import com.example.eziketobenna.bakingapp.remote.mapper.RecipeRemoteMapper 7 | import com.example.eziketobenna.bakingapp.remote.mapper.StepRemoteMapper 8 | import com.example.eziketobenna.bakingapp.remote.utils.REQUEST_PATH 9 | import com.example.eziketobenna.bakingapp.remote.utils.RecipeRequestDispatcher 10 | import com.example.eziketobenna.bakingapp.remote.utils.makeTestApiService 11 | import com.google.common.truth.Truth.assertThat 12 | import kotlinx.coroutines.runBlocking 13 | import okhttp3.mockwebserver.MockWebServer 14 | import org.junit.After 15 | import org.junit.Before 16 | import org.junit.Test 17 | 18 | class RecipeRemoteTest { 19 | 20 | private lateinit var mockWebServer: MockWebServer 21 | private lateinit var recipeRemote: RecipeRemote 22 | private val recipeMapper = RecipeRemoteMapper(IngredientRemoteMapper(), StepRemoteMapper()) 23 | 24 | @Before 25 | fun setup() { 26 | mockWebServer = MockWebServer() 27 | mockWebServer.dispatcher = RecipeRequestDispatcher() 28 | mockWebServer.start() 29 | recipeRemote = RecipeRemoteImpl(makeTestApiService(mockWebServer), recipeMapper) 30 | } 31 | 32 | @Test 33 | fun `check that calling fetchRecipes returns recipe list`() = runBlocking { 34 | val recipes: List = recipeRemote.fetchRecipes() 35 | assertThat(recipes).isNotEmpty() 36 | } 37 | 38 | @Test 39 | fun `check that calling fetchRecipes returns correct data`() = runBlocking { 40 | val recipes: List = recipeRemote.fetchRecipes() 41 | assertThat(recipes.first().name).isEqualTo("Nutella Pie") 42 | } 43 | 44 | @Test 45 | fun `check that calling fetchRecipes makes request to given path`() = runBlocking { 46 | recipeRemote.fetchRecipes() 47 | assertThat(REQUEST_PATH).isEqualTo(mockWebServer.takeRequest().path) 48 | } 49 | 50 | @Test 51 | fun `check that calling fetchRecipes makes a GET request`() = runBlocking { 52 | recipeRemote.fetchRecipes() 53 | assertThat("GET $REQUEST_PATH HTTP/1.1").isEqualTo(mockWebServer.takeRequest().requestLine) 54 | } 55 | 56 | @After 57 | fun tearDown() { 58 | mockWebServer.shutdown() 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /libraries/remote/src/test/java/com/example/eziketobenna/bakingapp/remote/mapper/IngredientRemoteMapperTest.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.remote.mapper 2 | 3 | import com.example.eziketobenna.bakingapp.data.model.IngredientEntity 4 | import com.example.eziketobenna.bakingapp.remote.model.IngredientRemoteModel 5 | import com.example.eziketobenna.bakingapp.remote.utils.DummyData 6 | import com.google.common.truth.Truth.assertThat 7 | import org.junit.Test 8 | 9 | class IngredientRemoteMapperTest { 10 | 11 | private val ingredientRemoteMapper = IngredientRemoteMapper() 12 | 13 | @Test 14 | fun `check that quantity is mapped correctly`() = testData { entity, model -> 15 | assertThat(entity.quantity).isEqualTo(model.quantity) 16 | } 17 | 18 | @Test 19 | fun `check that measure is mapped correctly`() = testData { entity, model -> 20 | assertThat(entity.measure).isEqualTo(model.measure) 21 | } 22 | 23 | @Test 24 | fun `check that ingredient is mapped correctly`() = testData { entity, model -> 25 | assertThat(entity.ingredient).isEqualTo(model.ingredient) 26 | } 27 | 28 | private fun testData(action: (IngredientEntity, IngredientRemoteModel) -> Unit) { 29 | val model: IngredientRemoteModel = DummyData.ingredientRemoteModel 30 | val entity: IngredientEntity = ingredientRemoteMapper.mapFromModel(model) 31 | action(entity, model) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /libraries/remote/src/test/java/com/example/eziketobenna/bakingapp/remote/mapper/StepRemoteMapperTest.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.remote.mapper 2 | 3 | import com.example.eziketobenna.bakingapp.data.model.StepEntity 4 | import com.example.eziketobenna.bakingapp.remote.model.StepRemoteModel 5 | import com.example.eziketobenna.bakingapp.remote.utils.DummyData 6 | import com.google.common.truth.Truth.assertThat 7 | import org.junit.Test 8 | 9 | class StepRemoteMapperTest { 10 | 11 | private val stepRemoteMapper = StepRemoteMapper() 12 | 13 | @Test 14 | fun `check that id is mapped correctly`() = testData { entity, model -> 15 | assertThat(entity.id).isEqualTo(model.id) 16 | } 17 | 18 | @Test 19 | fun `check that videoUrl is mapped correctly`() = testData { entity, model -> 20 | assertThat(entity.videoURL).isEqualTo(model.videoURL) 21 | } 22 | 23 | @Test 24 | fun `check that description is mapped correctly`() = testData { entity, model -> 25 | assertThat(entity.description).isEqualTo(model.description) 26 | } 27 | 28 | @Test 29 | fun `check that shortDescription is mapped correctly`() = testData { entity, model -> 30 | assertThat(entity.shortDescription).isEqualTo(model.shortDescription) 31 | } 32 | 33 | @Test 34 | fun `check that thumbNailUrl is mapped correctly`() { 35 | testData { entity, model -> 36 | assertThat(entity.thumbnailURL).isEqualTo(model.thumbnailURL) 37 | } 38 | } 39 | 40 | private fun testData(action: (StepEntity, StepRemoteModel) -> Unit) { 41 | val model: StepRemoteModel = DummyData.stepRemoteModel 42 | val entity: StepEntity = stepRemoteMapper.mapFromModel(model) 43 | action(entity, model) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /libraries/remote/src/test/java/com/example/eziketobenna/bakingapp/remote/utils/DummyData.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.remote.utils 2 | 3 | import com.example.eziketobenna.bakingapp.remote.model.IngredientRemoteModel 4 | import com.example.eziketobenna.bakingapp.remote.model.RecipeRemoteModel 5 | import com.example.eziketobenna.bakingapp.remote.model.StepRemoteModel 6 | 7 | internal object DummyData { 8 | val recipeRemoteModel: RecipeRemoteModel 9 | get() = RecipeRemoteModel( 10 | id = 1, 11 | name = "Eba", 12 | image = "some url", 13 | servings = 3, 14 | ingredients = listOf(ingredientRemoteModel), 15 | steps = listOf(stepRemoteModel) 16 | ) 17 | 18 | val ingredientRemoteModel: IngredientRemoteModel 19 | get() = IngredientRemoteModel( 20 | quantity = 3.4, 21 | measure = "4", 22 | ingredient = "Ata rodo" 23 | ) 24 | 25 | val stepRemoteModel: StepRemoteModel 26 | get() = StepRemoteModel( 27 | id = 0, 28 | videoURL = "another url", 29 | description = "mehh", 30 | shortDescription = "me", 31 | thumbnailURL = "lalala" 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /libraries/remote/src/test/java/com/example/eziketobenna/bakingapp/remote/utils/Helpers.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.remote.utils 2 | 3 | import com.example.eziketobenna.bakingapp.remote.ApiService 4 | import com.google.common.io.Resources 5 | import com.squareup.moshi.Moshi 6 | import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory 7 | import java.io.File 8 | import java.net.URL 9 | import okhttp3.OkHttpClient 10 | import okhttp3.mockwebserver.MockWebServer 11 | import retrofit2.Retrofit 12 | import retrofit2.converter.moshi.MoshiConverterFactory 13 | 14 | const val REQUEST_PATH: String = "/baking.json" 15 | 16 | private val okHttpClient: OkHttpClient 17 | get() = OkHttpClient.Builder().build() 18 | 19 | private val moshi: Moshi 20 | get() = Moshi.Builder() 21 | .add(KotlinJsonAdapterFactory()).build() 22 | 23 | @Suppress("UnstableApiUsage") 24 | internal fun getJson(path: String): String { 25 | val uri: URL = Resources.getResource(path) 26 | val file = File(uri.path) 27 | return String(file.readBytes()) 28 | } 29 | 30 | internal fun makeTestApiService(mockWebServer: MockWebServer): ApiService = Retrofit.Builder() 31 | .baseUrl(mockWebServer.url("/")) 32 | .client(okHttpClient) 33 | .addConverterFactory(MoshiConverterFactory.create(moshi)) 34 | .build() 35 | .create(ApiService::class.java) 36 | -------------------------------------------------------------------------------- /libraries/remote/src/test/java/com/example/eziketobenna/bakingapp/remote/utils/RecipeRequestDispatcher.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.remote.utils 2 | 3 | import java.net.HttpURLConnection 4 | import okhttp3.mockwebserver.Dispatcher 5 | import okhttp3.mockwebserver.MockResponse 6 | import okhttp3.mockwebserver.RecordedRequest 7 | 8 | internal class RecipeRequestDispatcher : Dispatcher() { 9 | 10 | override fun dispatch(request: RecordedRequest): MockResponse { 11 | return when (request.path) { 12 | REQUEST_PATH -> MockResponse() 13 | .setResponseCode(HttpURLConnection.HTTP_OK) 14 | .setBody(getJson("response/recipe_response.json")) 15 | else -> throw IllegalArgumentException("Unknown Request Path ${request.path}") 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /presentation/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /presentation/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import Dependencies.Test 2 | 3 | plugins { 4 | kotlinLibrary 5 | } 6 | 7 | dependencies { 8 | testImplementation(Test.junit) 9 | testImplementation(Test.truth) 10 | } 11 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/example/eziketobenna/bakingapp/presentation/event/SingleEvent.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.presentation.event 2 | 3 | import java.util.concurrent.atomic.AtomicBoolean 4 | 5 | /** 6 | * Reference: 7 | * https://medium.com/@mdabrowski89/hi-thanks-for-the-grate-article-7659ed09ddd3 8 | */ 9 | abstract class SingleEvent(private val content: T) { 10 | 11 | private val isConsumed = AtomicBoolean(false) 12 | 13 | fun consume(action: (T) -> Unit) { 14 | if (!isConsumed.getAndSet(true)) { 15 | action.invoke(content) 16 | } 17 | } 18 | 19 | fun reset() { 20 | isConsumed.set(false) 21 | } 22 | 23 | val peekContent: T 24 | get() = content 25 | } 26 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/example/eziketobenna/bakingapp/presentation/event/ViewEvent.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.presentation.event 2 | 3 | data class ViewEvent(val value: T) : SingleEvent(value) 4 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/example/eziketobenna/bakingapp/presentation/mapper/ModelMapper.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.presentation.mapper 2 | 3 | interface ModelMapper { 4 | 5 | fun mapToModel(domain: D): M 6 | fun mapToDomain(model: M): D 7 | 8 | fun mapToModelList(domainList: List): List { 9 | return domainList.mapTo(mutableListOf(), ::mapToModel) 10 | } 11 | 12 | fun mapToDomainList(modelList: List): List { 13 | return modelList.mapTo(mutableListOf(), ::mapToDomain) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/example/eziketobenna/bakingapp/presentation/mvi/ActionProcessor.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.presentation.mvi 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | 5 | interface ActionProcessor { 6 | fun actionToResult(viewAction: A): Flow 7 | } 8 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/example/eziketobenna/bakingapp/presentation/mvi/IntentProcessor.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.presentation.mvi 2 | 3 | interface IntentProcessor { 4 | fun intentToAction(intent: I): A 5 | } 6 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/example/eziketobenna/bakingapp/presentation/mvi/MVIPresenter.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.presentation.mvi 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import kotlinx.coroutines.flow.StateFlow 5 | 6 | interface MVIPresenter { 7 | fun processIntent(intents: Flow) 8 | val viewState: StateFlow 9 | } 10 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/example/eziketobenna/bakingapp/presentation/mvi/MVIView.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.presentation.mvi 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | 5 | interface MVIView { 6 | fun render(state: S) 7 | val intents: Flow 8 | } 9 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/example/eziketobenna/bakingapp/presentation/mvi/ViewAction.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.presentation.mvi 2 | 3 | interface ViewAction 4 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/example/eziketobenna/bakingapp/presentation/mvi/ViewIntent.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.presentation.mvi 2 | 3 | interface ViewIntent 4 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/example/eziketobenna/bakingapp/presentation/mvi/ViewResult.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.presentation.mvi 2 | 3 | interface ViewResult 4 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/example/eziketobenna/bakingapp/presentation/mvi/ViewState.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.presentation.mvi 2 | 3 | interface ViewState 4 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/example/eziketobenna/bakingapp/presentation/mvi/ViewStateReducer.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.presentation.mvi 2 | 3 | interface ViewStateReducer { 4 | fun reduce(previous: S, result: R): S 5 | } 6 | -------------------------------------------------------------------------------- /presentation/src/test/java/com/example/eziketobenna/bakingapp/presentation/SingleEventTest.kt: -------------------------------------------------------------------------------- 1 | package com.example.eziketobenna.bakingapp.presentation 2 | 3 | import com.example.eziketobenna.bakingapp.presentation.event.SingleEvent 4 | import com.google.common.truth.Truth.assertThat 5 | import org.junit.Test 6 | 7 | class SingleEventTest { 8 | 9 | @Test 10 | fun `check that event is consumed`() { 11 | var int = 1 12 | val event = object : SingleEvent(10) {} 13 | event.consume { value -> 14 | int += value 15 | } 16 | assertThat(int).isEqualTo(11) 17 | } 18 | 19 | @Test 20 | fun `check that event is not consumed twice`() { 21 | var int = 1 22 | val event = object : SingleEvent(10) {} 23 | event.consume { value -> 24 | int += value 25 | } 26 | event.consume { value -> 27 | int -= value 28 | } 29 | assertThat(int).isNotEqualTo(-9) 30 | assertThat(int).isEqualTo(11) 31 | } 32 | 33 | @Test 34 | fun `check that peekContent returns the initial value`() { 35 | val content = 2 36 | val event = object : SingleEvent(content) {} 37 | assertThat(content).isEqualTo(event.peekContent) 38 | } 39 | 40 | @Test 41 | fun `check that reset allows event to be consumed again`() { 42 | var int = 1 43 | val event = object : SingleEvent(10) {} 44 | event.consume { value -> 45 | int += value 46 | } 47 | event.reset() 48 | event.consume { value -> 49 | int -= value 50 | } 51 | assertThat(int).isNotEqualTo(11) 52 | assertThat(int).isEqualTo(1) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | include( 2 | ":app", 3 | ":libraries:remote", 4 | ":libraries:domain", 5 | ":libraries:data", 6 | ":core", 7 | ":features:recipes:recipe", 8 | ":presentation", 9 | ":features:recipes:model", 10 | ":common:views", 11 | ":features:recipes:recipeDetail", 12 | ":features:recipes:stepDetail", 13 | ":features:videoPlayer" 14 | ) 15 | --------------------------------------------------------------------------------