From da043608f062241f3abe3e47d2a42366013b9d53 Mon Sep 17 00:00:00 2001 From: "@tinacious" Date: Wed, 11 Sep 2024 18:06:53 -0400 Subject: [PATCH] stocks app --- .editorconfig | 9 + .github/workflows/android.yml | 19 ++ .gitignore | 17 ++ README.md | 265 ++++++++++++++++++ app/.gitignore | 1 + app/build.gradle.kts | 119 ++++++++ app/proguard-rules.pro | 21 ++ .../interviews/stocks/HiltTestRunner.kt | 13 + .../stocks/StockSearchBehaviourTest.kt | 93 ++++++ .../interviews/stocks/UiTestUtils.kt | 49 ++++ .../stocks/db/stock/StockDaoTest.kt | 123 ++++++++ .../interviews/stocks/di/AppTestModule.kt | 83 ++++++ app/src/main/AndroidManifest.xml | 42 +++ app/src/main/ic_launcher-playstore.png | Bin 0 -> 57949 bytes .../interviews/stocks/MainActivity.kt | 136 +++++++++ .../interviews/stocks/StocksApplication.kt | 23 ++ .../interviews/stocks/config/AppConfig.kt | 5 + .../interviews/stocks/db/.gitkeep | 0 .../interviews/stocks/db/AppDatabase.kt | 16 ++ .../interviews/stocks/db/stock/StockDao.kt | 33 +++ .../interviews/stocks/db/stock/StockEntity.kt | 29 ++ .../interviews/stocks/di/AppModule.kt | 108 +++++++ .../interviews/stocks/events/AppEvent.kt | 7 + .../interviews/stocks/events/EventBus.kt | 40 +++ .../stocks/events/ObserveAsEvents.kt | 27 ++ .../stocks/logging/CrashReportingTree.kt | 26 ++ .../stocks/logging/DebugConsoleLoggingTree.kt | 17 ++ .../interviews/stocks/logging/Logger.kt | 5 + .../interviews/stocks/models/Stock.kt | 37 +++ .../stocks/navigation/BottomNavigationBar.kt | 64 +++++ .../stocks/navigation/NavigationRouter.kt | 27 ++ .../interviews/stocks/navigation/Route.kt | 40 +++ .../interviews/stocks/networking/ApiError.kt | 6 + .../interviews/stocks/networking/ApiResult.kt | 24 ++ .../stocks/networking/api/StocksApi.kt | 14 + .../stocks/repositories/StocksRepository.kt | 62 ++++ .../interviews/stocks/ui/TestTags.kt | 5 + .../stocks/ui/components/Divider.kt | 22 ++ .../stocks/ui/components/EmptyState.kt | 44 +++ .../stocks/ui/components/SearchInputView.kt | 51 ++++ .../components/StockSearchResultListItem.kt | 64 +++++ .../ui/components/StockSearchResults.kt | 36 +++ .../stocks/ui/components/UnstyledButton.kt | 25 ++ .../stocks/ui/icons/IconDrawable.kt | 34 +++ .../ui/icons/TinaciousDesignLogoIcon.kt | 27 ++ .../stocks/ui/icons/TintedIconDrawable.kt | 22 ++ .../stocks/ui/screens/about/AboutScreen.kt | 59 ++++ .../screens/stocksearch/StockSearchScreen.kt | 54 ++++ .../stocksearch/StockSearchViewModel.kt | 71 +++++ .../stocks/ui/snackbar/SnackBarController.kt | 26 ++ .../interviews/stocks/ui/theme/Color.kt | 29 ++ .../interviews/stocks/ui/theme/Theme.kt | 58 ++++ .../interviews/stocks/ui/theme/Type.kt | 18 ++ .../stocks/ui/utils/KeyboardVisibleState.kt | 43 +++ .../utils/ObserveInternetConnectionState.kt | 39 +++ .../stocks/ui/utils/ObserveSnackBarEvents.kt | 36 +++ .../interviews/stocks/utils/.gitkeep | 0 .../interviews/stocks/utils/IntentUtils.kt | 12 + .../interviews/stocks/utils/StringUtils.kt | 4 + .../interviews/stocks/workers/.gitkeep | 0 app/src/main/res/drawable/ic_dollar.xml | 9 + .../res/drawable/ic_launcher_background.xml | 170 +++++++++++ .../res/drawable/ic_launcher_foreground.xml | 30 ++ app/src/main/res/drawable/ic_search.xml | 9 + .../res/drawable/tinacious_design_logo.xml | 26 ++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + app/src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 2202 bytes .../mipmap-hdpi/ic_launcher_foreground.webp | Bin 0 -> 3324 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 3964 bytes app/src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 1278 bytes .../mipmap-mdpi/ic_launcher_foreground.webp | Bin 0 -> 1976 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 2336 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 3294 bytes .../mipmap-xhdpi/ic_launcher_foreground.webp | Bin 0 -> 4420 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 5840 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 5326 bytes .../mipmap-xxhdpi/ic_launcher_foreground.webp | Bin 0 -> 7890 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 9642 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 7530 bytes .../ic_launcher_foreground.webp | Bin 0 -> 11436 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 13452 bytes app/src/main/res/values/colors.xml | 3 + .../res/values/ic_launcher_background.xml | 4 + app/src/main/res/values/strings.xml | 26 ++ app/src/main/res/values/themes.xml | 5 + app/src/main/res/xml/backup_rules.xml | 13 + .../main/res/xml/data_extraction_rules.xml | 19 ++ .../interviews/stocks/models/StockTest.kt | 112 ++++++++ .../repositories/StocksRepositoryTest.kt | 79 ++++++ .../stocks/testutils/TestCoroutineRule.kt | 24 ++ .../stocksearch/StockSearchViewModelTest.kt | 115 ++++++++ build.gradle.kts | 7 + gradle.properties | 23 ++ gradle/libs.versions.toml | 75 +++++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 185 ++++++++++++ gradlew.bat | 89 ++++++ screenshots/01 dark mode - search results.png | Bin 0 -> 172025 bytes .../01 light mode - search results.png | Bin 0 -> 157599 bytes screenshots/02 dark mode - typing.png | Bin 0 -> 184933 bytes screenshots/02 light mode - typing.png | Bin 0 -> 131826 bytes screenshots/03 dark mode - empty.png | Bin 0 -> 99687 bytes screenshots/03 light mode - empty.png | Bin 0 -> 97671 bytes screenshots/04 dark mode - no results.png | Bin 0 -> 93302 bytes screenshots/04 light mode - no results.png | Bin 0 -> 118165 bytes screenshots/05 dark mode - about.png | Bin 0 -> 86070 bytes screenshots/05 light mode - about.png | Bin 0 -> 90563 bytes screenshots/app icon.png | Bin 0 -> 9176 bytes screenshots/build-app-config.png | Bin 0 -> 68024 bytes screenshots/large-app-icon.png | Bin 0 -> 27714 bytes screenshots/test-results-android.png | Bin 0 -> 58138 bytes screenshots/test-results-unit.png | Bin 0 -> 95556 bytes screenshots/tests-running-android-studio.png | Bin 0 -> 212485 bytes settings.gradle.kts | 23 ++ 116 files changed, 3441 insertions(+) create mode 100644 .editorconfig create mode 100644 .github/workflows/android.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 app/.gitignore create mode 100644 app/build.gradle.kts create mode 100644 app/proguard-rules.pro create mode 100644 app/src/androidTest/java/com/tinaciousdesign/interviews/stocks/HiltTestRunner.kt create mode 100644 app/src/androidTest/java/com/tinaciousdesign/interviews/stocks/StockSearchBehaviourTest.kt create mode 100644 app/src/androidTest/java/com/tinaciousdesign/interviews/stocks/UiTestUtils.kt create mode 100644 app/src/androidTest/java/com/tinaciousdesign/interviews/stocks/db/stock/StockDaoTest.kt create mode 100644 app/src/androidTest/java/com/tinaciousdesign/interviews/stocks/di/AppTestModule.kt create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/ic_launcher-playstore.png create mode 100644 app/src/main/java/com/tinaciousdesign/interviews/stocks/MainActivity.kt create mode 100644 app/src/main/java/com/tinaciousdesign/interviews/stocks/StocksApplication.kt create mode 100644 app/src/main/java/com/tinaciousdesign/interviews/stocks/config/AppConfig.kt create mode 100644 app/src/main/java/com/tinaciousdesign/interviews/stocks/db/.gitkeep create mode 100644 app/src/main/java/com/tinaciousdesign/interviews/stocks/db/AppDatabase.kt create mode 100644 app/src/main/java/com/tinaciousdesign/interviews/stocks/db/stock/StockDao.kt create mode 100644 app/src/main/java/com/tinaciousdesign/interviews/stocks/db/stock/StockEntity.kt create mode 100644 app/src/main/java/com/tinaciousdesign/interviews/stocks/di/AppModule.kt create mode 100644 app/src/main/java/com/tinaciousdesign/interviews/stocks/events/AppEvent.kt create mode 100644 app/src/main/java/com/tinaciousdesign/interviews/stocks/events/EventBus.kt create mode 100644 app/src/main/java/com/tinaciousdesign/interviews/stocks/events/ObserveAsEvents.kt create mode 100644 app/src/main/java/com/tinaciousdesign/interviews/stocks/logging/CrashReportingTree.kt create mode 100644 app/src/main/java/com/tinaciousdesign/interviews/stocks/logging/DebugConsoleLoggingTree.kt create mode 100644 app/src/main/java/com/tinaciousdesign/interviews/stocks/logging/Logger.kt create mode 100644 app/src/main/java/com/tinaciousdesign/interviews/stocks/models/Stock.kt create mode 100644 app/src/main/java/com/tinaciousdesign/interviews/stocks/navigation/BottomNavigationBar.kt create mode 100644 app/src/main/java/com/tinaciousdesign/interviews/stocks/navigation/NavigationRouter.kt create mode 100644 app/src/main/java/com/tinaciousdesign/interviews/stocks/navigation/Route.kt create mode 100644 app/src/main/java/com/tinaciousdesign/interviews/stocks/networking/ApiError.kt create mode 100644 app/src/main/java/com/tinaciousdesign/interviews/stocks/networking/ApiResult.kt create mode 100644 app/src/main/java/com/tinaciousdesign/interviews/stocks/networking/api/StocksApi.kt create mode 100644 app/src/main/java/com/tinaciousdesign/interviews/stocks/repositories/StocksRepository.kt create mode 100644 app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/TestTags.kt create mode 100644 app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/components/Divider.kt create mode 100644 app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/components/EmptyState.kt create mode 100644 app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/components/SearchInputView.kt create mode 100644 app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/components/StockSearchResultListItem.kt create mode 100644 app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/components/StockSearchResults.kt create mode 100644 app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/components/UnstyledButton.kt create mode 100644 app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/icons/IconDrawable.kt create mode 100644 app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/icons/TinaciousDesignLogoIcon.kt create mode 100644 app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/icons/TintedIconDrawable.kt create mode 100644 app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/screens/about/AboutScreen.kt create mode 100644 app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/screens/stocksearch/StockSearchScreen.kt create mode 100644 app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/screens/stocksearch/StockSearchViewModel.kt create mode 100644 app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/snackbar/SnackBarController.kt create mode 100644 app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/theme/Color.kt create mode 100644 app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/theme/Theme.kt create mode 100644 app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/theme/Type.kt create mode 100644 app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/utils/KeyboardVisibleState.kt create mode 100644 app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/utils/ObserveInternetConnectionState.kt create mode 100644 app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/utils/ObserveSnackBarEvents.kt create mode 100644 app/src/main/java/com/tinaciousdesign/interviews/stocks/utils/.gitkeep create mode 100644 app/src/main/java/com/tinaciousdesign/interviews/stocks/utils/IntentUtils.kt create mode 100644 app/src/main/java/com/tinaciousdesign/interviews/stocks/utils/StringUtils.kt create mode 100644 app/src/main/java/com/tinaciousdesign/interviews/stocks/workers/.gitkeep create mode 100644 app/src/main/res/drawable/ic_dollar.xml create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 app/src/main/res/drawable/ic_search.xml create mode 100644 app/src/main/res/drawable/tinacious_design_logo.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/ic_launcher_background.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/themes.xml create mode 100644 app/src/main/res/xml/backup_rules.xml create mode 100644 app/src/main/res/xml/data_extraction_rules.xml create mode 100644 app/src/test/java/com/tinaciousdesign/interviews/stocks/models/StockTest.kt create mode 100644 app/src/test/java/com/tinaciousdesign/interviews/stocks/repositories/StocksRepositoryTest.kt create mode 100644 app/src/test/java/com/tinaciousdesign/interviews/stocks/testutils/TestCoroutineRule.kt create mode 100644 app/src/test/java/com/tinaciousdesign/interviews/stocks/ui/screens/stocksearch/StockSearchViewModelTest.kt create mode 100644 build.gradle.kts create mode 100644 gradle.properties create mode 100644 gradle/libs.versions.toml create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 screenshots/01 dark mode - search results.png create mode 100644 screenshots/01 light mode - search results.png create mode 100644 screenshots/02 dark mode - typing.png create mode 100644 screenshots/02 light mode - typing.png create mode 100644 screenshots/03 dark mode - empty.png create mode 100644 screenshots/03 light mode - empty.png create mode 100644 screenshots/04 dark mode - no results.png create mode 100644 screenshots/04 light mode - no results.png create mode 100644 screenshots/05 dark mode - about.png create mode 100644 screenshots/05 light mode - about.png create mode 100644 screenshots/app icon.png create mode 100644 screenshots/build-app-config.png create mode 100644 screenshots/large-app-icon.png create mode 100644 screenshots/test-results-android.png create mode 100644 screenshots/test-results-unit.png create mode 100644 screenshots/tests-running-android-studio.png create mode 100644 settings.gradle.kts diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5245895 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +indent_size = 4 +indent_style = space +insert_final_newline = true + +[*.yml] +indent_size = 2 diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml new file mode 100644 index 0000000..f0c1839 --- /dev/null +++ b/.github/workflows/android.yml @@ -0,0 +1,19 @@ +name: Android CI + +on: + push: +jobs: + build-test: + + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - uses: actions/checkout@v4 + - name: set up JDK 11 + uses: actions/setup-java@v4 + with: + java-version: 17 + distribution: 'temurin' + - name: Build, Test + run: ./gradlew assemble test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..30c3b70 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +*.iml +.gradle +/local.properties +/.idea +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +/.idea/inspectionProfiles +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/README.md b/README.md new file mode 100644 index 0000000..20957d1 --- /dev/null +++ b/README.md @@ -0,0 +1,265 @@ +# Stocks + +This is a sample stock search app for Android built with Jetpack Compose and various other [libraries](#libraries). + + +- [About this Guide](#about-this-guide) +- [Setup](#setup) + - [Requirements](#requirements) + - [Building](#building) + - [Unit and UI tests](#unit-and-ui-tests) +- [Features](#features) +- [Libraries](#libraries) +- [Architecture Walkthrough](#architecture-walkthrough) + - [Unit test walkthrough](#unit-test-walkthrough) + - [Android test walkthrough](#android-test-walkthrough) +- [App Screenshots](#app-screenshots) + - [App icon](#app-icon) + - [Search results](#search-results) + - [Empty states](#empty-states) + - [About screen](#about-screen) +- [Install](#install) + +![](./screenshots/app%20icon.png) + + + + + + + +## About this Guide + +This guide assumes that you have experience in Android Development and are familiar with Android Studio and the command line tools. + +## Setup + +### Requirements + +- [Android Studio](https://developer.android.com/studio) version Android Studio Jellyfish | 2023.3.1 Patch 1 or greater +- [Android command line tools](https://developer.android.com/tools) + + +### Building + +You can build the app either in Android Studio or the command line (requires the command line tools mentioned above). + +Build the app from the command line using: + + ./gradlew assemble + +Install the debug build to your connected device: + + adb install app/build/outputs/apk/debug/app-debug.apk + +The screenshot below shows that you need to choose the "app" build configuration and your chosen device. This section is in the top bar of the Android Studio UI. + +![](./screenshots/build-app-config.png) + + +### Unit and UI tests + +You can run the tests either in Android Studio or the command line (requires the command line tools mentioned above). + + +Run the tests in Android Studio by visiting the `test` and `androidTest` folders at `./app/src/test` and `./app/src/androidTest` respectively. Right click and choose `Run tests in com.tinaciousdesign.interviews.stocks`. + +Run the unit tests from the command line with the following command: + + ./gradlew test + +Run the UI tests from the command line with the following command: + + ./gradlew connectedCheck --stacktrace + +Below is a screenshot of the Android Studio UI and where to find these tests. This view is in the tree explorer view of Android Studio using either "Android" or "Project" versions of the tree explorer view. + +![Screenshot of the androidTest and test directories in Android Studio](./screenshots/tests-running-android-studio.png) + + + + +## Features + +- πŸ“ˆ Loads stocks from an API endpoint +- πŸ›œ Enables offline access to stock searching, provided that stocks had initially loaded from the API while there was a connection +- 🌝 Supports light and dark mode using the user's system preference setting +- πŸ—ΊοΈ Bottom navigation to enable navigating to different screens +- 🍫 Snackbar UI to enable global alert messages +- πŸ€– Unit tests and Android tests, including tests for the UI and the persistence layer +- πŸ«™ Empty states + + + +## Libraries + +The following libraries were used for the project: + +- [Jetpack Compose](https://developer.android.com/compose): Android's new declarative UI framework and Google's recommended way to build Android apps going forward. +- [Room Database](https://developer.android.com/jetpack/androidx/releases/room): On-device database storage using a SQLite database and the Room database ORM, a library by Google for a SQL-based on-device persistence layer. +- [Hilt](https://developer.android.com/training/dependency-injection/hilt-android): Compile-time dependency injection library by Google that uses Dagger. This is used to inject dependencies into Android components (e.g. Application, Activity, View Model, WorkManager classes, etc.) Test helpers for Hilt were also included to enable unit and instrumentation tests for Android components using Hilt. +- [JUnit](https://junit.org/junit4/) for unit testing and instrumented Android tests +- [Compose UI testing](https://developer.android.com/develop/ui/compose/testing) for instrumented UI tests that include UI interactions like tapping elements, filling out forms, and waiting for views to appear. +- [Mockk](https://mockk.io/): A mocking library for Kotlin with support for coroutines, making it possible to test suspending functions. +- [Turbine](https://github.com/cashapp/turbine): A testing library by Square that makes it easier to test Kotlin coroutine flows. +- [Navigation Compose](https://developer.android.com/develop/ui/compose/navigation): Type-safe navigation library for Jetpack Compose, by Google. This is used to support navigation globally in the app. +- [Timber](https://github.com/JakeWharton/timber): Production-grade logging library by Square. This is used to log to the console and can also be used in production apps to log to exception tracking services. +- [Retrofit](https://square.github.io/retrofit/): Networking library by Square. This is used to make network requests to the API endpoint. +- [kotlinx-serialization](https://github.com/Kotlin/kotlinx.serialization): Kotlin standard library serialization library and compiler plugin. It's used to support serializing JSON from network requests. +- [Coil](https://coil-kt.github.io/coil/): Image loading library with support for Jetpack Compose for loading remote images. This is used to load the placeholder images for the stocks. + + +## Architecture Walkthrough + +I decided to build the app using Android's [Jetpack Compose](https://developer.android.com/compose) framework with MVVM architecture and Kotlin flows. + +I used the [Hilt](https://developer.android.com/training/dependency-injection/hilt-android) dependency injection library to make it easier to work with all of the various dependencies as well as enable the app to be more easily tested. There was also another option I've enjoyed using called [Koin](https://insert-koin.io/), and while it's simpler to set up and work with than Hilt (especially in production apps with various Android components and services), I opted to go with Hilt because it's by Google and offers more compile-time assurances. Dependency injection is currently configured as a single module `AppModule` at `app/src/main/java/com/tinaciousdesign/interviews/stocks/di/AppModule.kt`. In a production app, it may make sense to split it up into multiple modules which can make it easier for testing. + +I used [Room Database](https://developer.android.com/jetpack/androidx/releases/room) as the local persistence layer to cache stock prices to enable more efficient searching. This also enables using the app offline. Another option I've used in the past is [Realm](https://github.com/realm), a cross-platform noSQL-based on-device database, though I opted to go with Room as it's officially by the Google Android team. Classes related to the database can be found in `app/src/main/java/com/tinaciousdesign/interviews/stocks/db`, which is where the Room database along with the Stock Data Access Object (DAO) and entity are configured. + +```text +app/src/main/java/com/tinaciousdesign/interviews/stocks/db +β”œβ”€β”€ AppDatabase.kt +└── stock + β”œβ”€β”€ StockDao.kt + └── StockEntity.kt +``` + +I'm using a repository pattern. The `StocksRepository` accesses Room database and the network API and exposes suspending methods and flows for accessing all of the stock data, as well as filtering stock data by a search query. It implements a Retrofit service which accesses the API endpoint. The Retrofit services are configured with a custom `OkHttpClient`, making it easy to add request and response interceptors. You can see a logging interceptor was added as an example. Networking code, including a custom `ApiResult` sealed class, can be viewed in `app/src/main/java/com/tinaciousdesign/interviews/stocks/networking`: + +```text +app/src/main/java/com/tinaciousdesign/interviews/stocks/networking +β”œβ”€β”€ ApiError.kt +β”œβ”€β”€ ApiResult.kt +└── api + └── StocksApi.kt +``` + +The app is scaffolded with a bottom navigation bar and support for global alert messages using Android's [Snackbar](https://developer.android.com/develop/ui/compose/components/snackbar) component. Navigation is implemented using Jetpack Compose's new type-safe navigation component. The implementation can be viewed in `app/src/main/java/com/tinaciousdesign/interviews/stocks/navigation`: + +```text +app/src/main/java/com/tinaciousdesign/interviews/stocks/navigation +β”œβ”€β”€ BottomNavigationBar.kt +β”œβ”€β”€ NavigationRouter.kt +└── Route.kt +``` + +The app uses an EventBus pattern for platform-agnostic event-based programming that leverages Kotlin coroutine flows, which can be useful for listening to events across the app. You can toggle your internet on and off to view the Snackbar messaging. The snackbar duration is set to "long" to make it easy for people to test when toggling airplane mode on and off, but in a production app should be set to "short" since they can continue to use the app with the cached prices. Event-related code is in the directory `app/src/main/java/com/tinaciousdesign/interviews/stocks/events`, which includes the `EventBus` which publishes and subscribes to `AppEvent`, and a compose-based utility for observing events in composable components (`ObserveAsEvents`): + +```text +app/src/main/java/com/tinaciousdesign/interviews/stocks/events +β”œβ”€β”€ AppEvent.kt +β”œβ”€β”€ EventBus.kt +└── ObserveAsEvents.kt +``` + +Constants are defined in `AppConfig`. In a production app, the app would have [multiple build variants called product flavours](https://developer.android.com/build/build-variants) and would be configured to build separate production, staging, and development artifacts. An `AppConfig` would be provided for each of these with their respective values. We can also use product flavours to configure other things based on the environment, like the app icon. + +Logging has been added in `app/src/main/java/com/tinaciousdesign/interviews/stocks/logging` which includes configuration for Timber and a placeholder for production logging: + +```text +app/src/main/java/com/tinaciousdesign/interviews/stocks/logging +β”œβ”€β”€ CrashReportingTree.kt +β”œβ”€β”€ DebugConsoleLoggingTree.kt +└── Logger.kt +``` + +The composables are split up into separate directories for screens, components, and icons. + +- Common components are in `components` +- Icons that can be reused across screens are in `icons` +- Screens are (usually) root-level components. + +The `ui` package also includes utilities for working with various Android elements, as well as test tags to make it easy to access elements for instrumented UI tests. + +Other utility functions are available in `app/src/main/java/com/tinaciousdesign/interviews/stocks/utils`. + +Tests are split up into 2 separate directories, `test` and `androidTest`: + +- `test` is where platform-agnostic unit tests can be found +- `androidTest` is where unit tests that use Android components are. This is where you can find tests for Room database queries and instrumented UI tests that test the stock searching flow. + + +### Unit test walkthrough + +Unit tests are available in `app/src/test/java/com/tinaciousdesign/interviews/stocks`: + +```text +app/src/test/java/com/tinaciousdesign/interviews/stocks +β”œβ”€β”€ models +β”‚Β Β  └── StockTest.kt +β”œβ”€β”€ repositories +β”‚Β Β  └── StocksRepositoryTest.kt +β”œβ”€β”€ testutils +β”‚Β Β  └── TestCoroutineRule.kt +└── ui + └── screens + └── stocksearch + └── StockSearchViewModelTest.kt +``` + +- **StockTest**: tests for the `Stock` model including formatting data for the UI, and filtering and sorting the model. While the filtering logic here did not end up getting used and was replaced by a Room database query, it can still be useful for filtering a collection of stocks in-memory. This test also tests the custom comparator which was implemented to support the criteria that exact matches for the ticker or name are prioritized above partial matches. While I wasn't able to see which data would enable me to test this in the app, it is unit tested, and a separate endpoint was added to the code, which can be commented in in-place of the default one (see `com/tinaciousdesign/interviews/stocks/networking/api/StocksApi.kt` and enable the code `@GET("tinacious/a3ddc32e49c04b5de21e4bb30eb47e68/raw/5b590f6f369fb92fc49e33a14ab2275eb5629c24/mock-stocks.json")`). +- **StocksRepositoryTest**: tests for the repository layer including testing that we can make API calls with Retrofit and cache the data to Room database. +- **StockSearchViewModelTest**: tests that the data is queried when the view model has its `loadStocks()` method called and that stocks are available on the exposed `stocks` StateFlow. It also includes tests for the sorting prioritization logic with a mock repository. + +Below is a screenshot of the unit test results. + +![](./screenshots/test-results-unit.png) + + +### Android test walkthrough + +Android tests are available in `androidTest` at `app/src/androidTest/java/com/tinaciousdesign/interviews/stocks`: + +```text +app/src/androidTest/java/com/tinaciousdesign/interviews/stocks +β”œβ”€β”€ HiltTestRunner.kt +β”œβ”€β”€ StockSearchBehaviourTest.kt +β”œβ”€β”€ UiTestUtils.kt +β”œβ”€β”€ db +β”‚Β Β  └── stock +β”‚Β Β  └── StockDaoTest.kt +└── di + └── AppTestModule.kt +``` + +- **HiltTestRunner** and **AppTestModule** are Hilt-related utilities for helping with instrumented tests in projects that use Hilt +- **StockDaoTest**: Tests for the Room database implementation which test that we can insert records, get all records, and query records. It includes the suspending methods as well as the flow which is the one the search screen uses. +- **StockSearchBehaviourTest**: This is an instrumented UI test that tests the behaviour of loading up the app and searching. It makes a real network request to fetch the data and uses an in-memory database. + +Below is a screenshot of the Android test results including database tests and UI tests. + +![](./screenshots/test-results-android.png) + + +## App Screenshots + +### App icon + + + +### Search results + +![](./screenshots/01%20dark%20mode%20-%20search%20results.png) +![](./screenshots/01%20light%20mode%20-%20search%20results.png) +![](./screenshots/02%20dark%20mode%20-%20typing.png) +![](./screenshots/02%20light%20mode%20-%20typing.png) + + +### Empty states + +![](./screenshots/03%20dark%20mode%20-%20empty.png) +![](./screenshots/03%20light%20mode%20-%20empty.png) +![](./screenshots/04%20dark%20mode%20-%20no%20results.png) +![](./screenshots/04%20light%20mode%20-%20no%20results.png) + + +### About screen + +![](./screenshots/05%20dark%20mode%20-%20about.png) +![](./screenshots/05%20light%20mode%20-%20about.png) + + +## Install + +You can clone the source and build with the provided instructions above, or you can use [adb](https://developer.android.com/tools/adb) to install a debug build APK available in [Releases](https://github.com/tinacious/stocks-android-jetpack-compose/releases). diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..f790d81 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,119 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.jetbrains.kotlin.android) + kotlin("kapt") + id("com.google.dagger.hilt.android") + id("dagger.hilt.android.plugin") + alias(libs.plugins.kotlinxSerialization) +} + +android { + namespace = "com.tinaciousdesign.interviews.stocks" + compileSdk = 34 + + defaultConfig { + applicationId = "com.tinaciousdesign.interviews.stocks" + minSdk = 29 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "com.tinaciousdesign.interviews.stocks.HiltTestRunner" + vectorDrawables { + useSupportLibrary = true + } + + kotlinOptions { + freeCompilerArgs += arrayOf( + "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi" + ) + } + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } + buildFeatures { + buildConfig = true + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = "1.5.1" + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } +} + +dependencies { + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.activity.compose) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.ui.tooling.preview) + implementation(libs.androidx.material3) + + // Logging + implementation(libs.timber) + implementation(libs.square.okhttp.logging.interceptor) + + // DI + implementation(libs.hilt.android) + kapt(libs.hilt.android.compiler) + kapt(libs.dagger.compiler) + kapt(libs.hilt.compiler) + + // Networking + implementation(libs.retrofit) + implementation(libs.retrofit.converter.kotlinx.serialization) + implementation(libs.coil.compose) + + // Navigation + implementation(libs.androidx.navigation.compose) + implementation(libs.androidx.hilt.navigation.compose) + implementation(libs.androidx.hilt.navigation.fragment) + implementation(libs.kotlinx.serialization.json) + + // Room database + implementation(libs.androidx.room.runtime) + annotationProcessor(libs.androidx.room.compiler) + kapt(libs.androidx.room.compiler) + implementation(libs.androidx.room.ktx) + + // WorkManager + implementation(libs.androidx.hilt.work) + implementation(libs.androidx.work.runtime.ktx) + + /// Testing + androidTestImplementation(libs.hilt.android.testing) + kaptAndroidTest(libs.hilt.android.compiler) + testImplementation(libs.junit) + testImplementation(libs.mockk) + testImplementation(libs.kotlinx.coroutines.test) + testImplementation(libs.turbine) + androidTestImplementation(libs.turbine) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.ui.test.junit4) + debugImplementation(libs.androidx.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/com/tinaciousdesign/interviews/stocks/HiltTestRunner.kt b/app/src/androidTest/java/com/tinaciousdesign/interviews/stocks/HiltTestRunner.kt new file mode 100644 index 0000000..355efc9 --- /dev/null +++ b/app/src/androidTest/java/com/tinaciousdesign/interviews/stocks/HiltTestRunner.kt @@ -0,0 +1,13 @@ +package com.tinaciousdesign.interviews.stocks + +import android.app.Application +import android.content.Context +import androidx.test.runner.AndroidJUnitRunner +import dagger.hilt.android.testing.HiltTestApplication + +class HiltTestRunner : AndroidJUnitRunner() { + + override fun newApplication(cl: ClassLoader?, className: String?, context: Context?): Application { + return super.newApplication(cl, HiltTestApplication::class.java.name, context) + } +} diff --git a/app/src/androidTest/java/com/tinaciousdesign/interviews/stocks/StockSearchBehaviourTest.kt b/app/src/androidTest/java/com/tinaciousdesign/interviews/stocks/StockSearchBehaviourTest.kt new file mode 100644 index 0000000..fee3b9b --- /dev/null +++ b/app/src/androidTest/java/com/tinaciousdesign/interviews/stocks/StockSearchBehaviourTest.kt @@ -0,0 +1,93 @@ +package com.tinaciousdesign.interviews.stocks + +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.ui.Modifier +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performTextInput +import androidx.compose.ui.unit.dp +import androidx.navigation.compose.rememberNavController +import com.tinaciousdesign.interviews.stocks.di.AppModule +import com.tinaciousdesign.interviews.stocks.navigation.BottomNavigationBar +import com.tinaciousdesign.interviews.stocks.navigation.NavigationRouter +import com.tinaciousdesign.interviews.stocks.ui.TestTags +import com.tinaciousdesign.interviews.stocks.ui.theme.StocksTheme +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import dagger.hilt.android.testing.UninstallModules +import org.junit.Before +import org.junit.Rule +import org.junit.Test + + +@HiltAndroidTest +@UninstallModules(AppModule::class) +class StockSearchBehaviourTest { + @get:Rule(order = 0) + val hiltRule = HiltAndroidRule(this) + + @get:Rule(order = 1) + val composeRule = createAndroidComposeRule() + + @Before + fun setUp() { + hiltRule.inject() + + composeRule.activity.setContent { + StocksTheme { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + val navController = rememberNavController() + + Scaffold( + modifier = Modifier.fillMaxSize(), + bottomBar = { BottomNavigationBar(navController = navController) } + ) { innerPadding -> + Box( + modifier = Modifier.padding( + PaddingValues( + 0.dp, + 0.dp, + 0.dp, + innerPadding.calculateBottomPadding() + ) + ) + ) { + NavigationRouter(navController) + } + } + } + } + } + } + + @Test + fun userCanSearchForStocks() { + with(composeRule) { + composeRule.onNodeWithText("Use the search field above to find stocks by ticker or by name").assertIsDisplayed() + + composeRule + .onNodeWithTag(TestTags.searchField) + .performTextInput("ow") + + waitForText("POWL") + + textIsDisplayed("POWL") + textIsDisplayed("Omni Resources") + + textIsDisplayed("FVOW") + textIsDisplayed("Harmony Enterprises") + } + } +} diff --git a/app/src/androidTest/java/com/tinaciousdesign/interviews/stocks/UiTestUtils.kt b/app/src/androidTest/java/com/tinaciousdesign/interviews/stocks/UiTestUtils.kt new file mode 100644 index 0000000..885a33e --- /dev/null +++ b/app/src/androidTest/java/com/tinaciousdesign/interviews/stocks/UiTestUtils.kt @@ -0,0 +1,49 @@ +package com.tinaciousdesign.interviews.stocks + +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onAllNodesWithText +import androidx.compose.ui.test.onNodeWithText +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue + +@OptIn(ExperimentalTestApi::class) +fun ComposeTestRule.waitForText( + text: String, + timeoutMillis: Long = 5000 +) { + waitUntilAtLeastOneExists(hasText(text), timeoutMillis = timeoutMillis) +} + +fun ComposeTestRule.textIsDisplayed( + text: String, + expectedOccurrences: Int = 1 +) { + if (expectedOccurrences == 1) { + onNodeWithText(text).assertIsDisplayed() + } else { + assertEquals(onAllNodesWithText(text).fetchSemanticsNodes().size, expectedOccurrences) + } +} + +fun ComposeTestRule.textIsDisplayedAtLeastOnce( + text: String, + minOccurrences: Int = 1 +) { + assertTrue(this.onAllNodesWithText(text).fetchSemanticsNodes().size >= minOccurrences) +} + +@OptIn(ExperimentalTestApi::class) +fun ComposeTestRule.sleep( + timeoutMillis: Long +) { + @Suppress("SwallowedException") + try { + // Random string that will never be found + waitUntilAtLeastOneExists(hasText("446fc9cdc8e63d9f11fb6bacd2f51ef5!"), timeoutMillis = timeoutMillis) + } catch (t: Throwable) { + // swallow this exception + } +} diff --git a/app/src/androidTest/java/com/tinaciousdesign/interviews/stocks/db/stock/StockDaoTest.kt b/app/src/androidTest/java/com/tinaciousdesign/interviews/stocks/db/stock/StockDaoTest.kt new file mode 100644 index 0000000..1476a86 --- /dev/null +++ b/app/src/androidTest/java/com/tinaciousdesign/interviews/stocks/db/stock/StockDaoTest.kt @@ -0,0 +1,123 @@ +package com.tinaciousdesign.interviews.stocks.db.stock + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import app.cash.turbine.test +import com.tinaciousdesign.interviews.stocks.db.AppDatabase +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import java.io.IOException + +@RunWith(AndroidJUnit4::class) +class StockDaoTest { + + private lateinit var stockDao: StockDao + private lateinit var db: AppDatabase + + @Before + fun createDb() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java).build() + stockDao = db.stocks() + } + + @After + @Throws(IOException::class) + fun closeDb() { + db.close() + } + + @Test + @Throws(Exception::class) + fun insertStocksAndGetAllStocks(): Unit = runTest { + assertEquals(emptyList(), stockDao.getAll()) + + stockDao.insertAll( + listOf( + StockEntity(ticker ="FFF", name = "FFF Co", price = 100.0), + StockEntity(ticker ="DDD", name = "DDD Co", price = 100.0), + StockEntity(ticker ="CCC", name = "CCC Co", price = 100.0), + StockEntity(ticker ="AB", name = "Absolute", price = 100.0), + StockEntity(ticker ="ABC", name = "ABC Industries", price = 100.0), + StockEntity(ticker ="XYZ", name = "XYZ and ABC", price = 100.0), + ) + ) + + val result = stockDao.getAll() + + assertEquals( + listOf( + StockEntity(id = 1, ticker ="FFF", name = "FFF Co", price = 100.0), + StockEntity(id = 2, ticker ="DDD", name = "DDD Co", price = 100.0), + StockEntity(id = 3, ticker ="CCC", name = "CCC Co", price = 100.0), + StockEntity(id = 4, ticker ="AB", name = "Absolute", price = 100.0), + StockEntity(id = 5, ticker ="ABC", name = "ABC Industries", price = 100.0), + StockEntity(id = 6, ticker ="XYZ", name = "XYZ and ABC", price = 100.0), + ), + result, + ) + } + + @Test + @Throws(Exception::class) + fun searchStocks(): Unit = runTest { + assertEquals(emptyList(), stockDao.getAll()) + + stockDao.insertAll( + listOf( + StockEntity(ticker = "FFF", name = "FFF Co", price = 100.0), + StockEntity(ticker = "DDD", name = "DDD Co", price = 100.0), + StockEntity(ticker = "CCC", name = "CCC Co", price = 100.0), + StockEntity(ticker = "AB", name = "Absolute", price = 100.0), + StockEntity(ticker = "ABC", name = "ABC Industries", price = 100.0), + StockEntity(ticker = "XYZ", name = "XYZ and ABC", price = 100.0), + ) + ) + + val result = stockDao.find("ab") + + assertEquals( + listOf( + StockEntity(id = 4, ticker ="AB", name = "Absolute", price = 100.0), + StockEntity(id = 5, ticker ="ABC", name = "ABC Industries", price = 100.0), + StockEntity(id = 6, ticker ="XYZ", name = "XYZ and ABC", price = 100.0), + ), + result, + ) + } + + @Test + @Throws(Exception::class) + fun searchStocksWithFlow(): Unit = runTest { + assertEquals(emptyList(), stockDao.getAll()) + + stockDao.insertAll( + listOf( + StockEntity(ticker = "FFF", name = "FFF Co", price = 100.0), + StockEntity(ticker = "DDD", name = "DDD Co", price = 100.0), + StockEntity(ticker = "CCC", name = "CCC Co", price = 100.0), + StockEntity(ticker = "AB", name = "Absolute", price = 100.0), + StockEntity(ticker = "ABC", name = "ABC Industries", price = 100.0), + StockEntity(ticker = "XYZ", name = "XYZ and ABC", price = 100.0), + ) + ) + + stockDao.findStocksFlow("abc").test { + val result = awaitItem() + + assertEquals( + listOf( + StockEntity(id = 5, ticker ="ABC", name = "ABC Industries", price = 100.0), + StockEntity(id = 6, ticker ="XYZ", name = "XYZ and ABC", price = 100.0), + ), + result, + ) + } + } +} diff --git a/app/src/androidTest/java/com/tinaciousdesign/interviews/stocks/di/AppTestModule.kt b/app/src/androidTest/java/com/tinaciousdesign/interviews/stocks/di/AppTestModule.kt new file mode 100644 index 0000000..ef71c1b --- /dev/null +++ b/app/src/androidTest/java/com/tinaciousdesign/interviews/stocks/di/AppTestModule.kt @@ -0,0 +1,83 @@ +package com.tinaciousdesign.interviews.stocks.di + +import android.content.Context +import androidx.room.Room +import com.tinaciousdesign.interviews.stocks.BuildConfig +import com.tinaciousdesign.interviews.stocks.config.AppConfig +import com.tinaciousdesign.interviews.stocks.db.AppDatabase +import com.tinaciousdesign.interviews.stocks.db.stock.StockDao +import com.tinaciousdesign.interviews.stocks.events.EventBus +import com.tinaciousdesign.interviews.stocks.networking.api.StocksApi +import com.tinaciousdesign.interviews.stocks.repositories.StocksRepository +import com.tinaciousdesign.interviews.stocks.repositories.StocksRepositoryImpl +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.converter.kotlinx.serialization.asConverterFactory +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +class AppTestModule { + // region Repositories + + @Provides @Singleton + fun provideStocksRepository( + stocksApi: StocksApi, + stockDao: StockDao, + ): StocksRepository = StocksRepositoryImpl( + stocksApi, + stockDao, + ) + + @Provides @Singleton + fun provideEventBus(): EventBus = EventBus() + + // endregion Repositories + + // region Networking + + @Provides @Singleton + fun provideRetrofit(): Retrofit { + val converterFactory = Json.asConverterFactory( + "application/json; charset=UTF8".toMediaType() + ) + + return Retrofit.Builder() + .baseUrl(AppConfig.stocksApiBaseUrl) + .addConverterFactory(converterFactory) + .build() + } + + // region Networking -> API + + @Provides @Singleton + fun provideStocksApi(retrofit: Retrofit): StocksApi = retrofit.create(StocksApi::class.java) + + // endregion Networking -> API + + // endregion Networking + + // region Database + + @Provides @Singleton + fun provideAppDatabase( + @ApplicationContext appContext: Context + ): AppDatabase = + Room.inMemoryDatabaseBuilder( + appContext, + AppDatabase::class.java, + ).build() + + @Provides @Singleton + fun provideStockDao(appDatabase: AppDatabase): StockDao = appDatabase.stocks() + + // endregion Database +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..ff902ed --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000000000000000000000000000000000000..62e2bd8e422baf16f567cc960ca0c611b7390ffe GIT binary patch literal 57949 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�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 U@Je4e`4j=#&kUYcs@aGC4*^`pEC2ui literal 0 HcmV?d00001 diff --git a/app/src/main/java/com/tinaciousdesign/interviews/stocks/MainActivity.kt b/app/src/main/java/com/tinaciousdesign/interviews/stocks/MainActivity.kt new file mode 100644 index 0000000..aaa777f --- /dev/null +++ b/app/src/main/java/com/tinaciousdesign/interviews/stocks/MainActivity.kt @@ -0,0 +1,136 @@ +package com.tinaciousdesign.interviews.stocks + +import android.content.Context +import android.net.ConnectivityManager +import android.net.Network +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.BottomAppBar +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Surface +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.lifecycleScope +import androidx.navigation.compose.rememberNavController +import com.tinaciousdesign.interviews.stocks.events.AppEvent +import com.tinaciousdesign.interviews.stocks.events.EventBus +import com.tinaciousdesign.interviews.stocks.navigation.BottomNavigationBar +import com.tinaciousdesign.interviews.stocks.navigation.NavigationRouter +import com.tinaciousdesign.interviews.stocks.ui.snackbar.SnackBarController +import com.tinaciousdesign.interviews.stocks.ui.theme.StocksTheme +import com.tinaciousdesign.interviews.stocks.ui.utils.KeyboardState +import com.tinaciousdesign.interviews.stocks.ui.utils.ObserveInternetConnectionState +import com.tinaciousdesign.interviews.stocks.ui.utils.ObserveSnackBarEvents +import com.tinaciousdesign.interviews.stocks.ui.utils.keyboardVisibleState +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch +import javax.inject.Inject + +@AndroidEntryPoint +class MainActivity : ComponentActivity() { + + @Inject + lateinit var eventBus: EventBus + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + registerNetworkListener() + + setContent { + StocksTheme { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + val scope = rememberCoroutineScope() + val navController = rememberNavController() + val snackBarHostState = remember { SnackbarHostState() } + val keyboardState by keyboardVisibleState() + + ObserveInternetConnectionState(eventBus, scope) + ObserveSnackBarEvents(SnackBarController.events, snackBarHostState, scope) + + Scaffold( + modifier = Modifier.fillMaxSize(), + snackbarHost = { + SnackbarHost(hostState = snackBarHostState) + }, + bottomBar = { + if (keyboardState == KeyboardState.Closed) { + BottomAppBar { + BottomNavigationBar(navController = navController) + } + } + } + ) { innerPadding -> + Box( + modifier = Modifier.padding( + PaddingValues( + 0.dp, + 0.dp, + 0.dp, + innerPadding.calculateBottomPadding() + ) + ) + ) { + NavigationRouter(navController) + } + } + } + } + } + } + + + // region Network Listener + + private fun registerNetworkListener() { + val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + connectivityManager.registerDefaultNetworkCallback(networkListener) + } + + private var hasDisconnected = false + + private val networkListener = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + super.onAvailable(network) + + if (hasDisconnected) { + handleNetworkConnectionRestored() + } + } + + override fun onLost(network: Network) { + super.onLost(network) + + hasDisconnected = true + handleNetworkConnectionLost() + } + } + + private fun handleNetworkConnectionLost() { + lifecycleScope.launch { + eventBus.emitEvent(AppEvent.ConnectionLost) + } + } + + private fun handleNetworkConnectionRestored() { + lifecycleScope.launch { + eventBus.emitEvent(AppEvent.ConnectionRestored) + } + } + + // endregion Network Listener +} diff --git a/app/src/main/java/com/tinaciousdesign/interviews/stocks/StocksApplication.kt b/app/src/main/java/com/tinaciousdesign/interviews/stocks/StocksApplication.kt new file mode 100644 index 0000000..6390201 --- /dev/null +++ b/app/src/main/java/com/tinaciousdesign/interviews/stocks/StocksApplication.kt @@ -0,0 +1,23 @@ +package com.tinaciousdesign.interviews.stocks + +import android.app.Application +import com.tinaciousdesign.interviews.stocks.BuildConfig +import com.tinaciousdesign.interviews.stocks.logging.CrashReportingTree +import com.tinaciousdesign.interviews.stocks.logging.DebugConsoleLoggingTree +import dagger.hilt.android.HiltAndroidApp +import timber.log.Timber + +@HiltAndroidApp +class StocksApplication : Application() { + + override fun onCreate() { + super.onCreate() + + setUpLogging() + } + + private fun setUpLogging() { + val loggingTree = if (BuildConfig.DEBUG) DebugConsoleLoggingTree() else CrashReportingTree() + Timber.plant(loggingTree) + } +} diff --git a/app/src/main/java/com/tinaciousdesign/interviews/stocks/config/AppConfig.kt b/app/src/main/java/com/tinaciousdesign/interviews/stocks/config/AppConfig.kt new file mode 100644 index 0000000..468bb7d --- /dev/null +++ b/app/src/main/java/com/tinaciousdesign/interviews/stocks/config/AppConfig.kt @@ -0,0 +1,5 @@ +package com.tinaciousdesign.interviews.stocks.config + +object AppConfig { + val stocksApiBaseUrl = "https://gist.githubusercontent.com/" +} diff --git a/app/src/main/java/com/tinaciousdesign/interviews/stocks/db/.gitkeep b/app/src/main/java/com/tinaciousdesign/interviews/stocks/db/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/app/src/main/java/com/tinaciousdesign/interviews/stocks/db/AppDatabase.kt b/app/src/main/java/com/tinaciousdesign/interviews/stocks/db/AppDatabase.kt new file mode 100644 index 0000000..3c7fd25 --- /dev/null +++ b/app/src/main/java/com/tinaciousdesign/interviews/stocks/db/AppDatabase.kt @@ -0,0 +1,16 @@ +package com.tinaciousdesign.interviews.stocks.db + +import androidx.room.Database +import androidx.room.RoomDatabase +import com.tinaciousdesign.interviews.stocks.db.stock.StockDao +import com.tinaciousdesign.interviews.stocks.db.stock.StockEntity + +@Database( + entities = [ + StockEntity::class + ], + version = 1 +) +abstract class AppDatabase : RoomDatabase() { + abstract fun stocks(): StockDao +} diff --git a/app/src/main/java/com/tinaciousdesign/interviews/stocks/db/stock/StockDao.kt b/app/src/main/java/com/tinaciousdesign/interviews/stocks/db/stock/StockDao.kt new file mode 100644 index 0000000..ce172a2 --- /dev/null +++ b/app/src/main/java/com/tinaciousdesign/interviews/stocks/db/stock/StockDao.kt @@ -0,0 +1,33 @@ +package com.tinaciousdesign.interviews.stocks.db.stock + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import kotlinx.coroutines.flow.Flow + +@Dao +interface StockDao { + + @Query("SELECT * FROM stocks") + suspend fun getAll(): List + + @Query("SELECT * FROM stocks") + fun stocksFlow(): Flow> + + @Query("SELECT * FROM stocks WHERE LOWER(ticker) LIKE '%' || :query || '%' OR LOWER(name) LIKE '%' || :query || '%'") + suspend fun find(query: String): List + + @Query("SELECT * FROM stocks WHERE LOWER(ticker) LIKE '%' || :query || '%' OR LOWER(name) LIKE '%' || :query || '%'") + fun findStocksFlow(query: String): Flow> + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(stock: StockEntity) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAll(stocks: List) + + @Query("DELETE FROM stocks") + suspend fun deleteAll() +} diff --git a/app/src/main/java/com/tinaciousdesign/interviews/stocks/db/stock/StockEntity.kt b/app/src/main/java/com/tinaciousdesign/interviews/stocks/db/stock/StockEntity.kt new file mode 100644 index 0000000..7723686 --- /dev/null +++ b/app/src/main/java/com/tinaciousdesign/interviews/stocks/db/stock/StockEntity.kt @@ -0,0 +1,29 @@ +package com.tinaciousdesign.interviews.stocks.db.stock + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey +import com.tinaciousdesign.interviews.stocks.models.Stock + +@Entity( + tableName = "stocks", + indices = [ + Index(value = ["ticker"], unique = true) + ] +) +data class StockEntity( + @PrimaryKey(autoGenerate = true) val id: Int = 0, + @ColumnInfo(name = "ticker") val ticker: String, + @ColumnInfo(name = "name") val name: String, + @ColumnInfo(name = "price") val price: Double, +) { + companion object { + fun fromStock(stock: Stock): StockEntity = + StockEntity( + ticker = stock.ticker, + name = stock.name, + price = stock.price, + ) + } +} diff --git a/app/src/main/java/com/tinaciousdesign/interviews/stocks/di/AppModule.kt b/app/src/main/java/com/tinaciousdesign/interviews/stocks/di/AppModule.kt new file mode 100644 index 0000000..b77a239 --- /dev/null +++ b/app/src/main/java/com/tinaciousdesign/interviews/stocks/di/AppModule.kt @@ -0,0 +1,108 @@ +package com.tinaciousdesign.interviews.stocks.di + +import android.content.Context +import androidx.room.Room +import com.tinaciousdesign.interviews.stocks.BuildConfig +import com.tinaciousdesign.interviews.stocks.config.AppConfig +import com.tinaciousdesign.interviews.stocks.db.AppDatabase +import com.tinaciousdesign.interviews.stocks.db.stock.StockDao +import com.tinaciousdesign.interviews.stocks.events.EventBus +import com.tinaciousdesign.interviews.stocks.logging.Logger +import com.tinaciousdesign.interviews.stocks.networking.api.StocksApi +import com.tinaciousdesign.interviews.stocks.repositories.StocksRepository +import com.tinaciousdesign.interviews.stocks.repositories.StocksRepositoryImpl +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import okhttp3.logging.HttpLoggingInterceptor.Level +import retrofit2.Retrofit +import retrofit2.converter.kotlinx.serialization.asConverterFactory +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +class AppModule { + + // region Repositories + + @Provides @Singleton + fun provideStocksRepository( + stocksApi: StocksApi, + stockDao: StockDao, + ): StocksRepository = StocksRepositoryImpl( + stocksApi, + stockDao, + ) + + @Provides @Singleton + fun provideEventBus(): EventBus = EventBus() + + // endregion Repositories + + // region Networking + + @Provides @Singleton + fun provideRetrofit( + okHttpClient: OkHttpClient + ): Retrofit { + val converterFactory = Json.asConverterFactory( + "application/json; charset=UTF8".toMediaType() + ) + + return Retrofit.Builder() + .baseUrl(AppConfig.stocksApiBaseUrl) + .client(okHttpClient) + .addConverterFactory(converterFactory) + .build() + } + + @Provides @Singleton + fun provideHttpLoggingInterceptor(): HttpLoggingInterceptor = + HttpLoggingInterceptor { Logger.tag("HttpLog").d(it) } + .apply { + level = if (BuildConfig.DEBUG) Level.BODY else Level.NONE + } + + @Provides @Singleton + fun provideOkHttpClient( + httpLoggingInterceptor: HttpLoggingInterceptor + ): OkHttpClient { + return OkHttpClient.Builder() + // We can add other interceptors here, e.g. auth + .addInterceptor(httpLoggingInterceptor) + .build() + } + + // region Networking -> API + + @Provides @Singleton + fun provideStocksApi(retrofit: Retrofit): StocksApi = retrofit.create(StocksApi::class.java) + + // endregion Networking -> API + + // endregion Networking + + // region Database + + @Provides @Singleton + fun provideAppDatabase( + @ApplicationContext appContext: Context + ): AppDatabase = + Room.databaseBuilder( + appContext, + AppDatabase::class.java, + "stocks", + ) + .build() + + @Provides @Singleton + fun provideStockDao(appDatabase: AppDatabase): StockDao = appDatabase.stocks() + + // endregion Database +} diff --git a/app/src/main/java/com/tinaciousdesign/interviews/stocks/events/AppEvent.kt b/app/src/main/java/com/tinaciousdesign/interviews/stocks/events/AppEvent.kt new file mode 100644 index 0000000..210cb93 --- /dev/null +++ b/app/src/main/java/com/tinaciousdesign/interviews/stocks/events/AppEvent.kt @@ -0,0 +1,7 @@ +package com.tinaciousdesign.interviews.stocks.events + +sealed class AppEvent { + data object ConnectionLost : AppEvent() + + data object ConnectionRestored : AppEvent() +} diff --git a/app/src/main/java/com/tinaciousdesign/interviews/stocks/events/EventBus.kt b/app/src/main/java/com/tinaciousdesign/interviews/stocks/events/EventBus.kt new file mode 100644 index 0000000..ba55dfa --- /dev/null +++ b/app/src/main/java/com/tinaciousdesign/interviews/stocks/events/EventBus.kt @@ -0,0 +1,40 @@ +package com.tinaciousdesign.interviews.stocks.events + +import com.tinaciousdesign.interviews.stocks.logging.Logger +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlin.coroutines.coroutineContext + +class EventBus { + private val _events = MutableSharedFlow(replay = 10) + val events = _events.asSharedFlow() + + suspend fun emitEvent(event: AppEvent) { + Logger.d("🚌🏁 Emitting event = $event") + _events.emit(event) + } + + suspend inline fun subscribe(crossinline onEvent: (T) -> Unit) { + events.filterIsInstance() + .collectLatest { appEvent -> + if (!coroutineContext.isActive) { + Logger.d("πŸšŒπŸ›‘ Coroutine inactive - Not collecting event: $appEvent") + return@collectLatest + } + + Logger.d("πŸšŒπŸ›οΈ Collecting event: $appEvent") + onEvent(appEvent) + } + } + + inline fun subscribe(coroutineScope: CoroutineScope, crossinline onEvent: (T) -> Unit) { + coroutineScope.launch { + subscribe(onEvent) + } + } +} diff --git a/app/src/main/java/com/tinaciousdesign/interviews/stocks/events/ObserveAsEvents.kt b/app/src/main/java/com/tinaciousdesign/interviews/stocks/events/ObserveAsEvents.kt new file mode 100644 index 0000000..2981826 --- /dev/null +++ b/app/src/main/java/com/tinaciousdesign/interviews/stocks/events/ObserveAsEvents.kt @@ -0,0 +1,27 @@ +package com.tinaciousdesign.interviews.stocks.events + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.repeatOnLifecycle +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.withContext + +@Composable +fun ObserveAsEvents( + flow: Flow, + key1: Any? = null, + key2: Any? = null, + onEvent: (T) -> Unit +) { + val lifecycleOwner = LocalLifecycleOwner.current + LaunchedEffect(lifecycleOwner.lifecycle, key1, key2, flow) { + lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + withContext(Dispatchers.Main.immediate) { + flow.collect(onEvent) + } + } + } +} diff --git a/app/src/main/java/com/tinaciousdesign/interviews/stocks/logging/CrashReportingTree.kt b/app/src/main/java/com/tinaciousdesign/interviews/stocks/logging/CrashReportingTree.kt new file mode 100644 index 0000000..43fa342 --- /dev/null +++ b/app/src/main/java/com/tinaciousdesign/interviews/stocks/logging/CrashReportingTree.kt @@ -0,0 +1,26 @@ +package com.tinaciousdesign.interviews.stocks.logging + +import android.annotation.SuppressLint +import android.util.Log +import timber.log.Timber + +@SuppressLint("LogNotTimber") +class CrashReportingTree : Timber.Tree() { + override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { + if (priority < Log.INFO) return + + logToConsole(priority, tag, message, t) + logToMonitoringService(priority, tag, message, t) + } + + private fun logToConsole(priority: Int, tag: String?, message: String, t: Throwable?) { + when (priority) { + Log.ASSERT, Log.ERROR -> Log.e(tag, message, t) + Log.WARN -> Log.w(tag, message, t) + } + } + + private fun logToMonitoringService(priority: Int, tag: String?, message: String, t: Throwable?) { + // todo: Implement third-party logging service, e.g. Crashlytics + } +} diff --git a/app/src/main/java/com/tinaciousdesign/interviews/stocks/logging/DebugConsoleLoggingTree.kt b/app/src/main/java/com/tinaciousdesign/interviews/stocks/logging/DebugConsoleLoggingTree.kt new file mode 100644 index 0000000..715a307 --- /dev/null +++ b/app/src/main/java/com/tinaciousdesign/interviews/stocks/logging/DebugConsoleLoggingTree.kt @@ -0,0 +1,17 @@ +package com.tinaciousdesign.interviews.stocks.logging + +import timber.log.Timber + +/** + * Should only be used in debug builds since references to [StackTraceElement] will be lost in minified builds + */ +class DebugConsoleLoggingTree : Timber.DebugTree() { + override fun createStackElementTag(element: StackTraceElement): String? { + return String.format( + "%s:%s#%s", + element.fileName.replace(".kt", ""), + element.lineNumber, + element.methodName, + ) + } +} diff --git a/app/src/main/java/com/tinaciousdesign/interviews/stocks/logging/Logger.kt b/app/src/main/java/com/tinaciousdesign/interviews/stocks/logging/Logger.kt new file mode 100644 index 0000000..9484362 --- /dev/null +++ b/app/src/main/java/com/tinaciousdesign/interviews/stocks/logging/Logger.kt @@ -0,0 +1,5 @@ +package com.tinaciousdesign.interviews.stocks.logging + +import timber.log.Timber + +typealias Logger = Timber diff --git a/app/src/main/java/com/tinaciousdesign/interviews/stocks/models/Stock.kt b/app/src/main/java/com/tinaciousdesign/interviews/stocks/models/Stock.kt new file mode 100644 index 0000000..b92da85 --- /dev/null +++ b/app/src/main/java/com/tinaciousdesign/interviews/stocks/models/Stock.kt @@ -0,0 +1,37 @@ +package com.tinaciousdesign.interviews.stocks.models + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class Stock( + @SerialName("ticker") val ticker: String, + @SerialName("name") val name: String, + @SerialName("currentPrice") val price: Double, +) { + val formattedPrice: String get() = "%.2f".format(price) + + companion object { + fun compareQuery(query: String): Comparator = + Comparator { a, b -> + val queryLowered = query.lowercase() + + val aExactMatch = a.name.lowercase() == queryLowered || + a.ticker.lowercase() == queryLowered + val bExactMatch = b.name.lowercase() == queryLowered || + b.ticker.lowercase() == queryLowered + + if (aExactMatch && bExactMatch) { + return@Comparator 0 + } + + if (aExactMatch) -1 else 1 + } + } +} + +fun List.matches(query: String): List = + this.filter { stock -> + stock.ticker.contains(query.trim(), ignoreCase = true) || + stock.name.contains(query.trim(), ignoreCase = true) + } diff --git a/app/src/main/java/com/tinaciousdesign/interviews/stocks/navigation/BottomNavigationBar.kt b/app/src/main/java/com/tinaciousdesign/interviews/stocks/navigation/BottomNavigationBar.kt new file mode 100644 index 0000000..afee008 --- /dev/null +++ b/app/src/main/java/com/tinaciousdesign/interviews/stocks/navigation/BottomNavigationBar.kt @@ -0,0 +1,64 @@ +package com.tinaciousdesign.interviews.stocks.navigation + +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext +import androidx.navigation.NavController +import com.tinaciousdesign.interviews.stocks.utils.lastSegment + +@Composable +fun BottomNavigationBar(navController: NavController) { + val context = LocalContext.current + + val navItems = listOf( + Route.StockSearch, + Route.About + ) + var selectedItem by remember { mutableStateOf(Route.StockSearch) } + + // Update the active item's highlighted state and navigate to the desired screen + fun handleRouteClicked(route: Route) { + selectedItem = route + + navController.navigate(route) { + navController.graph.startDestinationRoute?.let { startRoute -> + popUpTo(startRoute) { + saveState = true + } + } + launchSingleTop = true + restoreState = true + } + } + + // Update the active item's highlighted state + LaunchedEffect(0) { + navController.addOnDestinationChangedListener { _, destination, _ -> + navItems.forEach { navItem -> + val current = destination.route?.lastSegment(".") + if (current == navItem.routeName) { + selectedItem = navItem + } + } + } + } + + // Render the tabs with icons and localized titles + NavigationBar { + navItems.forEach { route -> + NavigationBarItem( + selected = route == selectedItem, + label = { Text(context.getString(route.titleRes)) }, + icon = route.icon, + onClick = { handleRouteClicked(route) }, + ) + } + } +} diff --git a/app/src/main/java/com/tinaciousdesign/interviews/stocks/navigation/NavigationRouter.kt b/app/src/main/java/com/tinaciousdesign/interviews/stocks/navigation/NavigationRouter.kt new file mode 100644 index 0000000..547ff34 --- /dev/null +++ b/app/src/main/java/com/tinaciousdesign/interviews/stocks/navigation/NavigationRouter.kt @@ -0,0 +1,27 @@ +package com.tinaciousdesign.interviews.stocks.navigation + +import androidx.compose.runtime.Composable +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import com.tinaciousdesign.interviews.stocks.ui.screens.about.AboutScreen +import com.tinaciousdesign.interviews.stocks.ui.screens.stocksearch.StockSearchScreen +import com.tinaciousdesign.interviews.stocks.ui.screens.stocksearch.StockSearchViewModel + +@Composable +fun NavigationRouter( + navHostController: NavHostController +) { + NavHost(navController = navHostController, startDestination = Route.StockSearch) { + composable { backStackEntry -> + val viewModel = hiltViewModel() + + StockSearchScreen(viewModel) + } + + composable { backStackEntry -> + AboutScreen() + } + } +} diff --git a/app/src/main/java/com/tinaciousdesign/interviews/stocks/navigation/Route.kt b/app/src/main/java/com/tinaciousdesign/interviews/stocks/navigation/Route.kt new file mode 100644 index 0000000..2536c69 --- /dev/null +++ b/app/src/main/java/com/tinaciousdesign/interviews/stocks/navigation/Route.kt @@ -0,0 +1,40 @@ +package com.tinaciousdesign.interviews.stocks.navigation + +import androidx.annotation.Keep +import androidx.annotation.StringRes +import androidx.compose.runtime.Composable +import com.tinaciousdesign.interviews.stocks.R +import com.tinaciousdesign.interviews.stocks.ui.icons.TinaciousDesignLogoIcon +import com.tinaciousdesign.interviews.stocks.ui.icons.TintedIconDrawable +import kotlinx.serialization.Serializable + +@Serializable @Keep +sealed class Route { + abstract val icon: @Composable () -> Unit + + @get:StringRes + abstract val titleRes: Int + + val routeName: String? get() = javaClass.simpleName + + @Serializable @Keep + data object StockSearch : Route() { + override val titleRes: Int get() = R.string.route_stock_search + + override val icon: @Composable () -> Unit = { + TintedIconDrawable( + R.drawable.ic_dollar, + R.string.route_stock_search + ) + } + } + + @Serializable @Keep + data object About : Route() { + override val titleRes: Int get() = R.string.route_about + + override val icon: @Composable () -> Unit = { + TinaciousDesignLogoIcon() + } + } +} diff --git a/app/src/main/java/com/tinaciousdesign/interviews/stocks/networking/ApiError.kt b/app/src/main/java/com/tinaciousdesign/interviews/stocks/networking/ApiError.kt new file mode 100644 index 0000000..a88e460 --- /dev/null +++ b/app/src/main/java/com/tinaciousdesign/interviews/stocks/networking/ApiError.kt @@ -0,0 +1,6 @@ +package com.tinaciousdesign.interviews.stocks.networking + + open class ApiError( + cause: Throwable? = null, + message: String? = null, +) : Exception(message, cause) diff --git a/app/src/main/java/com/tinaciousdesign/interviews/stocks/networking/ApiResult.kt b/app/src/main/java/com/tinaciousdesign/interviews/stocks/networking/ApiResult.kt new file mode 100644 index 0000000..1b19125 --- /dev/null +++ b/app/src/main/java/com/tinaciousdesign/interviews/stocks/networking/ApiResult.kt @@ -0,0 +1,24 @@ +package com.tinaciousdesign.interviews.stocks.networking + +sealed class ApiResult { + data class Success( + override val data: ResultData + ) : ApiResult() { + override val ok: Boolean = true + override val error: Error? = null + } + + data class Failed( + override val error: Error + ) : ApiResult() { + override val ok: Boolean = false + override val data: Result? = null + } + + abstract val data: ResultData? + abstract val error: Error? + + abstract val ok: Boolean + + val failed: Boolean get() = !ok +} diff --git a/app/src/main/java/com/tinaciousdesign/interviews/stocks/networking/api/StocksApi.kt b/app/src/main/java/com/tinaciousdesign/interviews/stocks/networking/api/StocksApi.kt new file mode 100644 index 0000000..16b978f --- /dev/null +++ b/app/src/main/java/com/tinaciousdesign/interviews/stocks/networking/api/StocksApi.kt @@ -0,0 +1,14 @@ +package com.tinaciousdesign.interviews.stocks.networking.api + +import com.tinaciousdesign.interviews.stocks.models.Stock +import retrofit2.http.GET + +interface StocksApi { + // This endpoint will allow you to search for "omni" or "lol" and see matches where exact matches are prioritized. + // To use, comment out the provided endpoint GET("...") and comment this one back in +// @GET("tinacious/a3ddc32e49c04b5de21e4bb30eb47e68/raw/5b590f6f369fb92fc49e33a14ab2275eb5629c24/mock-stocks.json") + + // Provided endpoint + @GET("priyanshrastogi/0e1d4f8d517698cfdced49f5e59567be/raw/9158ad254e92aaffe215e950f4846a23a0680703/mock-stocks.json") + suspend fun getStocks(): List +} diff --git a/app/src/main/java/com/tinaciousdesign/interviews/stocks/repositories/StocksRepository.kt b/app/src/main/java/com/tinaciousdesign/interviews/stocks/repositories/StocksRepository.kt new file mode 100644 index 0000000..0f03aea --- /dev/null +++ b/app/src/main/java/com/tinaciousdesign/interviews/stocks/repositories/StocksRepository.kt @@ -0,0 +1,62 @@ +package com.tinaciousdesign.interviews.stocks.repositories + +import com.tinaciousdesign.interviews.stocks.db.stock.StockDao +import com.tinaciousdesign.interviews.stocks.db.stock.StockEntity +import com.tinaciousdesign.interviews.stocks.logging.Logger +import com.tinaciousdesign.interviews.stocks.models.Stock +import com.tinaciousdesign.interviews.stocks.networking.ApiError +import com.tinaciousdesign.interviews.stocks.networking.ApiResult +import com.tinaciousdesign.interviews.stocks.networking.api.StocksApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +interface StocksRepository { + class GetStocksError(): ApiError() + + fun findStocksFlow(query: String): Flow> + + suspend fun fetchStocks(forceRefresh: Boolean = false): ApiResult, GetStocksError> +} + +class StocksRepositoryImpl @Inject constructor( + private val stocksApi: StocksApi, + private val stockDao: StockDao, +) : StocksRepository { + private var cachedStocks = listOf() + + override fun findStocksFlow(query: String): Flow> { + return stockDao.findStocksFlow(query).flowOn(Dispatchers.IO).map { stocks -> + stocks.map { stockEntity -> + Stock( + ticker = stockEntity.ticker, + name = stockEntity.name, + price = stockEntity.price + ) + } + } + } + + override suspend fun fetchStocks(forceRefresh: Boolean): ApiResult, StocksRepository.GetStocksError> { + if (cachedStocks.isNotEmpty() && !forceRefresh) { + return ApiResult.Success(cachedStocks) + } + + return try { + val response = stocksApi.getStocks() + + cachedStocks = response + + val stockEntities = response.map(StockEntity::fromStock) + stockDao.deleteAll() + stockDao.insertAll(stockEntities) + + ApiResult.Success(response) + } catch (e: Exception) { + Logger.e(e) + ApiResult.Failed(StocksRepository.GetStocksError()) + } + } +} diff --git a/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/TestTags.kt b/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/TestTags.kt new file mode 100644 index 0000000..b0ddd1a --- /dev/null +++ b/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/TestTags.kt @@ -0,0 +1,5 @@ +package com.tinaciousdesign.interviews.stocks.ui + +object TestTags { + val searchField = "searchField" +} diff --git a/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/components/Divider.kt b/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/components/Divider.kt new file mode 100644 index 0000000..e403539 --- /dev/null +++ b/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/components/Divider.kt @@ -0,0 +1,22 @@ +package com.tinaciousdesign.interviews.stocks.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + +@Composable +fun Divider( + color: Color, +) { + Box( + modifier = Modifier + .background(color) + .height(1.dp) + .fillMaxWidth() + ) +} diff --git a/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/components/EmptyState.kt b/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/components/EmptyState.kt new file mode 100644 index 0000000..1e8d7a1 --- /dev/null +++ b/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/components/EmptyState.kt @@ -0,0 +1,44 @@ +package com.tinaciousdesign.interviews.stocks.ui.components + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun EmptyState( + title: String, + message: String, + modifier: Modifier = Modifier, +) { + Box( + contentAlignment = Alignment.Center, + modifier = modifier, + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.padding(20.dp), + ) { + Text( + text = title, + textAlign = TextAlign.Center, + fontWeight = FontWeight.Bold, + fontSize = 20.sp, + modifier = Modifier.padding(bottom = 20.dp) + ) + Text( + text = message, + textAlign = TextAlign.Center, + fontSize = 17.sp, + ) + } + + } +} diff --git a/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/components/SearchInputView.kt b/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/components/SearchInputView.kt new file mode 100644 index 0000000..17d77fc --- /dev/null +++ b/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/components/SearchInputView.kt @@ -0,0 +1,51 @@ +package com.tinaciousdesign.interviews.stocks.ui.components + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.unit.dp +import com.tinaciousdesign.interviews.stocks.R +import com.tinaciousdesign.interviews.stocks.ui.TestTags +import com.tinaciousdesign.interviews.stocks.ui.icons.TintedIconDrawable + +@Composable +fun SearchInputView(currentValue: String, onSearch: (String) -> Unit) { + val context = LocalContext.current + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding( + vertical = 20.dp, + horizontal = 14.dp, + ) + ) { + OutlinedTextField( + value = currentValue, + onValueChange = onSearch, + placeholder = { + Text(context.getString(R.string.search_field_placeholder)) + }, + leadingIcon = { + TintedIconDrawable(R.drawable.ic_search, R.string.search_icon_content_description) + }, + modifier = Modifier + .weight(1.0f) + .padding(end = 14.dp) + .testTag(TestTags.searchField) + ) + + Button({ + onSearch("") + }) { + Text(context.getString(R.string.search_clear_button)) + } + } +} diff --git a/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/components/StockSearchResultListItem.kt b/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/components/StockSearchResultListItem.kt new file mode 100644 index 0000000..c8fcd07 --- /dev/null +++ b/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/components/StockSearchResultListItem.kt @@ -0,0 +1,64 @@ +package com.tinaciousdesign.interviews.stocks.ui.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import coil.compose.AsyncImage +import com.tinaciousdesign.interviews.stocks.models.Stock + +@Composable +fun StockSearchResultListItem( + stock: Stock +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding( + horizontal = 12.dp, + vertical = 12.dp, + ) + ) { + AsyncImage( + model = "https://api.dicebear.com/9.x/glass/png?seed=${stock.ticker}", + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .width(60.dp) + .height(60.dp) + .clip(CircleShape) + ) + + Column( + modifier = Modifier + .padding(start = 12.dp) + ) { + Text( + text = stock.ticker, + fontSize = 17.sp, + fontWeight = FontWeight.SemiBold, + ) + Text( + text = stock.name, + ) + } + + Spacer( + modifier = Modifier.weight(1.0f) + ) + + Text(stock.formattedPrice) + } +} diff --git a/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/components/StockSearchResults.kt b/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/components/StockSearchResults.kt new file mode 100644 index 0000000..0e2ca24 --- /dev/null +++ b/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/components/StockSearchResults.kt @@ -0,0 +1,36 @@ +package com.tinaciousdesign.interviews.stocks.ui.components + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import com.tinaciousdesign.interviews.stocks.R +import com.tinaciousdesign.interviews.stocks.models.Stock + +@Composable +fun StockSearchResults( + stocks: List, +) { + val context = LocalContext.current + + LazyColumn { + itemsIndexed(stocks) { idx, stock -> + StockSearchResultListItem(stock) + + if (idx < stocks.lastIndex) { + Divider(MaterialTheme.colorScheme.surface) + } + } + } + if (stocks.isEmpty()) { + EmptyState( + title = context.getString(R.string.stock_search_no_results_heading), + message = context.getString(R.string.stock_search_no_results_message), + modifier = Modifier + .fillMaxSize() + ) + } +} diff --git a/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/components/UnstyledButton.kt b/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/components/UnstyledButton.kt new file mode 100644 index 0000000..9a917a6 --- /dev/null +++ b/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/components/UnstyledButton.kt @@ -0,0 +1,25 @@ +package com.tinaciousdesign.interviews.stocks.ui.components + +import androidx.compose.foundation.layout.RowScope +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color + +@Composable +fun UnstyledButton( + modifier: Modifier = Modifier, + onClick: () -> Unit, + content: @Composable RowScope.() -> Unit, +) { + Button( + colors = ButtonDefaults.buttonColors( + containerColor = Color.Transparent, + contentColor = Color.Transparent, + ), + modifier = modifier, + onClick = onClick, + content = content + ) +} diff --git a/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/icons/IconDrawable.kt b/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/icons/IconDrawable.kt new file mode 100644 index 0000000..e283102 --- /dev/null +++ b/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/icons/IconDrawable.kt @@ -0,0 +1,34 @@ +package com.tinaciousdesign.interviews.stocks.ui.icons + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +@Composable +fun IconDrawable( + @DrawableRes drawableId: Int, + @StringRes contentDescriptionRes: Int, + tint: Color = Color.Unspecified, + size: Dp = 24.dp +) { + val context = LocalContext.current + + Image( + painterResource(drawableId), + context.getString(contentDescriptionRes), + colorFilter = ColorFilter.tint(tint), + modifier = Modifier + .width(size) + .height(size) + ) +} diff --git a/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/icons/TinaciousDesignLogoIcon.kt b/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/icons/TinaciousDesignLogoIcon.kt new file mode 100644 index 0000000..0c7ace8 --- /dev/null +++ b/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/icons/TinaciousDesignLogoIcon.kt @@ -0,0 +1,27 @@ +package com.tinaciousdesign.interviews.stocks.ui.icons + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.tinaciousdesign.interviews.stocks.R + +@Composable +fun TinaciousDesignLogoIcon( + size: Dp = 24.dp +) { + val context = LocalContext.current + + Image( + painterResource(R.drawable.tinacious_design_logo), + context.getString(R.string.tinacious_design_logo_content_description), + modifier = Modifier + .width(size) + .height(size) + ) +} diff --git a/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/icons/TintedIconDrawable.kt b/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/icons/TintedIconDrawable.kt new file mode 100644 index 0000000..263349f --- /dev/null +++ b/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/icons/TintedIconDrawable.kt @@ -0,0 +1,22 @@ +package com.tinaciousdesign.interviews.stocks.ui.icons + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.compose.material3.LocalContentColor +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +@Composable +fun TintedIconDrawable( + @DrawableRes drawableId: Int, + @StringRes contentDescriptionRes: Int, + size: Dp = 24.dp +) { + IconDrawable( + drawableId = drawableId, + contentDescriptionRes = contentDescriptionRes, + size = size, + tint = LocalContentColor.current, + ) +} diff --git a/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/screens/about/AboutScreen.kt b/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/screens/about/AboutScreen.kt new file mode 100644 index 0000000..ea878e7 --- /dev/null +++ b/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/screens/about/AboutScreen.kt @@ -0,0 +1,59 @@ +package com.tinaciousdesign.interviews.stocks.ui.screens.about + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.tinaciousdesign.interviews.stocks.R +import com.tinaciousdesign.interviews.stocks.ui.components.UnstyledButton +import com.tinaciousdesign.interviews.stocks.ui.icons.TinaciousDesignLogoIcon +import com.tinaciousdesign.interviews.stocks.utils.openExternalBrowser + +@Composable +fun AboutScreen() { + val context = LocalContext.current + + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .fillMaxHeight() + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth() + .padding(30.dp) + ) { + UnstyledButton(onClick = { + context.openExternalBrowser("https://tinaciousdesign.com") + }) { + TinaciousDesignLogoIcon(100.dp) + } + + Text( + text = context.getString(R.string.about_screen_title), + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center, + modifier = Modifier + .padding(bottom = 16.dp, top = 20.dp) + ) + + Text( + text = context.getString(R.string.about_screen_message), + textAlign = TextAlign.Center, + fontSize = 17.sp, + ) + } + } +} diff --git a/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/screens/stocksearch/StockSearchScreen.kt b/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/screens/stocksearch/StockSearchScreen.kt new file mode 100644 index 0000000..c20c17b --- /dev/null +++ b/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/screens/stocksearch/StockSearchScreen.kt @@ -0,0 +1,54 @@ +package com.tinaciousdesign.interviews.stocks.ui.screens.stocksearch + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.LifecycleEventEffect +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tinaciousdesign.interviews.stocks.R +import com.tinaciousdesign.interviews.stocks.ui.components.Divider +import com.tinaciousdesign.interviews.stocks.ui.components.EmptyState +import com.tinaciousdesign.interviews.stocks.ui.components.SearchInputView +import com.tinaciousdesign.interviews.stocks.ui.components.StockSearchResults + +@Composable +fun StockSearchScreen( + viewModel: StockSearchViewModel +) { + val context = LocalContext.current + + val stocks by viewModel.stocks.collectAsStateWithLifecycle() + val searchQuery by viewModel.searchQuery.collectAsStateWithLifecycle() + val isSearching by viewModel.isSearching.collectAsStateWithLifecycle(false) + + LifecycleEventEffect(Lifecycle.Event.ON_START) { + viewModel.loadStocks() + } + + Column { + SearchInputView(searchQuery) { newValue -> + viewModel.onSearch(newValue) + } + + Divider(MaterialTheme.colorScheme.secondary) + + Box(modifier = Modifier.weight(1.0f)) { + if (isSearching) { + StockSearchResults(stocks) + } else { + EmptyState( + title = context.getString(R.string.stock_search_empty_heading), + message = context.getString(R.string.stock_search_empty_message), + modifier = Modifier + .fillMaxSize() + ) + } + } + } +} diff --git a/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/screens/stocksearch/StockSearchViewModel.kt b/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/screens/stocksearch/StockSearchViewModel.kt new file mode 100644 index 0000000..21a12f7 --- /dev/null +++ b/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/screens/stocksearch/StockSearchViewModel.kt @@ -0,0 +1,71 @@ +package com.tinaciousdesign.interviews.stocks.ui.screens.stocksearch + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.tinaciousdesign.interviews.stocks.R +import com.tinaciousdesign.interviews.stocks.models.Stock +import com.tinaciousdesign.interviews.stocks.repositories.StocksRepository +import com.tinaciousdesign.interviews.stocks.ui.snackbar.SnackBarController +import com.tinaciousdesign.interviews.stocks.ui.snackbar.SnackBarEvent +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import javax.inject.Inject + +interface StockSearchVM { + val stocks: StateFlow> + val searchQuery: StateFlow + val isSearching: Flow + + fun loadStocks() + fun onSearch(query: String) +} + +@HiltViewModel +class StockSearchViewModel @Inject constructor( + private val savedStateHandle: SavedStateHandle, + private val stocksRepository: StocksRepository, +) : StockSearchVM, ViewModel() { + + override val searchQuery: StateFlow = savedStateHandle.getStateFlow("searchQuery", "") + + override val stocks: StateFlow> = + searchQuery.flatMapLatest { query -> + stocksRepository.findStocksFlow(query) + } + .combine(searchQuery, ::Pair) + .map { (list, query) -> + list.sortedWith(Stock.compareQuery(query)) + } + .stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5000), + emptyList(), + ) + + override val isSearching = searchQuery.map { it.isNotBlank() } + + override fun loadStocks() { + viewModelScope.launch { + val result = stocksRepository.fetchStocks() + if (result.failed) { + SnackBarController.sendEvent( + SnackBarEvent({ resources -> + resources.getString(R.string.stock_search_error_stocks_fetch_failed) + }) + ) + } + } + } + + override fun onSearch(query: String) { + savedStateHandle["searchQuery"] = query + } +} diff --git a/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/snackbar/SnackBarController.kt b/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/snackbar/SnackBarController.kt new file mode 100644 index 0000000..86b646d --- /dev/null +++ b/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/snackbar/SnackBarController.kt @@ -0,0 +1,26 @@ +package com.tinaciousdesign.interviews.stocks.ui.snackbar + +import android.content.res.Resources +import androidx.compose.material3.SnackbarDuration +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.receiveAsFlow + +data class SnackBarEvent( + val getLocalizedMessage: (Resources) -> String, + val duration: SnackbarDuration = SnackbarDuration.Long, + val action: SnackBarAction? = null +) + +data class SnackBarAction( + val getLocalizedName: (Resources) -> String, + val action: suspend () -> Unit +) + +object SnackBarController { + private val _events = Channel() + val events = _events.receiveAsFlow() + + suspend fun sendEvent(event: SnackBarEvent) { + _events.send(event) + } +} diff --git a/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/theme/Color.kt b/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/theme/Color.kt new file mode 100644 index 0000000..8361e82 --- /dev/null +++ b/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/theme/Color.kt @@ -0,0 +1,29 @@ +package com.tinaciousdesign.interviews.stocks.ui.theme + +import androidx.compose.ui.graphics.Color + +/** + * Colours: https://codepen.io/tinacious/pen/pobYoWj + */ +object BrandColours { + val pink = Color(0xFFFF3399) + val blue = Color(0xFF33D5FF) + val green = Color(0xFF00D364) + val turquoise = Color(0xFF00CED1) + val purple = Color(0xFFCC66FF) + val yellow = Color(0xFFFFCC66) + val red = Color(0xFFF10F36) + + // Shades generated for #1d1d26 with: https://colour-tools.pages.dev/shades + object Greys { + val black = Color(0xFF000000) + val grey_50 = Color(0xFF0B0B0E) + val grey_100 = Color(0xFF2C2C3A) + val grey_200 = Color(0xFF585874) + val grey_300 = Color(0xFF8B8BA7) + val grey_400 = Color(0xFFC5C5D3) + val grey_450 = Color(0xFFF1F1F4) + val white = Color(0xFFFFFFFF) + } +} + diff --git a/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/theme/Theme.kt b/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/theme/Theme.kt new file mode 100644 index 0000000..c31fc7d --- /dev/null +++ b/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/theme/Theme.kt @@ -0,0 +1,58 @@ +package com.tinaciousdesign.interviews.stocks.ui.theme + +import android.app.Activity +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalView +import androidx.core.view.WindowCompat + +private val DarkColorScheme = darkColorScheme( + background = BrandColours.Greys.grey_50, + onBackground = BrandColours.Greys.white, + surface = BrandColours.Greys.grey_100, + onSurface = BrandColours.Greys.white, + surfaceTint = BrandColours.Greys.grey_300, + primary = BrandColours.pink, + secondary = BrandColours.blue, + tertiary = BrandColours.turquoise +) + +private val LightColorScheme = lightColorScheme( + background = BrandColours.Greys.white, + onBackground = BrandColours.Greys.black, + surface = BrandColours.Greys.grey_450, + surfaceVariant = BrandColours.Greys.grey_400, + onSurface = BrandColours.Greys.black, + surfaceTint = BrandColours.Greys.grey_300, + primary = BrandColours.pink, + secondary = BrandColours.blue, + tertiary = BrandColours.turquoise, +) + +@Composable +fun StocksTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit +) { + val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme + + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + val window = (view.context as Activity).window + window.statusBarColor = colorScheme.primary.toArgb() + WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme + } + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} diff --git a/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/theme/Type.kt b/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/theme/Type.kt new file mode 100644 index 0000000..7068189 --- /dev/null +++ b/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/theme/Type.kt @@ -0,0 +1,18 @@ +package com.tinaciousdesign.interviews.stocks.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) +) diff --git a/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/utils/KeyboardVisibleState.kt b/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/utils/KeyboardVisibleState.kt new file mode 100644 index 0000000..6c00f31 --- /dev/null +++ b/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/utils/KeyboardVisibleState.kt @@ -0,0 +1,43 @@ +package com.tinaciousdesign.interviews.stocks.ui.utils + +import android.graphics.Rect +import android.view.ViewTreeObserver +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalView + +enum class KeyboardState { + Opened, + Closed +} + +@Composable +fun keyboardVisibleState(): State { + val keyboardState = remember { mutableStateOf(KeyboardState.Closed) } + val view = LocalView.current + + DisposableEffect(view) { + val onGlobalListener = ViewTreeObserver.OnGlobalLayoutListener { + val rect = Rect() + view.getWindowVisibleDisplayFrame(rect) + val screenHeight = view.rootView.height + val keypadHeight = screenHeight - rect.bottom + + keyboardState.value = if (keypadHeight > screenHeight * 0.15) { + KeyboardState.Opened + } else { + KeyboardState.Closed + } + } + view.viewTreeObserver.addOnGlobalLayoutListener(onGlobalListener) + + onDispose { + view.viewTreeObserver.removeOnGlobalLayoutListener(onGlobalListener) + } + } + + return keyboardState +} diff --git a/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/utils/ObserveInternetConnectionState.kt b/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/utils/ObserveInternetConnectionState.kt new file mode 100644 index 0000000..a34aebf --- /dev/null +++ b/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/utils/ObserveInternetConnectionState.kt @@ -0,0 +1,39 @@ +package com.tinaciousdesign.interviews.stocks.ui.utils + +import androidx.compose.runtime.Composable +import com.tinaciousdesign.interviews.stocks.R +import com.tinaciousdesign.interviews.stocks.events.AppEvent +import com.tinaciousdesign.interviews.stocks.events.EventBus +import com.tinaciousdesign.interviews.stocks.events.ObserveAsEvents +import com.tinaciousdesign.interviews.stocks.ui.snackbar.SnackBarController +import com.tinaciousdesign.interviews.stocks.ui.snackbar.SnackBarEvent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +@Composable +fun ObserveInternetConnectionState( + eventBus: EventBus, + coroutineScope: CoroutineScope, +) { + ObserveAsEvents(eventBus.events) { appEvent -> + coroutineScope.launch { + when (appEvent) { + AppEvent.ConnectionLost -> { + SnackBarController.sendEvent( + SnackBarEvent({ resources -> + resources.getString(R.string.connection_lost_message) + }) + ) + } + AppEvent.ConnectionRestored -> { + SnackBarController.sendEvent( + SnackBarEvent({ resources -> + resources.getString(R.string.connection_restored_message) + }) + ) + } + else -> {} + } + } + } +} diff --git a/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/utils/ObserveSnackBarEvents.kt b/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/utils/ObserveSnackBarEvents.kt new file mode 100644 index 0000000..49f6469 --- /dev/null +++ b/app/src/main/java/com/tinaciousdesign/interviews/stocks/ui/utils/ObserveSnackBarEvents.kt @@ -0,0 +1,36 @@ +package com.tinaciousdesign.interviews.stocks.ui.utils + +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarResult +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import com.tinaciousdesign.interviews.stocks.events.ObserveAsEvents +import com.tinaciousdesign.interviews.stocks.ui.snackbar.SnackBarEvent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch + +@Composable +fun ObserveSnackBarEvents( + snackBarFlow: Flow, + snackBarHostState: SnackbarHostState, + coroutineScope: CoroutineScope, +) { + val resources = LocalContext.current.resources + + ObserveAsEvents(snackBarFlow, snackBarHostState) { event -> + coroutineScope.launch { + snackBarHostState.currentSnackbarData?.dismiss() + + val result = snackBarHostState.showSnackbar( + message = event.getLocalizedMessage(resources), + actionLabel = event.action?.getLocalizedName?.invoke(resources), + duration = event.duration, + ) + + if (result == SnackbarResult.ActionPerformed) { + event.action?.action?.invoke() + } + } + } +} diff --git a/app/src/main/java/com/tinaciousdesign/interviews/stocks/utils/.gitkeep b/app/src/main/java/com/tinaciousdesign/interviews/stocks/utils/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/app/src/main/java/com/tinaciousdesign/interviews/stocks/utils/IntentUtils.kt b/app/src/main/java/com/tinaciousdesign/interviews/stocks/utils/IntentUtils.kt new file mode 100644 index 0000000..9ea051a --- /dev/null +++ b/app/src/main/java/com/tinaciousdesign/interviews/stocks/utils/IntentUtils.kt @@ -0,0 +1,12 @@ +package com.tinaciousdesign.interviews.stocks.utils + +import android.content.Context +import android.content.Intent +import android.net.Uri + +fun Context.openExternalBrowser(url: String) { + val intent = Intent(Intent.ACTION_VIEW).apply { + data = Uri.parse(url) + } + startActivity(intent) +} diff --git a/app/src/main/java/com/tinaciousdesign/interviews/stocks/utils/StringUtils.kt b/app/src/main/java/com/tinaciousdesign/interviews/stocks/utils/StringUtils.kt new file mode 100644 index 0000000..24c81e0 --- /dev/null +++ b/app/src/main/java/com/tinaciousdesign/interviews/stocks/utils/StringUtils.kt @@ -0,0 +1,4 @@ +package com.tinaciousdesign.interviews.stocks.utils + +fun String.lastSegment(delimiter: String): String? = + split(delimiter).lastOrNull() \ No newline at end of file diff --git a/app/src/main/java/com/tinaciousdesign/interviews/stocks/workers/.gitkeep b/app/src/main/java/com/tinaciousdesign/interviews/stocks/workers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/app/src/main/res/drawable/ic_dollar.xml b/app/src/main/res/drawable/ic_dollar.xml new file mode 100644 index 0000000..6dddcbb --- /dev/null +++ b/app/src/main/res/drawable/ic_dollar.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_search.xml b/app/src/main/res/drawable/ic_search.xml new file mode 100644 index 0000000..d1140de --- /dev/null +++ b/app/src/main/res/drawable/ic_search.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/tinacious_design_logo.xml b/app/src/main/res/drawable/tinacious_design_logo.xml new file mode 100644 index 0000000..a5ed7b0 --- /dev/null +++ b/app/src/main/res/drawable/tinacious_design_logo.xml @@ -0,0 +1,26 @@ + + + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..bfa8dc9a0ee28db14f49f3d9dce31446910df4f9 GIT binary patch literal 2202 zcmV;L2xa$DNk&GJ2mk zuf%wKZ;8JtaSj<~pnfiP+lq+VG_gA{65X{5s{Sf$P=5Kf6J zfgxz+2RnL)W?zw1laPx_#N?ogC)kf?BokiY8q|u*ZFoIwy>e+}A=0G5)iQGU;s>1o zC1%F#@%kYPQD}W3oMYLRB~FiWfgxCSdX@#rwj>s84m2vLS@KA7uS>KJ&AKG2>FF)g z{1zIZrHPwMY2;+gE;VHeIt}bJt_vl^^p$VE3rs4c003snxhiTH8MhC4HY9wbCK`fz zRw6ch%5kWq=F8@+PUkv-Sr(CU0tp7-Hi>{?0CJEOs~fg`-IOu*YszsP4`@k)mT6NM(S&YE)HSRQ z##vb!IT@rvi|$M4T8-AOPNE0^m{vgqDA80CP((iRp(PJp7y#FE znk5{>gQqGcW|TIy>T&>Vq4k|cG4q1h@uhZQyo3}tPA|+^yiTzLvE}1i$L>*a0N`4* zd*WgEDKM9D99AhQ;aR(WOgMnU#w)xl!?L1xz?d$OR*U4BaIUFmXRcM*-EkE2ZHg$v zCS1q!&050)RAXEB_)#;Fa?zy#m+q78XQ_}vNi{uRiP5RK)`Ta-HLx@7G_GUhEz{^b z#tZJqWPwV}aU^*|*fUJM=HHUKGGK*tm^@5gk$qXLxcpS-v0MTGo5`%%BQ$EnCjEUT z4Dd{IVpJPE!St44z6;EAy4mt|yME3>3l!q>2eykia%nexh-ytaj?i_wREjZ#snd=U zOnl|c^Af67UBy!{(!2#BiYqK3gYt4<>e#KsAiq zA?YY2oX1cwoKK#dZbnc>EfWobJWvj*J`~?=Aa*sEalFTx$9oFr+R-jz`O9}Co^?@f zTFGmMiR2p7egJ!-jASAz{S5jGTd$(-l8reRYht`-p&hx$Podp?_o6YoFaU|?-;~2b zR4Mar(Ua59Vg!^%!0h-sNy#?P>EFSffsnf0&|M?&HgXIYfcb-I?@M1EN- z&$^(E?D^BYEu2QtONNo77}rOZ1;KkpY*xdX;0^JaX4=rOI!-3>O!Hz?6QfM$HIvR_ z$gv;bEVNO=0W8X-UMPaC)6a@|tH1G~%XspKu{PGQ2Rf2~ zO#`dcGfO6{?YN_4K$8Y(s%T9MucU#S0!-5`DEXKs1=O{pWMI2U0k&^qPF>W z-0><|@YX{;t0Vy$I+6fg1R)vNB?a6g8PrWk1^3)@&q0J_VD~}4(|7UvV+Rq$W5mG+ zJw^}&N(xA$9(pc@I(!;%^`2p$RbfwGM1TGec5!g<4dIL*ItLN$iWS)xhBf@gL>J_U^|b(oxK>#+sGAIMzHFL6mS!nx)JsM zEnoSe@aHey@;Y=m#`6chvO~P32Tg{oE%^Umc=CFRmyZsM_S?zc9rpD3`#HaV>ai^b zKuHm8#9rIcb_PG_O?6O04`O#?4PqzoBhoH0CuL`IfzyXAS2oM`@djhaQ c;+LWgKt|X~kZJ@-mhtXh9z1?}7?LR}0S;F>wg3PC literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..c93761076e5a95010fa48ebb515b0ee3474bb950 GIT binary patch literal 3324 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..50f6b5ee0e0aea494e6b37d082c81ee3a41d8246 GIT binary patch literal 1976 zcmV;p2S@l)Nk&Gn2LJ$9MM6+kP&iDa2LJ#sYrq;16;Zmj?X=|lsE%#h*2uOs%T#|L z|0mP@0Nd8R)~$=>->ik{S14!yKDSiB}tOx$W20PoHZr124?wyz2X6@DkFQF@`gX~ z=Mn$_EE~zTZQHhO+qP}nwr$(C`Szdf3j?=}6nPns?-3FhxWL++lmWdD!f0g35%Qa1 zCZ7X6Kp8=r2aO4jHXdKkS%@26*z9)nWxnoSQ7(a25hm77J&M}dHXeZ<|Vy_hL z1o>A=1tvkdMH)Pvd`Nv_W~cZUd7({&oA2-*f`&>s@gE9lL5>#^(!?EssCRkK+h=# zu|ZyJ%wj2TRuVMqzbI3l35-T;qXl%h_790H7?37pBC|^+xiI^0A=4ddGdo5{bYeSw zSBgGJvp=PJ8Djz`A~fPT3ig>Z$80%1v^u(4TwCBUIstNp%piY<_K70;KvL9eCUVLw zZV{Pkw4u==MN3oqD|1OrqJ&f;PZVQigj|Mc_<+#Yp*CZJ9)X4B91!PY&M{f(L`3Qh zeXEtwMn@W(B%3fCU9L`%5&4VWnYHX`7KqyE93ds53pDYjg;|M;j;4tu&dFq(NkpBH z-*+LBJDqZnOLRV%h_oS{3}`sj5p*bnXaV8?O2zn=m|7&1Mk=F68C znMLkRWgywH@{3?XXdFbasD@(vq-wPJ5h#u5ftZzvgXfcUYx>hBhza}{>4}LnC1zo1 zb*9X)<1wws2&JFjbb>ZgV^Wf&s!-|?&mj)M761g5le-=v}LF{Eoh%+%+AD1x<4f1M@qEx3( z|NSV-&{%evkJESg>p9C(2c^!1_KsJ5d_1&kMvvy#eD#sSr=Dtx_FxuwY7Kgr!j5+R zw?XPJDL&ukQb(VUX$)pJ3uuS2sQ!Y+M4E|BiUl+yOJt06V|A1IK;-1`hvp0XBX9 z1a^FV4^IC26=-3fw>Kc$7B3&cF`vJK=hl1n0wM8+nYPE+~W~A z;qzCp`HM%8ZKcNp*yiN}$hO|IXK>i33vk?*?_iHNw_wFb15m|^_aE^CJsPc+E`9Fr z)3ZB{SMIOa-`KeRVC~@ijl(Zzem`Hme0O8@)}xivJC0Xw{yp#i(dt=0PwwtGUU|LY zaP{z(qt!1r9IkyF{_*1fo^Nk|T(-9fZa>oRYY*0UZ#`W7z46PZ)mx8N-_Q8=<@3Jp zudXiG+65QxZvEN%<-@_X2kYRvgSEq}_Sd&=J6`*>{mZ-6(xprPD2fD-J?H|o0~!H^ zg5U&H02&n#Jm?e@ARtIkBWN5{0h$BB3J4}4=oYjEdIR->P(Z|>W6&4S0;meaC<+CT zIVcen3u*zqgIK(Ppg{egJ&@~@GeL16JJ4MtkS!>>&IHgHC=7Jd5=0Iv1Vw=|K>t9Q zDgnuWipqeRK^FfC$ODut0zv}?fx1DhAe*QOK^maiG6O-_H9!&qVg>nvh(Hmb4p0v$ K1%xkw9Sj1d)v{;+ literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..70266a7d643710e66c5737369f785a767c677d97 GIT binary patch literal 2336 zcmV+*3E%coNk&E(2><|BMM6+kP&iBs2><{uFTe{B;xLf5P3zzGwqHU-OaN5{AEF#> z{}f62;~Kf^xOaCI8}B8!|5Nwk_`R!;vIOkreVuYb%`!GBYzX4bL=Y&A}`7>2L%9EF`UM+urKzyUw<4 ztF!q4+xG?A>R0vIR>Ht-BSp#bG0ol^t^#dTiUa^RaE)e;j7pwr$@`o!{^G zs5`Lj+FX-uf6{8Z?!dZ%R{h6pZgr-AtL@rsZw+t+03e`rwr$(CjcnVtZ8zDr@rdRg zB+IsKV~={6kp|@Kvk$)5w)14$w#v3$4XCGT2!W(c{~`}?Uhl}jZ6ifq#^ZYgrWFD? ztOCs%u~~GOEy`-lP-u?IZ;ru~k^U*HQJ9yMJ0`5dlq#2TD=*UxJ>_sV#Lu>vY_r%k zr^njYtBx++IWMzD5#l<2mceJsjJn3xm=JZErxco;h{C9>%KTf9fnbClx z^7-V-`Q=1h9!i&Th>YgtnYHBFbq9e0=+VNY5g852wPz>;53U1C9M-6vC}gZko^hKQ z;E55QkdXpdoXOA>YEcQEH!r)7CbvsjF(FB{iYQmi$bcM_O!_nGy|Ku-61#jv<>Zq1 zdY8OTrD;;iQU{1oPUiU&2o&1hFoj$$l4s_-#mZ+~VkRJ$^sEMToTm*5Z8<7i%^FFZ zER_Cv(?F7*#pqhhn<#2>m#qtO^nwiE(SQ@``AbuuSm|1|o zLd@hV5x7EXKez0>$lVVE>0t?g1r_|VspPas-=YMl0pjUgfe|qGm_D8cd2h)fE7&e3YcrD@$2QH9`j)Shn`~+3GJ7VJ}d?8)54VZl@GUt{s#iwBP#EDtEpv1Jm zVsJP8J5(bMXoV?Sg%Od$%P3=;Dm5c|-|x?cO>S|jxFkGNWbQQ1I#4V$U z#WZ~N%^{gOz%QE!#k7C;V3ae`;$Hf;>52KBV>Cigj13!9Zf8-s1(B-{nm2#c+9OG> zajLK+9!;Fss=}h^SMM-U=TUx9pp;W=chOYf*#B(L$?GgDW*Dr|v9K@ftT>X{5V0b50pQI40@F3HAI zX?9<6YTOqxb5DVfUm}+uG0I(*`Ru(+r|*~+tu`qfOykBD%PlEqwY;9WlyV#DcHm(|_uosH_`d`WA8KR-C{%W&0T1 zg_I2A->|t|6u!UdGx#sM>!vZS{4OQ9 zFW+DHUw3BFLI~WHKCPxiSDfvUcgw{gfBu>ByK>LL-mRofDJyg{k4GwS5KIW8=dar1 zYU>_OP)dvauXVUvO4>8W^OqeErc+A0{7)DQA!yUYY4D%bhxfT{=%4m9khDn+R}Ajy z+VIUUlh50&x(I>@!H3XyYbnh07fk_gOObsa#k97B;nL*pzTF!U+xOA?@24RQ&3B3* zP?`O?Jc6J#UxzTZY2wnkg@7(C1}^>bm9950ex!1kq55Z}0;e9`fgnRrFO0eKNEQ7| Ge-H$4oOsay literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..5279dd6effeda23b23fe7deb779cc1ac37edd1f8 GIT binary patch literal 3294 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..6e5214d3e6e5fc888a82b7a2fd6d95b1070fabab GIT binary patch literal 4420 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 K`akvmIQ;`Ej$)1g literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..83acc82cbf2c904496b7fff3936cbee200200906 GIT binary patch literal 5840 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..b2e30a1c30391ccc39490a2e99db75fd56a0038e GIT binary patch literal 9642 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 Ay#N3J literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..91e4d27334f926ced60c61e147b0c7aac102681f GIT binary patch literal 11436 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Μ-@tLUjXXB2b(;N%2Nn~*QS>5WypgLBY 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..3557953b989345201e568979124e084af35674fd GIT binary patch literal 13452 zcmV;7G;_;RNk&G5GynisMM6+kP&iC@GyniEzrZgL35abQNs=Jj`?@#(fP1=j?;?ok z{{--VGqOh+ct(;df5X8u2S|P(0YspI18D&U4yd%%#gyJ#sh+j{Bo*{MQ`)F5)fciq zZ3^Aip+%+rZb1tJ{l#+w3KVETFX{O_m{z)YU1V#47HnRd9*LOSwj?FV$R}k#d7|9! zz`9r?>3(NlYaJGFB->W4tp6eU5pL0qC_*CF`HzAkwu9YTxLo@~fE!7+Yo)DUQ^)!t zd^Gt-gDC^h@T^me+BOno998!2p35K7A?g1FKsyNHLwpDV80JF|1OR;CL%@fT&iQo* zoDIdH608Be2m!!86%NqN&ISN*MZvR{%>uBjJV8Gj8-UFQY?dw01}qEMSr%KKSr%3# zM3yLQmc`DpEOzgwESpuNCpSRVW?5{&W?7c4w!{)=EKxw7SHe zY&{Z?nyqH59RZN&%9CZY<*B#;An^nMKFf-8md%Fp%*KZDjAsDfBmiI!06xCB+}@qN z0FVFxNY9o4h|Aw|dwT(92dFOr^PVyA&fRN;Mn1Fy95$ME;@&MPgAOML+|Goxg6OT~HbJ!g@ntjv@5*2?fRQ$`F=wW%V^ z%&sVz;lC9`bcGpfWf)FV#a$VXqd^gxj$yMs3=NAi{|8nRH)41i7Q@UW*FsClZ6gVi z#K@X?9`1SmTXRO$REA}3+txOcp7;CpOR`hc-NVexSq}58c$$|(r{OsP-tmgF8|F!` zhIyKqlDaAtbZp7`;q!KDw`tq9ZM`YwT&kOTZ(YqAwr$(Ct^YDUY1-zn(QMmxtFE<_ zoYDl~zqWsw+eSkg^oW^RGDUIXfHr0dm|bROW@ct)bjnQ5hBS8`2Fej1q(q|b2v4Jv|HPCrHsDq7pJdxo9BDNds`7rLQ zJ{#KHQ}+kaWF)JzCt~UCce7zlellW4J=aCBlV%>g_(NL6c7eX^@Z8+Nv?LE;RpE!ja!&+m6E#Wes@z%eyWR2e(Y|9`%qo-{bU5! zQ#ag-A#aKyJ!Oc=8xik7`KQ_PA7GYzV@q?u>ou0pem5J|JYa|3_4E1b%<`1Q?H)(T zT3%tm>_-nVOm6sP(qPl!6F>a+gMZ)nR(Q+Ox#u`|zzKUXd$`{(&m+2yl-h%@W&e7J zVFGTRPNIBkhF`d+fBX{`*D}cQb!0 z4_Z+3wD;Kf3#*j+w6Z8SzZo!UaQZKfobK-1^II<~KE0Lj-F)FHv-XTH7f>QND~ojV ze}98Ngw?YrDYt?J72LRm8SoQbta4KyLAaKz0kg!ST?IK3;o8UC#KHd-nUYM7ZUAlv zLL8i4Vi9|82E5C^{UFJk3+u80-^FL@eeF(z2G*}o7W8?~8Qk&4XWzuZWd(IBGC5pk za1|-nvcRteDfx4cByUwj`gl&*D?{xX-~;co{g<8HGwVT_e&aV10pdQ@BLT^ng{4pZmVk{Vz@~iSP+(uTUV+I>(MaH#Y&pPX z`krGxZzY@uF?&NR_!1+FUv6A#E-ECcRc(7TkfE1X)3^D7yFH-6+#Apj_oY>jAIRn6 zn;soGR#g?DIAMB*(x9+ykMs&$OY6t``GOp@>jIH+3oAbQFSo#z#m`YMd5fv=I{Urz z?b3>+AazVS5^RE7N61H?#Bo>&#V*_$rM`bc4Loh(5Hg454<^{Y?9 z%3lsb825S-zxB^R1~8Fj=pqgfN!bA6kWMg%U6A4@JQ;^mw_Tlqo0+lAJz z8CixVN72*Mnv8$~YJou3qRBwRHi8^Wao+6|#KOy}0H)JHhWtd4f_L~07wObV3y&Cg*?=$_Wkn@kYCFLNeR8;A#=w*P&gg{WtxH;b#5F$z&M@=`mJYMgaV}5G@zC>E;=uN zK*t?W5A@o~Zu1;p%;J8pA`c6h&eTB2sK(hfH862W0Hx;DtUEo zJ48^TnAGr>ow{x@Cy$^CMYNvY)2pK4gbFsHTAmxj#=ryN%Tt5n~YvbNyVMpKIZB>TdMZgt@ z1S=y2MnBmrcE!Se`7dLF4Sh(YshX3%btV`F-VW9r8w~)ZQd2_lGs@V)sid@16enXfQDGrobDdo z?V@%G7JSgIRCb&D2m}nBrZs02^XL?aw}JvBU1Foy5`abz-wFaZTIE^?gCsLh$MuA3OI^kmc-5hW@ZbTsGX-3Xa0g(xl}=p zP3S!DVhh_dfqWj4yuu2w=`MU@AG{c#F+VlkIp~?Hjp=U!N&zvP12f`?>jW=rB1lt& zl3c!w0pTjZJ$%nGH6xxWtQb6Wt=22mnqx7bn!^nNdRst^2{#h79Y(#)0GvM6pm%#o$@@ zt1A>FXAg+h+SwXMwgBYNRqiHq6vWBTA)CTFk`P?22-|qw>$))pJ6d&P+O^bqIB{?L7SEP}}gN%SA@r>Qxa-ojdHr<%;D$VRr}U9ek?BM&~kcr>sNiGToI&sC&(2M`qAL&5cl8<30+a0MHibvAjTE z-(BR$WGJ@W3IH@sbSocce9}??Xb0-h4!pdGPErsr%RpLiT;7PE_NJcQqGK=%`))O- zKOa=f9}df{%|}%1MSu#it%DN7a}|efd${A9T$ITm2XqH{^>4Z`mn;-tpD7qcgaR7w zE_IfY0Q6o=^8p*ku`317KweK|2v#^@&-@ad^*UORevk*WYBpZTXux5aFFqm4WdQig zXb_?o7UyLLi>?DbR_7IP$3^PM$6;QF@1Y7Blpur}4St#n{d%#g^guuZ3N^u9@z@{( zi4Esy8Jgt+fIrH!L>B@#Gwf>RYZnr#GHjXjSjZ*7AQoNvjp8t=h6CtKB3ZMtsO-S6 zLp5kr8-kop7ubA3qh*8fO+aQ-l5Neg{wxC3WI6|~m!s`DKD&5fjg6g5 z5CTh!JO)qBNr08w!$=@;6sKsarqt;w*{SnxxkgAM2<^r<0`_&#nb3r|GOUEkLPe;#mO4^MR#f!_-pNr5*Biz%`@{-&x1qzc_O5yVG&OvGVZ~ytH_; zW+`o{6ShiE}Cz>JvC^)E1o7+L&R%lM8qd;#r zXc#Ot?m>(Tg!eE&d%eCKQ9VHjh^8qD!2K_J9P8GrQIp~Vj~Jth*M<(Z_Wt6vBE0}_ zyHy<<%Yj1;gqp`&9g1Jsz8!D{HFGj;Hw*{YOk&k8&pV>9Z92QNj+)nkRnQX!)Mjd{ z4ExzMi+6cIiZ%_Dj6*yq6#v!(FOMn-pF&0{W*_eQC%P43>Wi;_8m&GHAW$5j&X4@+ zUOtW9F7VWvrP3yA(zo+c>PWx9%xH2#twDks(6TzjNsj6){o2S_Z)C9#f>C;ClKz*a z8%s?orKQX6*{N{={p#5z9($Au1>~E8iUri2;?sM}p=ZjlQ^kS>H3YZQ{`)h5G=@7nwn7Kf5_Ocf%l+(d3(c21k^p$2 zLx&7ZY*1g9+yRDkLfnjXEjk(DzrI&z_Qy!u4Hm5wT^sa4W33Q1}xO~tcBw(NDR?Z)*= z=$+cOfXjdQTMlhr5>pG=7(0c86aWZ>xim1SJ{Y};hn5Yt)TV|AKR~@Vzh+4SMnfr2 zjR*)3$YqDQ^{!)@W_(} zzIB0+%s^v3;~Ug(*~CuupN{(bU1V-TB8<9+T$a@3I%2CO(6LzZG+2$SfD5xG>Ti5Ph)N9Y5gRpdM9+RURy^pFG7)SKCS8# zC-_*bbaJI3gvPD0N}&B4k1_R}bi=mQJ7(EWd?)9SP_SPvmnx(TeDSkpNpl+ z4)yIEFujX$P&}+KL=)z|C{KWg*M+z+cH~6Z3t2X5rl@5%j-bgZ8CB_Swd-p^jd7Pa zn+BEjJ=S0A9=n#U`j}9krT~te;#lDv@|B3U1N3dqa%`GSHTBur5orhl5~6T1!8i9P z>+wh((MEtsSbL1z>8Vj!NKjaKkk5P@ePhV6l`i-Ac;#^}5LBgsqOXNMC9Q{RK1DgG z=2O7N#?)c)qC8LlP(N_$IxvZd0zjp9|9|Gk$O-1~UiG9gfI)afjcVpa#17}Yh}5tU z5DKehExNC+Iw}|(&Beequm@;2iSr!KfR09nq^6+)BzRl8)sOLNq10dnA24~76UAs} z3INRTVQNYv0O ziWW?cO^2jZ2+a^!Ctc{QSqrr*0A0s+j$c1VWLsboK9k4Z4y8=-Buw^dQzyPo`{SSI zF$grsYvDQ3vg6D^03~Er!$RXQdTtr&zuf!C0fj2zEo0P3Z-H@SY< zGeaj)2TDQ)xP~@N#@TPD{NwM9wk(ty;G`3O@PK`@BH3Zl;0+pO7%)c@Z#wTti>_N> zvmp`HnV_^(+mel>y5%}!$#s-HQvJsNk$OBfpbAou3yqL#_&U?|PglQggihS?Uu9fL zE!zlGE>q7dmhq;1;z3}~)fwlzlf>;UVYP(R+lLwv7nq67zW%_mJ&s>I+R-HQa=RR4 zA_k#I#|(+~0jWK62C7!0>@Xvu+aDeA#r7S35|WWTus zmTU+8RLVsi2edk-ZdU_w_mkH=phwX>dlO5!4xk!a;*q5Bb1juqSMeDcR1v}@9>2)< zY|OrMD(~h<7AEZK6yB7UOcr0EEKW(4fdV?j4J0;z1ANF$S|bn-C#WBO5AMvMU2PIgY1dKl8u>C}k9NS8(9jxRs|OLh5uq zE)69ND$2zCQdyOvsKM2iCK8=9z!ItyP!#Dv1fI$CVUx=Napc~0tf1wWY$SjIs^uy` zraRTgp_U)n$m<#gGe&S-!2q<}^u(gR-Il`WCukr!!5oU+EXaMQEw;O_)BsdEkaM}WS9z2@8lZ07Cy%qGScrZt zDvM5Ol99r|6u;i#+FZ+@JZoM9$h`7ZK5PMck%ri*I9EuV%{mPz7!fqer6@ATsSeV z7eAX=Qlns!3fgPMc}~@!Ic%S^o_)WivQKa6Mk^^<&84RH<*HUOxr`|&*H7Iu+hSrX zSZVM^tH^a%2?mz%o-2l_h6O0sSC?lX!8UexJYbJJfCB7Q&DgpmT4Bf5p~~DlT!xyY z1XAC6*)MDr=Q^T90RWXv&nMVgajBCW0I>Qv?c@KOgjnWkP{OE5>6(Ygyqt?X%r}W0 zPuye#C5Zy0Q#eo5T%3!rHx0gMsB8>5t?#Vu(XPvdee7{umCZDk?A?-vw?#$dh*c&# z_{Ja-Y0lGb>z)|L+L3%7L?-R8d+cx+2wN_A8JMJn(#8?%cz^t*V7W*KlxlgPVMAO} zq6=zv_zTKt;109mMj68cqmx=8%5SZum~xRm1e3jvBB_mE1&xI zLMJndcC(HgiLQYHlw`|h570vp=|Biii5&L-q-$QFg$9C|G=l2n*RX7?lBuwhi$b1a zb^uVp3LZ1JUhP@IWB|=DC0MSV_-&2sXTSbdZ7Gyj81{Qy(60Adya&~lk3Gd}jbodU z)xDf3g+V!Odam7Szxq;}BG0)*(VWQfwFxI-fjyxdj}*9p0C6cZ4Eas%>R0YNaW@W; z%0xcyT7lALK_?Dx1SFry31v{1kKj7RUL3-AKN&_j$TNz`pzw^ZUFfxI4R3NK8tT?{ zYN!4br;r*)5nVP(pkM%4CV-{9cWudwY>5q!*%2Hq0EI_Ib}X+0NN$)Qh&RK@mr>}T zKy_2~{#pPAzP)U+iH2UH23l32>P?`kr6yxS)!quB2eg(Vnpz%U#n2D{~e57nyt+@(ojw5U!LgJ zOAg&ijWi%t$NhBcSTv*}osjpA$2>Lf)nx!42q#`^O&f)i+R7vB;1pJI(=wyN+kZw1 z#jI$IrfB7{?o^ndYD`=^IG$Goz%#ifo16vsXa?cF^Bc*E)l0@lq#iwJkI^-&Xc|3j z;Dq66ZQv3Pa!lrSQ&Tg+UY>zO#is}wb^43_54HnYuUQ^uwT&5h3J}YbodbHvfs3E- zUpuaOiWVAI&nGR|aJ0a!87k*4ea(In1w+IA>SnTBGr?q5`+zB0)H_*Fk2Ex@I>0H5M`$G; zcHPMW;vP40%rf*7#2y;VGOPPOBcH}1VO2w2+D>h#`_+{y)#y>j`Lxz)tCrg7p{~x> zmRP;ll8vTR)V=W7A8VhDZ>gZ5uHlAls_)jJNCU3-{f;y?a`hf-23%)CF>cT}yT^k- z@N@}=$AVg)E5-hE{5Am}n^7w@aEcp3vqW3@C-33dtAo1xb!(^j6w4wpzQ!Sga;jFV zu(HZlWJzsy{^VKC%}h|YVI}%NA`4$H{>gO8W3|#$QN|+?94O#wZGT`xbs+6JK#se) zWgx;O07~hEl*G!#OFS^@l|>GNkr)GFFhxLSEfOX{wVW|HUCm8-5}-_1s+A^Q(WpWy(Wxj7aufuSOEjK*y2BCN371U`eP zPpfDs!Pe(})|tj5v)dW!&Y$Ynt#s=_JgBi!JK5{HPh%=~_x4h#uDGe#+H`&If(fd5 zILLOQx79`n<>2j5lisnUbsMtl0Fqo|Wz7j`Z2T}r0C&`7UnreXr2gwV&HB3`h@Tb+ zx0a+8)*hpFlPE4F)MRZVSg3iey?NlP?c*0jW`vpzw>rbmpRsK03S~FmBe#7nRp5d#X5t*n|-U{e^}O<%jCIcDFK2Gv-vhMpD*sLfr5fcW#_>Gam#d`w)s}^=#Fb?krV4C9R1Fu~;OkGb9->4Zu85W7xHy zte7Xf%|AJ&Sv+vlEV`h*>pLGv(l|w=y)vk8QFBTGlmb%QW^AOL+|5@XxrHdqhy#PM zq>xD{m&JFV52NUm!VY`^6bUXoE@haleFOMA!Dk|%T(GL9dQMH4YuRqHrj=pu%y5+3 z!CU>Ban`3CfaZfoBHX23h^Yjn*mv?R7e7oGc<@^SpipuFWhXmUoOIW`yXcn5pyQiJ z0`bW*ET}o#Qk9P<=i!GJ!8~~|ogBG4Eg(uK9xI$$u6$j3w=;|lj}hUKCb0=9M|BdL zyMMkZ2(6NzfC5&stqnm)2o2Vlwrr-7ezmGDj8xcQhhS4q4rD;qL`x@IKy7IS?fcp%R< zqp3jskkj!F!`wA?Ytj3&bQ7p~e~wvut)uU3=?U2g#nZMmu0bdtO?Nz#3Oh6wCMdhB z+a!}pXtdo;gi`>%(4!v*EUaU*T+n8uI7?A3D_4}llrYR#BZLji*l3>%<`)!V_>&9( ztf!?zM0b{1rbQ#Ia?zv!rDP6^YDAtj-FZZJ!FLQU$KFVE0RFdAaK)hVRK;7W<+tiZ zjHaZy`H?%=5u?xWJ2q~Yrp?(r5COdbiC5XO#%4qEr$!_T5WA|%7SIAsS|*$N!(N49 z;(^|r!wr5jpeI8^k{ZyMczoKhodSPP)ozf~oiI9TmiDp(Z0t?hKfrUFWsuSKzV3ik zc3buD|Nf}c^;TCu-O)f7F~g0_h&k(?3Fk|HpMq4oq$Eeptxjj}Pfh`t@sP#K13XNY z0W&@PD@V;dKuPTmNzEi=l7903g#A*z&7bu8|M>@>ebt2W3Fury%sUUZ>%erJe^`3_Vv)qM6>h6^Ym;A{+V@P~ zz8s4z5&$y_rqg}zc?fzacNOYw#92zd>}AK=$#raYNL{gp3mGi zGb(#Msu~?-TPCW0!t$x!1>12s56qDlkz-axGs{xz`2T#)?Jo4~OD#~p0E=Wzk${0> z%0V5uds|@vfqkA-&+vBMGF?OyN&#y-xYwU7z=GUcB!?O7ssp)W5tL4L%N@^7VZjxU zI}nvw=Z?x9EEni$r@yy?Rm|LPUDm#XJwLT<8JpCj+uH7P6qY^DNMC)_xYl4~xbcE& z_wKLaq}+gDW0I|jc$j$>vNta;u{&q6(~{sC(3Ms&MAph)0RSg2re|i8k&R9AF|=js zDedynZXY2jY=hic>+1vPAh?Uni}2mji(#@d_{=+h)p-ey*%-y3_x`+j-g}(&%hvdlokL>G?#u{_!EH*LkirihaO&9l4iBUka*s?o@FC!le4?J%FzcR`Hej@hZZ@%&}f#&B&rO`>~|Fzwlu8+7m!(K|qBqi!usBDtoCUGm?hZOdFGp zkOPlfj-{@t4oXKxyKXKP$w@1<9GaNB`r5)*Mo{-U+|>wHc1TYy6t&Hr{mb{ZT67_@ zt=Y)6)$u6+pt1@@9dTqW0S@6gSF>w)>6nJUU8B;7hg&I3E4Ld-EP-3s-X|3o1HGaJ z9AYNs&lq0$MUSuQ0Gl--2?z`HN6OzEQv5JL^>$- zWzh)@Y|>{Uk`kR~DAQgeBj$EQxa1DXYryfa`^U$rjb9pUd!Z}g-e{QCW(-@Xq~ zP~>^y&L4x}VHM-)wE0M)&D)A`mwT;ABP@lRXTf&0#?`q@0x&&>F3z3L`v*qz#EEb+ zw)@Oy9;r`nN=KrX;Yr0JkM1B5gz0Ytu(v~y7-GcB8*SDcg6)EVEF`rzBmrfnNT$(a z&R0#80ymyX)tiw&;60zvudB$mJX%N?n`VovG|G$13-#w7!W{@!9rAK-liHUo8&9Q) zxFok4*VNjMi6C)mdZs;qx{DZ1=kiu107<#?`(B^mXNO@+Nd%d<8J+6S&wqI`?DTgz zly7o??^$DRy52pYHwK_#>J?)~`AOI2pOV=K0cD6H&=M3q)@84+JR`uW+M6B!zp^IC56$1k6-e-ul)sq0=*SF4HfwpK8!;@U?AgmkN0!4Gnoy-V8HYS+V zFqY1gcct!Nxb-EytG6ZWgNv8YP;^>y?#kS*ar8zF6e>A6G}UW8%Mb}rVIYuYn_!&c z8kk8En=ON5WPn~gdfQ*&X6{{poXLV%mW7Xx%T4sMim)R8xW0!l$V&;>X08Bybk<+y zwJ2?!H-fTj5rF&exKmpeAZw088Hl9K@+Pz$(rq~1C7}ukz*n!ydBqopdpT|9e)jN( zm)Sr-`Ie&ljLIc0_7()g7;3^cE;!lWW^R5d5FsU(gNBR==c}XVh6^l3Kw?0>=;nI9 zC;sy@Ncj){3*|e2%quiZU8=>fGv(UiO}R}Vq=cXpkz9`)0@&upa}0ywszKdA*A1tT zQ-Y)2@6Hv;MG_He2qZ;*X`9Rd@Ad4XHci8jD~p5;aCS1~9eV-UCeSa5Fj$e8!$E7R zJ##~gWtBt6XmPL%;#~4fYb!vrkse7r1sam!oOh5Coj8{+$nCB#Bh_j4A-2`GA%_yQqEI^LTND}(_Jn2=B-@D zE1p&k>#uMUoV=awut2s*q$p%wm|W8q%j6tP=h-Yi-N|HC-7LJJYp;$jjMWf+u=f!Fk{TAj)junzFfZcyjjF|KE@RPDIE8 zVH9~TIpefDwVBIJa57uSW`TJIFN%%`R<$t~e!lpWUiq_B3Su3j6pjezsh_kd^-+6Y&34u544Kpbry6=lkqr3& zdNLVDE@#rvF+pDfKA@XVs^_6dv5*vzSl4x(S_Qa~Won*Uo|)_apUcpdXY?~mc$qgY z6vdDr+3?&vHjVtbgq|7c|5K9|1y`K%pswpWiUR|2{Hw$Mm)^k~>q^gBYW*m_IH>{pV@O%2 z0cQ*;kjuMs39Zx0e~t`25^u8xXiM~amk@-nU zJ8tNahyZt<2%MBFfq4eX`Uc228JQ7zHg2w(X+=Lw89zBTaX}((ODXpudk#VZ1?%L;8 z?Z~q(-QG)=HQJx{<+c$Idx2rNV7Q*TK}o~WlWRy>H>!Gp3};WcV`jo1{AHG5Y&K^d zQ@z$cHCLBKa3e!kd!g^r73+|(F+ld5v(KC@<5o_BtuTNgFk zpWMu>$~MNVh(yE-h8OOB`87s`>U=B5^T0g6vF-(&y8GpiJLfsRDku1qY^z~4%`lnf zyl1NWJyUbAEF!p(QmuojP@fr`IWthLRFx&EZ6rm=tu2UN6O1pa%A{MRd-i#!j^^^2 ziT>22U45cGmWU-UNB|?i0&5iEkfZ~SvqJI|RXH5~%y;~ZQ*-An zHdfsz$1OW-Vli7q@3?kH%K(P8wY6ogkVA0Lt61_JLr1uL=;Jw8vX1YhiR-Suk`B|Y z`rJgyTprSZ04hx_r58#i2g$GN!C(ORgSxI`RAtF(8-=S|TU)DJs{ljmH8rOBg;GhB z$|`AyZZ8$v+tel#k2hKlTj8i-<(8G8#DOqa$dcItnWxipHQadwPIX=>dNq&KY|fG% z`%c-)5_j`0YqMc_HeoLh*+`)@6;5xjspy^*>p?vj4C+CwB?`+}NlJT^7-h9u-GY{z zU~PU|q!LIasahos;lw;<8$(tMC5*wCP$gHzNkx}b%&oI|N&yfNl0r6hoZboiR+uz3 z6{A*>Dyqs#D=N!dIA2^{U9DEDc8I14T-p3aKyoSQxm+kjYC@lw-ULX&-~WD0O%i2E zDhicU)k-Utzlg-RTD7fKRi%}xYNZgF16Cv>|DKRUqKHV8ND4_RRkc#3%3^>N(W=r_ zov+gQNhK+y%pd2WQk9jeQn_dpQrj!7v{J3+p^{1=h4Vy_C{$%dRi$!qsiab=^HNo* uN@crHgd#!_5oJZCs&G-Nq>@UdlFAM!q);d%m5W_vyK-J7DV$f>E-YL@X{C7p literal 0 HcmV?d00001 diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..045e125 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,3 @@ + + + diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..5eead77 --- /dev/null +++ b/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #023465 + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..4c7d18d --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,26 @@ + + Stocks + Tinacious Design logo + + + Stocks + About + + Lost internet connection. Using cached data if available. + Internet connection restored. + + Failed to fetch stocks. Will be using cached data if available. + + Search + Search for a stock… + Clear + + 🧐 Search for stocks + Use the search field above to find stocks by ticker or by name + + ☹️ No stocks + No stocks matched your search query + + About + This stocks app was built by Tina Holly at Tinacious Design. + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..16d4c58 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +