├── .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 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/.idea/kotlinScripting.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
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 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
2 |
3 | buildscript {
4 | repositories.applyDefault()
5 | }
6 |
7 | allprojects {
8 | repositories.applyDefault()
9 | tasks.withType {
10 | kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString()
11 | }
12 | }
13 |
14 | subprojects {
15 | applySpotless
16 | tasks.withType().configureEach {
17 | kotlinOptions.freeCompilerArgs +=
18 | "-Xuse-experimental=" +
19 | "kotlin.Experimental," +
20 | "kotlinx.coroutines.ExperimentalCoroutinesApi," +
21 | "kotlinx.coroutines.InternalCoroutinesApi," +
22 | "kotlinx.coroutines.FlowPreview"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/buildSrc/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
2 |
3 | plugins {
4 | `kotlin-dsl`
5 | `kotlin-dsl-precompiled-script-plugins`
6 | }
7 |
8 | repositories {
9 | google()
10 | jcenter()
11 | mavenCentral()
12 | maven("https://dl.bintray.com/kotlin/kotlin-eap")
13 | maven("https://oss.sonatype.org/content/repositories/snapshots/")
14 | }
15 |
16 | val compileKotlin: KotlinCompile by tasks
17 | compileKotlin.kotlinOptions {
18 | languageVersion = Plugin.Version.kotlin
19 | }
20 |
21 | object Plugin {
22 | object Version {
23 | const val spotless: String = "4.0.1"
24 | const val kotlin = "1.4-M2"
25 | const val androidGradle: String = "4.2.0-alpha03"
26 | const val navigation: String = "2.3.0"
27 | const val daggerHiltAndroid: String = "2.28-alpha"
28 | }
29 |
30 | const val spotless: String = "com.diffplug.spotless:spotless-plugin-gradle:${Version.spotless}"
31 | const val kotlin: String = "org.jetbrains.kotlin:kotlin-gradle-plugin:${Version.kotlin}"
32 | const val androidGradle: String = "com.android.tools.build:gradle:${Version.androidGradle}"
33 | const val navigationSafeArgs: String =
34 | "androidx.navigation:navigation-safe-args-gradle-plugin:${Version.navigation}"
35 | const val daggerHilt: String =
36 | "com.google.dagger:hilt-android-gradle-plugin:${Version.daggerHiltAndroid}"
37 | }
38 |
39 | dependencies {
40 | implementation(Plugin.spotless)
41 | implementation(Plugin.kotlin)
42 | implementation(Plugin.androidGradle)
43 | implementation(Plugin.navigationSafeArgs)
44 | implementation(Plugin.daggerHilt)
45 | }
46 |
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/BuildType.kt:
--------------------------------------------------------------------------------
1 | interface BuildType {
2 |
3 | companion object {
4 | const val DEBUG: String = "debug"
5 | const val RELEASE: String = "release"
6 | }
7 |
8 | val isMinifyEnabled: Boolean
9 | val isTestCoverageEnabled: Boolean
10 | }
11 |
12 | object BuildTypeDebug : BuildType {
13 | override val isMinifyEnabled: Boolean = false
14 | override val isTestCoverageEnabled: Boolean = true
15 |
16 | const val applicationIdSuffix: String = ".debug"
17 | const val versionNameSuffix: String = "-DEBUG"
18 | }
19 |
20 | object BuildTypeRelease : BuildType {
21 | override val isMinifyEnabled: Boolean = true
22 | override val isTestCoverageEnabled: Boolean = false
23 | }
24 |
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/Extensions.kt:
--------------------------------------------------------------------------------
1 | import org.gradle.api.Project
2 | import org.gradle.api.artifacts.Dependency
3 | import org.gradle.api.artifacts.dsl.DependencyHandler
4 | import org.gradle.api.artifacts.dsl.RepositoryHandler
5 | import org.gradle.api.initialization.dsl.ScriptHandler
6 | import org.gradle.kotlin.dsl.apply
7 | import org.gradle.plugin.use.PluginDependenciesSpec
8 | import org.gradle.plugin.use.PluginDependencySpec
9 |
10 | val PluginDependenciesSpec.androidApplication: PluginDependencySpec
11 | get() = id("com.android.application")
12 |
13 | val PluginDependenciesSpec.androidLibrary: PluginDependencySpec
14 | get() = id("com.android.library")
15 |
16 | val PluginDependenciesSpec.dynamicFeature: PluginDependencySpec
17 | get() = id("com.android.dynamic-feature")
18 |
19 | val PluginDependenciesSpec.kotlin: PluginDependencySpec
20 | get() = id("kotlin")
21 |
22 | val PluginDependenciesSpec.daggerHilt: PluginDependencySpec
23 | get() = id("dagger.hilt.android.plugin")
24 |
25 | val Project.applySpotless
26 | get() = apply(plugin = "spotless")
27 |
28 | val PluginDependenciesSpec.kotlinLibrary: PluginDependencySpec
29 | get() = id("kotlin-library")
30 |
31 | val PluginDependenciesSpec.safeArgs: PluginDependencySpec
32 | get() = id("androidx.navigation.safeargs.kotlin")
33 |
34 | fun RepositoryHandler.maven(url: String) {
35 | maven {
36 | setUrl(url)
37 | }
38 | }
39 |
40 | fun RepositoryHandler.applyDefault() {
41 | google()
42 | jcenter()
43 | mavenCentral()
44 | maven("https://dl.bintray.com/kotlin/kotlin-eap")
45 | maven("https://oss.sonatype.org/content/repositories/snapshots/")
46 | }
47 |
48 | fun DependencyHandler.implementAll(list: List) {
49 | list.forEach {
50 | add("implementation", it)
51 | }
52 | }
53 |
54 | fun DependencyHandler.addPlugins(list: List) {
55 | list.forEach {
56 | add(ScriptHandler.CLASSPATH_CONFIGURATION, it)
57 | }
58 | }
59 |
60 | fun DependencyHandler.kapt(dependencyNotation: String): Dependency? =
61 | add("kapt", dependencyNotation)
62 |
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/plugin/kotlin-library.gradle.kts:
--------------------------------------------------------------------------------
1 | import Dependencies.Coroutines
2 | import Dependencies.DI
3 | import Dependencies.Kotlin
4 |
5 | plugins {
6 | id("kotlin")
7 | }
8 |
9 | java {
10 | sourceCompatibility = JavaVersion.VERSION_1_8
11 | targetCompatibility = JavaVersion.VERSION_1_8
12 | }
13 |
14 | dependencies {
15 | implementation(Kotlin.stdlib)
16 | implementation(Coroutines.core)
17 | implementation(DI.javaxInject)
18 | }
19 |
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/plugin/spotless.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.diffplug.gradle.spotless")
3 | }
4 |
5 | spotless {
6 | kotlin {
7 | target(fileTree(mapOf(
8 | "dir" to ".",
9 | "include" to listOf("**/*.kt"),
10 | "exclude" to listOf("**/build/**", "**/buildSrc/**", "**/.*", ".idea/"))))
11 | ktlint(ktLintVersion)
12 | trimTrailingWhitespace()
13 | indentWithSpaces()
14 | endWithNewline()
15 | }
16 | format("xml") {
17 | target("**/res/**/*.xml")
18 | indentWithSpaces()
19 | trimTrailingWhitespace()
20 | endWithNewline()
21 | }
22 | kotlinGradle {
23 | target(fileTree(mapOf(
24 | "dir" to ".",
25 | "include" to listOf("**/*.gradle.kts", "*.gradle.kts"),
26 | "exclude" to listOf("**/build/**"))))
27 | trimTrailingWhitespace()
28 | indentWithSpaces()
29 | endWithNewline()
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/common/views/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/common/views/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import Dependencies.AndroidX.coreKtx
2 | import Dependencies.Kotlin
3 | import Dependencies.View
4 |
5 | plugins {
6 | androidLibrary
7 | kotlin(kotlinAndroid)
8 | kotlin(kotlinAndroidExtension)
9 | }
10 |
11 | android {
12 | compileSdkVersion(Config.Version.compileSdkVersion)
13 | defaultConfig {
14 | minSdkVersion(Config.Version.minSdkVersion)
15 | targetSdkVersion(Config.Version.targetSdkVersion)
16 | }
17 |
18 | @Suppress("UnstableApiUsage")
19 | compileOptions {
20 | sourceCompatibility = JavaVersion.VERSION_1_8
21 | targetCompatibility = JavaVersion.VERSION_1_8
22 | }
23 |
24 | kotlinOptions {
25 | jvmTarget = JavaVersion.VERSION_1_8.toString()
26 | }
27 |
28 | buildTypes {
29 | named(BuildType.DEBUG) {
30 | isMinifyEnabled = BuildTypeDebug.isMinifyEnabled
31 | versionNameSuffix = BuildTypeDebug.versionNameSuffix
32 | }
33 | }
34 | }
35 |
36 | dependencies {
37 | implementation(Kotlin.stdlib)
38 | View.run {
39 | implementation(materialComponent)
40 | implementation(appCompat)
41 | }
42 | implementation(coreKtx)
43 | }
44 |
--------------------------------------------------------------------------------
/common/views/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mitchtabian/Baking-App-Kotlin/9d2684ac2fcf8efb182bbd7e16a8c16152bc9ea7/common/views/consumer-rules.pro
--------------------------------------------------------------------------------
/common/views/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
--------------------------------------------------------------------------------
/common/views/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/common/views/src/main/res/drawable/ic_error_page_2.xml:
--------------------------------------------------------------------------------
1 |
7 |
9 |
10 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/common/views/src/main/res/layout/simple_empty_state_view_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
15 |
16 |
24 |
25 |
36 |
37 |
43 |
44 |
--------------------------------------------------------------------------------
/common/views/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/common/views/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | empty state icon
4 | Retry
5 |
6 |
--------------------------------------------------------------------------------
/common/views/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |
--------------------------------------------------------------------------------