├── .github
└── workflows
│ ├── feature-workflow.yml
│ └── master-and-pr-workflow.yml
├── .gitignore
├── LICENSE
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
├── screenshots
│ └── debug
│ │ ├── com.alexzh.coffeedrinks.ui.component.FavouriteTest_favourite-animation-state-0.png
│ │ ├── com.alexzh.coffeedrinks.ui.component.FavouriteTest_favourite-animation-state-1.png
│ │ └── com.alexzh.coffeedrinks.ui.component.FavouriteTest_favourite-animation-state-2.png
└── src
│ ├── androidTest
│ ├── AndroidManifest.xml
│ └── java
│ │ └── com
│ │ └── alexzh
│ │ └── coffeedrinks
│ │ └── ui
│ │ ├── CoffeeDrinksScreenTest.kt
│ │ └── component
│ │ └── FavouriteTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── alexzh
│ │ │ └── coffeedrinks
│ │ │ ├── CoffeeDrinksApp.kt
│ │ │ ├── data
│ │ │ ├── CoffeeDrink.kt
│ │ │ ├── CoffeeDrinkDataSource.kt
│ │ │ ├── CoffeeDrinkRepository.kt
│ │ │ ├── DummyCoffeeDrinksDataSource.kt
│ │ │ ├── RuntimeCoffeeDrinkRepository.kt
│ │ │ └── order
│ │ │ │ ├── OrderCoffeeDrink.kt
│ │ │ │ ├── OrderCoffeeDrinkMapper.kt
│ │ │ │ ├── OrderCoffeeDrinksRepository.kt
│ │ │ │ └── RuntimeOrderCoffeeDrinksRepository.kt
│ │ │ ├── di
│ │ │ └── AppModules.kt
│ │ │ └── ui
│ │ │ ├── MainActivity.kt
│ │ │ ├── Navigation.kt
│ │ │ ├── Theme.kt
│ │ │ ├── Typography.kt
│ │ │ ├── component
│ │ │ ├── Counter.kt
│ │ │ ├── Divider.kt
│ │ │ └── Favourite.kt
│ │ │ ├── screen
│ │ │ ├── coffeedetails
│ │ │ │ ├── CoffeeDrinkDetailsFragment.kt
│ │ │ │ ├── CoffeeDrinkDetailsScreen.kt
│ │ │ │ ├── CoffeeDrinkDetailsViewModel.kt
│ │ │ │ ├── exception
│ │ │ │ │ └── NoCoffeeDrinkFoundException.kt
│ │ │ │ ├── mapper
│ │ │ │ │ └── CoffeeDrinkDetailMapper.kt
│ │ │ │ └── model
│ │ │ │ │ ├── CoffeeDrinkDetail.kt
│ │ │ │ │ └── CoffeeDrinkDetailState.kt
│ │ │ ├── coffeedrinks
│ │ │ │ ├── CoffeeDrinksFragment.kt
│ │ │ │ ├── CoffeeDrinksScreen.kt
│ │ │ │ ├── CoffeeDrinksViewModel.kt
│ │ │ │ ├── DetailedListItem.kt
│ │ │ │ ├── ListItem.kt
│ │ │ │ ├── mapper
│ │ │ │ │ └── CoffeeDrinkItemMapper.kt
│ │ │ │ └── model
│ │ │ │ │ ├── CoffeeDrinkItem.kt
│ │ │ │ │ ├── CoffeeDrinksState.kt
│ │ │ │ │ └── DisplayingOptions.kt
│ │ │ └── order
│ │ │ │ ├── OrderCoffeeDrinkFragment.kt
│ │ │ │ ├── OrderCoffeeDrinkScreen.kt
│ │ │ │ ├── OrderCoffeeDrinkViewModel.kt
│ │ │ │ ├── mapper
│ │ │ │ └── OrderCoffeeDrinkMapper.kt
│ │ │ │ └── model
│ │ │ │ ├── OrderCoffeeDrink.kt
│ │ │ │ └── OrderCoffeeDrinkState.kt
│ │ │ └── state
│ │ │ └── UiState.kt
│ └── res
│ │ ├── drawable-hdpi
│ │ ├── ic_action_coffee.png
│ │ ├── ic_arrow_back_white.png
│ │ ├── ic_extended_list_white.png
│ │ ├── ic_favorite_border_white.png
│ │ ├── ic_favorite_white.png
│ │ ├── ic_list_white.png
│ │ └── ic_order_white.png
│ │ ├── drawable-mdpi
│ │ ├── ic_action_coffee.png
│ │ ├── ic_arrow_back_white.png
│ │ ├── ic_extended_list_white.png
│ │ ├── ic_favorite_border_white.png
│ │ ├── ic_favorite_white.png
│ │ ├── ic_list_white.png
│ │ └── ic_order_white.png
│ │ ├── drawable-v24
│ │ └── ic_launcher_foreground.xml
│ │ ├── drawable-xhdpi
│ │ ├── ic_action_coffee.png
│ │ ├── ic_arrow_back_white.png
│ │ ├── ic_extended_list_white.png
│ │ ├── ic_favorite_border_white.png
│ │ ├── ic_favorite_white.png
│ │ ├── ic_list_white.png
│ │ └── ic_order_white.png
│ │ ├── drawable-xxhdpi
│ │ ├── ic_action_coffee.png
│ │ ├── ic_arrow_back_white.png
│ │ ├── ic_extended_list_white.png
│ │ ├── ic_favorite_border_white.png
│ │ ├── ic_favorite_white.png
│ │ ├── ic_list_white.png
│ │ └── ic_order_white.png
│ │ ├── drawable
│ │ ├── americano_small.png
│ │ ├── cappuccino_small.png
│ │ ├── cold_brew_coffee_small.png
│ │ ├── espresso_macchiato_small.png
│ │ ├── espresso_small.png
│ │ ├── frappino_small.png
│ │ ├── gingerbread_coffee_small.png
│ │ ├── ic_baseline_favorite_24.xml
│ │ ├── ic_baseline_favorite_border_24.xml
│ │ ├── ic_launcher_background.xml
│ │ ├── iced_mocha_small.png
│ │ ├── irish_coffee_small.png
│ │ ├── latte_macchiato_small.png
│ │ ├── latte_small.png
│ │ ├── mocha_small.png
│ │ └── turkish_coffee_small.png
│ │ ├── font
│ │ ├── roboto_black.ttf
│ │ ├── roboto_black_italic.ttf
│ │ ├── roboto_bold.ttf
│ │ ├── roboto_bold_italic.ttf
│ │ ├── roboto_light.ttf
│ │ ├── roboto_light_italic.ttf
│ │ ├── roboto_medium.ttf
│ │ ├── roboto_medium_italic.ttf
│ │ ├── roboto_regular.ttf
│ │ ├── roboto_regular_italic.ttf
│ │ ├── roboto_thin.ttf
│ │ └── roboto_thin_italic.ttf
│ │ ├── layout
│ │ └── activity_main.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── navigation
│ │ └── nav_graph.xml
│ │ ├── values-night
│ │ └── colors.xml
│ │ └── values
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ └── test
│ └── java
│ └── com
│ └── alexzh
│ └── coffeedrinks
│ ├── data
│ ├── RuntimeCoffeeDrinkRepositoryTest.kt
│ └── order
│ │ ├── OrderCoffeeDrinkMapperTest.kt
│ │ └── RuntimeOrderCoffeeDrinksRepositoryTest.kt
│ └── generator
│ ├── GenerateCoffeeDrink.kt
│ ├── GenerateOrderCoffeeDrink.kt
│ └── RandomData.kt
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── screenshots
├── coffee-drink-details-screen-dark.png
├── coffee-drink-details-screen-light.png
├── coffee-drinks-screen-dark.png
├── coffee-drinks-screen-light.png
├── order-coffee-drinks-screen-dark.png
└── order-coffee-drinks-screen-light.png
├── scripts
├── git-hooks.gradle
├── git-hooks
│ └── pre-commit.sh
└── ktlint.gradle
└── settings.gradle
/.github/workflows/feature-workflow.yml:
--------------------------------------------------------------------------------
1 | name: Commits and Pull Requests
2 | on:
3 | push:
4 | branches:
5 | - '*'
6 | - '!master'
7 | jobs:
8 | build:
9 | name: Run Unit tests
10 | runs-on: ubuntu-18.04
11 | steps:
12 | - uses: actions/checkout@v3
13 | - name: Set up JDK 11
14 | uses: actions/setup-java@v3
15 | with:
16 | distribution: 'temurin'
17 | java-version: '11'
18 | - name: Build
19 | run: bash ./gradlew build
20 | instrumentation-test:
21 | name: Run Instrumentation tests
22 | runs-on: macos-latest
23 | steps:
24 | - name: Set up JDK 11
25 | uses: actions/setup-java@v3
26 | with:
27 | distribution: 'temurin'
28 | java-version: '11'
29 | - name: Checkout
30 | uses: actions/checkout@v3
31 | - name: Make files executable
32 | run: chmod +x ./gradlew
33 | - name: Instrumentation tests
34 | uses: reactivecircus/android-emulator-runner@v2
35 | with:
36 | api-level: 31
37 | arch: x86_64
38 | profile: pixel
39 | target: google_apis
40 | script: ./gradlew connectedCheck
41 | screenshot-test:
42 | name: Run Screenshot tests
43 | runs-on: macos-latest
44 | steps:
45 | - name: Set up JDK 11
46 | uses: actions/setup-java@v3
47 | with:
48 | distribution: 'temurin'
49 | java-version: '11'
50 | - name: Checkout
51 | uses: actions/checkout@v3
52 | - name: Make files executable
53 | run: chmod +x ./gradlew
54 | - name: Screenshot tests
55 | uses: reactivecircus/android-emulator-runner@v2.24.0
56 | with:
57 | api-level: 31
58 | arch: x86_64
59 | profile: pixel
60 | target: google_apis
61 | sdcard-path-or-size: 512M
62 | emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim
63 | script: ./gradlew executeScreenshotTests -Pandroid.testInstrumentationRunnerArguments.size=medium
--------------------------------------------------------------------------------
/.github/workflows/master-and-pr-workflow.yml:
--------------------------------------------------------------------------------
1 | name: Commits and Pull Requests
2 | on:
3 | pull_request:
4 | branches:
5 | - '*'
6 | push:
7 | branches:
8 | - 'master'
9 | jobs:
10 | unit-tests:
11 | name: Run Unit tests
12 | runs-on: ubuntu-18.04
13 | steps:
14 | - uses: actions/checkout@v3
15 | - name: set up JDK 11
16 | uses: actions/setup-java@v3
17 | with:
18 | distribution: 'temurin'
19 | java-version: '11'
20 | - name: Unit tests
21 | run: bash ./gradlew test
22 | apk:
23 | name: Generate APK
24 | runs-on: ubuntu-18.04
25 | steps:
26 | - uses: actions/checkout@v3
27 | - name: set up JDK 11
28 | uses: actions/setup-java@v3
29 | with:
30 | distribution: 'temurin'
31 | java-version: '11'
32 | - name: Build debug APK
33 | run: bash ./gradlew assembleDebug
34 | - name: Upload APK
35 | uses: actions/upload-artifact@v2
36 | with:
37 | name: app
38 | path: app/build/outputs/apk/debug/app-debug.apk
39 | instrumentation-test:
40 | name: Run Instrumentation tests
41 | runs-on: macos-latest
42 | steps:
43 | - name: Set up JDK 11
44 | uses: actions/setup-java@v3
45 | with:
46 | distribution: 'temurin'
47 | java-version: '11'
48 | - name: Checkout
49 | uses: actions/checkout@v3
50 | - name: Make files executable
51 | run: chmod +x ./gradlew
52 | - name: Instrumentation tests
53 | uses: reactivecircus/android-emulator-runner@v2
54 | with:
55 | api-level: 31
56 | arch: x86_64
57 | profile: pixel
58 | target: google_apis
59 | script: ./gradlew connectedCheck
60 | screenshot-test:
61 | name: Run Screenshot tests
62 | runs-on: macos-latest
63 | steps:
64 | - name: Set up JDK 11
65 | uses: actions/setup-java@v3
66 | with:
67 | distribution: 'temurin'
68 | java-version: '11'
69 | - name: Checkout
70 | uses: actions/checkout@v3
71 | - name: Make files executable
72 | run: chmod +x ./gradlew
73 | - name: Screenshot tests
74 | uses: reactivecircus/android-emulator-runner@v2
75 | with:
76 | api-level: 31
77 | arch: x86_64
78 | profile: pixel
79 | target: google_apis
80 | sdcard-path-or-size: 512M
81 | emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim
82 | script: ./gradlew executeScreenshotTests -Pandroid.testInstrumentationRunnerArguments.size=medium
83 | code-style-verification:
84 | name: Code style verification
85 | runs-on: ubuntu-18.04
86 | steps:
87 | - name: set up JDK 11
88 | uses: actions/setup-java@v3
89 | with:
90 | distribution: 'temurin'
91 | java-version: '11'
92 | - uses: actions/checkout@v3
93 | - name: KtLint verification
94 | run: bash ./gradlew ktlint
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Built application files
2 | *.apk
3 | *.aar
4 | *.ap_
5 | *.aab
6 |
7 | # Files for the ART/Dalvik VM
8 | *.dex
9 |
10 | # Java class files
11 | *.class
12 |
13 | # Generated files
14 | bin/
15 | gen/
16 | out/
17 | # Uncomment the following line in case you need and you don't have the release build type files in your app
18 | # release/
19 |
20 | # Gradle files
21 | .gradle/
22 | build/
23 |
24 | # Local configuration file (sdk path, etc)
25 | local.properties
26 |
27 | # Proguard folder generated by Eclipse
28 | proguard/
29 |
30 | # Log Files
31 | *.log
32 |
33 | # Android Studio Navigation editor temp files
34 | .navigation/
35 |
36 | # Android Studio captures folder
37 | captures/
38 |
39 | # IntelliJ
40 | *.iml
41 | .idea/
42 |
43 | # Keystore files
44 | # Uncomment the following lines if you do not want to check your keystore files in.
45 | #*.jks
46 | #*.keystore
47 |
48 | # External native build folder generated in Android Studio 2.2 and later
49 | .externalNativeBuild
50 | .cxx/
51 |
52 | # Google Services (e.g. APIs or Firebase)
53 | # google-services.json
54 |
55 | # Freeline
56 | freeline.py
57 | freeline/
58 | freeline_project_description.json
59 |
60 | # fastlane
61 | fastlane/report.xml
62 | fastlane/Preview.html
63 | fastlane/screenshots
64 | fastlane/test_output
65 | fastlane/readme.md
66 |
67 | # Version control
68 | vcs.xml
69 |
70 | # lint
71 | lint/intermediates/
72 | lint/generated/
73 | lint/outputs/
74 | lint/tmp/
75 | # lint/reports/
76 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Coffee drinks app with Jetpack Compose
2 |
3 | The **Coffee Drinks** is an Android application created for playing with Jetpack Compose framework.
4 |
5 | Light Color Palette
6 |
7 |
8 |
9 |
10 |
11 |
12 | Dark Color Palette
13 |
14 |
15 |
16 |
17 |
18 |
19 | ## Articles
20 | * [Jetpack Compose: Overview](https://alexzh.com/jetpack-compose-overview/)
21 | * [Jetpack Compose: State](https://alexzh.com/jetpack-compose-state/)
22 | * [Jetpack Compose: Theme and Typography](https://alexzh.com/jetpack-compose-theme-and-typography/)
23 |
24 | ## Features
25 | * Demonstrating list of coffee drinks
26 | * User can mark/unmark coffee drink as favourite
27 | * User can read information about each coffee drink
28 | * User can change the design of the card in a list
29 | * User can calculate the total price of order
30 | * Support light and dark color theme
31 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'shot'
4 | apply from: '../scripts/ktlint.gradle'
5 |
6 | shot {
7 | applicationId = "com.alexzh.coffeedrinks"
8 | tolerance = 2
9 | }
10 |
11 | android {
12 | compileSdkVersion 31
13 | defaultConfig {
14 | applicationId "com.alexzh.coffeedrinks"
15 | minSdkVersion 21
16 | targetSdkVersion 31
17 | versionCode 1
18 | versionName "1.0"
19 | multiDexEnabled = true
20 | testInstrumentationRunner "com.karumi.shot.ShotTestRunner"
21 | vectorDrawables {
22 | useSupportLibrary true
23 | }
24 | }
25 | buildTypes {
26 | release {
27 | minifyEnabled true
28 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
29 | }
30 | }
31 | compileOptions {
32 | sourceCompatibility JavaVersion.VERSION_1_8
33 | targetCompatibility JavaVersion.VERSION_1_8
34 | }
35 | kotlinOptions {
36 | jvmTarget = '1.8'
37 | useIR = true
38 | }
39 | buildFeatures {
40 | compose true
41 | }
42 | composeOptions {
43 | kotlinCompilerExtensionVersion compose_version
44 | }
45 | packagingOptions {
46 | resources {
47 | excludes += ['/META-INF/AL2.0', '/META-INF/LGPL2.1']
48 | }
49 | }
50 | }
51 |
52 | dependencies {
53 | implementation "androidx.core:core-ktx:$androidx_core_version"
54 | implementation "androidx.appcompat:appcompat:$appcompat_version"
55 |
56 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
57 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$androidx_lifecycle_version"
58 | implementation "androidx.navigation:navigation-fragment-ktx:$androidx_navigation_version"
59 | implementation "androidx.navigation:navigation-ui-ktx:$androidx_navigation_version"
60 | implementation "androidx.fragment:fragment-ktx:$androidx_fragment_version"
61 |
62 | // Koin
63 | implementation "org.koin:koin-android:$koin_version"
64 | implementation "org.koin:koin-androidx-viewmodel:$koin_version"
65 | androidTestImplementation "org.koin:koin-test:$koin_version"
66 |
67 | // Jetpack Compose
68 | implementation "androidx.compose.runtime:runtime:$compose_version"
69 | implementation "androidx.compose.ui:ui:$compose_version"
70 | implementation "androidx.compose.foundation:foundation-layout:$compose_version"
71 | implementation "androidx.compose.material:material:$compose_version"
72 | implementation "androidx.compose.material:material-icons-extended:$compose_version"
73 | implementation "androidx.compose.foundation:foundation:$compose_version"
74 | implementation "androidx.compose.animation:animation:$compose_version"
75 | implementation "androidx.compose.ui:ui-tooling:$compose_version"
76 | implementation "androidx.compose.runtime:runtime-livedata:$compose_version"
77 | implementation "androidx.navigation:navigation-compose:$compose_nav_version"
78 | implementation "androidx.constraintlayout:constraintlayout-compose:$compose_constraintlayout_version"
79 |
80 | // Local testing dependencies
81 | testImplementation "junit:junit:$junit_version"
82 | testImplementation "io.mockk:mockk:$mockk_version"
83 |
84 | // Instrumentation testing dependencies
85 | androidTestImplementation "junit:junit:$junit_version"
86 | androidTestImplementation "androidx.test.ext:junit:$test_junit_runner"
87 | androidTestImplementation("androidx.test:rules:$test_rules")
88 | androidTestImplementation("androidx.test:runner:$test_runner")
89 | androidTestImplementation "androidx.compose.ui:ui-test:$compose_version"
90 | androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
91 | androidTestImplementation "io.mockk:mockk-android:$mockk_version"
92 |
93 | debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"
94 | debugImplementation "androidx.fragment:fragment-testing:$androidx_fragment_version"
95 | debugImplementation "androidx.test:monitor:$test_monitor"
96 | }
97 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/app/screenshots/debug/com.alexzh.coffeedrinks.ui.component.FavouriteTest_favourite-animation-state-0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/screenshots/debug/com.alexzh.coffeedrinks.ui.component.FavouriteTest_favourite-animation-state-0.png
--------------------------------------------------------------------------------
/app/screenshots/debug/com.alexzh.coffeedrinks.ui.component.FavouriteTest_favourite-animation-state-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/screenshots/debug/com.alexzh.coffeedrinks.ui.component.FavouriteTest_favourite-animation-state-1.png
--------------------------------------------------------------------------------
/app/screenshots/debug/com.alexzh.coffeedrinks.ui.component.FavouriteTest_favourite-animation-state-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/screenshots/debug/com.alexzh.coffeedrinks.ui.component.FavouriteTest_favourite-animation-state-2.png
--------------------------------------------------------------------------------
/app/src/androidTest/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/alexzh/coffeedrinks/ui/CoffeeDrinksScreenTest.kt:
--------------------------------------------------------------------------------
1 | package com.alexzh.coffeedrinks.ui
2 |
3 | import androidx.compose.ui.test.assertIsDisplayed
4 | import androidx.compose.ui.test.junit4.createComposeRule
5 | import androidx.compose.ui.test.onNodeWithText
6 | import androidx.fragment.app.testing.launchFragmentInContainer
7 | import com.alexzh.coffeedrinks.R
8 | import com.alexzh.coffeedrinks.ui.screen.coffeedrinks.CoffeeDrinksFragment
9 | import kotlinx.coroutines.ExperimentalCoroutinesApi
10 | import org.junit.Before
11 | import org.junit.Rule
12 | import org.junit.Test
13 | import org.junit.runner.RunWith
14 | import org.junit.runners.JUnit4
15 | import org.koin.test.KoinTest
16 |
17 | @ExperimentalCoroutinesApi
18 | @RunWith(JUnit4::class)
19 | class CoffeeDrinksScreenTest : KoinTest {
20 |
21 | @get:Rule
22 | val composeTestRule = createComposeRule()
23 |
24 | @Before
25 | fun setUp() {
26 | launchFragmentInContainer(
27 | themeResId = R.style.AppTheme
28 | )
29 | }
30 |
31 | @Test
32 | fun shouldLaunchApp() {
33 | composeTestRule
34 | .onNodeWithText("Coffee Drinks")
35 | .assertIsDisplayed()
36 | }
37 |
38 | @Test
39 | fun shouldLoadAmericano() {
40 | composeTestRule
41 | .onNodeWithText("Americano", true)
42 | .assertIsDisplayed()
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/alexzh/coffeedrinks/ui/component/FavouriteTest.kt:
--------------------------------------------------------------------------------
1 | package com.alexzh.coffeedrinks.ui.component
2 |
3 | import androidx.compose.foundation.layout.size
4 | import androidx.compose.runtime.LaunchedEffect
5 | import androidx.compose.runtime.mutableStateOf
6 | import androidx.compose.runtime.remember
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.graphics.Color
9 | import androidx.compose.ui.test.junit4.createComposeRule
10 | import androidx.compose.ui.unit.dp
11 | import androidx.test.filters.MediumTest
12 | import com.karumi.shot.ScreenshotTest
13 | import org.junit.Rule
14 | import org.junit.Test
15 | import org.junit.runner.RunWith
16 | import org.junit.runners.JUnit4
17 |
18 | @RunWith(JUnit4::class)
19 | class FavouriteTest : ScreenshotTest {
20 |
21 | @get:Rule
22 | val composeTestRule = createComposeRule()
23 |
24 | @MediumTest
25 | @Test
26 | fun animationShouldBeRenderedCorrectly() {
27 | composeTestRule.apply {
28 | setContent {
29 | mainClock.autoAdvance = false
30 |
31 | val state = remember { mutableStateOf(false) }
32 |
33 | LaunchedEffect("LaunchAnimation") {
34 | state.value = true
35 | }
36 | Favourite(
37 | state = state,
38 | modifier = Modifier.size(150.dp),
39 | onValueChanged = { },
40 | tint = Color.Red
41 | )
42 | }
43 |
44 | compareScreenshot(composeTestRule, "favourite-animation-state-0")
45 |
46 | mainClock.advanceTimeBy(100)
47 | compareScreenshot(composeTestRule, "favourite-animation-state-1")
48 |
49 | mainClock.advanceTimeBy(150)
50 | compareScreenshot(composeTestRule, "favourite-animation-state-2")
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
13 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexzh/coffeedrinks/CoffeeDrinksApp.kt:
--------------------------------------------------------------------------------
1 | package com.alexzh.coffeedrinks
2 |
3 | import android.app.Application
4 | import com.alexzh.coffeedrinks.di.dataModule
5 | import com.alexzh.coffeedrinks.di.mapperModule
6 | import com.alexzh.coffeedrinks.di.viewModelModule
7 | import kotlinx.coroutines.ExperimentalCoroutinesApi
8 | import org.koin.android.ext.koin.androidContext
9 | import org.koin.android.ext.koin.androidLogger
10 | import org.koin.core.context.startKoin
11 | import org.koin.core.logger.Level
12 |
13 | @ExperimentalCoroutinesApi
14 | class CoffeeDrinksApp : Application() {
15 |
16 | override fun onCreate() {
17 | super.onCreate()
18 | initDI()
19 | }
20 |
21 | private fun initDI() {
22 | startKoin {
23 | androidLogger(Level.ERROR)
24 | androidContext(this@CoffeeDrinksApp)
25 | modules(
26 | listOf(
27 | dataModule,
28 | mapperModule,
29 | viewModelModule
30 | )
31 | )
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexzh/coffeedrinks/data/CoffeeDrink.kt:
--------------------------------------------------------------------------------
1 | package com.alexzh.coffeedrinks.data
2 |
3 | import androidx.annotation.DrawableRes
4 |
5 | data class CoffeeDrink(
6 | val id: Long,
7 | val name: String,
8 | @DrawableRes val imageUrl: Int,
9 | val description: String,
10 | val ingredients: String,
11 | val orderDescription: String,
12 | val price: Double,
13 | val isFavourite: Boolean
14 | )
15 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexzh/coffeedrinks/data/CoffeeDrinkDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.alexzh.coffeedrinks.data
2 |
3 | interface CoffeeDrinkDataSource {
4 |
5 | fun getCoffeeDrinks(): List
6 | }
7 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexzh/coffeedrinks/data/CoffeeDrinkRepository.kt:
--------------------------------------------------------------------------------
1 | package com.alexzh.coffeedrinks.data
2 |
3 | import kotlinx.coroutines.flow.Flow
4 |
5 | interface CoffeeDrinkRepository {
6 |
7 | suspend fun getCoffeeDrinks(): Flow>
8 |
9 | suspend fun getCoffeeDrink(id: Long): Flow
10 |
11 | suspend fun updateFavouriteState(id: Long, newFavouriteState: Boolean): Flow
12 | }
13 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexzh/coffeedrinks/data/DummyCoffeeDrinksDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.alexzh.coffeedrinks.data
2 |
3 | import com.alexzh.coffeedrinks.R
4 |
5 | class DummyCoffeeDrinksDataSource : CoffeeDrinkDataSource {
6 |
7 | override fun getCoffeeDrinks(): List {
8 | return mutableListOf(
9 | CoffeeDrink(
10 | id = 1L,
11 | name = "Americano",
12 | imageUrl = R.drawable.americano_small,
13 | description = "Americano is a type of coffee drink prepared by diluting an espresso with hot water, giving it a similar strength to, but different flavour from, traditionally brewed coffee. ",
14 | ingredients = "Espresso, Water",
15 | orderDescription = "150 ml",
16 | price = 6.5,
17 | isFavourite = false
18 | ),
19 | CoffeeDrink(
20 | id = 2L,
21 | name = "Cappuccino",
22 | imageUrl = R.drawable.cappuccino_small,
23 | description = "A cappuccino is an espresso-based coffee drink that originated in Italy, and is traditionally prepared with steamed milk foam.",
24 | ingredients = "Espresso, Steamed milk foam",
25 | orderDescription = "250 ml",
26 | price = 6.0,
27 | isFavourite = false
28 | ),
29 | CoffeeDrink(
30 | id = 3L,
31 | name = "Espresso",
32 | imageUrl = R.drawable.espresso_small,
33 | description = "Espresso is coffee of Italian origin, brewed by forcing a small amount of nearly boiling water under pressure (expressing) through finely-ground coffee beans.",
34 | ingredients = "Ground coffee, Water",
35 | orderDescription = "200 ml",
36 | price = 5.0,
37 | isFavourite = false
38 | ),
39 | CoffeeDrink(
40 | id = 4L,
41 | name = "Espresso Macchiato",
42 | imageUrl = R.drawable.espresso_macchiato_small,
43 | description = "Espresso Macchiato is a coffee beverage (a single or double espresso topped with a dollop of heated, foamed milk).",
44 | ingredients = "Espresso, Foamed milk",
45 | orderDescription = "300 ml",
46 | price = 6.5,
47 | isFavourite = false
48 | ),
49 | CoffeeDrink(
50 | id = 5L,
51 | name = "Frappino",
52 | imageUrl = R.drawable.frappino_small,
53 | description = "Frappino is a blended coffee drinks. It consists of coffee base, blended with ice and other various ingredients, usually topped with whipped cream.",
54 | ingredients = "Espresso, Cold milk, Sugar, Ice cubes, Irish Cream flavoured syrup, Whipped cream, Chocolate sauce",
55 | orderDescription = "400 ml",
56 | price = 6.0,
57 | isFavourite = false
58 | ),
59 | CoffeeDrink(
60 | id = 6L,
61 | name = "Iced Mocha",
62 | imageUrl = R.drawable.iced_mocha_small,
63 | description = "Iced Mocha is a coffee beverage. It based on Espresso and chocolate syrup with cold milk, foam and whipped cream or ice cream.",
64 | ingredients = "Cold coffee, Milk, Chocolate syrup, Whipped cream, Ice cream",
65 | orderDescription = "400 ml",
66 | price = 6.5,
67 | isFavourite = false
68 | ),
69 | CoffeeDrink(
70 | id = 7L,
71 | name = "Irish coffee",
72 | imageUrl = R.drawable.irish_coffee_small,
73 | description = "Irish coffee is a cocktail consisting of hot coffee, Irish whiskey, and sugar stirred, and topped with cream.",
74 | ingredients = "Irish whiskey, Hot strong brewed coffee, Heavy whipping cream, Sugar, Creme de menthe liqueur",
75 | orderDescription = "250 ml",
76 | price = 6.0,
77 | isFavourite = false
78 | ),
79 | CoffeeDrink(
80 | id = 8L,
81 | name = "Latte",
82 | imageUrl = R.drawable.latte_small,
83 | description = "A latte is a coffee drink made with espresso and steamed milk.",
84 | ingredients = "Espresso, Steamed milk",
85 | orderDescription = "300 ml",
86 | price = 6.0,
87 | isFavourite = false
88 | ),
89 | CoffeeDrink(
90 | id = 9L,
91 | name = "Latte Macchiato",
92 | imageUrl = R.drawable.latte_macchiato_small,
93 | description = "Latte Macchiato is a coffee beverage; the name literally means stained milk.",
94 | ingredients = "Espresso, Milk, Milk foam, Flavoured coffee syrup",
95 | orderDescription = "300 ml",
96 | price = 6.5,
97 | isFavourite = false
98 | ),
99 | CoffeeDrink(
100 | id = 10L,
101 | name = "Mocha",
102 | imageUrl = R.drawable.mocha_small,
103 | description = "A Mocha, also called mocaccino, is a chocolate-flavored variant of a Latte.",
104 | ingredients = "Espresso, Chocolate flavoring",
105 | orderDescription = "300 ml",
106 | price = 6.0,
107 | isFavourite = false
108 | )
109 | )
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexzh/coffeedrinks/data/RuntimeCoffeeDrinkRepository.kt:
--------------------------------------------------------------------------------
1 | package com.alexzh.coffeedrinks.data
2 |
3 | import kotlinx.coroutines.flow.Flow
4 | import kotlinx.coroutines.flow.flow
5 | import kotlinx.coroutines.flow.flowOf
6 |
7 | object RuntimeCoffeeDrinkRepository : CoffeeDrinkRepository {
8 | private val coffeeDrinks: MutableList = initCoffeeDrinks()
9 |
10 | override suspend fun getCoffeeDrinks(): Flow> {
11 | return flowOf(coffeeDrinks)
12 | }
13 |
14 | override suspend fun getCoffeeDrink(id: Long): Flow {
15 | return flowOf(
16 | coffeeDrinks.firstOrNull { it.id == id }
17 | )
18 | }
19 |
20 | override suspend fun updateFavouriteState(id: Long, newFavouriteState: Boolean): Flow {
21 | return flow {
22 | val position = coffeeDrinks.indexOfFirst { it.id == id }
23 | val result = if (position > -1) {
24 | val oldCoffeeDrink = coffeeDrinks.first { it.id == id }
25 | val newCoffeeDrink = oldCoffeeDrink.copy(isFavourite = newFavouriteState)
26 | coffeeDrinks.remove(oldCoffeeDrink)
27 | coffeeDrinks.add(position, newCoffeeDrink)
28 | true
29 | } else {
30 | false
31 | }
32 | emit(result)
33 | }
34 | }
35 |
36 | private fun initCoffeeDrinks(): MutableList {
37 | return DummyCoffeeDrinksDataSource().getCoffeeDrinks() as MutableList
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexzh/coffeedrinks/data/order/OrderCoffeeDrink.kt:
--------------------------------------------------------------------------------
1 | package com.alexzh.coffeedrinks.data.order
2 |
3 | import androidx.annotation.DrawableRes
4 |
5 | data class OrderCoffeeDrink(
6 | val id: Long,
7 | val name: String,
8 | @DrawableRes val imageUrl: Int,
9 | val ingredients: String,
10 | val price: Double,
11 | val count: Int
12 | )
13 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexzh/coffeedrinks/data/order/OrderCoffeeDrinkMapper.kt:
--------------------------------------------------------------------------------
1 | package com.alexzh.coffeedrinks.data.order
2 |
3 | import com.alexzh.coffeedrinks.data.CoffeeDrink
4 |
5 | class OrderCoffeeDrinkMapper {
6 |
7 | fun map(coffeeDrink: CoffeeDrink, count: Int = 0): OrderCoffeeDrink {
8 | return OrderCoffeeDrink(
9 | id = coffeeDrink.id,
10 | name = coffeeDrink.name,
11 | imageUrl = coffeeDrink.imageUrl,
12 | ingredients = coffeeDrink.ingredients,
13 | price = coffeeDrink.price,
14 | count = count
15 | )
16 | }
17 |
18 | fun map(coffeeDrinks: List, count: Int = 0): List {
19 | return coffeeDrinks.map { map(it, count) }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexzh/coffeedrinks/data/order/OrderCoffeeDrinksRepository.kt:
--------------------------------------------------------------------------------
1 | package com.alexzh.coffeedrinks.data.order
2 |
3 | import kotlinx.coroutines.flow.Flow
4 |
5 | interface OrderCoffeeDrinksRepository {
6 |
7 | suspend fun getCoffeeDrinks(): Flow>
8 |
9 | suspend fun add(coffeeDrinkId: Long): Flow
10 |
11 | suspend fun remove(coffeeDrinkId: Long): Flow
12 | }
13 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexzh/coffeedrinks/data/order/RuntimeOrderCoffeeDrinksRepository.kt:
--------------------------------------------------------------------------------
1 | package com.alexzh.coffeedrinks.data.order
2 |
3 | import com.alexzh.coffeedrinks.data.CoffeeDrinkDataSource
4 | import kotlinx.coroutines.flow.Flow
5 | import kotlinx.coroutines.flow.flow
6 |
7 | class RuntimeOrderCoffeeDrinksRepository(
8 | private val coffeeDrinkDataSource: CoffeeDrinkDataSource,
9 | private val orderCoffeeDrinkMapper: OrderCoffeeDrinkMapper
10 | ) : OrderCoffeeDrinksRepository {
11 | companion object {
12 | const val MIN_VALUE = 0
13 | const val MAX_VALUE = 99
14 | }
15 |
16 | private val coffeeDrinks = mutableListOf()
17 |
18 | override suspend fun getCoffeeDrinks(): Flow> {
19 | return flow {
20 | if (coffeeDrinks.isEmpty()) {
21 | coffeeDrinks.addAll(
22 | orderCoffeeDrinkMapper.map(coffeeDrinkDataSource.getCoffeeDrinks())
23 | )
24 | }
25 | emit(coffeeDrinks)
26 | }
27 | }
28 |
29 | override suspend fun add(coffeeDrinkId: Long): Flow {
30 | return flow {
31 | val index = coffeeDrinks.indexOfFirst { it.id == coffeeDrinkId }
32 | val result = if (index > -1) {
33 | val coffeeDrink = coffeeDrinks[index]
34 | val newValue =
35 | if (coffeeDrink.count == MAX_VALUE) MAX_VALUE else coffeeDrink.count + 1
36 | coffeeDrinks[index] = coffeeDrink.copy(count = newValue)
37 | true
38 | } else {
39 | false
40 | }
41 | emit(result)
42 | }
43 | }
44 |
45 | override suspend fun remove(coffeeDrinkId: Long): Flow {
46 | return flow {
47 | val index = coffeeDrinks.indexOfFirst { it.id == coffeeDrinkId }
48 | val result = if (index > -1) {
49 | val coffeeDrink = coffeeDrinks[index]
50 | val newValue =
51 | if (coffeeDrink.count == MIN_VALUE) MIN_VALUE else coffeeDrink.count - 1
52 | coffeeDrinks[index] = coffeeDrink.copy(count = newValue)
53 | true
54 | } else {
55 | false
56 | }
57 | emit(result)
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexzh/coffeedrinks/di/AppModules.kt:
--------------------------------------------------------------------------------
1 | package com.alexzh.coffeedrinks.di
2 |
3 | import com.alexzh.coffeedrinks.data.CoffeeDrinkDataSource
4 | import com.alexzh.coffeedrinks.data.CoffeeDrinkRepository
5 | import com.alexzh.coffeedrinks.data.DummyCoffeeDrinksDataSource
6 | import com.alexzh.coffeedrinks.data.RuntimeCoffeeDrinkRepository
7 | import com.alexzh.coffeedrinks.data.order.OrderCoffeeDrinksRepository
8 | import com.alexzh.coffeedrinks.data.order.RuntimeOrderCoffeeDrinksRepository
9 | import com.alexzh.coffeedrinks.ui.screen.coffeedetails.CoffeeDrinkDetailsViewModel
10 | import com.alexzh.coffeedrinks.ui.screen.coffeedetails.mapper.CoffeeDrinkDetailMapper
11 | import com.alexzh.coffeedrinks.ui.screen.coffeedrinks.CoffeeDrinksViewModel
12 | import com.alexzh.coffeedrinks.ui.screen.coffeedrinks.mapper.CoffeeDrinkItemMapper
13 | import com.alexzh.coffeedrinks.ui.screen.order.OrderCoffeeDrinkViewModel
14 | import com.alexzh.coffeedrinks.ui.screen.order.mapper.OrderCoffeeDrinkMapper
15 | import org.koin.androidx.viewmodel.dsl.viewModel
16 | import org.koin.dsl.module
17 |
18 | val dataModule = module {
19 | factory { DummyCoffeeDrinksDataSource() }
20 | single { RuntimeCoffeeDrinkRepository }
21 | single {
22 | RuntimeOrderCoffeeDrinksRepository(
23 | coffeeDrinkDataSource = get(),
24 | orderCoffeeDrinkMapper = get()
25 | )
26 | }
27 | }
28 |
29 | val mapperModule = module {
30 | factory { CoffeeDrinkItemMapper() }
31 | factory { CoffeeDrinkDetailMapper() }
32 | factory { OrderCoffeeDrinkMapper() }
33 |
34 | factory { com.alexzh.coffeedrinks.data.order.OrderCoffeeDrinkMapper() }
35 | }
36 |
37 | val viewModelModule = module {
38 | viewModel {
39 | OrderCoffeeDrinkViewModel(
40 | repository = get()
41 | )
42 | }
43 | viewModel {
44 | CoffeeDrinksViewModel(
45 | repository = get(),
46 | mapper = get()
47 | )
48 | }
49 | viewModel {
50 | CoffeeDrinkDetailsViewModel(
51 | repository = get(),
52 | mapper = get()
53 | )
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexzh/coffeedrinks/ui/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.alexzh.coffeedrinks.ui
2 |
3 | import android.os.Bundle
4 | import androidx.appcompat.app.AppCompatActivity
5 | import com.alexzh.coffeedrinks.R
6 |
7 | class MainActivity : AppCompatActivity() {
8 |
9 | override fun onCreate(savedInstanceState: Bundle?) {
10 | super.onCreate(savedInstanceState)
11 | setContentView(R.layout.activity_main)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexzh/coffeedrinks/ui/Navigation.kt:
--------------------------------------------------------------------------------
1 | package com.alexzh.coffeedrinks.ui
2 |
3 | import android.os.Bundle
4 | import androidx.fragment.app.Fragment
5 | import androidx.navigation.fragment.findNavController
6 | import com.alexzh.coffeedrinks.R
7 | import java.lang.IllegalArgumentException
8 |
9 | enum class Screen {
10 | CoffeeDrinks,
11 | CoffeeDrinkDetails,
12 | OrderCoffeeDrinks
13 | }
14 |
15 | fun Fragment.navigate(from: Screen, to: Screen, bundle: Bundle? = null) {
16 | val id = mapScreenToId(from, to)
17 | if (bundle == null) {
18 | findNavController().navigate(id)
19 | } else {
20 | findNavController().navigate(id, bundle)
21 | }
22 | }
23 |
24 | fun Fragment.navigateToPreviousScreen(from: Screen, to: Screen) {
25 | val id = mapScreenToId(from, to)
26 | findNavController().popBackStack(id, false)
27 | }
28 |
29 | private fun mapScreenToId(from: Screen, to: Screen): Int {
30 | if (to == from) {
31 | throw IllegalArgumentException("Cannot navigate from $from to $to")
32 | }
33 | return when (to) {
34 | Screen.CoffeeDrinks -> R.id.coffeeDrinksFragment
35 | Screen.CoffeeDrinkDetails -> R.id.coffeeDrinkDetailsFragment
36 | Screen.OrderCoffeeDrinks -> R.id.orderCoffeeDrinkFragment
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexzh/coffeedrinks/ui/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.alexzh.coffeedrinks.ui
2 |
3 | import androidx.compose.foundation.isSystemInDarkTheme
4 | import androidx.compose.material.MaterialTheme
5 | import androidx.compose.material.darkColors
6 | import androidx.compose.material.lightColors
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.graphics.Color
9 |
10 | val lightThemeColors = lightColors(
11 | primary = Color(0xFF855446),
12 | primaryVariant = Color(0xFF9C684B),
13 | secondary = Color(0xFF03DAC5),
14 | secondaryVariant = Color(0xFF0AC9F0),
15 | background = Color.White,
16 | surface = Color.White,
17 | error = Color(0xFFB00020),
18 | onPrimary = Color.White,
19 | onSecondary = Color.White,
20 | onBackground = Color.Black,
21 | onSurface = Color.Black,
22 | onError = Color.White
23 | )
24 |
25 | val darkThemeColors = darkColors(
26 | primary = Color(0xFF1F1F1F),
27 | primaryVariant = Color(0xFF3E2723),
28 | secondary = Color(0xFF03DAC5),
29 | background = Color(0xFF121212),
30 | surface = Color.Black,
31 | error = Color(0xFFCF6679),
32 | onPrimary = Color.White,
33 | onSecondary = Color.White,
34 | onBackground = Color.White,
35 | onSurface = Color.White,
36 | onError = Color.Black
37 | )
38 |
39 | @SuppressWarnings
40 | @Composable
41 | fun AppTheme(
42 | content: @Composable () -> Unit
43 | ) {
44 | val colors = if (isSystemInDarkTheme()) {
45 | darkThemeColors
46 | } else {
47 | lightThemeColors
48 | }
49 |
50 | MaterialTheme(
51 | colors = colors,
52 | typography = appTypography,
53 | content = content
54 | )
55 | }
56 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexzh/coffeedrinks/ui/Typography.kt:
--------------------------------------------------------------------------------
1 | package com.alexzh.coffeedrinks.ui
2 |
3 | import androidx.compose.material.Typography
4 | import androidx.compose.ui.text.font.Font
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontStyle
7 | import androidx.compose.ui.text.font.FontWeight
8 | import com.alexzh.coffeedrinks.R
9 |
10 | private val appFontFamily = FontFamily(
11 | fonts = listOf(
12 | Font(
13 | resId = R.font.roboto_black,
14 | weight = FontWeight.W900,
15 | style = FontStyle.Normal
16 | ),
17 | Font(
18 | resId = R.font.roboto_black_italic,
19 | weight = FontWeight.W900,
20 | style = FontStyle.Italic
21 | ),
22 | Font(
23 | resId = R.font.roboto_bold,
24 | weight = FontWeight.W700,
25 | style = FontStyle.Normal
26 | ),
27 | Font(
28 | resId = R.font.roboto_bold_italic,
29 | weight = FontWeight.W700,
30 | style = FontStyle.Italic
31 | ),
32 | Font(
33 | resId = R.font.roboto_light,
34 | weight = FontWeight.W300,
35 | style = FontStyle.Normal
36 | ),
37 | Font(
38 | resId = R.font.roboto_light_italic,
39 | weight = FontWeight.W300,
40 | style = FontStyle.Italic
41 | ),
42 | Font(
43 | resId = R.font.roboto_medium,
44 | weight = FontWeight.W500,
45 | style = FontStyle.Normal
46 | ),
47 | Font(
48 | resId = R.font.roboto_medium_italic,
49 | weight = FontWeight.W500,
50 | style = FontStyle.Italic
51 | ),
52 | Font(
53 | resId = R.font.roboto_regular,
54 | weight = FontWeight.W400,
55 | style = FontStyle.Normal
56 | ),
57 | Font(
58 | resId = R.font.roboto_regular_italic,
59 | weight = FontWeight.W400,
60 | style = FontStyle.Italic
61 | ),
62 | Font(
63 | resId = R.font.roboto_thin,
64 | weight = FontWeight.W100,
65 | style = FontStyle.Normal
66 | ),
67 | Font(
68 | resId = R.font.roboto_thin_italic,
69 | weight = FontWeight.W100,
70 | style = FontStyle.Italic
71 | )
72 | )
73 | )
74 |
75 | private val defaultTypography = Typography()
76 | val appTypography = Typography(
77 | h1 = defaultTypography.h1.copy(fontFamily = appFontFamily),
78 | h2 = defaultTypography.h2.copy(fontFamily = appFontFamily),
79 | h3 = defaultTypography.h3.copy(fontFamily = appFontFamily),
80 | h4 = defaultTypography.h4.copy(fontFamily = appFontFamily),
81 | h5 = defaultTypography.h5.copy(fontFamily = appFontFamily),
82 | h6 = defaultTypography.h6.copy(fontFamily = appFontFamily),
83 | subtitle1 = defaultTypography.subtitle1.copy(fontFamily = appFontFamily),
84 | subtitle2 = defaultTypography.subtitle2.copy(fontFamily = appFontFamily),
85 | body1 = defaultTypography.body1.copy(fontFamily = appFontFamily),
86 | body2 = defaultTypography.body2.copy(fontFamily = appFontFamily),
87 | button = defaultTypography.button.copy(fontFamily = appFontFamily),
88 | caption = defaultTypography.caption.copy(fontFamily = appFontFamily),
89 | overline = defaultTypography.overline.copy(fontFamily = appFontFamily)
90 | )
91 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexzh/coffeedrinks/ui/component/Counter.kt:
--------------------------------------------------------------------------------
1 | package com.alexzh.coffeedrinks.ui.component
2 |
3 | import androidx.compose.animation.AnimatedContent
4 | import androidx.compose.animation.ExperimentalAnimationApi
5 | import androidx.compose.animation.SizeTransform
6 | import androidx.compose.animation.fadeIn
7 | import androidx.compose.animation.fadeOut
8 | import androidx.compose.animation.slideInVertically
9 | import androidx.compose.animation.slideOutVertically
10 | import androidx.compose.animation.with
11 | import androidx.compose.foundation.BorderStroke
12 | import androidx.compose.foundation.layout.PaddingValues
13 | import androidx.compose.foundation.layout.Row
14 | import androidx.compose.foundation.layout.height
15 | import androidx.compose.foundation.shape.RoundedCornerShape
16 | import androidx.compose.material.MaterialTheme
17 | import androidx.compose.material.Surface
18 | import androidx.compose.material.Text
19 | import androidx.compose.material.TextButton
20 | import androidx.compose.runtime.Composable
21 | import androidx.compose.runtime.mutableStateOf
22 | import androidx.compose.runtime.remember
23 | import androidx.compose.ui.Alignment
24 | import androidx.compose.ui.Modifier
25 | import androidx.compose.ui.graphics.Color
26 | import androidx.compose.ui.res.stringResource
27 | import androidx.compose.ui.text.style.TextAlign
28 | import androidx.compose.ui.tooling.preview.Preview
29 | import androidx.compose.ui.unit.dp
30 | import com.alexzh.coffeedrinks.R
31 |
32 | @ExperimentalAnimationApi
33 | @Composable
34 | fun Counter(
35 | value: Int,
36 | onIncrease: () -> Unit,
37 | onDecrease: () -> Unit
38 | ) {
39 | Surface(
40 | shape = RoundedCornerShape(size = 5.dp),
41 | border = BorderStroke(1.dp, Color.LightGray),
42 | color = Color.White,
43 | modifier = Modifier.height(36.dp)
44 | ) {
45 | Row(
46 | verticalAlignment = Alignment.CenterVertically,
47 | ) {
48 | TextButton(
49 | onClick = { onDecrease() },
50 | contentPadding = PaddingValues(2.dp),
51 | modifier = Modifier.weight(1f)
52 | ) {
53 | Text(
54 | text = stringResource(R.string.counter_decrease),
55 | style = MaterialTheme.typography.body1,
56 | color = MaterialTheme.colors.onBackground,
57 | )
58 | }
59 |
60 | AnimatedContent(
61 | modifier = Modifier.weight(1f),
62 | targetState = value,
63 | transitionSpec = {
64 | if (targetState > initialState) {
65 | slideInVertically { height -> height } + fadeIn() with
66 | slideOutVertically { height -> -height } + fadeOut()
67 | } else {
68 | slideInVertically { height -> -height } + fadeIn() with
69 | slideOutVertically { height -> height } + fadeOut()
70 | }.using(
71 | SizeTransform(clip = false)
72 | )
73 | }
74 | ) { targetValue ->
75 | Text(
76 | text = "$targetValue",
77 | style = MaterialTheme.typography.subtitle1.copy(textAlign = TextAlign.Center),
78 | color = MaterialTheme.colors.onBackground,
79 | )
80 | }
81 |
82 | TextButton(
83 | onClick = { onIncrease() },
84 | contentPadding = PaddingValues(2.dp),
85 | modifier = Modifier.weight(1f)
86 | ) {
87 | Text(
88 | text = stringResource(R.string.counter_increase),
89 | style = MaterialTheme.typography.body1,
90 | color = MaterialTheme.colors.onBackground
91 | )
92 | }
93 | }
94 | }
95 | }
96 |
97 | @OptIn(ExperimentalAnimationApi::class)
98 | @Preview
99 | @Composable
100 | fun Preview_Counter() {
101 | val state = remember { mutableStateOf(0) }
102 | Counter(
103 | value = state.value,
104 | onIncrease = { state.value++ },
105 | onDecrease = { state.value-- },
106 | )
107 | }
108 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexzh/coffeedrinks/ui/component/Divider.kt:
--------------------------------------------------------------------------------
1 | package com.alexzh.coffeedrinks.ui.component
2 |
3 | import androidx.compose.foundation.isSystemInDarkTheme
4 | import androidx.compose.foundation.layout.PaddingValues
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.material.Divider
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.Modifier
9 | import androidx.compose.ui.draw.alpha
10 | import androidx.compose.ui.graphics.Color
11 |
12 | @Composable
13 | fun AppDivider(
14 | padding: PaddingValues = PaddingValues()
15 | ) {
16 | Divider(
17 | modifier = Modifier
18 | .padding(padding)
19 | .alpha(0.12f),
20 | color = if (isSystemInDarkTheme()) {
21 | Color.White
22 | } else {
23 | Color.Black
24 | }
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexzh/coffeedrinks/ui/component/Favourite.kt:
--------------------------------------------------------------------------------
1 | package com.alexzh.coffeedrinks.ui.component
2 |
3 | import androidx.annotation.DrawableRes
4 | import androidx.annotation.StringRes
5 | import androidx.compose.animation.Crossfade
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.foundation.layout.padding
8 | import androidx.compose.foundation.layout.size
9 | import androidx.compose.foundation.selection.toggleable
10 | import androidx.compose.material.Icon
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.LaunchedEffect
13 | import androidx.compose.runtime.MutableState
14 | import androidx.compose.runtime.mutableStateOf
15 | import androidx.compose.runtime.remember
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.graphics.Color
18 | import androidx.compose.ui.res.painterResource
19 | import androidx.compose.ui.res.stringResource
20 | import androidx.compose.ui.tooling.preview.Preview
21 | import androidx.compose.ui.unit.dp
22 | import com.alexzh.coffeedrinks.R
23 |
24 | @Composable
25 | fun Favourite(
26 | state: MutableState,
27 | modifier: Modifier = Modifier,
28 | onValueChanged: (Boolean) -> Unit,
29 | @DrawableRes favouriteVectorId: Int = R.drawable.ic_baseline_favorite_24,
30 | @DrawableRes nonFavouriteVectorId: Int = R.drawable.ic_baseline_favorite_border_24,
31 | @StringRes favouriteContentDescription: Int = R.string.mark_as_favorite,
32 | @StringRes nonFavouriteContentDescription: Int = R.string.unmark_as_favorite,
33 | tint: Color = Color.Red
34 | ) {
35 | Crossfade(
36 | modifier = modifier.size(48.dp)
37 | .padding(8.dp)
38 | .toggleable(value = state.value, onValueChange = onValueChanged),
39 | targetState = state.value
40 | ) { isFavourite ->
41 | if (isFavourite) {
42 | Icon(
43 | painter = painterResource(favouriteVectorId),
44 | contentDescription = stringResource(favouriteContentDescription),
45 | modifier = Modifier.fillMaxSize(),
46 | tint = tint
47 | )
48 | } else {
49 | Icon(
50 | painter = painterResource(nonFavouriteVectorId),
51 | contentDescription = stringResource(nonFavouriteContentDescription),
52 | modifier = Modifier.fillMaxSize(),
53 | tint = tint
54 | )
55 | }
56 | }
57 | }
58 |
59 | @Preview
60 | @Composable
61 | fun Preview_Favourite() {
62 | val state = remember { mutableStateOf(false) }
63 |
64 | LaunchedEffect("LaunchAnimation") {
65 | state.value = true
66 | }
67 | Favourite(
68 | state = state,
69 | modifier = Modifier.size(100.dp),
70 | onValueChanged = { },
71 | tint = Color.Red
72 | )
73 | }
74 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexzh/coffeedrinks/ui/screen/coffeedetails/CoffeeDrinkDetailsFragment.kt:
--------------------------------------------------------------------------------
1 | package com.alexzh.coffeedrinks.ui.screen.coffeedetails
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import androidx.compose.runtime.livedata.observeAsState
8 | import androidx.compose.ui.platform.ComposeView
9 | import androidx.fragment.app.Fragment
10 | import com.alexzh.coffeedrinks.R
11 | import com.alexzh.coffeedrinks.ui.AppTheme
12 | import com.alexzh.coffeedrinks.ui.Screen
13 | import com.alexzh.coffeedrinks.ui.navigateToPreviousScreen
14 | import com.alexzh.coffeedrinks.ui.state.UiState
15 | import org.koin.androidx.viewmodel.ext.android.viewModel
16 |
17 | class CoffeeDrinkDetailsFragment : Fragment() {
18 | private val viewModel: CoffeeDrinkDetailsViewModel by viewModel()
19 |
20 | override fun onCreateView(
21 | inflater: LayoutInflater,
22 | container: ViewGroup?,
23 | savedInstanceState: Bundle?
24 | ): View {
25 | return ComposeView(requireContext()).apply {
26 | id = R.id.coffeeDrinkDetailsFragment
27 | layoutParams = ViewGroup.LayoutParams(
28 | ViewGroup.LayoutParams.MATCH_PARENT,
29 | ViewGroup.LayoutParams.MATCH_PARENT
30 | )
31 |
32 | setContent {
33 | AppTheme {
34 | viewModel.uiState.observeAsState(initial = UiState.Loading).value.let { uiState ->
35 | when (uiState) {
36 | is UiState.Loading -> {
37 | ShowLoadingCoffeeDrinkDetailsScreen()
38 | }
39 | is UiState.Success -> {
40 | ShowSuccessCoffeeDrinkDetailsScreen(
41 | onBack = {
42 | this@CoffeeDrinkDetailsFragment.navigateToPreviousScreen(
43 | Screen.CoffeeDrinkDetails,
44 | Screen.CoffeeDrinks
45 | )
46 | },
47 | coffeeDrinkDetailState = uiState.data,
48 | viewModel = viewModel
49 | )
50 | }
51 | is UiState.Error -> {
52 | ShowErrorCoffeeDrinkDetailsScreen()
53 | }
54 | }
55 | }
56 | }
57 | }
58 | }
59 | }
60 |
61 | override fun onResume() {
62 | super.onResume()
63 | viewModel.loadCoffeeDrinkDetails(arguments?.getLong("id") ?: -1)
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexzh/coffeedrinks/ui/screen/coffeedetails/CoffeeDrinkDetailsScreen.kt:
--------------------------------------------------------------------------------
1 | package com.alexzh.coffeedrinks.ui.screen.coffeedetails
2 |
3 | import androidx.compose.foundation.Image
4 | import androidx.compose.foundation.background
5 | import androidx.compose.foundation.isSystemInDarkTheme
6 | import androidx.compose.foundation.layout.Box
7 | import androidx.compose.foundation.layout.fillMaxSize
8 | import androidx.compose.foundation.layout.fillMaxWidth
9 | import androidx.compose.foundation.layout.height
10 | import androidx.compose.foundation.layout.size
11 | import androidx.compose.foundation.shape.CircleShape
12 | import androidx.compose.material.FloatingActionButton
13 | import androidx.compose.material.Icon
14 | import androidx.compose.material.IconButton
15 | import androidx.compose.material.MaterialTheme
16 | import androidx.compose.material.Text
17 | import androidx.compose.material.TopAppBar
18 | import androidx.compose.runtime.Composable
19 | import androidx.compose.ui.Modifier
20 | import androidx.compose.ui.draw.alpha
21 | import androidx.compose.ui.draw.paint
22 | import androidx.compose.ui.graphics.Color
23 | import androidx.compose.ui.graphics.ImageBitmap
24 | import androidx.compose.ui.graphics.painter.BitmapPainter
25 | import androidx.compose.ui.graphics.painter.ColorPainter
26 | import androidx.compose.ui.res.imageResource
27 | import androidx.compose.ui.res.stringResource
28 | import androidx.compose.ui.text.style.TextAlign
29 | import androidx.compose.ui.unit.dp
30 | import androidx.constraintlayout.compose.ConstraintLayout
31 | import androidx.constraintlayout.compose.Dimension
32 | import com.alexzh.coffeedrinks.R
33 | import com.alexzh.coffeedrinks.ui.screen.coffeedetails.model.CoffeeDrinkDetailState
34 |
35 | @Composable
36 | fun ShowLoadingCoffeeDrinkDetailsScreen() {
37 | // TODO: implement it
38 | }
39 |
40 | @Composable
41 | fun ShowSuccessCoffeeDrinkDetailsScreen(
42 | onBack: () -> Unit,
43 | coffeeDrinkDetailState: CoffeeDrinkDetailState,
44 | viewModel: CoffeeDrinkDetailsViewModel
45 | ) {
46 | CoffeeDrinkDetailsScreenUI(
47 | onBack = onBack,
48 | coffeeDrinkDetailState = coffeeDrinkDetailState,
49 | viewModel = viewModel
50 | )
51 | }
52 |
53 | @Composable
54 | fun ShowErrorCoffeeDrinkDetailsScreen() {
55 | // TODO: implement it
56 | }
57 |
58 | @Composable
59 | private fun CoffeeDrinkDetailsScreenUI(
60 | onBack: () -> Unit,
61 | coffeeDrinkDetailState: CoffeeDrinkDetailState,
62 | viewModel: CoffeeDrinkDetailsViewModel
63 | ) {
64 | ConstraintLayout {
65 | val startGuideline = createGuidelineFromStart(16.dp)
66 | val endGuideline = createGuidelineFromEnd(16.dp)
67 | val (surface, header, appBar, fab, name, logo, description, ingredients) = createRefs()
68 |
69 | Box(
70 | modifier = Modifier
71 | .constrainAs(surface) { centerTo(parent) }
72 | .fillMaxSize()
73 | .background(color = MaterialTheme.colors.surface)
74 | )
75 |
76 | Box(
77 | modifier = Modifier
78 | .constrainAs(header) { centerHorizontallyTo(surface) }
79 | .fillMaxWidth()
80 | .height(220.dp)
81 | .paint(
82 | painter = if (isSystemInDarkTheme()) {
83 | ColorPainter(Color.White)
84 | } else {
85 | ColorPainter(MaterialTheme.colors.primary)
86 | },
87 | alpha = 0.95f
88 | )
89 | )
90 |
91 | TopAppBar(
92 | title = { },
93 | backgroundColor = Color.Transparent,
94 | elevation = 0.dp,
95 | modifier = Modifier.constrainAs(appBar) { centerHorizontallyTo(header) },
96 | navigationIcon = {
97 | IconButton(
98 | onClick = { onBack() }
99 | ) {
100 | Icon(
101 | painter = BitmapPainter(image = ImageBitmap.imageResource(id = R.drawable.ic_arrow_back_white)),
102 | contentDescription = stringResource(R.string.action_back),
103 | tint = if (isSystemInDarkTheme()) {
104 | Color.Black
105 | } else {
106 | MaterialTheme.colors.onPrimary
107 | }
108 | )
109 | }
110 | }
111 | )
112 |
113 | Image(
114 | modifier = Modifier
115 | .constrainAs(logo) { centerTo(header) }
116 | .size(180.dp),
117 | painter = BitmapPainter(ImageBitmap.imageResource(id = R.drawable.americano_small)),
118 | contentDescription = null
119 | )
120 |
121 | FloatingActionButton(
122 | modifier = Modifier.constrainAs(fab) {
123 | linkTo(header.bottom, header.bottom)
124 | bottom.linkTo(header.bottom)
125 | end.linkTo(endGuideline)
126 | },
127 | shape = CircleShape,
128 | backgroundColor = MaterialTheme.colors.secondary,
129 | onClick = {
130 | viewModel.changeFavouriteState(coffeeDrinkDetailState.coffeeDrinks)
131 | }
132 | ) {
133 | Icon(
134 | painter = BitmapPainter(
135 | ImageBitmap.imageResource(
136 | if (coffeeDrinkDetailState.coffeeDrinks.isFavourite) {
137 | R.drawable.ic_favorite_white
138 | } else {
139 | R.drawable.ic_favorite_border_white
140 | }
141 | )
142 | ),
143 | contentDescription = if (coffeeDrinkDetailState.coffeeDrinks.isFavourite) {
144 | stringResource(R.string.mark_as_favorite)
145 | } else {
146 | stringResource(R.string.unmark_as_favorite)
147 | },
148 | tint = MaterialTheme.colors.onSecondary
149 | )
150 | }
151 |
152 | Text(
153 | text = coffeeDrinkDetailState.coffeeDrinks.name,
154 | style = MaterialTheme.typography.h4.copy(MaterialTheme.colors.onSurface),
155 | modifier = Modifier.constrainAs(name) {
156 | top.linkTo(header.bottom, margin = 16.dp)
157 | linkTo(startGuideline, endGuideline)
158 | width = Dimension.fillToConstraints
159 | }
160 | )
161 |
162 | Text(
163 | text = coffeeDrinkDetailState.coffeeDrinks.description,
164 | style = MaterialTheme.typography.body1.copy(
165 | color = MaterialTheme.colors.onSurface,
166 | textAlign = TextAlign.Justify
167 | ),
168 | modifier = Modifier
169 | .constrainAs(description) {
170 | top.linkTo(name.bottom, margin = 8.dp)
171 | linkTo(startGuideline, endGuideline)
172 | width = Dimension.fillToConstraints
173 | }
174 | .alpha(0.54f)
175 | )
176 |
177 | Text(
178 | text = coffeeDrinkDetailState.coffeeDrinks.ingredients,
179 | style = MaterialTheme.typography.body1.copy(
180 | color = MaterialTheme.colors.onSurface,
181 | textAlign = TextAlign.Justify
182 | ),
183 | modifier = Modifier
184 | .constrainAs(ingredients) {
185 | top.linkTo(description.bottom, margin = 8.dp)
186 | linkTo(startGuideline, endGuideline)
187 | width = Dimension.fillToConstraints
188 | }
189 | .alpha(0.54f)
190 | )
191 | }
192 | }
193 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexzh/coffeedrinks/ui/screen/coffeedetails/CoffeeDrinkDetailsViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.alexzh.coffeedrinks.ui.screen.coffeedetails
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MutableLiveData
5 | import androidx.lifecycle.ViewModel
6 | import androidx.lifecycle.viewModelScope
7 | import com.alexzh.coffeedrinks.data.CoffeeDrinkRepository
8 | import com.alexzh.coffeedrinks.ui.screen.coffeedetails.exception.NoCoffeeDrinkFoundException
9 | import com.alexzh.coffeedrinks.ui.screen.coffeedetails.mapper.CoffeeDrinkDetailMapper
10 | import com.alexzh.coffeedrinks.ui.screen.coffeedetails.model.CoffeeDrinkDetail
11 | import com.alexzh.coffeedrinks.ui.screen.coffeedetails.model.CoffeeDrinkDetailState
12 | import com.alexzh.coffeedrinks.ui.state.UiState
13 | import kotlinx.coroutines.flow.collect
14 | import kotlinx.coroutines.flow.map
15 | import kotlinx.coroutines.launch
16 |
17 | class CoffeeDrinkDetailsViewModel(
18 | private val repository: CoffeeDrinkRepository,
19 | private val mapper: CoffeeDrinkDetailMapper
20 | ) : ViewModel() {
21 | private val _uiState: MutableLiveData> = MutableLiveData()
22 | val uiState: LiveData>
23 | get() = _uiState
24 |
25 | fun loadCoffeeDrinkDetails(coffeeDrinkId: Long) {
26 | viewModelScope.launch {
27 | _uiState.value = UiState.Loading
28 | repository.getCoffeeDrink(coffeeDrinkId)
29 | .map { mapper.map(it) }
30 | .collect { coffeeDrink ->
31 | if (coffeeDrink != null) {
32 | _uiState.value = UiState.Success(CoffeeDrinkDetailState(coffeeDrink))
33 | } else {
34 | _uiState.value = UiState.Error(NoCoffeeDrinkFoundException())
35 | }
36 | }
37 | }
38 | }
39 |
40 | fun changeFavouriteState(coffeeDrink: CoffeeDrinkDetail) {
41 | viewModelScope.launch {
42 | repository.updateFavouriteState(coffeeDrink.id, !coffeeDrink.isFavourite)
43 | .collect { result ->
44 | if (result) {
45 | loadCoffeeDrinkDetails(coffeeDrink.id)
46 | }
47 | }
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexzh/coffeedrinks/ui/screen/coffeedetails/exception/NoCoffeeDrinkFoundException.kt:
--------------------------------------------------------------------------------
1 | package com.alexzh.coffeedrinks.ui.screen.coffeedetails.exception
2 |
3 | import java.lang.Exception
4 |
5 | class NoCoffeeDrinkFoundException : Exception("No coffee drink found")
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexzh/coffeedrinks/ui/screen/coffeedetails/mapper/CoffeeDrinkDetailMapper.kt:
--------------------------------------------------------------------------------
1 | package com.alexzh.coffeedrinks.ui.screen.coffeedetails.mapper
2 |
3 | import com.alexzh.coffeedrinks.data.CoffeeDrink
4 | import com.alexzh.coffeedrinks.ui.screen.coffeedetails.model.CoffeeDrinkDetail
5 |
6 | class CoffeeDrinkDetailMapper {
7 |
8 | fun map(coffeeDrink: CoffeeDrink?): CoffeeDrinkDetail? {
9 | if (coffeeDrink == null) {
10 | return null
11 | }
12 |
13 | return CoffeeDrinkDetail(
14 | id = coffeeDrink.id,
15 | name = coffeeDrink.name,
16 | imageUrl = coffeeDrink.imageUrl,
17 | ingredients = coffeeDrink.ingredients,
18 | description = coffeeDrink.description,
19 | isFavourite = coffeeDrink.isFavourite
20 | )
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexzh/coffeedrinks/ui/screen/coffeedetails/model/CoffeeDrinkDetail.kt:
--------------------------------------------------------------------------------
1 | package com.alexzh.coffeedrinks.ui.screen.coffeedetails.model
2 |
3 | data class CoffeeDrinkDetail(
4 | val id: Long,
5 | val name: String,
6 | val imageUrl: Int,
7 | val ingredients: String,
8 | val description: String,
9 | var isFavourite: Boolean
10 | )
11 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexzh/coffeedrinks/ui/screen/coffeedetails/model/CoffeeDrinkDetailState.kt:
--------------------------------------------------------------------------------
1 | package com.alexzh.coffeedrinks.ui.screen.coffeedetails.model
2 |
3 | data class CoffeeDrinkDetailState(
4 | val coffeeDrinks: CoffeeDrinkDetail
5 | )
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexzh/coffeedrinks/ui/screen/coffeedrinks/CoffeeDrinksFragment.kt:
--------------------------------------------------------------------------------
1 | package com.alexzh.coffeedrinks.ui.screen.coffeedrinks
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import androidx.compose.runtime.livedata.observeAsState
8 | import androidx.compose.ui.platform.ComposeView
9 | import androidx.core.os.bundleOf
10 | import androidx.fragment.app.Fragment
11 | import com.alexzh.coffeedrinks.R
12 | import com.alexzh.coffeedrinks.ui.AppTheme
13 | import com.alexzh.coffeedrinks.ui.Screen
14 | import com.alexzh.coffeedrinks.ui.navigate
15 | import com.alexzh.coffeedrinks.ui.state.UiState
16 | import org.koin.androidx.viewmodel.ext.android.viewModel
17 |
18 | class CoffeeDrinksFragment : Fragment() {
19 | private val viewModel: CoffeeDrinksViewModel by viewModel()
20 |
21 | override fun onCreateView(
22 | inflater: LayoutInflater,
23 | container: ViewGroup?,
24 | savedInstanceState: Bundle?
25 | ): View {
26 | return ComposeView(requireContext()).apply {
27 | id = R.id.coffeeDrinkDetailsFragment
28 | layoutParams = ViewGroup.LayoutParams(
29 | ViewGroup.LayoutParams.MATCH_PARENT,
30 | ViewGroup.LayoutParams.MATCH_PARENT
31 | )
32 |
33 | setContent {
34 | AppTheme {
35 | viewModel.uiState.observeAsState(initial = UiState.Loading).value.let { uiState ->
36 | when (uiState) {
37 | is UiState.Loading -> {
38 | ShowLoadingCoffeeDrinksScreen()
39 | }
40 | is UiState.Success -> {
41 | ShowSuccessCoffeeDrinksScreen(
42 | coffeeDrinksState = uiState.data,
43 | viewModel = viewModel,
44 | onOrderCoffeeDrinksMenuItem = {
45 | this@CoffeeDrinksFragment.navigate(
46 | Screen.CoffeeDrinks,
47 | Screen.OrderCoffeeDrinks
48 | )
49 | },
50 | onCoffeeDrinkClicked = { coffeeDrink ->
51 | this@CoffeeDrinksFragment.navigate(
52 | Screen.CoffeeDrinks,
53 | Screen.CoffeeDrinkDetails,
54 | bundleOf("id" to coffeeDrink.id)
55 | )
56 | }
57 | )
58 | }
59 | is UiState.Error -> {
60 | ShowErrorCoffeeDrinksScreen()
61 | }
62 | }
63 | }
64 | }
65 | }
66 | }
67 | }
68 |
69 | override fun onResume() {
70 | super.onResume()
71 | viewModel.loadCoffeeDrinks()
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexzh/coffeedrinks/ui/screen/coffeedrinks/CoffeeDrinksScreen.kt:
--------------------------------------------------------------------------------
1 | package com.alexzh.coffeedrinks.ui.screen.coffeedrinks
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.foundation.lazy.LazyColumn
8 | import androidx.compose.foundation.lazy.items
9 | import androidx.compose.material.Icon
10 | import androidx.compose.material.IconButton
11 | import androidx.compose.material.MaterialTheme
12 | import androidx.compose.material.Surface
13 | import androidx.compose.material.Text
14 | import androidx.compose.material.TopAppBar
15 | import androidx.compose.runtime.Composable
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.graphics.ImageBitmap
18 | import androidx.compose.ui.graphics.painter.BitmapPainter
19 | import androidx.compose.ui.res.imageResource
20 | import androidx.compose.ui.res.stringResource
21 | import androidx.compose.ui.unit.dp
22 | import com.alexzh.coffeedrinks.R
23 | import com.alexzh.coffeedrinks.ui.screen.coffeedrinks.model.CoffeeDrinkItem
24 | import com.alexzh.coffeedrinks.ui.screen.coffeedrinks.model.CoffeeDrinksState
25 | import com.alexzh.coffeedrinks.ui.screen.coffeedrinks.model.DisplayingOptions
26 |
27 | @Composable
28 | fun ShowLoadingCoffeeDrinksScreen() {
29 | // TODO: implement it
30 | }
31 |
32 | @Composable
33 | fun ShowSuccessCoffeeDrinksScreen(
34 | coffeeDrinksState: CoffeeDrinksState,
35 | viewModel: CoffeeDrinksViewModel,
36 | onOrderCoffeeDrinksMenuItem: () -> Unit,
37 | onCoffeeDrinkClicked: (CoffeeDrinkItem) -> Unit
38 | ) {
39 | CoffeeDrinksScreenUI(
40 | coffeeDrinksState = coffeeDrinksState,
41 | viewModel = viewModel,
42 | onOrderCoffeeDrinksMenuItem,
43 | onCoffeeDrinkClicked
44 | )
45 | }
46 |
47 | @Composable
48 | fun ShowErrorCoffeeDrinksScreen() {
49 | // TODO: implement it
50 | }
51 |
52 | @Composable
53 | fun CoffeeDrinksScreenUI(
54 | coffeeDrinksState: CoffeeDrinksState,
55 | viewModel: CoffeeDrinksViewModel,
56 | onOrderCoffeeDrinksMenuItem: () -> Unit,
57 | onCoffeeDrinkClicked: (CoffeeDrinkItem) -> Unit
58 | ) {
59 | Surface {
60 | Column {
61 | CoffeeDrinkAppBar(
62 | coffeeDrinksState.displayingOption,
63 | onChangeDisplayOption = { viewModel.changeDisplayingOption() },
64 | onOrderCoffeeDrinksMenuItem = { onOrderCoffeeDrinksMenuItem() }
65 | )
66 | CoffeeDrinkList(
67 | coffeeDrinksState = coffeeDrinksState,
68 | onCoffeeDrinkClicked = { coffeeDrink -> onCoffeeDrinkClicked(coffeeDrink) },
69 | onFavouriteStateChanged = { viewModel.changeFavouriteState(it) }
70 | )
71 | }
72 | }
73 | }
74 |
75 | @Composable
76 | fun CoffeeDrinkAppBar(
77 | displayingOption: DisplayingOptions,
78 | onChangeDisplayOption: () -> Unit,
79 | onOrderCoffeeDrinksMenuItem: () -> Unit
80 | ) {
81 | TopAppBar(
82 | title = {
83 | Text(
84 | text = "Coffee Drinks",
85 | style = MaterialTheme.typography.h6.copy(
86 | color = MaterialTheme.colors.onPrimary
87 | )
88 | )
89 | },
90 | backgroundColor = MaterialTheme.colors.primary,
91 | actions = {
92 | IconButton(
93 | onClick = { onChangeDisplayOption() }
94 | ) {
95 | Icon(
96 | painter = BitmapPainter(
97 | ImageBitmap.imageResource(id = if (displayingOption == DisplayingOptions.CARDS) R.drawable.ic_list_white else R.drawable.ic_extended_list_white)
98 | ),
99 | contentDescription = stringResource(R.string.action_show_detailed_cards),
100 | tint = MaterialTheme.colors.onPrimary
101 | )
102 | }
103 | IconButton(
104 | onClick = { onOrderCoffeeDrinksMenuItem() }
105 | ) {
106 | Icon(
107 | painter = BitmapPainter(ImageBitmap.imageResource(id = R.drawable.ic_order_white)),
108 | tint = MaterialTheme.colors.onPrimary,
109 | contentDescription = stringResource(R.string.action_order_coffee_drinks)
110 | )
111 | }
112 | }
113 | )
114 | }
115 |
116 | @Composable
117 | fun CoffeeDrinkList(
118 | coffeeDrinksState: CoffeeDrinksState,
119 | onCoffeeDrinkClicked: (CoffeeDrinkItem) -> Unit,
120 | onFavouriteStateChanged: (CoffeeDrinkItem) -> Unit
121 | ) {
122 | LazyColumn {
123 | items(items = coffeeDrinksState.coffeeDrinks) { coffeeDrink ->
124 | Box(
125 | modifier = Modifier.clickable(
126 | onClick = {
127 | onCoffeeDrinkClicked(coffeeDrink)
128 | }
129 | )
130 | ) {
131 | if (coffeeDrinksState.displayingOption == DisplayingOptions.CARDS) {
132 | Box(modifier = Modifier.padding(8.dp)) {
133 | CoffeeDrinkDetailedItem(
134 | coffeeDrink = coffeeDrink,
135 | onFavouriteStateChanged = { onFavouriteStateChanged(it) }
136 | )
137 | }
138 | } else {
139 | CoffeeDrinkList(
140 | coffeeDrink = coffeeDrink,
141 | onFavouriteStateChanged = { onFavouriteStateChanged(it) }
142 | )
143 | }
144 | }
145 | }
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexzh/coffeedrinks/ui/screen/coffeedrinks/CoffeeDrinksViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.alexzh.coffeedrinks.ui.screen.coffeedrinks
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MutableLiveData
5 | import androidx.lifecycle.ViewModel
6 | import androidx.lifecycle.viewModelScope
7 | import com.alexzh.coffeedrinks.data.CoffeeDrinkRepository
8 | import com.alexzh.coffeedrinks.ui.screen.coffeedrinks.mapper.CoffeeDrinkItemMapper
9 | import com.alexzh.coffeedrinks.ui.screen.coffeedrinks.model.CoffeeDrinkItem
10 | import com.alexzh.coffeedrinks.ui.screen.coffeedrinks.model.CoffeeDrinksState
11 | import com.alexzh.coffeedrinks.ui.screen.coffeedrinks.model.DisplayingOptions
12 | import com.alexzh.coffeedrinks.ui.state.UiState
13 | import kotlinx.coroutines.flow.collect
14 | import kotlinx.coroutines.flow.map
15 | import kotlinx.coroutines.launch
16 |
17 | class CoffeeDrinksViewModel(
18 | private val repository: CoffeeDrinkRepository,
19 | private val mapper: CoffeeDrinkItemMapper
20 | ) : ViewModel() {
21 | private var currentDisplayingOption = DisplayingOptions.LIST
22 |
23 | private val _uiState: MutableLiveData> = MutableLiveData()
24 | val uiState: LiveData>
25 | get() = _uiState
26 |
27 | fun loadCoffeeDrinks() {
28 | viewModelScope.launch {
29 | _uiState.value = UiState.Loading
30 | repository.getCoffeeDrinks()
31 | .map { coffeeDrinks ->
32 | coffeeDrinks.map { mapper.map(it) }
33 | }
34 | .collect {
35 | _uiState.value = UiState.Success(
36 | CoffeeDrinksState(
37 | it,
38 | currentDisplayingOption
39 | )
40 | )
41 | }
42 | }
43 | }
44 |
45 | fun changeDisplayingOption() {
46 | when (val state = _uiState.value) {
47 | is UiState.Success -> {
48 | currentDisplayingOption = if (currentDisplayingOption == DisplayingOptions.LIST) {
49 | DisplayingOptions.CARDS
50 | } else {
51 | DisplayingOptions.LIST
52 | }
53 | _uiState.value = UiState.Success(state.data.copy(displayingOption = currentDisplayingOption))
54 | }
55 | else -> loadCoffeeDrinks()
56 | }
57 | }
58 |
59 | fun changeFavouriteState(coffeeDrink: CoffeeDrinkItem) {
60 | viewModelScope.launch {
61 | repository.updateFavouriteState(coffeeDrink.id, !coffeeDrink.isFavourite)
62 | .collect { result ->
63 | if (result) {
64 | loadCoffeeDrinks()
65 | }
66 | }
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexzh/coffeedrinks/ui/screen/coffeedrinks/DetailedListItem.kt:
--------------------------------------------------------------------------------
1 | package com.alexzh.coffeedrinks.ui.screen.coffeedrinks
2 |
3 | import androidx.compose.foundation.Image
4 | import androidx.compose.foundation.background
5 | import androidx.compose.foundation.layout.Box
6 | import androidx.compose.foundation.layout.Column
7 | import androidx.compose.foundation.layout.fillMaxWidth
8 | import androidx.compose.foundation.layout.height
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.foundation.layout.size
11 | import androidx.compose.material.Card
12 | import androidx.compose.material.MaterialTheme
13 | import androidx.compose.material.Text
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.runtime.mutableStateOf
16 | import androidx.compose.runtime.remember
17 | import androidx.compose.ui.Alignment
18 | import androidx.compose.ui.Modifier
19 | import androidx.compose.ui.draw.alpha
20 | import androidx.compose.ui.graphics.ImageBitmap
21 | import androidx.compose.ui.graphics.painter.BitmapPainter
22 | import androidx.compose.ui.res.imageResource
23 | import androidx.compose.ui.text.style.TextOverflow
24 | import androidx.compose.ui.tooling.preview.Preview
25 | import androidx.compose.ui.unit.dp
26 | import com.alexzh.coffeedrinks.data.DummyCoffeeDrinksDataSource
27 | import com.alexzh.coffeedrinks.ui.appTypography
28 | import com.alexzh.coffeedrinks.ui.component.Favourite
29 | import com.alexzh.coffeedrinks.ui.lightThemeColors
30 | import com.alexzh.coffeedrinks.ui.screen.coffeedrinks.mapper.CoffeeDrinkItemMapper
31 | import com.alexzh.coffeedrinks.ui.screen.coffeedrinks.model.CoffeeDrinkItem
32 |
33 | @Preview
34 | @Composable
35 | fun Preview_DetailedListItem() {
36 | MaterialTheme(colors = lightThemeColors, typography = appTypography) {
37 | val mapper = CoffeeDrinkItemMapper()
38 | val coffeeDrink = mapper.map(
39 | DummyCoffeeDrinksDataSource().getCoffeeDrinks().first()
40 | )
41 | CoffeeDrinkDetailedItem(coffeeDrink) {}
42 | }
43 | }
44 |
45 | @Composable
46 | fun CoffeeDrinkDetailedItem(
47 | coffeeDrink: CoffeeDrinkItem,
48 | onFavouriteStateChanged: (CoffeeDrinkItem) -> Unit
49 | ) {
50 | val favouriteState = remember { mutableStateOf(coffeeDrink.isFavourite) }
51 |
52 | Card {
53 | Column {
54 | Box(
55 | modifier = Modifier.height(200.dp)
56 | .fillMaxWidth()
57 | .background(MaterialTheme.colors.primary)
58 | .padding(8.dp)
59 | ) {
60 | Favourite(
61 | state = favouriteState,
62 | modifier = Modifier.align(Alignment.TopEnd).alpha(0.78f),
63 | onValueChanged = {
64 | onFavouriteStateChanged(coffeeDrink)
65 | favouriteState.value = !favouriteState.value
66 | },
67 | tint = MaterialTheme.colors.onPrimary
68 | )
69 | Image(
70 | painter = BitmapPainter(ImageBitmap.imageResource(id = coffeeDrink.imageUrl)),
71 | modifier = Modifier.align(Alignment.Center)
72 | .size(100.dp),
73 | contentDescription = null
74 | )
75 | Text(
76 | modifier = Modifier.align(Alignment.BottomStart),
77 | text = coffeeDrink.name,
78 | style = MaterialTheme.typography.h5.copy(
79 | color = MaterialTheme.colors.onPrimary
80 | )
81 | )
82 | }
83 | Text(
84 | text = coffeeDrink.description,
85 | overflow = TextOverflow.Ellipsis,
86 | maxLines = 3,
87 | style = MaterialTheme.typography.body1,
88 | modifier = Modifier.padding(8.dp)
89 | .alpha(0.54f)
90 | )
91 | }
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexzh/coffeedrinks/ui/screen/coffeedrinks/ListItem.kt:
--------------------------------------------------------------------------------
1 | package com.alexzh.coffeedrinks.ui.screen.coffeedrinks
2 |
3 | import androidx.compose.foundation.Image
4 | import androidx.compose.foundation.isSystemInDarkTheme
5 | import androidx.compose.foundation.layout.Box
6 | import androidx.compose.foundation.layout.Column
7 | import androidx.compose.foundation.layout.PaddingValues
8 | import androidx.compose.foundation.layout.Row
9 | import androidx.compose.foundation.layout.fillMaxSize
10 | import androidx.compose.foundation.layout.fillMaxWidth
11 | import androidx.compose.foundation.layout.padding
12 | import androidx.compose.foundation.layout.size
13 | import androidx.compose.foundation.shape.CircleShape
14 | import androidx.compose.material.MaterialTheme
15 | import androidx.compose.material.Surface
16 | import androidx.compose.material.Text
17 | import androidx.compose.runtime.Composable
18 | import androidx.compose.runtime.CompositionLocalProvider
19 | import androidx.compose.runtime.mutableStateOf
20 | import androidx.compose.runtime.remember
21 | import androidx.compose.ui.Modifier
22 | import androidx.compose.ui.draw.alpha
23 | import androidx.compose.ui.graphics.Color
24 | import androidx.compose.ui.graphics.ImageBitmap
25 | import androidx.compose.ui.graphics.painter.BitmapPainter
26 | import androidx.compose.ui.platform.LocalLayoutDirection
27 | import androidx.compose.ui.res.imageResource
28 | import androidx.compose.ui.text.TextStyle
29 | import androidx.compose.ui.text.style.TextOverflow
30 | import androidx.compose.ui.tooling.preview.Preview
31 | import androidx.compose.ui.unit.LayoutDirection
32 | import androidx.compose.ui.unit.dp
33 | import androidx.compose.ui.unit.sp
34 | import com.alexzh.coffeedrinks.data.DummyCoffeeDrinksDataSource
35 | import com.alexzh.coffeedrinks.ui.appTypography
36 | import com.alexzh.coffeedrinks.ui.component.AppDivider
37 | import com.alexzh.coffeedrinks.ui.component.Favourite
38 | import com.alexzh.coffeedrinks.ui.lightThemeColors
39 | import com.alexzh.coffeedrinks.ui.screen.coffeedrinks.mapper.CoffeeDrinkItemMapper
40 | import com.alexzh.coffeedrinks.ui.screen.coffeedrinks.model.CoffeeDrinkItem
41 |
42 | private val COFFEE_DRINK_IMAGE_SIZE = 72.dp
43 |
44 | @Preview
45 | @Composable
46 | fun PreviewListItem() {
47 | val mapper = CoffeeDrinkItemMapper()
48 | val coffeeDrink = mapper.map(
49 | DummyCoffeeDrinksDataSource().getCoffeeDrinks()[5]
50 | )
51 | CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
52 | MaterialTheme(colors = lightThemeColors, typography = appTypography) {
53 | CoffeeDrinkListItem(
54 | coffeeDrink = coffeeDrink,
55 | onFavouriteStateChanged = {}
56 | )
57 | }
58 | }
59 | }
60 |
61 | @Composable
62 | fun CoffeeDrinkList(
63 | coffeeDrink: CoffeeDrinkItem,
64 | onFavouriteStateChanged: (CoffeeDrinkItem) -> Unit
65 | ) {
66 | Column {
67 | CoffeeDrinkListItem(
68 | coffeeDrink = coffeeDrink,
69 | onFavouriteStateChanged = { onFavouriteStateChanged(it) }
70 | )
71 | AppDivider(PaddingValues(start = COFFEE_DRINK_IMAGE_SIZE))
72 | }
73 | }
74 |
75 | @Composable
76 | fun CoffeeDrinkListItem(
77 | coffeeDrink: CoffeeDrinkItem,
78 | onFavouriteStateChanged: (CoffeeDrinkItem) -> Unit
79 | ) {
80 | val favouriteState = remember { mutableStateOf(coffeeDrink.isFavourite) }
81 | val favouriteIconColor = if (isSystemInDarkTheme()) {
82 | MaterialTheme.colors.onPrimary
83 | } else {
84 | MaterialTheme.colors.primaryVariant
85 | }
86 |
87 | Row {
88 | Surface(
89 | modifier = Modifier.size(COFFEE_DRINK_IMAGE_SIZE)
90 | .padding(8.dp),
91 | shape = CircleShape,
92 | color = Color(0xFFFAFAFA)
93 | ) {
94 | Image(
95 | painter = BitmapPainter(ImageBitmap.imageResource(id = coffeeDrink.imageUrl)),
96 | modifier = Modifier.fillMaxSize(),
97 | contentDescription = null
98 | )
99 | }
100 | Box(
101 | modifier = Modifier.fillMaxWidth()
102 | .weight(1f)
103 | ) {
104 | Column {
105 | Text(
106 | text = coffeeDrink.name,
107 | modifier = Modifier.padding(top = 8.dp, end = 8.dp),
108 | style = TextStyle(fontSize = 24.sp),
109 | color = MaterialTheme.colors.onSurface,
110 | maxLines = 1
111 | )
112 | Text(
113 | text = coffeeDrink.ingredients,
114 | modifier = Modifier.padding(end = 8.dp)
115 | .alpha(0.54f),
116 | maxLines = 1,
117 | overflow = TextOverflow.Ellipsis,
118 | style = MaterialTheme.typography.body1,
119 | color = MaterialTheme.colors.onSurface
120 | )
121 | }
122 | }
123 |
124 | Favourite(
125 | state = favouriteState,
126 | onValueChanged = {
127 | onFavouriteStateChanged(coffeeDrink)
128 | favouriteState.value = !favouriteState.value
129 | },
130 | tint = favouriteIconColor
131 | )
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexzh/coffeedrinks/ui/screen/coffeedrinks/mapper/CoffeeDrinkItemMapper.kt:
--------------------------------------------------------------------------------
1 | package com.alexzh.coffeedrinks.ui.screen.coffeedrinks.mapper
2 |
3 | import com.alexzh.coffeedrinks.data.CoffeeDrink
4 | import com.alexzh.coffeedrinks.ui.screen.coffeedrinks.model.CoffeeDrinkItem
5 |
6 | class CoffeeDrinkItemMapper {
7 |
8 | fun map(coffeeDrink: CoffeeDrink): CoffeeDrinkItem {
9 | return CoffeeDrinkItem(
10 | id = coffeeDrink.id,
11 | name = coffeeDrink.name,
12 | imageUrl = coffeeDrink.imageUrl,
13 | ingredients = coffeeDrink.ingredients,
14 | description = coffeeDrink.description,
15 | isFavourite = coffeeDrink.isFavourite
16 | )
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexzh/coffeedrinks/ui/screen/coffeedrinks/model/CoffeeDrinkItem.kt:
--------------------------------------------------------------------------------
1 | package com.alexzh.coffeedrinks.ui.screen.coffeedrinks.model
2 |
3 | data class CoffeeDrinkItem(
4 | val id: Long,
5 | val name: String,
6 | val imageUrl: Int,
7 | val ingredients: String,
8 | val description: String,
9 | val isFavourite: Boolean
10 | )
11 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexzh/coffeedrinks/ui/screen/coffeedrinks/model/CoffeeDrinksState.kt:
--------------------------------------------------------------------------------
1 | package com.alexzh.coffeedrinks.ui.screen.coffeedrinks.model
2 |
3 | data class CoffeeDrinksState(
4 | val coffeeDrinks: List,
5 | val displayingOption: DisplayingOptions
6 | )
7 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexzh/coffeedrinks/ui/screen/coffeedrinks/model/DisplayingOptions.kt:
--------------------------------------------------------------------------------
1 | package com.alexzh.coffeedrinks.ui.screen.coffeedrinks.model
2 |
3 | enum class DisplayingOptions {
4 | LIST,
5 | CARDS
6 | }
7 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexzh/coffeedrinks/ui/screen/order/OrderCoffeeDrinkFragment.kt:
--------------------------------------------------------------------------------
1 | package com.alexzh.coffeedrinks.ui.screen.order
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import androidx.compose.animation.ExperimentalAnimationApi
8 | import androidx.compose.runtime.livedata.observeAsState
9 | import androidx.compose.ui.platform.ComposeView
10 | import androidx.fragment.app.Fragment
11 | import com.alexzh.coffeedrinks.R
12 | import com.alexzh.coffeedrinks.ui.AppTheme
13 | import com.alexzh.coffeedrinks.ui.Screen
14 | import com.alexzh.coffeedrinks.ui.navigateToPreviousScreen
15 | import com.alexzh.coffeedrinks.ui.state.UiState
16 | import org.koin.androidx.viewmodel.ext.android.viewModel
17 |
18 | @OptIn(ExperimentalAnimationApi::class)
19 | class OrderCoffeeDrinkFragment : Fragment() {
20 | private val viewModel: OrderCoffeeDrinkViewModel by viewModel()
21 |
22 | override fun onCreateView(
23 | inflater: LayoutInflater,
24 | container: ViewGroup?,
25 | savedInstanceState: Bundle?
26 | ): View {
27 | return ComposeView(requireContext()).apply {
28 | id = R.id.coffeeDrinkDetailsFragment
29 | layoutParams = ViewGroup.LayoutParams(
30 | ViewGroup.LayoutParams.MATCH_PARENT,
31 | ViewGroup.LayoutParams.MATCH_PARENT
32 | )
33 |
34 | setContent {
35 | AppTheme {
36 | viewModel.uiState.observeAsState(initial = UiState.Loading).value.let { uiState ->
37 | when (uiState) {
38 | is UiState.Loading -> {
39 | ShowLoadingOrderCoffeeDrinksScreen()
40 | }
41 | is UiState.Success -> {
42 | ShowSuccessOrderCoffeeDrinksScreen(
43 | orderCoffeeDrinkState = uiState.data,
44 | viewModel = viewModel,
45 | onBack = {
46 | this@OrderCoffeeDrinkFragment.navigateToPreviousScreen(
47 | Screen.OrderCoffeeDrinks,
48 | Screen.CoffeeDrinks
49 | )
50 | }
51 | )
52 | }
53 | is UiState.Error -> {
54 | ShowErrorOrderCoffeeDrinksScreen()
55 | }
56 | }
57 | }
58 | }
59 | }
60 | }
61 | }
62 |
63 | override fun onResume() {
64 | super.onResume()
65 | viewModel.loadDrinks()
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexzh/coffeedrinks/ui/screen/order/OrderCoffeeDrinkScreen.kt:
--------------------------------------------------------------------------------
1 | package com.alexzh.coffeedrinks.ui.screen.order
2 |
3 | import androidx.annotation.DrawableRes
4 | import androidx.compose.animation.ExperimentalAnimationApi
5 | import androidx.compose.foundation.Image
6 | import androidx.compose.foundation.layout.Box
7 | import androidx.compose.foundation.layout.Column
8 | import androidx.compose.foundation.layout.PaddingValues
9 | import androidx.compose.foundation.layout.Row
10 | import androidx.compose.foundation.layout.fillMaxSize
11 | import androidx.compose.foundation.layout.fillMaxWidth
12 | import androidx.compose.foundation.layout.padding
13 | import androidx.compose.foundation.layout.size
14 | import androidx.compose.foundation.layout.width
15 | import androidx.compose.foundation.lazy.LazyColumn
16 | import androidx.compose.foundation.lazy.items
17 | import androidx.compose.foundation.shape.CircleShape
18 | import androidx.compose.material.CircularProgressIndicator
19 | import androidx.compose.material.Icon
20 | import androidx.compose.material.IconButton
21 | import androidx.compose.material.MaterialTheme
22 | import androidx.compose.material.Surface
23 | import androidx.compose.material.Text
24 | import androidx.compose.material.TopAppBar
25 | import androidx.compose.runtime.Composable
26 | import androidx.compose.ui.Alignment
27 | import androidx.compose.ui.Modifier
28 | import androidx.compose.ui.graphics.Color
29 | import androidx.compose.ui.graphics.ImageBitmap
30 | import androidx.compose.ui.graphics.painter.BitmapPainter
31 | import androidx.compose.ui.res.imageResource
32 | import androidx.compose.ui.res.stringResource
33 | import androidx.compose.ui.text.style.TextAlign
34 | import androidx.compose.ui.text.style.TextOverflow
35 | import androidx.compose.ui.unit.dp
36 | import com.alexzh.coffeedrinks.R
37 | import com.alexzh.coffeedrinks.ui.component.AppDivider
38 | import com.alexzh.coffeedrinks.ui.component.Counter
39 | import com.alexzh.coffeedrinks.ui.screen.order.model.OrderCoffeeDrinkState
40 | import java.math.BigDecimal
41 |
42 | @Composable
43 | fun ShowLoadingOrderCoffeeDrinksScreen() {
44 | Box(modifier = Modifier.fillMaxSize()) {
45 | CircularProgressIndicator(
46 | color = MaterialTheme.colors.primaryVariant,
47 | modifier = Modifier
48 | .align(Alignment.Center)
49 | .size(36.dp)
50 | )
51 | }
52 | }
53 |
54 | @ExperimentalAnimationApi
55 | @Composable
56 | fun ShowSuccessOrderCoffeeDrinksScreen(
57 | orderCoffeeDrinkState: OrderCoffeeDrinkState,
58 | viewModel: OrderCoffeeDrinkViewModel,
59 | onBack: () -> Unit
60 | ) {
61 | Column {
62 | AppBarWithOrderSummary(
63 | totalPrice = orderCoffeeDrinkState.totalPrice,
64 | onBackClick = onBack
65 | )
66 | Surface {
67 | LazyColumn {
68 | items(items = orderCoffeeDrinkState.coffeeDrinks) { coffeeDrink ->
69 | Column {
70 | OrderCoffeeDrinkItem(
71 | orderCoffeeDrink = coffeeDrink,
72 | onAdded = { viewModel.addDrink(coffeeDrink.id) },
73 | onRemoved = { viewModel.removeDrink(coffeeDrink.id) }
74 | )
75 | AppDivider(PaddingValues(start = 84.dp))
76 | }
77 | }
78 | }
79 | }
80 | }
81 | }
82 |
83 | @Composable
84 | fun ShowErrorOrderCoffeeDrinksScreen() {
85 | // TODO: implement UI
86 | }
87 |
88 | @Composable
89 | private fun AppBar(
90 | onBackClick: () -> Unit
91 | ) {
92 | TopAppBar(
93 | title = {
94 | Text(
95 | text = "Order coffee drinks",
96 | style = MaterialTheme.typography.h6.copy(
97 | color = MaterialTheme.colors.onPrimary
98 | )
99 | )
100 | },
101 | backgroundColor = MaterialTheme.colors.primary,
102 | elevation = 0.dp,
103 | navigationIcon = {
104 | IconButton(onClick = onBackClick) {
105 | Icon(
106 | painter = BitmapPainter(ImageBitmap.imageResource(id = R.drawable.ic_arrow_back_white)),
107 | contentDescription = stringResource(R.string.action_back),
108 | tint = MaterialTheme.colors.onPrimary
109 | )
110 | }
111 | }
112 | )
113 | }
114 |
115 | @OptIn(ExperimentalAnimationApi::class)
116 | @Composable
117 | fun OrderCoffeeDrinkItem(
118 | orderCoffeeDrink: com.alexzh.coffeedrinks.data.order.OrderCoffeeDrink,
119 | onAdded: () -> Unit,
120 | onRemoved: () -> Unit
121 | ) {
122 | Row(
123 | modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
124 | ) {
125 | Logo(orderCoffeeDrink.imageUrl)
126 | Column(
127 | modifier = Modifier.weight(1f)
128 | .padding(horizontal = 4.dp)
129 | ) {
130 | Text(
131 | text = orderCoffeeDrink.name,
132 | style = MaterialTheme.typography.h6,
133 | modifier = Modifier.padding(bottom = 4.dp)
134 | .fillMaxWidth()
135 | )
136 | Text(
137 | text = orderCoffeeDrink.ingredients,
138 | maxLines = 2,
139 | overflow = TextOverflow.Ellipsis,
140 | style = MaterialTheme.typography.body2
141 | )
142 | }
143 | Column(modifier = Modifier.width(90.dp)) {
144 | Text(
145 | modifier = Modifier
146 | .padding(bottom = 4.dp)
147 | .fillMaxWidth(),
148 | text = "€ ${orderCoffeeDrink.price}",
149 | style = MaterialTheme.typography.subtitle1.copy(textAlign = TextAlign.Right)
150 | )
151 | Counter(
152 | value = orderCoffeeDrink.count,
153 | onIncrease = onAdded,
154 | onDecrease = onRemoved
155 | )
156 | }
157 | }
158 | }
159 |
160 | @Composable
161 | private fun Logo(
162 | @DrawableRes logoId: Int,
163 | modifier: Modifier = Modifier
164 | ) {
165 | Surface(
166 | modifier = modifier
167 | .size(72.dp)
168 | .padding(8.dp),
169 | shape = CircleShape,
170 | color = Color(0xFFFAFAFA)
171 | ) {
172 | Image(
173 | modifier = Modifier.size(64.dp),
174 | painter = BitmapPainter(ImageBitmap.imageResource(id = logoId)),
175 | contentDescription = null
176 | )
177 | }
178 | }
179 |
180 | @ExperimentalAnimationApi
181 | @Composable
182 | private fun AppBarWithOrderSummary(
183 | totalPrice: BigDecimal,
184 | onBackClick: () -> Unit
185 | ) {
186 | Surface(
187 | color = MaterialTheme.colors.primary,
188 | elevation = 4.dp
189 | ) {
190 | Column {
191 | AppBar {
192 | onBackClick()
193 | }
194 | Row(modifier = Modifier.padding(8.dp)) {
195 | Text(
196 | text = "Total cost",
197 | modifier = Modifier.weight(1f),
198 | style = MaterialTheme.typography.h6.copy(
199 | color = MaterialTheme.colors.onPrimary
200 | )
201 | )
202 | Text(
203 | text = "€ $totalPrice",
204 | style = MaterialTheme.typography.h6.copy(
205 | color = MaterialTheme.colors.onPrimary
206 | )
207 | )
208 | }
209 | }
210 | }
211 | }
212 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexzh/coffeedrinks/ui/screen/order/OrderCoffeeDrinkViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.alexzh.coffeedrinks.ui.screen.order
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MutableLiveData
5 | import androidx.lifecycle.ViewModel
6 | import androidx.lifecycle.viewModelScope
7 | import com.alexzh.coffeedrinks.data.order.OrderCoffeeDrinksRepository
8 | import com.alexzh.coffeedrinks.ui.screen.order.model.OrderCoffeeDrinkState
9 | import com.alexzh.coffeedrinks.ui.state.UiState
10 | import kotlinx.coroutines.flow.collect
11 | import kotlinx.coroutines.launch
12 | import java.math.BigDecimal
13 |
14 | class OrderCoffeeDrinkViewModel(
15 | private val repository: OrderCoffeeDrinksRepository
16 | ) : ViewModel() {
17 | private val _uiState: MutableLiveData> = MutableLiveData()
18 | val uiState: LiveData>
19 | get() = _uiState
20 |
21 | fun loadDrinks() {
22 | viewModelScope.launch {
23 | _uiState.value = UiState.Loading
24 | repository.getCoffeeDrinks()
25 | .collect { coffeeDrinks ->
26 | val totalCount = coffeeDrinks
27 | .filter { it.count > 0 }
28 | .map { it.count * it.price }
29 | .sum()
30 |
31 | _uiState.value = UiState.Success(
32 | OrderCoffeeDrinkState(
33 | coffeeDrinks = coffeeDrinks,
34 | totalPrice = BigDecimal(totalCount)
35 | )
36 | )
37 | }
38 | }
39 | }
40 |
41 | fun addDrink(coffeeDrinkId: Long) {
42 | viewModelScope.launch {
43 | repository.add(coffeeDrinkId)
44 | .collect { isAdded ->
45 | if (isAdded) {
46 | loadDrinks()
47 | }
48 | }
49 | }
50 | }
51 |
52 | fun removeDrink(coffeeDrinkId: Long) {
53 | viewModelScope.launch {
54 | repository.remove(coffeeDrinkId)
55 | .collect { isRemoved ->
56 | if (isRemoved) {
57 | loadDrinks()
58 | }
59 | }
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexzh/coffeedrinks/ui/screen/order/mapper/OrderCoffeeDrinkMapper.kt:
--------------------------------------------------------------------------------
1 | package com.alexzh.coffeedrinks.ui.screen.order.mapper
2 |
3 | import androidx.compose.runtime.mutableStateOf
4 | import com.alexzh.coffeedrinks.data.CoffeeDrink
5 | import com.alexzh.coffeedrinks.ui.screen.order.model.OrderCoffeeDrink
6 |
7 | class OrderCoffeeDrinkMapper {
8 |
9 | fun map(coffeeDrink: CoffeeDrink): OrderCoffeeDrink {
10 | return OrderCoffeeDrink(
11 | id = coffeeDrink.id,
12 | name = coffeeDrink.name,
13 | imageRes = coffeeDrink.imageUrl,
14 | description = coffeeDrink.orderDescription,
15 | price = coffeeDrink.price,
16 | count = mutableStateOf(0)
17 | )
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexzh/coffeedrinks/ui/screen/order/model/OrderCoffeeDrink.kt:
--------------------------------------------------------------------------------
1 | package com.alexzh.coffeedrinks.ui.screen.order.model
2 |
3 | import androidx.annotation.DrawableRes
4 | import androidx.compose.runtime.MutableState
5 | import androidx.compose.runtime.mutableStateOf
6 |
7 | data class OrderCoffeeDrink(
8 | val id: Long,
9 | val name: String,
10 | @DrawableRes val imageRes: Int,
11 | val description: String,
12 | val price: Double,
13 | // TODO: should be immutable
14 | var count: MutableState = mutableStateOf(0)
15 | )
16 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexzh/coffeedrinks/ui/screen/order/model/OrderCoffeeDrinkState.kt:
--------------------------------------------------------------------------------
1 | package com.alexzh.coffeedrinks.ui.screen.order.model
2 |
3 | import com.alexzh.coffeedrinks.data.order.OrderCoffeeDrink
4 | import java.math.BigDecimal
5 |
6 | data class OrderCoffeeDrinkState(
7 | val coffeeDrinks: List,
8 | val totalPrice: BigDecimal
9 | )
10 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexzh/coffeedrinks/ui/state/UiState.kt:
--------------------------------------------------------------------------------
1 | package com.alexzh.coffeedrinks.ui.state
2 |
3 | sealed class UiState {
4 |
5 | object Loading : UiState()
6 |
7 | data class Success(val data: T) : UiState()
8 |
9 | data class Error(val exception: Exception) : UiState()
10 | }
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_action_coffee.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/drawable-hdpi/ic_action_coffee.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_arrow_back_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/drawable-hdpi/ic_arrow_back_white.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_extended_list_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/drawable-hdpi/ic_extended_list_white.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_favorite_border_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/drawable-hdpi/ic_favorite_border_white.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_favorite_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/drawable-hdpi/ic_favorite_white.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_list_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/drawable-hdpi/ic_list_white.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_order_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/drawable-hdpi/ic_order_white.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_action_coffee.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/drawable-mdpi/ic_action_coffee.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_arrow_back_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/drawable-mdpi/ic_arrow_back_white.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_extended_list_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/drawable-mdpi/ic_extended_list_white.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_favorite_border_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/drawable-mdpi/ic_favorite_border_white.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_favorite_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/drawable-mdpi/ic_favorite_white.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_list_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/drawable-mdpi/ic_list_white.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_order_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/drawable-mdpi/ic_order_white.png
--------------------------------------------------------------------------------
/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-xhdpi/ic_action_coffee.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/drawable-xhdpi/ic_action_coffee.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_arrow_back_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/drawable-xhdpi/ic_arrow_back_white.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_extended_list_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/drawable-xhdpi/ic_extended_list_white.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_favorite_border_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/drawable-xhdpi/ic_favorite_border_white.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_favorite_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/drawable-xhdpi/ic_favorite_white.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_list_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/drawable-xhdpi/ic_list_white.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_order_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/drawable-xhdpi/ic_order_white.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_action_coffee.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/drawable-xxhdpi/ic_action_coffee.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_arrow_back_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/drawable-xxhdpi/ic_arrow_back_white.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_extended_list_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/drawable-xxhdpi/ic_extended_list_white.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_favorite_border_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/drawable-xxhdpi/ic_favorite_border_white.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_favorite_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/drawable-xxhdpi/ic_favorite_white.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_list_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/drawable-xxhdpi/ic_list_white.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_order_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/drawable-xxhdpi/ic_order_white.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/americano_small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/drawable/americano_small.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/cappuccino_small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/drawable/cappuccino_small.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/cold_brew_coffee_small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/drawable/cold_brew_coffee_small.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/espresso_macchiato_small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/drawable/espresso_macchiato_small.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/espresso_small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/drawable/espresso_small.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/frappino_small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/drawable/frappino_small.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/gingerbread_coffee_small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/drawable/gingerbread_coffee_small.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_favorite_24.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_favorite_border_24.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/iced_mocha_small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/drawable/iced_mocha_small.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/irish_coffee_small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/drawable/irish_coffee_small.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/latte_macchiato_small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/drawable/latte_macchiato_small.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/latte_small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/drawable/latte_small.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/mocha_small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/drawable/mocha_small.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/turkish_coffee_small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/drawable/turkish_coffee_small.png
--------------------------------------------------------------------------------
/app/src/main/res/font/roboto_black.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/font/roboto_black.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/roboto_black_italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/font/roboto_black_italic.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/roboto_bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/font/roboto_bold.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/roboto_bold_italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/font/roboto_bold_italic.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/roboto_light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/font/roboto_light.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/roboto_light_italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/font/roboto_light_italic.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/roboto_medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/font/roboto_medium.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/roboto_medium_italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/font/roboto_medium_italic.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/roboto_regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/font/roboto_regular.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/roboto_regular_italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/font/roboto_regular_italic.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/roboto_thin.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/font/roboto_thin.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/roboto_thin_italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/font/roboto_thin_italic.ttf
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/navigation/nav_graph.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
11 |
15 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/values-night/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #1F1F1F
4 | #000000
5 | #03DAC5
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #855446
4 | #663E34
5 | #663E34
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Coffee drinks
3 |
4 | Back
5 | Order coffee drinks
6 | Show detailed cards
7 |
8 | Mark as favorite
9 | Unmark as favorite
10 |
11 | +
12 | —
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/test/java/com/alexzh/coffeedrinks/data/RuntimeCoffeeDrinkRepositoryTest.kt:
--------------------------------------------------------------------------------
1 | package com.alexzh.coffeedrinks.data
2 |
3 | import kotlinx.coroutines.flow.single
4 | import kotlinx.coroutines.runBlocking
5 | import org.junit.Assert.assertEquals
6 | import org.junit.Assert.assertFalse
7 | import org.junit.Assert.assertTrue
8 | import org.junit.Test
9 |
10 | class RuntimeCoffeeDrinkRepositoryTest {
11 | private val repository = RuntimeCoffeeDrinkRepository
12 |
13 | @Test
14 | fun `should first call of getCoffeeDrinks() returns 10 items`() = runBlocking {
15 | val expectedItemsCount = 10
16 |
17 | assertEquals(
18 | expectedItemsCount,
19 | repository.getCoffeeDrinks().single().size
20 | )
21 | }
22 |
23 | @Test
24 | fun `should first call of getCoffeeDrinks() returns all non-favourite items`() = runBlocking {
25 | val expectedItemsCount = 10
26 |
27 | val nonFavouriteItemsCount = repository.getCoffeeDrinks()
28 | .single()
29 | .filter { !it.isFavourite }
30 | .size
31 |
32 | assertEquals(expectedItemsCount, nonFavouriteItemsCount)
33 | }
34 |
35 | @Test
36 | fun `should update favourite state of not-favourite coffee`() = runBlocking {
37 | val coffeeName = repository.getCoffeeDrinks()
38 | .single()
39 | .first().name
40 | val coffee = repository.getCoffeeDrinks()
41 | .single()
42 | .first { it.name == coffeeName }
43 |
44 | val updateCoffeeDrinkStatus = repository.updateFavouriteState(coffee.id, true).single()
45 |
46 | val result = repository.getCoffeeDrinks()
47 | .single()
48 | .first { it.name == coffeeName }
49 |
50 | assertTrue(updateCoffeeDrinkStatus)
51 | assertTrue(result.isFavourite)
52 | }
53 |
54 | @Test
55 | fun `should update favourite state of favourite coffee`() = runBlocking {
56 | val coffeeName = repository.getCoffeeDrinks()
57 | .single()
58 | .first().name
59 | val coffee = repository.getCoffeeDrinks()
60 | .single()
61 | .first { it.name == coffeeName }
62 |
63 | repository.updateFavouriteState(coffee.id, true)
64 | repository.updateFavouriteState(coffee.id, false)
65 |
66 | val result = repository.getCoffeeDrinks()
67 | .single()
68 | .first { it.name == coffeeName }
69 | assertFalse(result.isFavourite)
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/app/src/test/java/com/alexzh/coffeedrinks/data/order/OrderCoffeeDrinkMapperTest.kt:
--------------------------------------------------------------------------------
1 | package com.alexzh.coffeedrinks.data.order
2 |
3 | import com.alexzh.coffeedrinks.data.CoffeeDrink
4 | import com.alexzh.coffeedrinks.generator.GenerateCoffeeDrink.generateCoffeeDrink
5 | import com.alexzh.coffeedrinks.generator.GenerateCoffeeDrink.generateCoffeeDrinks
6 | import org.junit.Assert.assertEquals
7 | import org.junit.Test
8 |
9 | class OrderCoffeeDrinkMapperTest {
10 | private val orderCoffeeDrinkMapper = OrderCoffeeDrinkMapper()
11 |
12 | @Test
13 | fun should_mapToOrderCoffeeDrink_withDefaultCountParam_correctly() {
14 | val coffeeDrink = generateCoffeeDrink()
15 | val orderCoffeeDrink = orderCoffeeDrinkMapper.map(coffeeDrink)
16 | assertOrderCoffeeDrink(coffeeDrink, orderCoffeeDrink, 0)
17 | }
18 |
19 | @Test
20 | fun should_mapToOrderCoffeeDrink_withCustomCountParam_correctly() {
21 | val customCount = 42
22 | val coffeeDrink = generateCoffeeDrink()
23 | val orderCoffeeDrink = orderCoffeeDrinkMapper.map(coffeeDrink, customCount)
24 | assertOrderCoffeeDrink(coffeeDrink, orderCoffeeDrink, customCount)
25 | }
26 |
27 | @Test
28 | fun should_mapListToOrderCoffeeDrinks_withDefaultCountParam_correctly() {
29 | val coffeeDrinks = generateCoffeeDrinks(2)
30 | val orderCoffeeDrinks = orderCoffeeDrinkMapper.map(coffeeDrinks)
31 | assertOrderCoffeeDrinks(coffeeDrinks, orderCoffeeDrinks, 0)
32 | }
33 |
34 | @Test
35 | fun should_mapListToOrderCoffeeDrinks_withCustomCountParam_correctly() {
36 | val customCount = 42
37 | val coffeeDrinks = generateCoffeeDrinks(2)
38 | val orderCoffeeDrinks = orderCoffeeDrinkMapper.map(coffeeDrinks, customCount)
39 | assertOrderCoffeeDrinks(coffeeDrinks, orderCoffeeDrinks, customCount)
40 | }
41 |
42 | private fun assertOrderCoffeeDrink(
43 | coffeeDrink: CoffeeDrink,
44 | orderCoffeeDrink: OrderCoffeeDrink,
45 | orderCoffeeDrinkCount: Int
46 | ) {
47 | assertEquals(coffeeDrink.id, orderCoffeeDrink.id)
48 | assertEquals(coffeeDrink.name, orderCoffeeDrink.name)
49 | assertEquals(coffeeDrink.imageUrl, orderCoffeeDrink.imageUrl)
50 | assertEquals(coffeeDrink.ingredients, orderCoffeeDrink.ingredients)
51 | assertEquals(coffeeDrink.price, orderCoffeeDrink.price, 0.00001)
52 | assertEquals(orderCoffeeDrink.count, orderCoffeeDrinkCount)
53 | }
54 |
55 | private fun assertOrderCoffeeDrinks(
56 | coffeeDrinks: List,
57 | orderCoffeeDrinks: List,
58 | orderCoffeeDrinkCount: Int
59 | ) {
60 | assertEquals(coffeeDrinks.size, orderCoffeeDrinks.size)
61 | for (index in coffeeDrinks.indices) {
62 | assertOrderCoffeeDrink(
63 | coffeeDrinks[index],
64 | orderCoffeeDrinks[index],
65 | orderCoffeeDrinkCount
66 | )
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/app/src/test/java/com/alexzh/coffeedrinks/data/order/RuntimeOrderCoffeeDrinksRepositoryTest.kt:
--------------------------------------------------------------------------------
1 | package com.alexzh.coffeedrinks.data.order
2 |
3 | import com.alexzh.coffeedrinks.data.CoffeeDrink
4 | import com.alexzh.coffeedrinks.data.CoffeeDrinkDataSource
5 | import com.alexzh.coffeedrinks.generator.GenerateCoffeeDrink.generateCoffeeDrink
6 | import com.alexzh.coffeedrinks.generator.GenerateCoffeeDrink.generateCoffeeDrinks
7 | import com.alexzh.coffeedrinks.generator.GenerateOrderCoffeeDrink.generateOrderCoffeeDrink
8 | import com.alexzh.coffeedrinks.generator.GenerateOrderCoffeeDrink.generateOrderCoffeeDrinks
9 | import io.mockk.every
10 | import io.mockk.mockk
11 | import kotlinx.coroutines.flow.single
12 | import kotlinx.coroutines.runBlocking
13 | import org.junit.Assert.assertEquals
14 | import org.junit.Test
15 |
16 | class RuntimeOrderCoffeeDrinksRepositoryTest {
17 | private val coffeeDrinkDataSource = mockk()
18 | private val orderCoffeeDrinkMapper = mockk()
19 |
20 | private val repository = RuntimeOrderCoffeeDrinksRepository(
21 | coffeeDrinkDataSource,
22 | orderCoffeeDrinkMapper
23 | )
24 |
25 | @Test
26 | fun should_returnCoffeeDrinks_when_dataSourceReturnData() {
27 | val coffeeDrinks = generateCoffeeDrinks(count = 10)
28 | val orderCoffeeDrinks = generateOrderCoffeeDrinks(count = 10)
29 |
30 | stubGetCoffeeDrinks(coffeeDrinks)
31 | stubMapToOrderCoffeeDrink(coffeeDrinks, orderCoffeeDrinks)
32 |
33 | runBlocking {
34 | assertEquals(
35 | orderCoffeeDrinks,
36 | repository.getCoffeeDrinks().single()
37 | )
38 | }
39 | }
40 |
41 | @Test
42 | fun should_addMethod_updateCoffeeDrinksList_when_countIsLessThanMaximum() {
43 | val coffeeDrinks = listOf(generateCoffeeDrink())
44 | val orderCoffeeDrinks = listOf(generateOrderCoffeeDrink(count = 0))
45 | val updateOrderCoffeeDrinks = listOf(orderCoffeeDrinks.first().copy(count = 1))
46 |
47 | stubGetCoffeeDrinks(coffeeDrinks)
48 | stubMapToOrderCoffeeDrink(coffeeDrinks, orderCoffeeDrinks)
49 |
50 | runBlocking {
51 | repository.getCoffeeDrinks().single()
52 | repository.add(orderCoffeeDrinks.first().id).single()
53 | assertEquals(
54 | updateOrderCoffeeDrinks,
55 | repository.getCoffeeDrinks().single()
56 | )
57 | }
58 | }
59 |
60 | @Test
61 | fun should_addMethod_notUpdateCoffeeDrinksList_when_countIsEqualToMaximum() {
62 | val coffeeDrinks = listOf(generateCoffeeDrink())
63 | val orderCoffeeDrinks = listOf(generateOrderCoffeeDrink(count = 99))
64 |
65 | stubGetCoffeeDrinks(coffeeDrinks)
66 | stubMapToOrderCoffeeDrink(coffeeDrinks, orderCoffeeDrinks)
67 |
68 | runBlocking {
69 | repository.getCoffeeDrinks().single()
70 | repository.add(orderCoffeeDrinks.first().id).single()
71 | assertEquals(
72 | orderCoffeeDrinks,
73 | repository.getCoffeeDrinks().single()
74 | )
75 | }
76 | }
77 |
78 | @Test
79 | fun should_removeMethod_updateCoffeeDrinksList_when_countIsMoreThanMinimum() {
80 | val coffeeDrinks = listOf(generateCoffeeDrink())
81 | val orderCoffeeDrinks = listOf(generateOrderCoffeeDrink(count = 42))
82 | val updateOrderCoffeeDrinks = listOf(orderCoffeeDrinks.first().copy(count = 41))
83 |
84 | stubGetCoffeeDrinks(coffeeDrinks)
85 | stubMapToOrderCoffeeDrink(coffeeDrinks, orderCoffeeDrinks)
86 |
87 | runBlocking {
88 | repository.getCoffeeDrinks().single()
89 | repository.remove(orderCoffeeDrinks.first().id).single()
90 | assertEquals(
91 | updateOrderCoffeeDrinks,
92 | repository.getCoffeeDrinks().single()
93 | )
94 | }
95 | }
96 |
97 | @Test
98 | fun should_removeMethod_notUpdateCoffeeDrinksList_when_countIsEqualToMinimum() {
99 | val coffeeDrinks = listOf(generateCoffeeDrink())
100 | val orderCoffeeDrinks = listOf(generateOrderCoffeeDrink(count = 0))
101 |
102 | stubGetCoffeeDrinks(coffeeDrinks)
103 | stubMapToOrderCoffeeDrink(coffeeDrinks, orderCoffeeDrinks)
104 |
105 | runBlocking {
106 | repository.getCoffeeDrinks().single()
107 | repository.remove(orderCoffeeDrinks.first().id).single()
108 | assertEquals(
109 | orderCoffeeDrinks,
110 | repository.getCoffeeDrinks().single()
111 | )
112 | }
113 | }
114 |
115 | private fun stubGetCoffeeDrinks(coffeeDrinks: List) {
116 | every { coffeeDrinkDataSource.getCoffeeDrinks() } returns coffeeDrinks
117 | }
118 |
119 | private fun stubMapToOrderCoffeeDrink(
120 | coffeeDrinks: List,
121 | orderCoffeeDrink: List
122 | ) {
123 | every { orderCoffeeDrinkMapper.map(coffeeDrinks) } returns orderCoffeeDrink
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/app/src/test/java/com/alexzh/coffeedrinks/generator/GenerateCoffeeDrink.kt:
--------------------------------------------------------------------------------
1 | package com.alexzh.coffeedrinks.generator
2 |
3 | import com.alexzh.coffeedrinks.data.CoffeeDrink
4 | import com.alexzh.coffeedrinks.generator.RandomData.randomBoolean
5 | import com.alexzh.coffeedrinks.generator.RandomData.randomDouble
6 | import com.alexzh.coffeedrinks.generator.RandomData.randomInt
7 | import com.alexzh.coffeedrinks.generator.RandomData.randomLong
8 | import com.alexzh.coffeedrinks.generator.RandomData.randomString
9 |
10 | object GenerateCoffeeDrink {
11 |
12 | fun generateCoffeeDrink(
13 | id: Long = randomLong(),
14 | name: String = randomString(),
15 | imageUrl: Int = randomInt(),
16 | description: String = randomString(),
17 | ingredients: String = randomString(),
18 | orderDescription: String = randomString(),
19 | price: Double = randomDouble(),
20 | isFavourite: Boolean = randomBoolean()
21 | ) = CoffeeDrink(
22 | id = id,
23 | name = name,
24 | imageUrl = imageUrl,
25 | description = description,
26 | ingredients = ingredients,
27 | orderDescription = orderDescription,
28 | price = price,
29 | isFavourite = isFavourite
30 | )
31 |
32 | fun generateCoffeeDrinks(
33 | count: Int = 10
34 | ): List {
35 | val coffeeDrinks = mutableListOf()
36 | repeat(count) {
37 | coffeeDrinks.add(generateCoffeeDrink())
38 | }
39 | return coffeeDrinks
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/app/src/test/java/com/alexzh/coffeedrinks/generator/GenerateOrderCoffeeDrink.kt:
--------------------------------------------------------------------------------
1 | package com.alexzh.coffeedrinks.generator
2 |
3 | import com.alexzh.coffeedrinks.data.order.OrderCoffeeDrink
4 | import com.alexzh.coffeedrinks.generator.RandomData.randomDouble
5 | import com.alexzh.coffeedrinks.generator.RandomData.randomInt
6 | import com.alexzh.coffeedrinks.generator.RandomData.randomLong
7 | import com.alexzh.coffeedrinks.generator.RandomData.randomString
8 |
9 | object GenerateOrderCoffeeDrink {
10 |
11 | fun generateOrderCoffeeDrink(
12 | id: Long = randomLong(),
13 | name: String = randomString(),
14 | imageUrl: Int = randomInt(),
15 | ingredients: String = randomString(),
16 | price: Double = randomDouble(),
17 | count: Int = randomInt(99)
18 | ) = OrderCoffeeDrink(
19 | id = id,
20 | name = name,
21 | imageUrl = imageUrl,
22 | ingredients = ingredients,
23 | price = price,
24 | count = count
25 | )
26 |
27 | fun generateOrderCoffeeDrinks(
28 | count: Int = 10
29 | ): List {
30 | val orderCoffeeDrinks = mutableListOf()
31 | repeat(count) {
32 | orderCoffeeDrinks.add(generateOrderCoffeeDrink())
33 | }
34 | return orderCoffeeDrinks
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/app/src/test/java/com/alexzh/coffeedrinks/generator/RandomData.kt:
--------------------------------------------------------------------------------
1 | package com.alexzh.coffeedrinks.generator
2 |
3 | import java.util.UUID
4 | import kotlin.random.Random
5 |
6 | object RandomData {
7 |
8 | fun randomString() = UUID.randomUUID().toString()
9 |
10 | fun randomLong() = Random.nextLong()
11 |
12 | fun randomInt(until: Int = 1000) = Random.nextInt(until)
13 |
14 | fun randomDouble() = Random.nextDouble()
15 |
16 | fun randomBoolean() = Random.nextBoolean()
17 | }
18 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | apply from: 'scripts/git-hooks.gradle'
4 |
5 | buildscript {
6 | ext.android_gradle_plugin_version = '7.2.0'
7 | ext.kotlin_version = '1.6.10'
8 | ext.kotlin_coroutines_version = '1.6.0'
9 | ext.compose_version = '1.1.1'
10 | ext.compose_nav_version = "2.4.2"
11 | ext.compose_constraintlayout_version = "1.0.0"
12 | ext.appcompat_version = '1.4.1'
13 | ext.androidx_core_version = '1.7.0'
14 | ext.androidx_fragment_version = '1.4.1'
15 | ext.androidx_lifecycle_version = '2.4.1'
16 | ext.androidx_navigation_version = '2.4.2'
17 | ext.koin_version = '2.2.2'
18 | ext.junit_version = '4.13.2'
19 | ext.test_core = '1.4.0'
20 | ext.test_junit_runner = '1.1.3'
21 | ext.test_runner = '1.4.0'
22 | ext.test_rules = '1.4.0'
23 | ext.test_monitor = '1.5.0'
24 | ext.mockk_version = '1.10.6'
25 | ext.shot_version = '5.14.0'
26 |
27 | repositories {
28 | mavenCentral()
29 | google()
30 | }
31 | dependencies {
32 | classpath "com.android.tools.build:gradle:$android_gradle_plugin_version"
33 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
34 | classpath "com.karumi:shot:$shot_version"
35 | // NOTE: Do not place your application dependencies here; they belong
36 | // in the individual module build.gradle files
37 | }
38 | }
39 |
40 | allprojects {
41 | repositories {
42 | google()
43 | mavenCentral()
44 | maven { url 'https://jitpack.io' }
45 | }
46 | configurations.all {
47 | resolutionStrategy.force 'org.objenesis:objenesis:2.6'
48 | }
49 | }
50 |
51 | subprojects {
52 | afterEvaluate {
53 | tasks['clean'].dependsOn installGitHooks
54 | tasks['assemble'].dependsOn installGitHooks
55 | }
56 | }
57 |
58 | task clean(type: Delete) {
59 | delete rootProject.buildDir
60 | }
61 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri Aug 28 07:51:28 CEST 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-7.3.3-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/screenshots/coffee-drink-details-screen-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/screenshots/coffee-drink-details-screen-dark.png
--------------------------------------------------------------------------------
/screenshots/coffee-drink-details-screen-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/screenshots/coffee-drink-details-screen-light.png
--------------------------------------------------------------------------------
/screenshots/coffee-drinks-screen-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/screenshots/coffee-drinks-screen-dark.png
--------------------------------------------------------------------------------
/screenshots/coffee-drinks-screen-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/screenshots/coffee-drinks-screen-light.png
--------------------------------------------------------------------------------
/screenshots/order-coffee-drinks-screen-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/screenshots/order-coffee-drinks-screen-dark.png
--------------------------------------------------------------------------------
/screenshots/order-coffee-drinks-screen-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose/cf95c8b5e2733e407b7ef70a108acb762c39a776/screenshots/order-coffee-drinks-screen-light.png
--------------------------------------------------------------------------------
/scripts/git-hooks.gradle:
--------------------------------------------------------------------------------
1 | static def isLinuxOrMacOs() {
2 | def osName = System.getProperty('os.name').toLowerCase(Locale.ROOT)
3 | return osName.contains('linux') || osName.contains('mac os') || osName.contains('macos')
4 | }
5 |
6 | task copyGitHooks(type: Copy) {
7 | description 'Copies the git hooks from scripts/git-hooks to the .git folder.'
8 | from("${rootDir}/scripts/git-hooks/") {
9 | include '**/*.sh'
10 | rename '(.*).sh', '$1'
11 | }
12 | into "${rootDir}/.git/hooks"
13 | fileMode = 0777
14 | }
15 |
16 | task installGitHooks(type: Exec) {
17 | description 'Installs the pre-commit git hooks from scripts/git-hooks.'
18 | group 'git hooks'
19 | workingDir rootDir
20 | commandLine 'chmod'
21 | args '-R', '+x', '.git/hooks/'
22 | dependsOn copyGitHooks
23 | onlyIf { isLinuxOrMacOs() }
24 | doLast {
25 | logger.info('Git hook installed successfully.')
26 | }
27 | }
--------------------------------------------------------------------------------
/scripts/git-hooks/pre-commit.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | echo "Running static analysis..."
4 |
5 | # KtLint verification
6 | ./gradlew app:ktlint --daemon
7 |
8 | status=$?
9 |
10 | if [ "$status" = 0 ] ; then
11 | echo "Static analysis found no problems."
12 | exit 0
13 | else
14 | echo 1>&2 "Static analysis found violations it could not fix."
15 | exit 1
16 | fi
--------------------------------------------------------------------------------
/scripts/ktlint.gradle:
--------------------------------------------------------------------------------
1 | repositories {
2 | jcenter()
3 | }
4 |
5 | configurations {
6 | ktlint
7 | }
8 |
9 | dependencies {
10 | ktlint 'com.pinterest:ktlint:0.42.1'
11 | }
12 |
13 | task ktlint(type: JavaExec, group: "verification") {
14 | description = "Check Kotlin code style."
15 | classpath = configurations.ktlint
16 | main = "com.pinterest.ktlint.Main"
17 | args "src/**/*.kt"
18 | }
19 |
20 | check.dependsOn ktlint
21 |
22 | task ktlintFormat(type: JavaExec, group: "formatting") {
23 | description = "Fix Kotlin code style deviations."
24 | classpath = configurations.ktlint
25 | main = "com.pinterest.ktlint.Main"
26 | args "-F", "src/**/*.kt"
27 | }
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 | rootProject.name='CoffeeDrinksWithJetpackCompose'
3 |
--------------------------------------------------------------------------------