├── .gitignore ├── LICENSE ├── README.md ├── _archive └── screenshot │ ├── screenshot-home.png │ ├── screenshot-loading.png │ ├── screenshot-local-properties.png │ ├── screenshot-location-directions.jpg │ ├── screenshot-search-blank.png │ └── screenshot-search-places.png ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── novalogics │ │ └── android │ │ └── bitemap │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ │ └── novalogics │ │ │ └── android │ │ │ └── bitemap │ │ │ ├── app │ │ │ ├── MainActivity.kt │ │ │ ├── MainApplication.kt │ │ │ ├── di │ │ │ │ └── AppModule.kt │ │ │ ├── navigation │ │ │ │ ├── MainNavigation.kt │ │ │ │ └── NavigationProvider.kt │ │ │ └── ui │ │ │ │ └── theme │ │ │ │ ├── Color.kt │ │ │ │ ├── Shapes.kt │ │ │ │ ├── Theme.kt │ │ │ │ └── Type.kt │ │ │ ├── core │ │ │ ├── base │ │ │ │ ├── BaseViewModel.kt │ │ │ │ └── state │ │ │ │ │ ├── ViewIntent.kt │ │ │ │ │ ├── ViewSideEffect.kt │ │ │ │ │ └── ViewState.kt │ │ │ ├── database │ │ │ │ └── AppDatabase.kt │ │ │ ├── di │ │ │ │ ├── module │ │ │ │ │ ├── DatabaseModule.kt │ │ │ │ │ └── NetworkModule.kt │ │ │ │ └── qualifier │ │ │ │ │ ├── DirectionApiBaseUrl.kt │ │ │ │ │ └── LocationRetrofit.kt │ │ │ ├── navigation │ │ │ │ ├── FeatureNavigationApi.kt │ │ │ │ ├── NavigationRoutes.kt │ │ │ │ └── events │ │ │ │ │ ├── LocationEvent.kt │ │ │ │ │ ├── PlacesResult.kt │ │ │ │ │ └── UiEvent.kt │ │ │ └── network │ │ │ │ ├── ApiConfig.kt │ │ │ │ └── MapsApiService.kt │ │ │ ├── dashboard │ │ │ ├── data │ │ │ │ ├── di │ │ │ │ │ └── DataModule.kt │ │ │ │ ├── mapper │ │ │ │ │ └── Mapper.kt │ │ │ │ ├── model │ │ │ │ │ └── PlacesResponse.kt │ │ │ │ └── repository │ │ │ │ │ └── MapsRepositoryImpl.kt │ │ │ ├── domain │ │ │ │ ├── di │ │ │ │ │ └── DomainModule.kt │ │ │ │ ├── repository │ │ │ │ │ └── MapsRepository.kt │ │ │ │ └── usecase │ │ │ │ │ └── GetNearByPlacesUseCase.kt │ │ │ └── presentation │ │ │ │ ├── di │ │ │ │ └── UiModule.kt │ │ │ │ ├── navigation │ │ │ │ ├── DashboardNavigationApi.kt │ │ │ │ └── DashboardNavigationGraph.kt │ │ │ │ └── screens │ │ │ │ ├── home │ │ │ │ ├── HomeContract.kt │ │ │ │ ├── HomeScreen.kt │ │ │ │ ├── HomeViewModel.kt │ │ │ │ └── component │ │ │ │ │ ├── LocationListItem.kt │ │ │ │ │ └── RestaurantItem.kt │ │ │ │ └── permission │ │ │ │ └── PermissionScreen.kt │ │ │ └── location │ │ │ ├── data │ │ │ ├── datasource │ │ │ │ └── network │ │ │ │ │ └── LocationService.kt │ │ │ ├── di │ │ │ │ └── DataModule.kt │ │ │ ├── mapper │ │ │ │ └── Mapper.kt │ │ │ ├── model │ │ │ │ ├── Bounds.kt │ │ │ │ ├── DirectionApiResponse.kt │ │ │ │ ├── Distance.kt │ │ │ │ ├── Duration.kt │ │ │ │ ├── EndLocation.kt │ │ │ │ ├── GeocodedWaypoints.kt │ │ │ │ ├── Legs.kt │ │ │ │ ├── Northeast.kt │ │ │ │ ├── OverviewPolyline.kt │ │ │ │ ├── Polyline.kt │ │ │ │ ├── Routes.kt │ │ │ │ ├── Southwest.kt │ │ │ │ ├── StartLocation.kt │ │ │ │ └── Steps.kt │ │ │ └── repository │ │ │ │ └── LocationRepositoryImpl.kt │ │ │ ├── domain │ │ │ ├── di │ │ │ │ └── DomainModule.kt │ │ │ ├── model │ │ │ │ ├── DirectionDetails.kt │ │ │ │ └── PlaceDetails.kt │ │ │ ├── repository │ │ │ │ └── LocationRepository.kt │ │ │ ├── room │ │ │ │ └── LocationDao.kt │ │ │ └── usecase │ │ │ │ ├── FetchRestaurantDetailUseCase.kt │ │ │ │ ├── GetAllPlacesFromDbUseCase.kt │ │ │ │ ├── GetDirectionUseCase.kt │ │ │ │ ├── GetLocationUpdateUseCase.kt │ │ │ │ ├── InsertPlacesToDbUseCase.kt │ │ │ │ └── SearchRestaurantUseCase.kt │ │ │ └── presentation │ │ │ ├── di │ │ │ └── UiModule.kt │ │ │ ├── navigation │ │ │ ├── LocationNavigationApi.kt │ │ │ └── LocationNavigationGraph.kt │ │ │ └── screens │ │ │ ├── googlemaps │ │ │ ├── GoogleMapContract.kt │ │ │ ├── GoogleMapScreen.kt │ │ │ └── GoogleMapViewModel.kt │ │ │ └── places │ │ │ ├── PlacesContract.kt │ │ │ ├── PlacesSearch.kt │ │ │ └── PlacesSearchViewModel.kt │ └── res │ │ ├── drawable │ │ ├── bitemap_logo_main.png │ │ ├── ic_launcher_background.xml │ │ ├── ic_launcher_foreground.xml │ │ └── ic_restaurant_menu.xml │ │ ├── font │ │ ├── montserrat_bold.ttf │ │ └── montserrat_regular.ttf │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_foreground.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_foreground.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_foreground.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_foreground.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_foreground.webp │ │ └── ic_launcher_round.webp │ │ ├── values │ │ ├── colors.xml │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ └── test │ └── java │ └── novalogics │ └── android │ └── bitemap │ └── ExampleUnitTest.kt ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── local.defaults.properties ├── secrets.properties └── settings.gradle.kts /.gitignore: -------------------------------------------------------------------------------- 1 | # Gradle files 2 | .gradle/ 3 | build/ 4 | 5 | # Local configuration file (sdk path, etc) 6 | local.properties 7 | secrets.properties 8 | gradle.properties 9 | 10 | # Log/OS Files 11 | *.log 12 | 13 | # Android Studio generated files and folders 14 | captures/ 15 | .externalNativeBuild/ 16 | .cxx/ 17 | *.apk 18 | output.json 19 | 20 | # IntelliJ 21 | *.iml 22 | .idea/ 23 | misc.xml 24 | deploymentTargetDropDown.xml 25 | render.experimental.xml 26 | 27 | # Keystore files 28 | *.jks 29 | *.keystore 30 | 31 | # Google Services (e.g. APIs or Firebase) 32 | google-services.json 33 | 34 | # Android Profiling 35 | *.hprof 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Shavinda / Nova 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | BiteMap
♨ [ ᴀɴᴅʀᴏɪᴅ ᴘʀᴏᴊᴇᴄᴛ ] ♨ 3 |

4 | 5 |
6 | 7 |

8 | 🛠️📚 This is a learning focused project to explore Kotlin's functionality and modular architecture in app development 🛠️📚 9 |

10 | 11 |
12 | 13 | ## ɪ ⁃ ᴘʀᴏᴊᴇᴄᴛ ɪɴꜰᴏ 14 | 15 | BiteMap is an Android app designed to simplify your dining experience by helping you discover nearby restaurants and navigate to them effortlessly. With just a tap, the app displays a list of nearby eateries, and selecting one opens Google Maps to guide you directly to your chosen destination. The app also includes a powerful search feature to find specific places with ease. 16 | 17 | > [!Note] 18 | > **This app is intended for educational purposes, showcasing the capabilities of the Kotlin programming language. It provides insights into how large-scale, complex applications are structured, particularly through modular architecture. In this project, separate packages are used to represent modules, offering a clear example of how to organize and categorize components in a scalable application.** 19 | # 20 | 21 |
22 | 23 | [![Platform](https://img.shields.io/badge/-Android%20|%20Platform-2E8B57?logo=android&logoColor=white&style=for-the-badge)](#) 24 | [![Language](https://img.shields.io/badge/-Kotlin%20|%20Language-%2307405e?logo=kotlin&logoColor=white&style=for-the-badge)](#) 25 | 26 | [![MIN API LEVEL](https://img.shields.io/badge/-MIN%20SDK%20|%2024-1C1E24?logo=planetscale&logoColor=00C7B7&style=for-the-badge)](#) 27 | [![Target Version](https://img.shields.io/badge/-Target%20SDK%20|%2034-1C1E24?logo=planetscale&logoColor=00C7B7&style=for-the-badge)](#) 28 | [![Architecture](https://img.shields.io/badge/-Architecture%20|%20MVI-1C1E24?logo=planetscale&logoColor=00C7B7&style=for-the-badge)](#) 29 | [![UI](https://img.shields.io/badge/-UI%20|%20Jetpack%20Compose-1C1E24?logo=planetscale&logoColor=00C7B7&style=for-the-badge)](#) 30 | [![License: MIT](https://img.shields.io/badge/-LICENSE%20|%20MIT-1C1E24?logo=maas&logoColor=00C7B7&style=for-the-badge)](./LICENSE) 31 | 32 |
33 | 34 |
35 |
36 | 37 |

38 | 42 | 46 | 50 | 54 | 58 |

59 | 60 | --- 61 | 62 | ### ⭓ Features 63 | 64 | - **Nearby Restaurants**: Instantly discover all nearby restaurants with just a tap. 65 | - **Google Maps Integration**: Get directions to your chosen restaurant directly through Google Maps. 66 | - **Search Feature**: Easily search for specific restaurants or cuisines. 67 | - **User-Friendly Design**: Simple and intuitive interface for seamless navigation. 68 | 69 | ## 70 | ### ⭓ Requirements 71 | - Android 7.0 and Above 72 | - Min SDK version 24 73 | 74 | ## 75 | ### ⭓ Permissions 76 | - Location 77 | - Internet 78 | 79 |
80 | 81 | --- 82 | 83 | 84 | ## ⭓ Important Note ⭓ 85 | 86 | To run this project, you need a **Google Maps API key**. 87 | Follow the steps below to retrieve your API key and add it to your `local.properties` file. Without this, the app will not function properly. 88 | 89 | 90 | --- 91 | 92 | ### ⭓ Steps to Retrieve Google Maps API Key 93 | 94 | 1. **Go to Google Cloud Console**: 95 | - Visit Google Cloud Console : 96 | ```https://console.cloud.google.com/``` 97 | 98 | - Sign in with your Google account. 99 | 100 | 2. **Create a New Project (if needed)**: 101 | - Click on the project dropdown at the top of the page. 102 | - Select "New Project" and follow the prompts to create a new project. 103 | 104 | 3. **Enable Required APIs**: 105 | - In the left sidebar, go to **APIs & Services > Library**. 106 | - Search for and enable the following APIs: 107 | - **Maps SDK for Android** 108 | - **Places API** (for restaurant search functionality) 109 | 110 | 4. **Create API Key**: 111 | - In the left sidebar, go to **APIs & Services > Credentials**. 112 | - Click **Create Credentials** and select **API Key**. 113 | - Copy the generated API key. 114 | 115 | 5. **Restrict API Key (Optional but Recommended)**: 116 | - Click on the API key you just created. 117 | - Under **Application restrictions**, select **Android apps**. 118 | - Add your app’s package name and SHA-1 fingerprint (found in your app’s `build.gradle` or via the `keytool` command). 119 | - Under **API restrictions**, restrict the key to **Maps SDK for Android** and **Places API**. 120 | 121 | --- 122 | 123 | ### ⭓ Steps to Add API Key to `local.properties` 124 | 125 | 1. **Open Your Project**: 126 | - Open your Android project in Android Studio. 127 | 128 | 2. **Locate `local.properties`**: 129 | - In the project view, find the `local.properties` file (usually in the root directory). 130 | 131 | 3. **Add the API Key**: 132 | - Add the following line to the file: 133 | ```properties 134 | PLACES_API_KEY=YOUR_API_KEY_HERE 135 | ``` 136 | - Replace `YOUR_API_KEY_HERE` with the API key you copied earlier. 137 | 138 |
139 | 140 |
141 | 142 |
143 | 144 | That's all, 145 | once the API key is added, the app will work as expected. 146 | 147 |
148 | 149 | # 150 | 151 | -------------------------------------------------------------------------------- /_archive/screenshot/screenshot-home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NovaLogics/bite-map-android-app/5c4ded795bf1084e654aae1a66dd0a12c8f896b5/_archive/screenshot/screenshot-home.png -------------------------------------------------------------------------------- /_archive/screenshot/screenshot-loading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NovaLogics/bite-map-android-app/5c4ded795bf1084e654aae1a66dd0a12c8f896b5/_archive/screenshot/screenshot-loading.png -------------------------------------------------------------------------------- /_archive/screenshot/screenshot-local-properties.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NovaLogics/bite-map-android-app/5c4ded795bf1084e654aae1a66dd0a12c8f896b5/_archive/screenshot/screenshot-local-properties.png -------------------------------------------------------------------------------- /_archive/screenshot/screenshot-location-directions.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NovaLogics/bite-map-android-app/5c4ded795bf1084e654aae1a66dd0a12c8f896b5/_archive/screenshot/screenshot-location-directions.jpg -------------------------------------------------------------------------------- /_archive/screenshot/screenshot-search-blank.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NovaLogics/bite-map-android-app/5c4ded795bf1084e654aae1a66dd0a12c8f896b5/_archive/screenshot/screenshot-search-blank.png -------------------------------------------------------------------------------- /_archive/screenshot/screenshot-search-places.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NovaLogics/bite-map-android-app/5c4ded795bf1084e654aae1a66dd0a12c8f896b5/_archive/screenshot/screenshot-search-places.png -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import java.io.FileInputStream 2 | import java.util.Properties 3 | 4 | plugins { 5 | alias(libs.plugins.android.application) 6 | alias(libs.plugins.kotlin.android) 7 | alias(libs.plugins.compose.compiler) 8 | alias(libs.plugins.kotlinx.serialization) 9 | alias(libs.plugins.hilt.android) 10 | alias(libs.plugins.ksp) 11 | alias(libs.plugins.kotlin.parcelize) 12 | } 13 | 14 | android { 15 | namespace = "novalogics.android.bitemap" 16 | compileSdk = 34 17 | 18 | defaultConfig { 19 | applicationId = "novalogics.android.bitemap" 20 | minSdk = 24 21 | targetSdk = 34 22 | versionCode = 1 23 | versionName = "1.0" 24 | 25 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 26 | vectorDrawables { 27 | useSupportLibrary = true 28 | } 29 | multiDexEnabled = true 30 | 31 | // Read from local.properties 32 | val localProperties = Properties() 33 | val localPropertiesFile = rootProject.file("local.properties") 34 | if (localPropertiesFile.exists()) { 35 | localProperties.load(FileInputStream(localPropertiesFile)) 36 | } 37 | val placesApiKey: String = localProperties.getProperty("PLACES_API_KEY", "") 38 | 39 | manifestPlaceholders["PLACES_API_KEY"] = placesApiKey 40 | buildConfigField("String", "PLACES_API_KEY", "\"$placesApiKey\"") 41 | } 42 | 43 | buildTypes { 44 | release { 45 | isMinifyEnabled = false 46 | proguardFiles( 47 | getDefaultProguardFile("proguard-android-optimize.txt"), 48 | "proguard-rules.pro" 49 | ) 50 | } 51 | } 52 | compileOptions { 53 | sourceCompatibility = JavaVersion.VERSION_17 54 | targetCompatibility = JavaVersion.VERSION_17 55 | } 56 | kotlinOptions { 57 | jvmTarget = "17" 58 | } 59 | buildFeatures { 60 | compose = true 61 | buildConfig = true 62 | } 63 | composeOptions { 64 | kotlinCompilerExtensionVersion = "1.5.1" 65 | } 66 | packaging { 67 | resources { 68 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 69 | } 70 | } 71 | } 72 | 73 | dependencies { 74 | // AndroidX Core & Lifecycle 75 | implementation(libs.androidx.core.ktx) 76 | implementation(libs.androidx.appcompat) 77 | implementation(libs.androidx.lifecycle.runtime) 78 | implementation(libs.androidx.lifecycle.viewmodel) 79 | implementation(libs.androidx.paging) 80 | 81 | // UI & Compose 82 | implementation(libs.androidx.activity.compose) 83 | implementation(platform(libs.androidx.compose.bom)) 84 | implementation(libs.androidx.ui) 85 | implementation(libs.androidx.ui.graphics) 86 | implementation(libs.androidx.ui.tooling) 87 | implementation(libs.androidx.ui.tooling.preview) 88 | implementation(libs.androidx.material3) 89 | implementation(libs.constraintlayout.compose) 90 | implementation(libs.androidx.navigate.compose) 91 | implementation(libs.androidx.hilt.compose) 92 | 93 | // Kotlin Coroutines 94 | implementation(libs.kotlinx.coroutines.core) 95 | implementation(libs.kotlinx.coroutines.android) 96 | implementation(libs.kotlinx.serialization.json) 97 | 98 | // Hilt Dependency Injection 99 | implementation(libs.hilt.android) 100 | ksp(libs.hilt.compiler) 101 | 102 | // Room Database 103 | implementation(libs.room.ktx) 104 | implementation(libs.room.runtime) 105 | ksp(libs.room.compiler) 106 | 107 | // Google Material & Maps 108 | implementation(libs.material) 109 | implementation(libs.gsm.maps) 110 | implementation(libs.gsm.location) 111 | implementation(libs.places) 112 | implementation(libs.maps.utils) 113 | implementation(libs.maps.compose) 114 | 115 | // Networking (Retrofit + OkHttp) 116 | implementation(libs.retrofit) 117 | implementation(libs.okhttp) 118 | implementation(libs.okhttp.logging.interceptor) 119 | implementation(libs.retrofit.converter.gson) 120 | 121 | // Utility Libraries 122 | implementation(libs.coil) 123 | implementation(libs.timber) 124 | implementation(libs.accompanist.permissions) 125 | 126 | // Pluto Debugging Tools 127 | implementation(libs.pluto) 128 | implementation(libs.pluto.network) 129 | 130 | // Testing Dependencies 131 | testImplementation(libs.junit) 132 | androidTestImplementation(platform(libs.androidx.compose.bom)) 133 | androidTestImplementation(libs.androidx.test.runner) 134 | androidTestImplementation(libs.androidx.test.espresso) 135 | androidTestImplementation(libs.androidx.ui.test.junit4) 136 | debugImplementation(libs.androidx.ui.test.manifest) 137 | } 138 | 139 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/novalogics/android/bitemap/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("novalogics.android.bitemap", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 22 | 23 | 24 | 27 | 28 | 29 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NovaLogics/bite-map-android-app/5c4ded795bf1084e654aae1a66dd0a12c8f896b5/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/app/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.app 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.activity.enableEdgeToEdge 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.navigationBarsPadding 9 | import androidx.compose.foundation.layout.statusBarsPadding 10 | import androidx.compose.material3.Surface 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Modifier 13 | import androidx.navigation.compose.rememberNavController 14 | import dagger.hilt.android.AndroidEntryPoint 15 | import novalogics.android.bitemap.app.navigation.MainNavigation 16 | import novalogics.android.bitemap.app.navigation.NavigationProvider 17 | import novalogics.android.bitemap.app.ui.theme.BiteMapTheme 18 | import javax.inject.Inject 19 | 20 | @AndroidEntryPoint 21 | class MainActivity : ComponentActivity() { 22 | 23 | @Inject 24 | lateinit var navigationProvider: NavigationProvider 25 | 26 | override fun onCreate(savedInstanceState: Bundle?) { 27 | super.onCreate(savedInstanceState) 28 | enableEdgeToEdge() 29 | setContent { 30 | BiteMapTheme { 31 | AppContent(navigationProvider = navigationProvider) 32 | } 33 | } 34 | } 35 | } 36 | 37 | @Composable 38 | fun AppContent(navigationProvider: NavigationProvider) { 39 | val navHostController = rememberNavController() 40 | Surface( 41 | modifier = Modifier 42 | .fillMaxSize() 43 | .statusBarsPadding() 44 | .navigationBarsPadding(), 45 | ) { 46 | MainNavigation( 47 | navHostController = navHostController, 48 | navigationProvider = navigationProvider 49 | ) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/app/MainApplication.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.app 2 | 3 | import android.app.Application 4 | import dagger.hilt.android.HiltAndroidApp 5 | 6 | @HiltAndroidApp 7 | class MainApplication : Application() 8 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/app/di/AppModule.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.app.di 2 | 3 | import dagger.Module 4 | import dagger.Provides 5 | import dagger.hilt.InstallIn 6 | import dagger.hilt.components.SingletonComponent 7 | import novalogics.android.bitemap.app.navigation.NavigationProvider 8 | import novalogics.android.bitemap.dashboard.presentation.navigation.DashboardNavigationApi 9 | import novalogics.android.bitemap.location.presentation.navigation.LocationNavigationApi 10 | import javax.inject.Singleton 11 | 12 | @Module 13 | @InstallIn(SingletonComponent::class) 14 | object AppModule { 15 | 16 | @Singleton 17 | @Provides 18 | fun provideNavigationProvider( 19 | dashboardApi: DashboardNavigationApi, 20 | locationFeatureApi: LocationNavigationApi 21 | ): NavigationProvider = 22 | NavigationProvider( 23 | dashboardApi = dashboardApi, 24 | locationApi = locationFeatureApi, 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/app/navigation/MainNavigation.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.app.navigation 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.navigation.NavHostController 5 | import androidx.navigation.compose.NavHost 6 | import novalogics.android.bitemap.core.navigation.NavigationRoute 7 | 8 | @Composable 9 | fun MainNavigation( 10 | navHostController: NavHostController, 11 | navigationProvider: NavigationProvider 12 | ) { 13 | NavHost( 14 | navController = navHostController, 15 | startDestination = NavigationRoute.DASHBOARD.route 16 | ) { 17 | navigationProvider.dashboardApi.registerNavigationGraph( 18 | navController = navHostController, 19 | navGraphBuilder = this 20 | ) 21 | navigationProvider.locationApi.registerNavigationGraph( 22 | navController = navHostController, 23 | navGraphBuilder = this 24 | ) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/app/navigation/NavigationProvider.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.app.navigation 2 | 3 | import novalogics.android.bitemap.dashboard.presentation.navigation.DashboardNavigationApi 4 | import novalogics.android.bitemap.location.presentation.navigation.LocationNavigationApi 5 | 6 | data class NavigationProvider ( 7 | val dashboardApi: DashboardNavigationApi, 8 | val locationApi: LocationNavigationApi 9 | ) 10 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/app/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.app.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | object OrangeBlazeColors { 6 | object Light { 7 | val primary = Color(0xFFFF9800) // Primary color (Orange) 8 | val onPrimary = Color(0xFFFFFFFF) // Content on primary (White) 9 | val primaryContainer = Color(0xFFFFC107) // Primary container 10 | val onPrimaryContainer = Color(0xFF3D3D3D) // Content on primary container (Dark) 11 | val secondary = Color(0xFFFFA726) // Secondary color (Lighter Orange) 12 | val onSecondary = Color(0xFFFFFFFF) // Content on secondary (White) 13 | val secondaryContainer = Color(0xFFFFD54F) // Secondary container 14 | val onSecondaryContainer = Color(0xFF3D3D3D) // Content on secondary container (Dark) 15 | val tertiary = Color(0xFFFB8C00) // Tertiary color (Darker Orange) 16 | val onTertiary = Color(0xFFFFFFFF) // Content on tertiary (White) 17 | val tertiaryContainer = Color(0xFFFFE57F) // Tertiary container 18 | val onTertiaryContainer = Color(0xFF3D3D3D) // Content on tertiary container (Dark) 19 | val error = Color(0xFFBA1A1A) // Error color 20 | val errorContainer = Color(0xFFFFDAD6) // Error container 21 | val onError = Color(0xFFFFFFFF) // Content on error (White) 22 | val onErrorContainer = Color(0xFF410002) // Content on error container 23 | val background = Color(0xFFFDFCFB) // Background (Soft cream) 24 | val onBackground = Color(0xFF191C1A) // Content on background (Dark) 25 | val surface = Color(0xFFFFF3E0) // Surface color 26 | val onSurface = Color(0xFF191C1A) // Content on surface (Dark) 27 | val surfaceVariant = Color(0xFFDBE5DD) // Surface variant 28 | val onSurfaceVariant = Color(0xFF404943) // Content on surface variant 29 | val outline = Color(0xFF707973) // Outline color 30 | val inverseOnSurface = Color(0xFFEFF1ED) // Inverse content on surface 31 | val inverseSurface = Color(0xFF2E312F) // Inverse surface 32 | val inversePrimary = Color(0xFF6CDBAC) // Inverse primary 33 | val shadow = Color(0xFF000000) // Shadow color 34 | val surfaceTint = Color(0xFF006C4C) // Surface tint 35 | val outlineVariant = Color(0xFFBFC9C2) // Outline variant 36 | val scrim = Color(0xFF000000) // Scrim color 37 | } 38 | 39 | object Dark { 40 | val primary = Color(0xFFF57C00) // Primary color (Dark orange) 41 | val onPrimary = Color(0xFFFFFFFF) // Content on primary (White) 42 | val primaryContainer = Color(0xFF3E2723) // Primary container 43 | val onPrimaryContainer = Color(0xFFFFE0B2) // Content on primary container 44 | val secondary = Color(0xFFFFA726) // Secondary color (Orange) 45 | val onSecondary = Color(0xFF1F352A) // Content on secondary 46 | val secondaryContainer = Color(0xFF354B40) // Secondary container 47 | val onSecondaryContainer = Color(0xFFFCF4E8) // Content on secondary container 48 | val tertiary = Color(0xFFFB8C00) // Tertiary color (Dark orange) 49 | val onTertiary = Color(0xFFFFFFFF) // Content on tertiary (White) 50 | val tertiaryContainer = Color(0xFF3E2723) // Tertiary container 51 | val onTertiaryContainer = Color(0xFFFFE57F) // Content on tertiary container 52 | val error = Color(0xFFFFB4AB) // Error color 53 | val errorContainer = Color(0xFF93000A) // Error container 54 | val onError = Color(0xFF690005) // Content on error 55 | val onErrorContainer = Color(0xFFFFDAD6) // Content on error container 56 | val background = Color(0xFF191C1A) // Background color 57 | val onBackground = Color(0xFFE1E3DF) // Content on background 58 | val surface = Color(0xFF191C1A) // Surface color 59 | val onSurface = Color(0xFFE1E3DF) // Content on surface 60 | val surfaceVariant = Color(0xFF404943) // Surface variant 61 | val onSurfaceVariant = Color(0xFFBFC9C2) // Content on surface variant 62 | val outline = Color(0xFF8A938C) // Outline color 63 | val inverseOnSurface = Color(0xFF191C1A) // Inverse content on surface 64 | val inverseSurface = Color(0xFFE1E3DF) // Inverse surface 65 | val inversePrimary = Color(0xFF006C4C) // Inverse primary 66 | val shadow = Color(0xFF000000) // Shadow color 67 | val surfaceTint = Color(0xFF6CDBAC) // Surface tint 68 | val outlineVariant = Color(0xFF404943) // Outline variant 69 | val scrim = Color(0xFF000000) // Scrim color 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/app/ui/theme/Shapes.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.app.ui.theme 2 | 3 | import androidx.compose.foundation.shape.RoundedCornerShape 4 | import androidx.compose.material3.Shapes 5 | import androidx.compose.ui.unit.dp 6 | 7 | val Shapes = Shapes( 8 | small = RoundedCornerShape( 9 | size = 4.dp 10 | ), 11 | medium = RoundedCornerShape( 12 | topStart = 8.dp, 13 | topEnd = 8.dp, 14 | bottomStart = 8.dp, 15 | bottomEnd = 8.dp 16 | ), 17 | large = RoundedCornerShape( 18 | topStart = 16.dp, 19 | topEnd = 16.dp, 20 | bottomStart = 16.dp, 21 | bottomEnd = 16.dp 22 | ), 23 | 24 | ) 25 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/app/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.app.ui.theme 2 | 3 | import android.app.Activity 4 | import android.os.Build 5 | import android.view.View 6 | import androidx.compose.foundation.isSystemInDarkTheme 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.material3.darkColorScheme 9 | import androidx.compose.material3.dynamicDarkColorScheme 10 | import androidx.compose.material3.dynamicLightColorScheme 11 | import androidx.compose.material3.lightColorScheme 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.SideEffect 14 | import androidx.compose.ui.graphics.Color 15 | import androidx.compose.ui.graphics.toArgb 16 | import androidx.compose.ui.platform.LocalContext 17 | import androidx.compose.ui.platform.LocalView 18 | import androidx.core.view.WindowCompat 19 | 20 | private val DarkColorScheme = darkColorScheme( 21 | primary = OrangeBlazeColors.Dark.primary, 22 | onPrimary = OrangeBlazeColors.Dark.onPrimary, 23 | primaryContainer = OrangeBlazeColors.Dark.primaryContainer, 24 | onPrimaryContainer = OrangeBlazeColors.Dark.onPrimaryContainer, 25 | secondary = OrangeBlazeColors.Dark.secondary, 26 | onSecondary = OrangeBlazeColors.Dark.onSecondary, 27 | secondaryContainer = OrangeBlazeColors.Dark.secondaryContainer, 28 | onSecondaryContainer = OrangeBlazeColors.Dark.onSecondaryContainer, 29 | tertiary = OrangeBlazeColors.Dark.tertiary, 30 | onTertiary = OrangeBlazeColors.Dark.onTertiary, 31 | tertiaryContainer = OrangeBlazeColors.Dark.tertiaryContainer, 32 | onTertiaryContainer = OrangeBlazeColors.Dark.onTertiaryContainer, 33 | error = OrangeBlazeColors.Dark.error, 34 | errorContainer = OrangeBlazeColors.Dark.errorContainer, 35 | onError = OrangeBlazeColors.Dark.onError, 36 | onErrorContainer = OrangeBlazeColors.Dark.onErrorContainer, 37 | background = OrangeBlazeColors.Dark.background, 38 | onBackground = OrangeBlazeColors.Dark.onBackground, 39 | surface = OrangeBlazeColors.Dark.surface, 40 | onSurface = OrangeBlazeColors.Dark.onSurface, 41 | surfaceVariant = OrangeBlazeColors.Dark.surfaceVariant, 42 | onSurfaceVariant = OrangeBlazeColors.Dark.onSurfaceVariant, 43 | outline = OrangeBlazeColors.Dark.outline, 44 | inverseOnSurface = OrangeBlazeColors.Dark.inverseOnSurface, 45 | inverseSurface = OrangeBlazeColors.Dark.inverseSurface, 46 | inversePrimary = OrangeBlazeColors.Dark.inversePrimary, 47 | surfaceTint = OrangeBlazeColors.Dark.surfaceTint, 48 | outlineVariant = OrangeBlazeColors.Dark.outlineVariant, 49 | scrim = OrangeBlazeColors.Dark.scrim 50 | ) 51 | 52 | private val LightColorScheme = lightColorScheme( 53 | primary = OrangeBlazeColors.Light.primary, 54 | onPrimary = OrangeBlazeColors.Light.onPrimary, 55 | primaryContainer = OrangeBlazeColors.Light.primaryContainer, 56 | onPrimaryContainer = OrangeBlazeColors.Light.onPrimaryContainer, 57 | secondary = OrangeBlazeColors.Light.secondary, 58 | onSecondary = OrangeBlazeColors.Light.onSecondary, 59 | secondaryContainer = OrangeBlazeColors.Light.secondaryContainer, 60 | onSecondaryContainer = OrangeBlazeColors.Light.onSecondaryContainer, 61 | tertiary = OrangeBlazeColors.Light.tertiary, 62 | onTertiary = OrangeBlazeColors.Light.onTertiary, 63 | tertiaryContainer = OrangeBlazeColors.Light.tertiaryContainer, 64 | onTertiaryContainer = OrangeBlazeColors.Light.onTertiaryContainer, 65 | error = OrangeBlazeColors.Light.error, 66 | errorContainer = OrangeBlazeColors.Light.errorContainer, 67 | onError = OrangeBlazeColors.Light.onError, 68 | onErrorContainer = OrangeBlazeColors.Light.onErrorContainer, 69 | background = OrangeBlazeColors.Light.background, 70 | onBackground = OrangeBlazeColors.Light.onBackground, 71 | surface = OrangeBlazeColors.Light.surface, 72 | onSurface = OrangeBlazeColors.Light.onSurface, 73 | surfaceVariant = OrangeBlazeColors.Light.surfaceVariant, 74 | onSurfaceVariant = OrangeBlazeColors.Light.onSurfaceVariant, 75 | outline = OrangeBlazeColors.Light.outline, 76 | inverseOnSurface = OrangeBlazeColors.Light.inverseOnSurface, 77 | inverseSurface = OrangeBlazeColors.Light.inverseSurface, 78 | inversePrimary = OrangeBlazeColors.Light.inversePrimary, 79 | surfaceTint = OrangeBlazeColors.Light.surfaceTint, 80 | outlineVariant = OrangeBlazeColors.Light.outlineVariant, 81 | scrim = OrangeBlazeColors.Light.scrim 82 | ) 83 | 84 | @Composable 85 | fun BiteMapTheme( 86 | darkTheme: Boolean = isSystemInDarkTheme(), 87 | // Dynamic color is available on Android 12+ 88 | dynamicColor: Boolean = false, 89 | content: @Composable () -> Unit 90 | ) { 91 | val colorScheme = when { 92 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 93 | val context = LocalContext.current 94 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 95 | } 96 | 97 | darkTheme -> DarkColorScheme 98 | else -> LightColorScheme 99 | } 100 | 101 | val view = LocalView.current 102 | if (!view.isInEditMode) { 103 | SideEffect { 104 | setUpEdgeToEdge(view, darkTheme, colorScheme.background) 105 | } 106 | } 107 | 108 | MaterialTheme( 109 | colorScheme = colorScheme, 110 | shapes = Shapes, 111 | typography = Typography, 112 | content = content 113 | ) 114 | } 115 | 116 | 117 | /** 118 | * Sets up edge-to-edge for the window of this [view]. The system icon colors are set to either 119 | * light or dark depending on whether the [isDarkTheme] is enabled or not. 120 | */ 121 | private fun setUpEdgeToEdge( 122 | view: View, 123 | isDarkTheme: Boolean, 124 | backgroundColour: Color = Color.Transparent 125 | ) { 126 | val window = (view.context as Activity).window 127 | WindowCompat.setDecorFitsSystemWindows(window, false) 128 | window.statusBarColor = backgroundColour.toArgb() 129 | val navigationBarColor = when { 130 | Build.VERSION.SDK_INT >= 29 -> Color.Transparent.toArgb() 131 | Build.VERSION.SDK_INT >= 26 -> Color(0xFF, 0xFF, 0xFF, 0x63).toArgb() 132 | // Min sdk version for this app is 24, this block is for SDK versions 24 and 25 133 | else -> Color(0x00, 0x00, 0x00, 0x50).toArgb() 134 | } 135 | window.navigationBarColor = navigationBarColor 136 | val controller = WindowCompat.getInsetsController(window, view) 137 | controller.isAppearanceLightStatusBars = !isDarkTheme 138 | controller.isAppearanceLightNavigationBars = !isDarkTheme 139 | } 140 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/app/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.app.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.Font 6 | import androidx.compose.ui.text.font.FontFamily 7 | import androidx.compose.ui.text.font.FontWeight 8 | import androidx.compose.ui.unit.sp 9 | import novalogics.android.bitemap.R 10 | 11 | val Montserrat = FontFamily( 12 | Font(R.font.montserrat_regular), 13 | Font(R.font.montserrat_bold, FontWeight.Bold) 14 | ) 15 | 16 | val Typography = Typography( 17 | bodyLarge = TextStyle( 18 | fontFamily = FontFamily.Default, 19 | fontWeight = FontWeight.Normal, 20 | fontSize = 16.sp, 21 | lineHeight = 24.sp, 22 | letterSpacing = 0.5.sp 23 | ), 24 | displayMedium = TextStyle( 25 | fontFamily = Montserrat, 26 | fontWeight = FontWeight.Normal, 27 | fontSize = 16.sp, 28 | lineHeight = 28.sp, 29 | ), 30 | 31 | /* Other default text styles to override 32 | titleLarge = TextStyle( 33 | fontFamily = FontFamily.Default, 34 | fontWeight = FontWeight.Normal, 35 | fontSize = 22.sp, 36 | lineHeight = 28.sp, 37 | letterSpacing = 0.sp 38 | ), 39 | labelSmall = TextStyle( 40 | fontFamily = FontFamily.Default, 41 | fontWeight = FontWeight.Medium, 42 | fontSize = 11.sp, 43 | lineHeight = 16.sp, 44 | letterSpacing = 0.5.sp 45 | ) 46 | */ 47 | ) 48 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/core/base/BaseViewModel.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.core.base 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import kotlinx.coroutines.channels.Channel 6 | import kotlinx.coroutines.flow.MutableSharedFlow 7 | import kotlinx.coroutines.flow.MutableStateFlow 8 | import kotlinx.coroutines.flow.StateFlow 9 | import kotlinx.coroutines.flow.receiveAsFlow 10 | import kotlinx.coroutines.launch 11 | import novalogics.android.bitemap.core.base.state.ViewIntent 12 | import novalogics.android.bitemap.core.base.state.ViewSideEffect 13 | import novalogics.android.bitemap.core.base.state.ViewState 14 | 15 | /** 16 | * Base ViewModel for managing intents, UI state, and side effects 17 | * 18 | * @param Intent Type of user actions/intents 19 | * @param UiState Type of the UI state 20 | * @param Effect Type of side effects 21 | * @param initialState Initial UI state 22 | */ 23 | abstract class BaseViewModel< 24 | Intent : ViewIntent, 25 | UiState : ViewState, 26 | Effect : ViewSideEffect 27 | > 28 | (initialState: UiState) : ViewModel() { 29 | 30 | /** Handles incoming intents */ 31 | abstract fun handleIntent(intent: Intent) 32 | 33 | /** Internal mutable state */ 34 | private val _uiState: MutableStateFlow = MutableStateFlow(initialState) 35 | /** Exposed immutable state for UI observation */ 36 | val uiState: StateFlow = _uiState 37 | 38 | /** Channel for emitting side effects */ 39 | private val _uiEffect = Channel(Channel.BUFFERED) 40 | /** Flow for observing side effects */ 41 | val uiEffect = _uiEffect.receiveAsFlow() 42 | 43 | /** Buffered flow for intents to prevent loss */ 44 | private val _intentFlow = MutableSharedFlow(extraBufferCapacity = 64) 45 | 46 | init { 47 | subscribeToIntents() 48 | } 49 | 50 | /** Collects and processes intents */ 51 | private fun subscribeToIntents() { 52 | viewModelScope.launch { 53 | _intentFlow.collect { intent -> 54 | handleIntent(intent) 55 | } 56 | } 57 | } 58 | 59 | /** Sends an intent for processing */ 60 | fun sendIntent(intent: Intent) { 61 | viewModelScope.launch { _intentFlow.emit(intent) } 62 | } 63 | 64 | /** Updates the current UI state using a reducer function */ 65 | protected fun updateState(reducer: UiState.() -> UiState) { 66 | _uiState.value = _uiState.value.reducer() 67 | } 68 | 69 | /** Sends a side effect to be processed */ 70 | protected fun sendEffect(builder: suspend () -> Effect) { 71 | viewModelScope.launch { 72 | _uiEffect.send(builder()) 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/core/base/state/ViewIntent.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.core.base.state 2 | 3 | interface ViewIntent 4 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/core/base/state/ViewSideEffect.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.core.base.state 2 | 3 | interface ViewSideEffect 4 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/core/base/state/ViewState.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.core.base.state 2 | 3 | interface ViewState 4 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/core/database/AppDatabase.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.core.database 2 | 3 | import android.content.Context 4 | import androidx.room.Database 5 | import androidx.room.Room 6 | import androidx.room.RoomDatabase 7 | import androidx.room.TypeConverters 8 | import novalogics.android.bitemap.location.domain.model.LatLngTypeConverters 9 | import novalogics.android.bitemap.location.domain.model.PlaceDetails 10 | import novalogics.android.bitemap.location.domain.room.LocationDao 11 | 12 | const val DATABASE_NAME = "app_database" 13 | 14 | @Database( 15 | entities = [PlaceDetails::class], 16 | version = 1, 17 | exportSchema = false 18 | ) 19 | @TypeConverters(LatLngTypeConverters::class) 20 | abstract class AppDatabase : RoomDatabase() { 21 | 22 | companion object { 23 | @Volatile 24 | private var INSTANCE: AppDatabase? = null 25 | 26 | fun getDatabase(context: Context): AppDatabase { 27 | return INSTANCE ?: synchronized(this) { 28 | val instance = Room.databaseBuilder( 29 | context.applicationContext, 30 | AppDatabase::class.java, 31 | DATABASE_NAME 32 | ).build() 33 | 34 | INSTANCE = instance 35 | instance 36 | } 37 | } 38 | } 39 | 40 | abstract fun getLocationDao() : LocationDao 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/core/di/module/DatabaseModule.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.core.di.module 2 | 3 | import android.content.Context 4 | import dagger.Module 5 | import dagger.Provides 6 | import dagger.hilt.InstallIn 7 | import dagger.hilt.android.qualifiers.ApplicationContext 8 | import dagger.hilt.components.SingletonComponent 9 | import novalogics.android.bitemap.core.database.AppDatabase 10 | import novalogics.android.bitemap.location.domain.room.LocationDao 11 | import javax.inject.Singleton 12 | 13 | @Module 14 | @InstallIn(SingletonComponent::class) 15 | object DatabaseModule { 16 | 17 | @Singleton 18 | @Provides 19 | fun provideAppDatabase( 20 | @ApplicationContext context: Context, 21 | ): AppDatabase { 22 | return AppDatabase.getDatabase(context) 23 | } 24 | 25 | @Provides 26 | fun provideLocationDao( 27 | appDatabase: AppDatabase, 28 | ): LocationDao { 29 | return appDatabase.getLocationDao() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/core/di/module/NetworkModule.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.core.di.module 2 | 3 | import android.content.Context 4 | import dagger.Module 5 | import dagger.Provides 6 | import dagger.hilt.InstallIn 7 | import dagger.hilt.android.qualifiers.ApplicationContext 8 | import dagger.hilt.components.SingletonComponent 9 | import novalogics.android.bitemap.core.di.qualifier.DirectionApiBaseUrl 10 | import novalogics.android.bitemap.core.di.qualifier.LocationRetrofit 11 | import novalogics.android.bitemap.core.network.ApiConfig 12 | import novalogics.android.bitemap.core.network.MapsApiService 13 | import novalogics.android.bitemap.location.data.datasource.network.LocationService 14 | import okhttp3.Cache 15 | import okhttp3.OkHttpClient 16 | import okhttp3.Protocol 17 | import okhttp3.logging.HttpLoggingInterceptor 18 | import retrofit2.Retrofit 19 | import retrofit2.converter.gson.GsonConverterFactory 20 | import java.util.concurrent.TimeUnit 21 | import javax.inject.Singleton 22 | 23 | @Module 24 | @InstallIn(SingletonComponent::class) 25 | object NetworkModule { 26 | 27 | private const val CACHE_SIZE = 10 * 1024 * 1024L // 10 MB 28 | 29 | @Singleton 30 | @Provides 31 | fun provideCache(@ApplicationContext context: Context): Cache = 32 | Cache(context.cacheDir, CACHE_SIZE) 33 | 34 | @Singleton 35 | @Provides 36 | fun provideOkHttpClient(cache: Cache): OkHttpClient { 37 | val logger = HttpLoggingInterceptor() 38 | logger.setLevel(HttpLoggingInterceptor.Level.BODY) 39 | 40 | return OkHttpClient.Builder() 41 | .protocols(listOf(Protocol.HTTP_1_1)) 42 | .addInterceptor(logger) 43 | .connectTimeout(15, TimeUnit.SECONDS) 44 | .readTimeout(15, TimeUnit.SECONDS) 45 | .writeTimeout(15, TimeUnit.SECONDS) 46 | .cache(cache) 47 | .build() 48 | } 49 | 50 | @DirectionApiBaseUrl 51 | @Provides 52 | fun provideDirectionApiBaseUrl(): String = ApiConfig.BASE_URL 53 | 54 | @LocationRetrofit 55 | @Provides 56 | fun provideLocationRetrofit( 57 | @DirectionApiBaseUrl url:String, 58 | okHttpClient: OkHttpClient 59 | ): Retrofit { 60 | return Retrofit.Builder() 61 | .baseUrl(url) 62 | .client(okHttpClient) 63 | .addConverterFactory(GsonConverterFactory.create()) 64 | .build() 65 | } 66 | 67 | @Provides 68 | fun provideMapsApiService(@LocationRetrofit retrofit: Retrofit): MapsApiService { 69 | return retrofit.create(MapsApiService::class.java) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/core/di/qualifier/DirectionApiBaseUrl.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.core.di.qualifier 2 | 3 | import javax.inject.Qualifier 4 | 5 | @Qualifier 6 | @Retention(AnnotationRetention.RUNTIME) 7 | annotation class DirectionApiBaseUrl 8 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/core/di/qualifier/LocationRetrofit.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.core.di.qualifier 2 | 3 | import javax.inject.Qualifier 4 | 5 | @Qualifier 6 | @Retention(AnnotationRetention.RUNTIME) 7 | annotation class LocationRetrofit 8 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/core/navigation/FeatureNavigationApi.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.core.navigation 2 | 3 | import androidx.navigation.NavGraphBuilder 4 | import androidx.navigation.NavHostController 5 | 6 | interface FeatureNavigationApi { 7 | fun registerNavigationGraph(navController: NavHostController, navGraphBuilder: NavGraphBuilder) 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/core/navigation/NavigationRoutes.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.core.navigation 2 | 3 | enum class NavigationRoute(val route: String) { 4 | DASHBOARD("dashboard"), 5 | LOCATION("location") 6 | } 7 | 8 | enum class DashboardRoute(val route: String){ 9 | HOME_SCREEN("home"), 10 | PERMISSION_SCREEN("permission") 11 | } 12 | 13 | enum class LocationRoute(val route: String){ 14 | PLACES_SEARCH("places"), 15 | GOOGLE_MAPS("google_maps") 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/core/navigation/events/LocationEvent.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.core.navigation.events 2 | 3 | import android.location.Location 4 | 5 | sealed class LocationEvent(val location: Location? = null) { 6 | class LocationInProgress(location: Location) : LocationEvent(location) 7 | class ReachDestination() : LocationEvent() 8 | class Idle() : LocationEvent() 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/core/navigation/events/PlacesResult.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.core.navigation.events 2 | 3 | import android.location.Location 4 | import com.google.android.libraries.places.api.model.AutocompletePrediction 5 | 6 | sealed class PlacesResult( 7 | val location: Location? = null, 8 | val list: MutableList = mutableListOf(), 9 | val message: String? = null 10 | ) { 11 | class Success( 12 | location: Location, 13 | list: MutableList 14 | ) : PlacesResult(location, list) 15 | 16 | class Loading() : PlacesResult() 17 | 18 | class Idle() : PlacesResult() 19 | 20 | class Error(message: String) : PlacesResult(message = message) 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/core/navigation/events/UiEvent.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.core.navigation.events 2 | 3 | sealed class UiEvent(val data: T? = null, val message: String? = null) { 4 | class Loading() : UiEvent() 5 | class Success(data: T?) : UiEvent(data = data) 6 | class Error(message: String) : UiEvent(message = message) 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/core/network/ApiConfig.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.core.network 2 | 3 | import novalogics.android.bitemap.BuildConfig 4 | 5 | object ApiConfig { 6 | const val BASE_URL = "https://maps.googleapis.com" 7 | const val DIRECTIONS_ENDPOINT = "/maps/api/directions/json" 8 | const val NEAR_BY_SEARCH_ENDPOINT = "/maps/api/place/nearbysearch/json" 9 | 10 | const val PLACES_API_KEY: String = BuildConfig.PLACES_API_KEY 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/core/network/MapsApiService.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.core.network 2 | 3 | import novalogics.android.bitemap.dashboard.data.model.PlacesResponse 4 | import novalogics.android.bitemap.location.data.model.DirectionApiResponse 5 | import retrofit2.Call 6 | import retrofit2.http.GET 7 | import retrofit2.http.Query 8 | 9 | interface MapsApiService { 10 | 11 | @GET(ApiConfig.DIRECTIONS_ENDPOINT) 12 | suspend fun getDirection( 13 | @Query("origin") origin:String, 14 | @Query("destination") destination:String, 15 | @Query("key") key:String 16 | ): DirectionApiResponse 17 | 18 | @GET(ApiConfig.NEAR_BY_SEARCH_ENDPOINT) 19 | suspend fun getNearbyRestaurants( 20 | @Query("location") location: String, 21 | @Query("radius") radius: Int, 22 | @Query("type") type: String = "restaurant", 23 | @Query("key") apiKey: String 24 | ): PlacesResponse 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/dashboard/data/di/DataModule.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.dashboard.data.di 2 | 3 | import com.google.android.gms.location.FusedLocationProviderClient 4 | import dagger.Module 5 | import dagger.Provides 6 | import dagger.hilt.InstallIn 7 | import dagger.hilt.components.SingletonComponent 8 | import novalogics.android.bitemap.core.network.MapsApiService 9 | import novalogics.android.bitemap.dashboard.data.repository.MapsRepositoryImpl 10 | import novalogics.android.bitemap.dashboard.domain.repository.MapsRepository 11 | import javax.inject.Singleton 12 | 13 | @Module 14 | @InstallIn(SingletonComponent::class) 15 | object DataModule { 16 | 17 | @Singleton 18 | @Provides 19 | fun provideMapsRepositoryImpl( 20 | fusedLocationProviderClient: FusedLocationProviderClient, 21 | mapsApiService: MapsApiService 22 | ): MapsRepository = MapsRepositoryImpl( 23 | fusedLocationProviderClient = fusedLocationProviderClient, 24 | mapsApiService = mapsApiService, 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/dashboard/data/mapper/Mapper.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.dashboard.data.mapper 2 | 3 | import com.google.android.gms.maps.model.LatLng 4 | import novalogics.android.bitemap.dashboard.data.model.PlaceMap 5 | import novalogics.android.bitemap.location.domain.model.PlaceDetails 6 | 7 | fun PlaceMap.toDetails( 8 | placeId: String = "", 9 | origin: LatLng, 10 | delivery: Boolean = false 11 | ): PlaceDetails { 12 | return PlaceDetails( 13 | placeId = placeId, 14 | name = this.name, 15 | destination = LatLng( 16 | this.geometry?.location?.latitude ?: 0.0, 17 | this.geometry?.location?.longitude ?: 0.0 18 | ), 19 | origin = origin, 20 | delivery = delivery, 21 | rating = this.rating.toFloat() ?: 0F 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/dashboard/data/model/PlacesResponse.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.dashboard.data.model 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class PlacesResponse( 6 | @SerializedName("results") 7 | val results: List = emptyList(), 8 | @SerializedName("current_location") 9 | var currentLocationMap: LocationMap? = null, 10 | ) 11 | 12 | data class PlaceMap( 13 | @SerializedName("name") 14 | val name: String = "", 15 | @SerializedName("vicinity") 16 | val vicinity: String = "", 17 | @SerializedName("rating") 18 | val rating: Double = 0.0, 19 | @SerializedName("geometry") 20 | val geometry: Geometry? = null, 21 | ) 22 | 23 | data class Geometry( 24 | @SerializedName("location") 25 | val location: LocationMap? = null 26 | ) 27 | 28 | data class LocationMap( 29 | @SerializedName("lat") 30 | val latitude: Double = 0.0, 31 | @SerializedName("lng") 32 | val longitude: Double = 0.0, 33 | ) 34 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/dashboard/data/repository/MapsRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.dashboard.data.repository 2 | 3 | import android.annotation.SuppressLint 4 | import android.location.Location 5 | import android.os.Looper 6 | import com.google.android.gms.location.FusedLocationProviderClient 7 | import com.google.android.gms.location.LocationCallback 8 | import com.google.android.gms.location.LocationRequest 9 | import com.google.android.gms.location.LocationResult 10 | import com.google.android.gms.location.Priority 11 | import kotlinx.coroutines.suspendCancellableCoroutine 12 | import novalogics.android.bitemap.core.network.ApiConfig 13 | import novalogics.android.bitemap.core.network.MapsApiService 14 | import novalogics.android.bitemap.dashboard.data.model.LocationMap 15 | import novalogics.android.bitemap.dashboard.data.model.PlacesResponse 16 | import novalogics.android.bitemap.dashboard.domain.repository.MapsRepository 17 | import javax.inject.Inject 18 | import kotlin.coroutines.resume 19 | 20 | @SuppressLint("MissingPermission") 21 | class MapsRepositoryImpl @Inject constructor( 22 | private val fusedLocationProviderClient: FusedLocationProviderClient, 23 | private val mapsApiService: MapsApiService 24 | ) : MapsRepository { 25 | 26 | private suspend fun getCurrentLocationOnce(): Location? = suspendCancellableCoroutine { continuation -> 27 | 28 | val locationRequest = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 0) 29 | .setMaxUpdates(1) 30 | .build() 31 | 32 | val locationCallback = object : LocationCallback() { 33 | override fun onLocationResult(locationResult: LocationResult) { 34 | fusedLocationProviderClient.removeLocationUpdates(this) 35 | continuation.resume(locationResult.lastLocation) 36 | } 37 | } 38 | 39 | fusedLocationProviderClient.requestLocationUpdates( 40 | locationRequest, 41 | locationCallback, 42 | Looper.getMainLooper() 43 | ) 44 | 45 | continuation.invokeOnCancellation { 46 | fusedLocationProviderClient.removeLocationUpdates(locationCallback) 47 | } 48 | } 49 | 50 | 51 | override suspend fun fetchNearRestaurants(radius: Int): PlacesResponse { 52 | return runCatching { 53 | 54 | val locationData = getCurrentLocationOnce() 55 | ?: throw IllegalStateException("Failed to retrieve location") 56 | 57 | val response = mapsApiService.getNearbyRestaurants( 58 | location = "${locationData.latitude},${locationData.longitude}", 59 | radius = radius, 60 | type = "restaurant", 61 | apiKey = ApiConfig.PLACES_API_KEY 62 | ) 63 | response.currentLocationMap = LocationMap( 64 | locationData.latitude, 65 | locationData.longitude 66 | ) 67 | response 68 | }.onFailure { 69 | }.getOrThrow() 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/dashboard/domain/di/DomainModule.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.dashboard.domain.di 2 | 3 | import dagger.Module 4 | import dagger.Provides 5 | import dagger.hilt.InstallIn 6 | import dagger.hilt.components.SingletonComponent 7 | import novalogics.android.bitemap.core.network.MapsApiService 8 | import novalogics.android.bitemap.dashboard.domain.repository.MapsRepository 9 | import novalogics.android.bitemap.dashboard.domain.usecase.GetNearByPlacesUseCase 10 | 11 | @Module 12 | @InstallIn(SingletonComponent::class) 13 | object DomainModule { 14 | @Provides 15 | fun provideGetNearByPlacesUseCase( 16 | mapsRepository: MapsRepository 17 | ): GetNearByPlacesUseCase = GetNearByPlacesUseCase( 18 | mapsRepository = mapsRepository 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/dashboard/domain/repository/MapsRepository.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.dashboard.domain.repository 2 | 3 | 4 | import novalogics.android.bitemap.dashboard.data.model.PlacesResponse 5 | 6 | interface MapsRepository { 7 | suspend fun fetchNearRestaurants( radius: Int): PlacesResponse 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/dashboard/domain/usecase/GetNearByPlacesUseCase.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.dashboard.domain.usecase 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.flow.catch 5 | import kotlinx.coroutines.flow.flow 6 | import kotlinx.coroutines.flow.flowOn 7 | import novalogics.android.bitemap.core.navigation.events.UiEvent 8 | import novalogics.android.bitemap.dashboard.data.model.PlacesResponse 9 | import novalogics.android.bitemap.dashboard.domain.repository.MapsRepository 10 | import javax.inject.Inject 11 | 12 | class GetNearByPlacesUseCase @Inject constructor( 13 | private val mapsRepository: MapsRepository 14 | ){ 15 | operator fun invoke( 16 | radius: Int, 17 | ) = flow> { 18 | emit(UiEvent.Loading()) 19 | emit(UiEvent.Success(data = mapsRepository.fetchNearRestaurants(radius))) 20 | }.catch { 21 | emit(UiEvent.Error(message = it.message.toString())) 22 | }.flowOn(Dispatchers.IO) 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/dashboard/presentation/di/UiModule.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.dashboard.presentation.di 2 | 3 | import dagger.Module 4 | import dagger.Provides 5 | import dagger.hilt.InstallIn 6 | import dagger.hilt.components.SingletonComponent 7 | import novalogics.android.bitemap.dashboard.presentation.navigation.DashboardNavigationApi 8 | import novalogics.android.bitemap.dashboard.presentation.navigation.DashboardNavigationApiImpl 9 | 10 | @Module 11 | @InstallIn(SingletonComponent::class) 12 | object UiModule { 13 | 14 | @Provides 15 | fun provideDashboardApi(): DashboardNavigationApi = DashboardNavigationApiImpl() 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/dashboard/presentation/navigation/DashboardNavigationApi.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.dashboard.presentation.navigation 2 | 3 | import androidx.navigation.NavGraphBuilder 4 | import androidx.navigation.NavHostController 5 | import novalogics.android.bitemap.core.navigation.FeatureNavigationApi 6 | 7 | interface DashboardNavigationApi : FeatureNavigationApi 8 | 9 | class DashboardNavigationApiImpl : DashboardNavigationApi { 10 | override fun registerNavigationGraph( 11 | navController: NavHostController, 12 | navGraphBuilder: NavGraphBuilder 13 | ) { 14 | DashboardNavigationGraph.registerNavigationGraph( 15 | navController = navController, 16 | navGraphBuilder = navGraphBuilder 17 | ) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/dashboard/presentation/navigation/DashboardNavigationGraph.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.dashboard.presentation.navigation 2 | 3 | import androidx.navigation.NavGraphBuilder 4 | import androidx.navigation.NavHostController 5 | import androidx.navigation.compose.composable 6 | import androidx.navigation.navigation 7 | import novalogics.android.bitemap.core.navigation.DashboardRoute 8 | import novalogics.android.bitemap.core.navigation.FeatureNavigationApi 9 | import novalogics.android.bitemap.core.navigation.LocationRoute 10 | import novalogics.android.bitemap.core.navigation.NavigationRoute 11 | import novalogics.android.bitemap.dashboard.presentation.screens.home.HomeScreen 12 | import novalogics.android.bitemap.dashboard.presentation.screens.permission.PermissionScreen 13 | 14 | object DashboardNavigationGraph : FeatureNavigationApi { 15 | override fun registerNavigationGraph( 16 | navController: NavHostController, 17 | navGraphBuilder: NavGraphBuilder 18 | ) { 19 | navGraphBuilder.navigation( 20 | startDestination = DashboardRoute.PERMISSION_SCREEN.route, 21 | route = NavigationRoute.DASHBOARD.route 22 | ){ 23 | composable(route = DashboardRoute.PERMISSION_SCREEN.route) { 24 | PermissionScreen(navController) 25 | } 26 | composable(route = DashboardRoute.HOME_SCREEN.route) { 27 | val savedStateHandle = navController.currentBackStackEntry?.savedStateHandle 28 | HomeScreen(navController){place -> 29 | savedStateHandle?.set("place", place) 30 | navController.navigate(LocationRoute.GOOGLE_MAPS.route) 31 | } 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/dashboard/presentation/screens/home/HomeContract.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.dashboard.presentation.screens.home 2 | 3 | import com.google.android.gms.maps.model.LatLng 4 | import novalogics.android.bitemap.core.base.state.ViewIntent 5 | import novalogics.android.bitemap.core.base.state.ViewSideEffect 6 | import novalogics.android.bitemap.core.base.state.ViewState 7 | import novalogics.android.bitemap.dashboard.data.model.LocationMap 8 | import novalogics.android.bitemap.dashboard.data.model.PlaceMap 9 | import novalogics.android.bitemap.location.domain.model.PlaceDetails 10 | 11 | class HomeContract { 12 | 13 | data class HomeUiState( 14 | val isLoading: Boolean = false, 15 | val nearbyRestaurants: List = emptyList(), 16 | val visitedRestaurants: List = emptyList(), 17 | var currentLocation: LatLng = LatLng(0.0, 0.0), 18 | val error: String? = null, 19 | ) : ViewState 20 | 21 | sealed class Intent : ViewIntent { 22 | data object LoadNearbyRestaurants : Intent() 23 | data object LoadVisitedRestaurants : Intent() 24 | data class OnItemClick(val placeDetails: PlaceDetails) : Intent() 25 | } 26 | 27 | sealed class Effect : ViewSideEffect { 28 | data class NavigateToMaps(val placeDetails: PlaceDetails) : Effect() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/dashboard/presentation/screens/home/HomeScreen.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.dashboard.presentation.screens.home 2 | 3 | import android.Manifest 4 | import android.content.res.Configuration.UI_MODE_NIGHT_NO 5 | import android.content.res.Configuration.UI_MODE_NIGHT_YES 6 | import androidx.activity.ComponentActivity 7 | import androidx.activity.compose.BackHandler 8 | import androidx.compose.foundation.layout.Box 9 | import androidx.compose.foundation.layout.Column 10 | import androidx.compose.foundation.layout.Spacer 11 | import androidx.compose.foundation.layout.fillMaxSize 12 | import androidx.compose.foundation.layout.height 13 | import androidx.compose.foundation.layout.heightIn 14 | import androidx.compose.foundation.layout.padding 15 | import androidx.compose.foundation.layout.wrapContentSize 16 | import androidx.compose.foundation.lazy.LazyColumn 17 | import androidx.compose.foundation.lazy.items 18 | import androidx.compose.material.icons.Icons 19 | import androidx.compose.material.icons.filled.Search 20 | import androidx.compose.material3.Card 21 | import androidx.compose.material3.CardDefaults 22 | import androidx.compose.material3.CircularProgressIndicator 23 | import androidx.compose.material3.ExperimentalMaterial3Api 24 | import androidx.compose.material3.Icon 25 | import androidx.compose.material3.IconButton 26 | import androidx.compose.material3.MaterialTheme 27 | import androidx.compose.material3.Scaffold 28 | import androidx.compose.material3.Text 29 | import androidx.compose.material3.TopAppBar 30 | import androidx.compose.runtime.Composable 31 | import androidx.compose.runtime.LaunchedEffect 32 | import androidx.compose.runtime.collectAsState 33 | import androidx.compose.runtime.getValue 34 | import androidx.compose.ui.Alignment 35 | import androidx.compose.ui.Modifier 36 | import androidx.compose.ui.res.stringResource 37 | import androidx.compose.ui.text.font.FontWeight 38 | import androidx.compose.ui.tooling.preview.Preview 39 | import androidx.compose.ui.unit.dp 40 | import androidx.compose.ui.unit.sp 41 | import androidx.core.app.ActivityCompat.finishAffinity 42 | import androidx.hilt.navigation.compose.hiltViewModel 43 | import androidx.navigation.NavHostController 44 | import com.google.accompanist.permissions.ExperimentalPermissionsApi 45 | import com.google.accompanist.permissions.rememberMultiplePermissionsState 46 | import com.google.android.gms.maps.model.LatLng 47 | import novalogics.android.bitemap.R 48 | import novalogics.android.bitemap.app.ui.theme.BiteMapTheme 49 | import novalogics.android.bitemap.core.navigation.DashboardRoute 50 | import novalogics.android.bitemap.core.navigation.LocationRoute 51 | import novalogics.android.bitemap.dashboard.data.mapper.toDetails 52 | import novalogics.android.bitemap.dashboard.data.model.Geometry 53 | import novalogics.android.bitemap.dashboard.data.model.LocationMap 54 | import novalogics.android.bitemap.dashboard.data.model.PlaceMap 55 | import novalogics.android.bitemap.dashboard.presentation.screens.home.component.LocationListItem 56 | import novalogics.android.bitemap.dashboard.presentation.screens.home.component.RestaurantItem 57 | import novalogics.android.bitemap.location.domain.model.PlaceDetails 58 | 59 | @Composable 60 | fun HomeScreen( 61 | navHostController: NavHostController, 62 | viewModel: HomeViewModel = hiltViewModel(), 63 | onNavigateToMaps: (PlaceDetails) -> Unit 64 | ) { 65 | val uiState by viewModel.uiState.collectAsState() 66 | 67 | BackHandler { 68 | finishAffinity(navHostController.context as ComponentActivity) 69 | } 70 | 71 | PermissionHandler(viewModel = viewModel, navController = navHostController) 72 | 73 | HandleSideEffects(viewModel = viewModel, onNavigateToMaps = onNavigateToMaps) 74 | 75 | ScreenUiContent( 76 | uiState = uiState, 77 | onSearch = { 78 | navHostController.navigate(LocationRoute.PLACES_SEARCH.route) 79 | }, 80 | onRestaurantItemClick = { restaurant -> 81 | viewModel.handleIntent(HomeContract.Intent.OnItemClick(restaurant)) 82 | } 83 | ) 84 | } 85 | 86 | 87 | @OptIn(ExperimentalMaterial3Api::class) 88 | @Composable 89 | fun ScreenUiContent( 90 | uiState: HomeContract.HomeUiState, 91 | onRestaurantItemClick: (PlaceDetails) -> Unit, 92 | onSearch: () -> Unit 93 | ) { 94 | Scaffold( 95 | topBar = { 96 | TopAppBar( 97 | title = { 98 | Text( 99 | text = stringResource(id = R.string.app_name), 100 | color = MaterialTheme.colorScheme.onBackground, 101 | fontSize = 24.sp, 102 | fontWeight = FontWeight.Bold, 103 | style = MaterialTheme.typography.displayMedium 104 | ) 105 | }, 106 | actions = { 107 | IconButton(onClick = onSearch) { 108 | Icon( 109 | imageVector = Icons.Default.Search, 110 | contentDescription = stringResource(id = R.string.search_icon_description) 111 | ) 112 | } 113 | } 114 | ) 115 | } 116 | ) { innerPadding -> 117 | when { 118 | // Show loading indicator 119 | uiState.isLoading -> { 120 | Box( 121 | modifier = Modifier 122 | .fillMaxSize() 123 | .wrapContentSize(Alignment.Center) 124 | ) { 125 | CircularProgressIndicator() 126 | } 127 | } 128 | 129 | // Show error message 130 | uiState.error != null -> { 131 | Box( 132 | modifier = Modifier 133 | .fillMaxSize() 134 | .wrapContentSize(Alignment.Center) 135 | ) { 136 | Card( 137 | modifier = Modifier.padding(16.dp), 138 | elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) 139 | ) { 140 | Text( 141 | text = uiState.error, 142 | color = MaterialTheme.colorScheme.error, 143 | modifier = Modifier.padding(16.dp) 144 | ) 145 | } 146 | } 147 | } 148 | 149 | // Show content 150 | else -> { 151 | Column( 152 | modifier = Modifier 153 | .fillMaxSize() 154 | .padding(innerPadding) 155 | ) { 156 | if (uiState.visitedRestaurants.isNotEmpty()) { 157 | Spacer(modifier = Modifier.height(8.dp)) 158 | 159 | SectionTitle(text = stringResource(id = R.string.recently_visited_restaurants)) 160 | 161 | LazyColumn(modifier = Modifier.heightIn(max = 180.dp)) { 162 | items(uiState.visitedRestaurants) { restaurant -> 163 | LocationListItem(restaurant) 164 | } 165 | } 166 | } 167 | 168 | SectionTitle(text = stringResource(id = R.string.nearby_restaurants)) 169 | LazyColumn( 170 | modifier = Modifier 171 | .fillMaxSize() 172 | ) { 173 | items(uiState.nearbyRestaurants) { restaurant -> 174 | RestaurantItem( 175 | restaurant, 176 | onClick = { place -> 177 | onRestaurantItemClick( 178 | place.toDetails(origin = uiState.currentLocation) 179 | ) 180 | } 181 | ) 182 | } 183 | } 184 | } 185 | } 186 | } 187 | } 188 | } 189 | 190 | @Composable 191 | fun SectionTitle(text: String) { 192 | Text( 193 | text = text, 194 | color = MaterialTheme.colorScheme.onBackground, 195 | style = MaterialTheme.typography.displayMedium.copy( 196 | fontSize = 18.sp 197 | ), 198 | fontWeight = FontWeight.W400, 199 | modifier = Modifier.padding(start = 16.dp, top = 8.dp, bottom = 8.dp) 200 | ) 201 | Spacer(modifier = Modifier.height(4.dp)) 202 | } 203 | 204 | @OptIn(ExperimentalPermissionsApi::class) 205 | @Composable 206 | fun PermissionHandler( 207 | viewModel: HomeViewModel, 208 | navController: NavHostController, 209 | permissionList: List = listOf( 210 | Manifest.permission.ACCESS_COARSE_LOCATION, 211 | Manifest.permission.ACCESS_FINE_LOCATION 212 | ) 213 | ) { 214 | val permissionState = rememberMultiplePermissionsState(permissions = permissionList) 215 | 216 | LaunchedEffect(Unit) { 217 | if (permissionState.allPermissionsGranted) { 218 | viewModel.handleIntent(HomeContract.Intent.LoadVisitedRestaurants) 219 | viewModel.handleIntent(HomeContract.Intent.LoadNearbyRestaurants) 220 | } else { 221 | navController.navigate(DashboardRoute.PERMISSION_SCREEN.route) 222 | } 223 | } 224 | } 225 | 226 | @Composable 227 | fun HandleSideEffects( 228 | viewModel: HomeViewModel, 229 | onNavigateToMaps: (PlaceDetails) -> Unit 230 | ) { 231 | LaunchedEffect(Unit) { 232 | viewModel.uiEffect.collect { effect -> 233 | when (effect) { 234 | is HomeContract.Effect.NavigateToMaps -> { 235 | onNavigateToMaps(effect.placeDetails) 236 | } 237 | } 238 | } 239 | } 240 | } 241 | 242 | @Preview( 243 | name = "Light Mode", 244 | showBackground = true, 245 | uiMode = UI_MODE_NIGHT_NO 246 | ) 247 | @Preview( 248 | name = "Dark Mode", 249 | showBackground = true, 250 | uiMode = UI_MODE_NIGHT_YES 251 | ) 252 | @Composable 253 | fun HomeScreenPreview() { 254 | 255 | val uiState = HomeContract.HomeUiState( 256 | isLoading = false, 257 | currentLocation = LatLng( 0.0, 0.0), 258 | error = null, 259 | nearbyRestaurants = listOf( 260 | PlaceMap( 261 | name= "Place Name", 262 | vicinity= "Address 123", 263 | rating = 3.4, 264 | geometry= Geometry(LocationMap(latitude = 0.0, longitude = 0.0)), 265 | ) 266 | ), 267 | visitedRestaurants = listOf( 268 | PlaceDetails( 269 | "", 270 | "Place Name", 271 | LatLng(0.0, 0.0), 272 | LatLng(0.0, 0.0), 273 | true, 274 | 4.5F 275 | ) 276 | ) 277 | ) 278 | BiteMapTheme { 279 | ScreenUiContent( 280 | uiState = uiState, 281 | onSearch = {}, 282 | onRestaurantItemClick = {} 283 | ) 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/dashboard/presentation/screens/home/HomeViewModel.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.dashboard.presentation.screens.home 2 | 3 | import androidx.lifecycle.viewModelScope 4 | import com.google.android.gms.maps.model.LatLng 5 | import dagger.hilt.android.lifecycle.HiltViewModel 6 | import kotlinx.coroutines.flow.collectLatest 7 | import kotlinx.coroutines.launch 8 | import novalogics.android.bitemap.core.base.BaseViewModel 9 | import novalogics.android.bitemap.core.navigation.events.UiEvent 10 | import novalogics.android.bitemap.location.domain.model.PlaceDetails 11 | import novalogics.android.bitemap.location.domain.usecase.GetAllPlacesFromDbUseCase 12 | import novalogics.android.bitemap.dashboard.domain.usecase.GetNearByPlacesUseCase 13 | import javax.inject.Inject 14 | 15 | @HiltViewModel 16 | class HomeViewModel @Inject constructor( 17 | private val getAllPlacesFromDbUseCase: GetAllPlacesFromDbUseCase, 18 | private val getNearByPlacesUseCase: GetNearByPlacesUseCase 19 | ) : BaseViewModel( 20 | HomeContract.HomeUiState() 21 | ) { 22 | override fun handleIntent(intent: HomeContract.Intent) { 23 | when (intent) { 24 | is HomeContract.Intent.LoadNearbyRestaurants -> { 25 | loadNearbyRestaurants() 26 | } 27 | 28 | is HomeContract.Intent.LoadVisitedRestaurants -> { 29 | loadVisitedRestaurants() 30 | } 31 | 32 | is HomeContract.Intent.OnItemClick -> { 33 | handleItemClick(intent.placeDetails) 34 | } 35 | } 36 | } 37 | 38 | private fun loadVisitedRestaurants() { 39 | viewModelScope.launch { 40 | try { 41 | getAllPlacesFromDbUseCase().collect { restaurants -> 42 | updateState { copy(visitedRestaurants = restaurants) } 43 | } 44 | } catch (exception: Exception) { 45 | handleException(exception) 46 | } 47 | } 48 | } 49 | 50 | private fun loadNearbyRestaurants() { 51 | updateState { copy(isLoading = true) } 52 | viewModelScope.launch { 53 | try { 54 | getNearByPlacesUseCase(1000).collectLatest { 55 | when (it) { 56 | is UiEvent.Loading -> {} 57 | is UiEvent.Error -> { 58 | updateState { copy(isLoading = false, error = it.message ?: "") } 59 | } 60 | 61 | is UiEvent.Success -> { 62 | updateState { 63 | copy( 64 | isLoading = false, 65 | nearbyRestaurants = it.data?.results ?: emptyList(), 66 | currentLocation = LatLng( 67 | it.data?.currentLocationMap?.latitude ?: 0.0, 68 | it.data?.currentLocationMap?.longitude ?: 0.0, 69 | ) 70 | ) 71 | } 72 | } 73 | } 74 | } 75 | } catch (exception: Exception) { 76 | handleException(exception) 77 | } 78 | } 79 | } 80 | 81 | private fun handleItemClick(placeDetails: PlaceDetails) { 82 | viewModelScope.launch { 83 | sendEffect { HomeContract.Effect.NavigateToMaps(placeDetails) } 84 | } 85 | } 86 | 87 | private fun handleException(exception: Exception?) { 88 | val errorMessage = exception?.message ?: "Unexpected Error" 89 | updateState { copy(isLoading = false, error = errorMessage) } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/dashboard/presentation/screens/home/component/LocationListItem.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.dashboard.presentation.screens.home.component 2 | 3 | import android.content.res.Configuration.UI_MODE_NIGHT_NO 4 | import android.content.res.Configuration.UI_MODE_NIGHT_YES 5 | import androidx.compose.foundation.background 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.Row 8 | import androidx.compose.foundation.layout.Spacer 9 | import androidx.compose.foundation.layout.fillMaxWidth 10 | import androidx.compose.foundation.layout.height 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.foundation.layout.size 13 | import androidx.compose.foundation.layout.width 14 | import androidx.compose.material.icons.Icons 15 | import androidx.compose.material.icons.filled.Check 16 | import androidx.compose.material.icons.filled.CheckCircle 17 | import androidx.compose.material.icons.filled.Close 18 | import androidx.compose.material.icons.filled.LocationOn 19 | import androidx.compose.material.icons.filled.Place 20 | import androidx.compose.material.icons.filled.Star 21 | import androidx.compose.material3.Icon 22 | import androidx.compose.material3.MaterialTheme 23 | import androidx.compose.material3.Text 24 | import androidx.compose.runtime.Composable 25 | import androidx.compose.ui.Alignment 26 | import androidx.compose.ui.Modifier 27 | import androidx.compose.ui.tooling.preview.Preview 28 | import androidx.compose.ui.unit.dp 29 | import androidx.compose.ui.unit.sp 30 | import com.google.android.gms.maps.model.LatLng 31 | import novalogics.android.bitemap.app.ui.theme.BiteMapTheme 32 | import novalogics.android.bitemap.location.domain.model.PlaceDetails 33 | 34 | @Composable 35 | fun LocationListItem(it: PlaceDetails) { 36 | Column( 37 | modifier = Modifier 38 | .fillMaxWidth() 39 | .padding(16.dp) 40 | .background( 41 | color = MaterialTheme.colorScheme.surfaceVariant, 42 | shape = MaterialTheme.shapes.medium 43 | ) 44 | .padding(16.dp) 45 | ) { 46 | // Name 47 | Row( 48 | verticalAlignment = Alignment.CenterVertically, 49 | modifier = Modifier.fillMaxWidth() 50 | ) { 51 | Icon( 52 | imageVector = Icons.Default.CheckCircle, 53 | contentDescription = "Name", 54 | tint = MaterialTheme.colorScheme.primary, 55 | modifier = Modifier.size(20.dp) 56 | ) 57 | Spacer(modifier = Modifier.width(8.dp)) 58 | Text( 59 | text = "Name: ${it.name}", 60 | color = MaterialTheme.colorScheme.onSurface, 61 | fontSize = 16.sp 62 | ) 63 | } 64 | Spacer(modifier = Modifier.height(4.dp)) 65 | 66 | // Rating 67 | Row( 68 | verticalAlignment = Alignment.CenterVertically, 69 | modifier = Modifier.fillMaxWidth() 70 | ) { 71 | Icon( 72 | imageVector = Icons.Default.Star, 73 | contentDescription = "Rating", 74 | tint = MaterialTheme.colorScheme.primary, 75 | modifier = Modifier.size(20.dp) 76 | ) 77 | Spacer(modifier = Modifier.width(8.dp)) 78 | Text( 79 | text = "Rating: ${it.rating}/5", 80 | color = MaterialTheme.colorScheme.onSurface, 81 | fontSize = 16.sp 82 | ) 83 | } 84 | Spacer(modifier = Modifier.height(4.dp)) 85 | 86 | // Delivery Availability 87 | Row( 88 | verticalAlignment = Alignment.CenterVertically, 89 | modifier = Modifier.fillMaxWidth() 90 | ) { 91 | Icon( 92 | imageVector = if (it.delivery) Icons.Default.Check else Icons.Default.Close, 93 | contentDescription = "Delivery Availability", 94 | tint = if (it.delivery) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error, 95 | modifier = Modifier.size(20.dp) 96 | ) 97 | Spacer(modifier = Modifier.width(8.dp)) 98 | Text( 99 | text = if (it.delivery) "Delivery is available" else "Delivery is not available", 100 | color = if (it.delivery) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.error, 101 | fontSize = 16.sp 102 | ) 103 | } 104 | Spacer(modifier = Modifier.height(4.dp)) 105 | // Start Location 106 | Row( 107 | verticalAlignment = Alignment.CenterVertically, 108 | modifier = Modifier.fillMaxWidth() 109 | ) { 110 | Icon( 111 | imageVector = Icons.Default.LocationOn, 112 | contentDescription = "Start Location", 113 | tint = MaterialTheme.colorScheme.primary, 114 | modifier = Modifier.size(20.dp) 115 | ) 116 | Spacer(modifier = Modifier.width(8.dp)) 117 | Text( 118 | text = "Start: ${it.origin.latitude}, ${it.origin.longitude}", 119 | color = MaterialTheme.colorScheme.onSurface, 120 | fontSize = 16.sp 121 | ) 122 | } 123 | Spacer(modifier = Modifier.height(4.dp)) 124 | 125 | // Destination Location 126 | Row( 127 | verticalAlignment = Alignment.CenterVertically, 128 | modifier = Modifier.fillMaxWidth() 129 | ) { 130 | Icon( 131 | imageVector = Icons.Default.Place, 132 | contentDescription = "Destination Location", 133 | tint = MaterialTheme.colorScheme.primary, 134 | modifier = Modifier.size(20.dp) 135 | ) 136 | Spacer(modifier = Modifier.width(4.dp)) 137 | Text( 138 | text = "Destination: ${it.destination.latitude}, ${it.destination.longitude}", 139 | color = MaterialTheme.colorScheme.onSurface, 140 | fontSize = 16.sp 141 | ) 142 | } 143 | } 144 | } 145 | 146 | 147 | @Preview( 148 | name = "Light Mode", 149 | showBackground = true, 150 | uiMode = UI_MODE_NIGHT_NO 151 | ) 152 | @Preview( 153 | name = "Dark Mode", 154 | showBackground = true, 155 | uiMode = UI_MODE_NIGHT_YES 156 | ) 157 | @Composable 158 | fun LocationListItemPreview() { 159 | val samplePlaceDetails = PlaceDetails( 160 | placeId = "123", 161 | name = "Sample Place", 162 | destination = LatLng( 37.7749, -122.4194), 163 | origin = LatLng( 34.0522, -118.2437), 164 | delivery = true, 165 | rating = 4.5f 166 | ) 167 | BiteMapTheme { 168 | LocationListItem(it = samplePlaceDetails) 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/dashboard/presentation/screens/home/component/RestaurantItem.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.dashboard.presentation.screens.home.component 2 | 3 | import android.content.res.Configuration.UI_MODE_NIGHT_NO 4 | import android.content.res.Configuration.UI_MODE_NIGHT_YES 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.Row 8 | import androidx.compose.foundation.layout.Spacer 9 | import androidx.compose.foundation.layout.fillMaxWidth 10 | import androidx.compose.foundation.layout.height 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.foundation.layout.size 13 | import androidx.compose.foundation.layout.width 14 | import androidx.compose.foundation.shape.RoundedCornerShape 15 | import androidx.compose.material.icons.Icons 16 | import androidx.compose.material.icons.filled.LocationOn 17 | import androidx.compose.material.icons.filled.Star 18 | import androidx.compose.material3.Card 19 | import androidx.compose.material3.CardDefaults 20 | import androidx.compose.material3.Icon 21 | import androidx.compose.material3.MaterialTheme 22 | import androidx.compose.material3.Text 23 | import androidx.compose.runtime.Composable 24 | import androidx.compose.ui.Alignment 25 | import androidx.compose.ui.Modifier 26 | import androidx.compose.ui.graphics.Color 27 | import androidx.compose.ui.res.painterResource 28 | import androidx.compose.ui.text.font.FontWeight 29 | import androidx.compose.ui.tooling.preview.Preview 30 | import androidx.compose.ui.unit.dp 31 | import novalogics.android.bitemap.R 32 | import novalogics.android.bitemap.app.ui.theme.BiteMapTheme 33 | import novalogics.android.bitemap.dashboard.data.model.Geometry 34 | import novalogics.android.bitemap.dashboard.data.model.LocationMap 35 | import novalogics.android.bitemap.dashboard.data.model.PlaceMap 36 | 37 | @Composable 38 | fun RestaurantItem(restaurant: PlaceMap, onClick: (PlaceMap) -> Unit) { 39 | Card( 40 | modifier = Modifier 41 | .fillMaxWidth() 42 | .padding(8.dp) 43 | .clickable { onClick(restaurant) }, 44 | elevation = CardDefaults.cardElevation(4.dp), 45 | shape = RoundedCornerShape(12.dp), 46 | colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface) 47 | ) { 48 | Row( 49 | modifier = Modifier.padding(16.dp), 50 | verticalAlignment = Alignment.CenterVertically 51 | ) { 52 | Icon( 53 | painter = painterResource(id = R.drawable.ic_restaurant_menu), 54 | contentDescription = "Restaurant Icon", 55 | tint = MaterialTheme.colorScheme.primary, 56 | modifier = Modifier.size(36.dp) 57 | ) 58 | 59 | Spacer(modifier = Modifier.width(12.dp)) 60 | 61 | Column { 62 | Text( 63 | text = restaurant.name, 64 | style = MaterialTheme.typography.displayMedium, 65 | fontWeight = FontWeight.Bold, 66 | color = MaterialTheme.colorScheme.onSurface 67 | ) 68 | Spacer(modifier = Modifier.height(4.dp)) 69 | Row(verticalAlignment = Alignment.CenterVertically) { 70 | Icon( 71 | imageVector = Icons.Default.LocationOn, 72 | contentDescription = "Location Icon", 73 | tint = MaterialTheme.colorScheme.secondary, 74 | modifier = Modifier.size(16.dp) 75 | ) 76 | Spacer(modifier = Modifier.width(4.dp)) 77 | Text( 78 | text = restaurant.vicinity, 79 | style = MaterialTheme.typography.bodyMedium, 80 | color = MaterialTheme.colorScheme.onSurfaceVariant 81 | ) 82 | } 83 | Spacer(modifier = Modifier.height(4.dp)) 84 | Row(verticalAlignment = Alignment.CenterVertically) { 85 | Icon( 86 | imageVector = Icons.Default.Star, 87 | contentDescription = "Rating Icon", 88 | tint = Color(0xFFFFC107), 89 | modifier = Modifier.size(16.dp) 90 | ) 91 | Spacer(modifier = Modifier.width(4.dp)) 92 | Text( 93 | text = "${restaurant.rating}", 94 | style = MaterialTheme.typography.bodyMedium, 95 | color = MaterialTheme.colorScheme.onSurface 96 | ) 97 | } 98 | } 99 | } 100 | } 101 | } 102 | 103 | 104 | @Preview( 105 | name = "Light Mode", 106 | showBackground = true, 107 | uiMode = UI_MODE_NIGHT_NO 108 | ) 109 | @Preview( 110 | name = "Dark Mode", 111 | showBackground = true, 112 | uiMode = UI_MODE_NIGHT_YES 113 | ) 114 | @Composable 115 | fun RestaurantItemPreview() { 116 | val samplePlaceMapDetails = PlaceMap( 117 | name= "Place Name", 118 | vicinity= "Address 123", 119 | rating = 3.4, 120 | geometry= Geometry(LocationMap(latitude = 0.0, longitude = 0.0)), 121 | ) 122 | BiteMapTheme { 123 | RestaurantItem(restaurant = samplePlaceMapDetails, onClick = {}) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/dashboard/presentation/screens/permission/PermissionScreen.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.dashboard.presentation.screens.permission 2 | 3 | import android.Manifest 4 | import android.content.res.Configuration.UI_MODE_NIGHT_NO 5 | import android.content.res.Configuration.UI_MODE_NIGHT_YES 6 | import androidx.activity.ComponentActivity 7 | import androidx.activity.compose.rememberLauncherForActivityResult 8 | import androidx.activity.result.contract.ActivityResultContracts 9 | import androidx.compose.foundation.Image 10 | import androidx.compose.foundation.background 11 | import androidx.compose.foundation.layout.Column 12 | import androidx.compose.foundation.layout.Spacer 13 | import androidx.compose.foundation.layout.fillMaxSize 14 | import androidx.compose.foundation.layout.fillMaxWidth 15 | import androidx.compose.foundation.layout.height 16 | import androidx.compose.foundation.layout.padding 17 | import androidx.compose.foundation.layout.size 18 | import androidx.compose.foundation.shape.RoundedCornerShape 19 | import androidx.compose.material3.Button 20 | import androidx.compose.material3.ButtonDefaults 21 | import androidx.compose.material3.ExperimentalMaterial3Api 22 | import androidx.compose.material3.MaterialTheme 23 | import androidx.compose.material3.Scaffold 24 | import androidx.compose.material3.Text 25 | import androidx.compose.material3.TopAppBar 26 | import androidx.compose.runtime.Composable 27 | import androidx.compose.runtime.LaunchedEffect 28 | import androidx.compose.ui.Alignment 29 | import androidx.compose.ui.Modifier 30 | import androidx.compose.ui.res.painterResource 31 | import androidx.compose.ui.res.stringResource 32 | import androidx.compose.ui.text.font.FontWeight 33 | import androidx.compose.ui.text.style.TextAlign 34 | import androidx.compose.ui.tooling.preview.Preview 35 | import androidx.compose.ui.unit.dp 36 | import androidx.compose.ui.unit.sp 37 | import androidx.core.app.ActivityCompat.finishAffinity 38 | import androidx.navigation.NavHostController 39 | import com.google.accompanist.permissions.ExperimentalPermissionsApi 40 | import com.google.accompanist.permissions.rememberMultiplePermissionsState 41 | import novalogics.android.bitemap.R 42 | import novalogics.android.bitemap.app.ui.theme.BiteMapTheme 43 | import novalogics.android.bitemap.core.navigation.DashboardRoute 44 | 45 | @OptIn(ExperimentalPermissionsApi::class) 46 | @Composable 47 | fun PermissionScreen(navController: NavHostController) { 48 | 49 | val permissionState = rememberMultiplePermissionsState( 50 | permissions = listOf( 51 | Manifest.permission.ACCESS_COARSE_LOCATION, 52 | Manifest.permission.ACCESS_FINE_LOCATION 53 | ) 54 | ) 55 | 56 | val launcher = rememberLauncherForActivityResult( 57 | contract = ActivityResultContracts.RequestMultiplePermissions() 58 | ) { permissions -> 59 | if (permissions.values.all { it }) { 60 | navController.navigate(DashboardRoute.HOME_SCREEN.route) 61 | } else { 62 | finishAffinity(navController.context as ComponentActivity) 63 | } 64 | } 65 | 66 | LaunchedEffect(Unit) { 67 | if (permissionState.allPermissionsGranted) { 68 | navController.navigate(DashboardRoute.HOME_SCREEN.route) 69 | } else { 70 | launcher.launch(permissionState.permissions.map { it.permission }.toTypedArray()) 71 | } 72 | } 73 | 74 | ScreenUiContent( 75 | onShowPermissions = { 76 | launcher.launch( 77 | permissionState.permissions.map { it.permission }.toTypedArray() 78 | ) 79 | } 80 | ) 81 | } 82 | 83 | @OptIn(ExperimentalMaterial3Api::class) 84 | @Composable 85 | fun ScreenUiContent( 86 | onShowPermissions:()-> Unit 87 | ){ 88 | Scaffold( 89 | topBar = { 90 | TopAppBar( 91 | title = { 92 | Text( 93 | text = stringResource(id = R.string.app_name), 94 | color = MaterialTheme.colorScheme.onBackground, 95 | fontSize = 24.sp, 96 | fontWeight = FontWeight.Bold, 97 | style = MaterialTheme.typography.displayMedium, 98 | modifier = Modifier 99 | .fillMaxWidth() 100 | .padding(4.dp), 101 | textAlign = TextAlign.Center, 102 | ) 103 | }, 104 | modifier = Modifier 105 | .height(56.dp) 106 | .background(MaterialTheme.colorScheme.background) 107 | ) 108 | } 109 | ) { innerPadding -> 110 | Column( 111 | modifier = Modifier 112 | .fillMaxSize() 113 | .padding(innerPadding), 114 | horizontalAlignment = Alignment.CenterHorizontally, 115 | ) { 116 | Text( 117 | text = stringResource(id = R.string.checking_permissions), 118 | color = MaterialTheme.colorScheme.onSurface, 119 | fontSize = 18.sp, 120 | textAlign = TextAlign.Center, 121 | modifier = Modifier.padding(12.dp) 122 | ) 123 | Spacer(modifier = Modifier.height(16.dp)) 124 | Image( 125 | painter = painterResource(id = R.drawable.bitemap_logo_main), 126 | contentDescription = "BiteMap Logo", 127 | modifier = Modifier 128 | .size(180.dp) 129 | .align(Alignment.CenterHorizontally) 130 | ) 131 | Spacer(modifier = Modifier.height(24.dp)) 132 | Button( 133 | onClick = onShowPermissions, 134 | colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primaryContainer), 135 | shape = RoundedCornerShape(16.dp), 136 | modifier = Modifier.padding(16.dp) 137 | ) { 138 | Text( 139 | text = "Allow Permissions", 140 | color = MaterialTheme.colorScheme.onBackground, 141 | fontSize = 18.sp, 142 | modifier = Modifier.padding(4.dp) 143 | ) 144 | } 145 | } 146 | } 147 | } 148 | 149 | @Preview( 150 | name = "Light Mode", 151 | showBackground = true, 152 | uiMode = UI_MODE_NIGHT_NO 153 | ) 154 | @Preview( 155 | name = "Dark Mode", 156 | showBackground = true, 157 | uiMode = UI_MODE_NIGHT_YES 158 | ) 159 | @Composable 160 | fun PermissionScreenPreview() { 161 | BiteMapTheme { 162 | ScreenUiContent( 163 | onShowPermissions = {} 164 | ) 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/location/data/datasource/network/LocationService.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.location.data.datasource.network 2 | 3 | import novalogics.android.bitemap.location.data.model.DirectionApiResponse 4 | import retrofit2.http.GET 5 | import retrofit2.http.Query 6 | 7 | interface LocationService { 8 | 9 | @GET("maps/api/directions/json") 10 | suspend fun getDirection( 11 | @Query("origin") origin:String, 12 | @Query("destination") destination:String, 13 | @Query("key") key:String 14 | ): DirectionApiResponse 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/location/data/di/DataModule.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.location.data.di 2 | 3 | import android.content.Context 4 | import com.google.android.gms.location.FusedLocationProviderClient 5 | import com.google.android.gms.location.LocationServices 6 | import com.google.android.libraries.places.api.Places 7 | import com.google.android.libraries.places.api.net.PlacesClient 8 | import dagger.Module 9 | import dagger.Provides 10 | import dagger.hilt.InstallIn 11 | import dagger.hilt.android.qualifiers.ApplicationContext 12 | import dagger.hilt.components.SingletonComponent 13 | import novalogics.android.bitemap.core.di.qualifier.LocationRetrofit 14 | import novalogics.android.bitemap.core.network.ApiConfig 15 | import novalogics.android.bitemap.location.data.datasource.network.LocationService 16 | import novalogics.android.bitemap.location.data.repository.LocationRepositoryImpl 17 | import novalogics.android.bitemap.location.domain.repository.LocationRepository 18 | import retrofit2.Retrofit 19 | import javax.inject.Singleton 20 | 21 | @Module 22 | @InstallIn(SingletonComponent::class) 23 | object DataModule { 24 | 25 | @Singleton 26 | @Provides 27 | fun provideFusedLocationProviderClient( 28 | @ApplicationContext context: Context 29 | ): FusedLocationProviderClient 30 | = LocationServices.getFusedLocationProviderClient(context) 31 | 32 | @Singleton 33 | @Provides 34 | fun providePlacesClient( 35 | @ApplicationContext context: Context 36 | ): PlacesClient { 37 | Places.initialize(context, ApiConfig.PLACES_API_KEY) 38 | return Places.createClient(context) 39 | } 40 | 41 | @Singleton 42 | @Provides 43 | fun provideLocationRepository( 44 | fusedLocationProviderClient: FusedLocationProviderClient, 45 | placesClient: PlacesClient, 46 | locationService: LocationService 47 | ): LocationRepository = LocationRepositoryImpl( 48 | fusedLocationProviderClient = fusedLocationProviderClient, 49 | placesClient = placesClient, 50 | locationService = locationService, 51 | ) 52 | 53 | @Provides 54 | fun provideLocationService(@LocationRetrofit retrofit: Retrofit): LocationService{ 55 | return retrofit.create(LocationService::class.java) 56 | } 57 | } 58 | 59 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/location/data/mapper/Mapper.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.location.data.mapper 2 | 3 | import com.google.android.gms.maps.model.LatLng 4 | import com.google.android.libraries.places.api.model.Place 5 | import com.google.maps.android.PolyUtil 6 | import novalogics.android.bitemap.location.data.model.DirectionApiResponse 7 | import novalogics.android.bitemap.location.domain.model.DirectionDetails 8 | import novalogics.android.bitemap.location.domain.model.PlaceDetails 9 | 10 | fun Place.toDomain(placeId: String?) : PlaceDetails{ 11 | return PlaceDetails( 12 | placeId = placeId.orEmpty(), 13 | name = this.name.orEmpty(), 14 | destination = this.latLng!!, 15 | origin = LatLng(0.0,0.0), 16 | delivery = this.delivery.equals(Place.BooleanPlaceAttributeValue.TRUE), 17 | rating = this.rating?.toFloat() ?: 0F 18 | ) 19 | } 20 | 21 | fun DirectionApiResponse.toDomain(): DirectionDetails{ 22 | return DirectionDetails( 23 | points = PolyUtil.decode(this.routes[0].overviewPolyline!!.points), 24 | distance = this.routes[0].legs[0].distance.toString(), 25 | duration = this.routes[0].legs[0].duration.toString(), 26 | html = this.routes[0].legs[0].steps[0].htmlInstructions!!, 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/location/data/model/Bounds.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.location.data.model 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | 6 | data class Bounds ( 7 | 8 | @SerializedName("northeast" ) var northeast : Northeast? = Northeast(), 9 | @SerializedName("southwest" ) var southwest : Southwest? = Southwest() 10 | 11 | ) -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/location/data/model/DirectionApiResponse.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.location.data.model 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | 6 | data class DirectionApiResponse ( 7 | 8 | @SerializedName("geocoded_waypoints" ) var geocodedWaypoints : ArrayList = arrayListOf(), 9 | @SerializedName("routes" ) var routes : ArrayList = arrayListOf(), 10 | @SerializedName("status" ) var status : String? = null 11 | 12 | ) -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/location/data/model/Distance.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.location.data.model 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | 6 | data class Distance ( 7 | 8 | @SerializedName("text" ) var text : String? = null, 9 | @SerializedName("value" ) var value : Int? = null 10 | 11 | ) -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/location/data/model/Duration.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.location.data.model 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | 6 | data class Duration ( 7 | 8 | @SerializedName("text" ) var text : String? = null, 9 | @SerializedName("value" ) var value : Int? = null 10 | 11 | ) -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/location/data/model/EndLocation.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.location.data.model 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | 6 | data class EndLocation ( 7 | 8 | @SerializedName("lat" ) var lat : Double? = null, 9 | @SerializedName("lng" ) var lng : Double? = null 10 | 11 | ) -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/location/data/model/GeocodedWaypoints.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.location.data.model 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | 6 | data class GeocodedWaypoints ( 7 | 8 | @SerializedName("geocoder_status" ) var geocoderStatus : String? = null, 9 | @SerializedName("place_id" ) var placeId : String? = null, 10 | @SerializedName("types" ) var types : ArrayList = arrayListOf() 11 | 12 | ) -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/location/data/model/Legs.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.location.data.model 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | 6 | data class Legs ( 7 | 8 | @SerializedName("distance" ) var distance : Distance? = Distance(), 9 | @SerializedName("duration" ) var duration : Duration? = Duration(), 10 | @SerializedName("end_address" ) var endAddress : String? = null, 11 | @SerializedName("end_location" ) var endLocation : EndLocation? = EndLocation(), 12 | @SerializedName("start_address" ) var startAddress : String? = null, 13 | @SerializedName("start_location" ) var startLocation : StartLocation? = StartLocation(), 14 | @SerializedName("steps" ) var steps : ArrayList = arrayListOf(), 15 | @SerializedName("traffic_speed_entry" ) var trafficSpeedEntry : ArrayList = arrayListOf(), 16 | @SerializedName("via_waypoint" ) var viaWaypoint : ArrayList = arrayListOf() 17 | 18 | ) -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/location/data/model/Northeast.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.location.data.model 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | 6 | data class Northeast ( 7 | 8 | @SerializedName("lat" ) var lat : Double? = null, 9 | @SerializedName("lng" ) var lng : Double? = null 10 | 11 | ) -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/location/data/model/OverviewPolyline.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.location.data.model 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | 6 | data class OverviewPolyline ( 7 | 8 | @SerializedName("points" ) var points : String? = null 9 | 10 | ) -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/location/data/model/Polyline.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.location.data.model 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | 6 | data class Polyline ( 7 | 8 | @SerializedName("points" ) var points : String? = null 9 | 10 | ) -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/location/data/model/Routes.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.location.data.model 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | 6 | data class Routes ( 7 | 8 | @SerializedName("bounds" ) var bounds : Bounds? = Bounds(), 9 | @SerializedName("copyrights" ) var copyrights : String? = null, 10 | @SerializedName("legs" ) var legs : ArrayList = arrayListOf(), 11 | @SerializedName("overview_polyline" ) var overviewPolyline : OverviewPolyline? = OverviewPolyline(), 12 | @SerializedName("summary" ) var summary : String? = null, 13 | @SerializedName("warnings" ) var warnings : ArrayList = arrayListOf(), 14 | @SerializedName("waypoint_order" ) var waypointOrder : ArrayList = arrayListOf() 15 | 16 | ) -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/location/data/model/Southwest.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.location.data.model 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | 6 | data class Southwest ( 7 | 8 | @SerializedName("lat" ) var lat : Double? = null, 9 | @SerializedName("lng" ) var lng : Double? = null 10 | 11 | ) -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/location/data/model/StartLocation.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.location.data.model 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | 6 | data class StartLocation ( 7 | 8 | @SerializedName("lat" ) var lat : Double? = null, 9 | @SerializedName("lng" ) var lng : Double? = null 10 | 11 | ) -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/location/data/model/Steps.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.location.data.model 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | 6 | data class Steps ( 7 | 8 | @SerializedName("distance" ) var distance : Distance? = Distance(), 9 | @SerializedName("duration" ) var duration : Duration? = Duration(), 10 | @SerializedName("end_location" ) var endLocation : EndLocation? = EndLocation(), 11 | @SerializedName("html_instructions" ) var htmlInstructions : String? = null, 12 | @SerializedName("polyline" ) var polyline : Polyline? = Polyline(), 13 | @SerializedName("start_location" ) var startLocation : StartLocation? = StartLocation(), 14 | @SerializedName("travel_mode" ) var travelMode : String? = null 15 | 16 | ) -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/location/data/repository/LocationRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.location.data.repository 2 | 3 | import android.annotation.SuppressLint 4 | import android.location.Location 5 | import android.util.Log 6 | import com.google.android.gms.location.FusedLocationProviderClient 7 | import com.google.android.gms.location.LocationCallback 8 | import com.google.android.gms.location.LocationRequest 9 | import com.google.android.gms.location.LocationResult 10 | import com.google.android.gms.location.Priority 11 | import com.google.android.gms.maps.model.LatLng 12 | import com.google.android.libraries.places.api.model.AutocompleteSessionToken 13 | import com.google.android.libraries.places.api.model.LocationRestriction 14 | import com.google.android.libraries.places.api.model.Place 15 | import com.google.android.libraries.places.api.model.RectangularBounds 16 | import com.google.android.libraries.places.api.net.FetchPlaceRequest 17 | import com.google.android.libraries.places.api.net.FindAutocompletePredictionsRequest 18 | import com.google.android.libraries.places.api.net.PlacesClient 19 | import com.google.gson.Gson 20 | import kotlinx.coroutines.channels.awaitClose 21 | import kotlinx.coroutines.flow.Flow 22 | import kotlinx.coroutines.flow.callbackFlow 23 | import novalogics.android.bitemap.core.navigation.events.LocationEvent 24 | import novalogics.android.bitemap.core.navigation.events.PlacesResult 25 | import novalogics.android.bitemap.core.network.ApiConfig 26 | import novalogics.android.bitemap.location.data.datasource.network.LocationService 27 | import novalogics.android.bitemap.location.data.mapper.toDomain 28 | import novalogics.android.bitemap.location.domain.model.DirectionDetails 29 | import novalogics.android.bitemap.location.domain.model.PlaceDetails 30 | import novalogics.android.bitemap.location.domain.repository.LocationRepository 31 | import timber.log.Timber 32 | import javax.inject.Inject 33 | 34 | @SuppressLint("MissingPermission") 35 | class LocationRepositoryImpl @Inject constructor( 36 | private val fusedLocationProviderClient: FusedLocationProviderClient, 37 | private val placesClient: PlacesClient, 38 | private val locationService: LocationService, 39 | ) : LocationRepository { 40 | 41 | private var currentLocation: LatLng = LatLng(0.0, 0.0) 42 | 43 | private val token = AutocompleteSessionToken.newInstance() 44 | 45 | override fun getLocation(destination: LatLng): Flow = callbackFlow{ 46 | 47 | val locationRequest = LocationRequest 48 | .Builder(Priority.PRIORITY_HIGH_ACCURACY, 100) 49 | .setIntervalMillis(5000) 50 | .build() 51 | 52 | val locationCallback = object : LocationCallback() { 53 | override fun onLocationResult(locationResult: LocationResult) { 54 | currentLocation = LatLng( 55 | locationResult.locations[0].latitude, 56 | locationResult.locations[0].longitude 57 | ) 58 | if(isReachedToDestination(currentLocation, destination)){ 59 | trySend(LocationEvent.ReachDestination()) 60 | } else{ 61 | trySend(LocationEvent.LocationInProgress(locationResult.locations[0])) 62 | } 63 | } 64 | } 65 | fusedLocationProviderClient.requestLocationUpdates(locationRequest, locationCallback, null) 66 | 67 | awaitClose { 68 | fusedLocationProviderClient.removeLocationUpdates(locationCallback) 69 | } 70 | } 71 | 72 | private fun isReachedToDestination(origin: LatLng, destination: LatLng): Boolean { 73 | val array = FloatArray(1) 74 | 75 | Location.distanceBetween( 76 | origin.latitude, 77 | origin.longitude, 78 | destination.latitude, 79 | destination.longitude, 80 | array 81 | ) 82 | return array[0] < 5F 83 | } 84 | 85 | override fun getLocationOnce(location: (Location) -> Unit) { 86 | val locationResult = 87 | LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 100) 88 | .setIntervalMillis(1000) 89 | .setMaxUpdates(1) 90 | .build() 91 | 92 | val locationCallback = object : LocationCallback() { 93 | override fun onLocationResult(locationResult: LocationResult) { 94 | locationResult.locations[0]?.let { locationData -> 95 | currentLocation = LatLng(locationData.latitude, locationData.longitude) 96 | location.invoke(locationData) 97 | } 98 | } 99 | } 100 | fusedLocationProviderClient.requestLocationUpdates(locationResult, locationCallback, null) 101 | } 102 | 103 | override fun searchRestaurants(query: String): Flow = callbackFlow { 104 | 105 | getLocationOnce { location -> 106 | 107 | val locationRestriction = findLocationRestriction(location) 108 | 109 | val request = FindAutocompletePredictionsRequest.builder() 110 | .setSessionToken(token) 111 | .setCountries(mutableListOf("LK")) 112 | .setQuery(query) 113 | .setOrigin(LatLng(location.latitude, location.longitude)) 114 | .setTypesFilter(mutableListOf("restaurant")) 115 | .setLocationRestriction(locationRestriction) 116 | .build() 117 | 118 | placesClient.findAutocompletePredictions(request) 119 | .addOnSuccessListener { 120 | trySend(PlacesResult.Success(location, it.autocompletePredictions)) 121 | } 122 | .addOnFailureListener { 123 | trySend(PlacesResult.Error(it.message.toString())) 124 | } 125 | } 126 | awaitClose{} 127 | } 128 | 129 | 130 | override fun fetchPlace(placeId: String): Flow = callbackFlow{ 131 | val placesList = listOf( 132 | Place.Field.DELIVERY, 133 | Place.Field.LAT_LNG, 134 | Place.Field.ADDRESS, 135 | Place.Field.PHONE_NUMBER, 136 | Place.Field.NAME, 137 | Place.Field.RATING, 138 | ) 139 | val request = FetchPlaceRequest.builder(placeId, placesList).build() 140 | placesClient.fetchPlace(request) 141 | .addOnSuccessListener { trySend(it.place.toDomain(placeId)) } 142 | .addOnFailureListener { Timber.tag("LOCE").e(it.message.toString()) } 143 | 144 | awaitClose{} 145 | } 146 | 147 | override suspend fun getDirection( 148 | start: LatLng, 149 | destination: LatLng, 150 | ): DirectionDetails { 151 | val startLatLng = "${start.latitude},${start.longitude}" 152 | val destinationLatLng = "${destination.latitude},${destination.longitude}" 153 | 154 | val response = locationService.getDirection(startLatLng, destinationLatLng, ApiConfig.PLACES_API_KEY) 155 | return response.toDomain() 156 | } 157 | 158 | private fun findLocationRestriction(location: Location): LocationRestriction { 159 | return RectangularBounds.newInstance( 160 | LatLng(location.latitude - 0.9, location.longitude - 0.9), 161 | LatLng(location.latitude + 0.9, location.longitude + 0.9), 162 | ) 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/location/domain/di/DomainModule.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.location.domain.di 2 | 3 | import dagger.Module 4 | import dagger.Provides 5 | import dagger.hilt.InstallIn 6 | import dagger.hilt.components.SingletonComponent 7 | import novalogics.android.bitemap.core.network.MapsApiService 8 | import novalogics.android.bitemap.location.domain.repository.LocationRepository 9 | import novalogics.android.bitemap.location.domain.room.LocationDao 10 | import novalogics.android.bitemap.location.domain.usecase.FetchRestaurantDetailUseCase 11 | import novalogics.android.bitemap.location.domain.usecase.GetAllPlacesFromDbUseCase 12 | import novalogics.android.bitemap.location.domain.usecase.GetDirectionUseCase 13 | import novalogics.android.bitemap.location.domain.usecase.GetLocationUpdateUseCase 14 | import novalogics.android.bitemap.dashboard.domain.usecase.GetNearByPlacesUseCase 15 | import novalogics.android.bitemap.location.domain.usecase.InsertPlacesToDbUseCase 16 | import novalogics.android.bitemap.location.domain.usecase.SearchRestaurantUseCase 17 | 18 | @Module 19 | @InstallIn(SingletonComponent::class) 20 | object DomainModule { 21 | 22 | @Provides 23 | fun provideSearchRestaurantUseCase( 24 | locationRepository: LocationRepository 25 | ): SearchRestaurantUseCase = SearchRestaurantUseCase( 26 | locationRepository = locationRepository 27 | ) 28 | 29 | @Provides 30 | fun provideFetchRestaurantDetailUseCase( 31 | locationRepository: LocationRepository 32 | ): FetchRestaurantDetailUseCase = FetchRestaurantDetailUseCase( 33 | locationRepository = locationRepository 34 | ) 35 | 36 | @Provides 37 | fun provideGetDirectionUseCase( 38 | locationRepository: LocationRepository 39 | ): GetDirectionUseCase = GetDirectionUseCase( 40 | locationRepository = locationRepository 41 | ) 42 | 43 | @Provides 44 | fun provideGetLocationUpdateUseCase( 45 | locationRepository: LocationRepository 46 | ): GetLocationUpdateUseCase = GetLocationUpdateUseCase( 47 | locationRepository = locationRepository 48 | ) 49 | 50 | @Provides 51 | fun provideGetAllPlacesFromDbUseCase( 52 | locationDao: LocationDao 53 | ): GetAllPlacesFromDbUseCase = GetAllPlacesFromDbUseCase( 54 | locationDao = locationDao 55 | ) 56 | 57 | @Provides 58 | fun provideInsertPlacesToDbUseCase( 59 | locationDao: LocationDao 60 | ): InsertPlacesToDbUseCase = InsertPlacesToDbUseCase( 61 | locationDao = locationDao 62 | ) 63 | 64 | 65 | } 66 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/location/domain/model/DirectionDetails.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.location.domain.model 2 | 3 | import com.google.android.gms.maps.model.LatLng 4 | 5 | data class DirectionDetails( 6 | val points: MutableList = mutableListOf(), 7 | val distance: String="", 8 | val duration: String="", 9 | val html: String="" 10 | ) 11 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/location/domain/model/PlaceDetails.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.location.domain.model 2 | 3 | import android.os.Parcelable 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | import androidx.room.TypeConverter 7 | import com.google.android.gms.maps.model.LatLng 8 | import com.google.gson.Gson 9 | import kotlinx.parcelize.Parcelize 10 | 11 | @Parcelize 12 | @Entity 13 | data class PlaceDetails( 14 | @PrimaryKey(autoGenerate = false) 15 | val placeId: String, 16 | val name: String, 17 | val destination: LatLng, 18 | var origin: LatLng, 19 | val delivery: Boolean, 20 | val rating: Float, 21 | ) : Parcelable 22 | 23 | class LatLngTypeConverters{ 24 | @TypeConverter 25 | fun stringToLatLng(value:String): LatLng = Gson().fromJson(value, LatLng::class.java) 26 | 27 | @TypeConverter 28 | fun latLngToString(latLng: LatLng): String = Gson().toJson(latLng) 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/location/domain/repository/LocationRepository.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.location.domain.repository 2 | 3 | import android.location.Location 4 | import com.google.android.gms.maps.model.LatLng 5 | import kotlinx.coroutines.flow.Flow 6 | import novalogics.android.bitemap.core.navigation.events.LocationEvent 7 | import novalogics.android.bitemap.core.navigation.events.PlacesResult 8 | import novalogics.android.bitemap.location.domain.model.DirectionDetails 9 | import novalogics.android.bitemap.location.domain.model.PlaceDetails 10 | 11 | interface LocationRepository { 12 | 13 | fun getLocation(destination: LatLng): Flow 14 | 15 | fun getLocationOnce(location: (Location) -> Unit) 16 | 17 | fun searchRestaurants(query: String): Flow 18 | 19 | fun fetchPlace(placeId : String) : Flow 20 | 21 | suspend fun getDirection(start: LatLng, destination:LatLng): DirectionDetails 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/location/domain/room/LocationDao.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.location.domain.room 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.OnConflictStrategy 6 | import androidx.room.Query 7 | import kotlinx.coroutines.flow.Flow 8 | import novalogics.android.bitemap.location.domain.model.PlaceDetails 9 | 10 | @Dao 11 | interface LocationDao { 12 | @Insert(onConflict = OnConflictStrategy.REPLACE) 13 | suspend fun insert(placeDetails: PlaceDetails) 14 | 15 | @Query("SELECT * FROM PlaceDetails") 16 | fun getAllPlaceDetails(): Flow> 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/location/domain/usecase/FetchRestaurantDetailUseCase.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.location.domain.usecase 2 | 3 | import novalogics.android.bitemap.location.domain.repository.LocationRepository 4 | import javax.inject.Inject 5 | 6 | class FetchRestaurantDetailUseCase @Inject constructor( 7 | private val locationRepository: LocationRepository 8 | ){ 9 | operator fun invoke(placeId:String) = locationRepository.fetchPlace(placeId = placeId) 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/location/domain/usecase/GetAllPlacesFromDbUseCase.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.location.domain.usecase 2 | 3 | import novalogics.android.bitemap.location.domain.room.LocationDao 4 | import javax.inject.Inject 5 | 6 | class GetAllPlacesFromDbUseCase @Inject constructor( 7 | private val locationDao: LocationDao 8 | ){ 9 | operator fun invoke() = locationDao.getAllPlaceDetails() 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/location/domain/usecase/GetDirectionUseCase.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.location.domain.usecase 2 | 3 | import com.google.android.gms.maps.model.LatLng 4 | import kotlinx.coroutines.Dispatchers 5 | import kotlinx.coroutines.flow.catch 6 | import kotlinx.coroutines.flow.flow 7 | import kotlinx.coroutines.flow.flowOn 8 | import novalogics.android.bitemap.core.navigation.events.UiEvent 9 | import novalogics.android.bitemap.location.domain.model.DirectionDetails 10 | import novalogics.android.bitemap.location.domain.repository.LocationRepository 11 | import javax.inject.Inject 12 | 13 | class GetDirectionUseCase @Inject constructor( 14 | private val locationRepository: LocationRepository 15 | ){ 16 | operator fun invoke( 17 | start: LatLng, 18 | destination: LatLng, 19 | ) = flow> { 20 | emit(UiEvent.Loading()) 21 | emit(UiEvent.Success(data = locationRepository.getDirection(start,destination))) 22 | }.catch { 23 | emit(UiEvent.Error(message = it.message.toString())) 24 | }.flowOn(Dispatchers.IO) 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/location/domain/usecase/GetLocationUpdateUseCase.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.location.domain.usecase 2 | 3 | import com.google.android.gms.maps.model.LatLng 4 | import novalogics.android.bitemap.location.domain.repository.LocationRepository 5 | import javax.inject.Inject 6 | 7 | class GetLocationUpdateUseCase @Inject constructor( 8 | private val locationRepository: LocationRepository 9 | ){ 10 | operator fun invoke(destination: LatLng) = locationRepository.getLocation(destination) 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/location/domain/usecase/InsertPlacesToDbUseCase.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.location.domain.usecase 2 | 3 | import novalogics.android.bitemap.location.domain.model.PlaceDetails 4 | import novalogics.android.bitemap.location.domain.room.LocationDao 5 | import javax.inject.Inject 6 | 7 | class InsertPlacesToDbUseCase @Inject constructor( 8 | private val locationDao: LocationDao 9 | ){ 10 | suspend operator fun invoke(placeDetails: PlaceDetails) = locationDao.insert(placeDetails) 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/location/domain/usecase/SearchRestaurantUseCase.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.location.domain.usecase 2 | 3 | import novalogics.android.bitemap.location.domain.repository.LocationRepository 4 | import javax.inject.Inject 5 | 6 | class SearchRestaurantUseCase @Inject constructor( 7 | private val locationRepository: LocationRepository 8 | ){ 9 | operator fun invoke (query: String) = locationRepository.searchRestaurants(query = query) 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/location/presentation/di/UiModule.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.location.presentation.di 2 | 3 | import dagger.Module 4 | import dagger.Provides 5 | import dagger.hilt.InstallIn 6 | import dagger.hilt.components.SingletonComponent 7 | import novalogics.android.bitemap.location.presentation.navigation.LocationNavigationApi 8 | import novalogics.android.bitemap.location.presentation.navigation.LocationNavigationApiImpl 9 | 10 | @Module 11 | @InstallIn(SingletonComponent::class) 12 | object UiModule { 13 | 14 | @Provides 15 | fun provideLocationFeatureApi(): LocationNavigationApi = LocationNavigationApiImpl() 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/location/presentation/navigation/LocationNavigationApi.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.location.presentation.navigation 2 | 3 | import androidx.navigation.NavGraphBuilder 4 | import androidx.navigation.NavHostController 5 | import novalogics.android.bitemap.core.navigation.FeatureNavigationApi 6 | 7 | interface LocationNavigationApi : FeatureNavigationApi 8 | 9 | class LocationNavigationApiImpl : LocationNavigationApi { 10 | override fun registerNavigationGraph( 11 | navController: NavHostController, 12 | navGraphBuilder: NavGraphBuilder 13 | ) { 14 | LocationNavigationGraph.registerNavigationGraph( 15 | navController = navController, 16 | navGraphBuilder = navGraphBuilder 17 | ) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/location/presentation/navigation/LocationNavigationGraph.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.location.presentation.navigation 2 | 3 | import androidx.navigation.NavGraphBuilder 4 | import androidx.navigation.NavHostController 5 | import androidx.navigation.compose.composable 6 | import androidx.navigation.navigation 7 | import novalogics.android.bitemap.core.navigation.FeatureNavigationApi 8 | import novalogics.android.bitemap.core.navigation.LocationRoute 9 | import novalogics.android.bitemap.core.navigation.NavigationRoute 10 | import novalogics.android.bitemap.location.domain.model.PlaceDetails 11 | import novalogics.android.bitemap.location.presentation.screens.googlemaps.GoogleMapScreen 12 | import novalogics.android.bitemap.location.presentation.screens.places.RestaurantFinderScreen 13 | 14 | object LocationNavigationGraph : FeatureNavigationApi { 15 | 16 | private const val KEY_PLACE = "place" 17 | 18 | override fun registerNavigationGraph( 19 | navController: NavHostController, 20 | navGraphBuilder: NavGraphBuilder 21 | ) { 22 | navGraphBuilder.navigation( 23 | startDestination = LocationRoute.PLACES_SEARCH.route, 24 | route = NavigationRoute.LOCATION.route 25 | ) { 26 | composable(route = LocationRoute.PLACES_SEARCH.route) { 27 | val savedStateHandle = navController.currentBackStackEntry?.savedStateHandle 28 | RestaurantFinderScreen(navHostController = navController) { place -> 29 | savedStateHandle?.set(KEY_PLACE, place) 30 | } 31 | } 32 | composable(route = LocationRoute.GOOGLE_MAPS.route) { 33 | val place = navController.previousBackStackEntry 34 | ?.savedStateHandle 35 | ?.get(KEY_PLACE) 36 | 37 | if (place != null) { 38 | GoogleMapScreen(navHostController = navController, place = place) 39 | } else { 40 | // navController.popBackStack() 41 | } 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/location/presentation/screens/googlemaps/GoogleMapContract.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.location.presentation.screens.googlemaps 2 | 3 | import com.google.android.gms.maps.model.LatLng 4 | import novalogics.android.bitemap.core.base.state.ViewIntent 5 | import novalogics.android.bitemap.core.base.state.ViewSideEffect 6 | import novalogics.android.bitemap.core.base.state.ViewState 7 | import novalogics.android.bitemap.core.navigation.events.LocationEvent 8 | import novalogics.android.bitemap.location.domain.model.DirectionDetails 9 | import novalogics.android.bitemap.location.domain.model.PlaceDetails 10 | 11 | class GoogleMapContract { 12 | 13 | data class GoogleMapUiState( 14 | val isLoading: Boolean = false, 15 | val currentLocation: LocationEvent = LocationEvent.Idle(), 16 | val routePoints: DirectionDetails? = null, 17 | val error: String? = null, 18 | ) : ViewState 19 | 20 | sealed class Intent : ViewIntent { 21 | data class GetLocationUpdates(val destination: LatLng) : Intent() 22 | data class InsertPlaceDetails(val placeDetails: PlaceDetails): Intent() 23 | data class GetDirections(val start: LatLng, val destination: LatLng): Intent() 24 | } 25 | 26 | sealed class Effect : ViewSideEffect {} 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/location/presentation/screens/googlemaps/GoogleMapScreen.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.location.presentation.screens.googlemaps 2 | 3 | import android.content.Context 4 | import android.location.Location 5 | import android.widget.Toast 6 | import androidx.activity.compose.BackHandler 7 | import androidx.compose.foundation.layout.Box 8 | import androidx.compose.foundation.layout.fillMaxSize 9 | import androidx.compose.foundation.layout.wrapContentSize 10 | import androidx.compose.material3.CircularProgressIndicator 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.LaunchedEffect 13 | import androidx.compose.runtime.collectAsState 14 | import androidx.compose.runtime.getValue 15 | import androidx.compose.runtime.rememberCoroutineScope 16 | import androidx.compose.ui.Alignment 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.graphics.Color 19 | import androidx.compose.ui.platform.LocalContext 20 | import androidx.hilt.navigation.compose.hiltViewModel 21 | import androidx.navigation.NavHostController 22 | import com.google.android.gms.maps.CameraUpdateFactory 23 | import com.google.android.gms.maps.model.CameraPosition 24 | import com.google.android.gms.maps.model.LatLng 25 | import com.google.maps.android.compose.CameraPositionState 26 | import com.google.maps.android.compose.GoogleMap 27 | import com.google.maps.android.compose.Polyline 28 | import com.google.maps.android.compose.rememberCameraPositionState 29 | import kotlinx.coroutines.CoroutineScope 30 | import kotlinx.coroutines.launch 31 | import novalogics.android.bitemap.core.navigation.LocationRoute 32 | import novalogics.android.bitemap.core.navigation.events.LocationEvent 33 | import novalogics.android.bitemap.location.domain.model.PlaceDetails 34 | 35 | @Composable 36 | fun GoogleMapScreen( 37 | navHostController: NavHostController, 38 | viewModel: GoogleMapViewModel = hiltViewModel(), 39 | place: PlaceDetails, 40 | ) { 41 | val uiState by viewModel.uiState.collectAsState() 42 | val destination = place.destination 43 | val context = LocalContext.current 44 | val scope = rememberCoroutineScope() 45 | 46 | HandleBackPress(navHostController) 47 | viewModel.handleIntent(GoogleMapContract.Intent.InsertPlaceDetails(place)) 48 | 49 | HandleLocationEvent(uiState, viewModel, destination, scope, context, navHostController) 50 | 51 | RequestLocationUpdates(viewModel, destination) 52 | } 53 | 54 | @Composable 55 | fun HandleBackPress(navHostController: NavHostController) { 56 | BackHandler { 57 | navHostController.popBackStack() 58 | } 59 | } 60 | 61 | @Composable 62 | fun HandleLocationEvent( 63 | uiState: GoogleMapContract.GoogleMapUiState, 64 | viewModel: GoogleMapViewModel, 65 | destination: LatLng, 66 | scope: CoroutineScope, 67 | context: Context, 68 | navHostController: NavHostController 69 | ) { 70 | when (val locationEvent = uiState.currentLocation) { 71 | is LocationEvent.Idle -> { 72 | Box( 73 | modifier = Modifier 74 | .fillMaxSize() 75 | .wrapContentSize(Alignment.Center) 76 | ) { 77 | CircularProgressIndicator() 78 | } 79 | } 80 | is LocationEvent.LocationInProgress -> { 81 | locationEvent.location?.let { location -> 82 | 83 | val cameraPosition = rememberCameraPositionState { 84 | position = createCameraPosition(location) 85 | } 86 | 87 | AnimateCameraOnRouteUpdate(uiState, scope, cameraPosition, location) 88 | FetchDirectionsOnLocationUpdate(viewModel, location, destination) 89 | RenderGoogleMap(uiState, cameraPosition) 90 | } 91 | } 92 | is LocationEvent.ReachDestination -> { 93 | HandleDestinationReached(viewModel, context, navHostController) 94 | } 95 | } 96 | } 97 | 98 | private fun createCameraPosition(location: Location): CameraPosition { 99 | return CameraPosition.builder() 100 | .zoom(17F) 101 | .bearing(location.bearing) 102 | .tilt(45F) 103 | .target(LatLng(location.latitude, location.longitude)) 104 | .build() 105 | } 106 | 107 | @Composable 108 | fun AnimateCameraOnRouteUpdate( 109 | uiState: GoogleMapContract.GoogleMapUiState, 110 | scope: CoroutineScope, 111 | cameraPosition: CameraPositionState, 112 | location: Location 113 | ) { 114 | LaunchedEffect(key1 = uiState.routePoints) { 115 | scope.launch { 116 | cameraPosition.animate( 117 | update = CameraUpdateFactory.newCameraPosition(createCameraPosition(location)), 118 | durationMs = 1000 119 | ) 120 | } 121 | } 122 | } 123 | 124 | @Composable 125 | fun FetchDirectionsOnLocationUpdate( 126 | viewModel: GoogleMapViewModel, 127 | location: Location, 128 | destination: LatLng 129 | ) { 130 | LaunchedEffect(key1 = location) { 131 | viewModel.handleIntent( 132 | GoogleMapContract.Intent.GetDirections( 133 | start = LatLng(location.latitude, location.longitude), 134 | destination = destination, 135 | ) 136 | ) 137 | } 138 | } 139 | 140 | @Composable 141 | fun RenderGoogleMap(uiState: GoogleMapContract.GoogleMapUiState, cameraPosition: CameraPositionState) { 142 | GoogleMap( 143 | modifier = Modifier.fillMaxSize(), 144 | cameraPositionState = cameraPosition 145 | ) { 146 | uiState.routePoints?.points?.takeIf { it.isNotEmpty() }?.let { points -> 147 | Polyline(points = points, color = Color.Blue, width = 20F) 148 | } 149 | } 150 | } 151 | 152 | @Composable 153 | fun HandleDestinationReached( 154 | viewModel: GoogleMapViewModel, 155 | context: Context, 156 | navHostController: NavHostController 157 | ) { 158 | //viewModel.handleIntent(GoogleMapContract.Intent.InsertPlaceDetails(place)) 159 | Toast.makeText(context, "Reached the destination", Toast.LENGTH_LONG).show() 160 | navHostController.popBackStack(LocationRoute.PLACES_SEARCH.route, inclusive = true) 161 | } 162 | 163 | @Composable 164 | fun RequestLocationUpdates(viewModel: GoogleMapViewModel, destination: LatLng) { 165 | LaunchedEffect(key1 = Unit) { 166 | viewModel.handleIntent(GoogleMapContract.Intent.GetLocationUpdates(destination)) 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/location/presentation/screens/googlemaps/GoogleMapViewModel.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.location.presentation.screens.googlemaps 2 | 3 | import androidx.lifecycle.viewModelScope 4 | import com.google.android.gms.maps.model.LatLng 5 | import dagger.hilt.android.lifecycle.HiltViewModel 6 | import kotlinx.coroutines.flow.collectLatest 7 | import kotlinx.coroutines.launch 8 | import novalogics.android.bitemap.core.base.BaseViewModel 9 | import novalogics.android.bitemap.core.navigation.events.UiEvent 10 | import novalogics.android.bitemap.location.domain.model.PlaceDetails 11 | import novalogics.android.bitemap.location.domain.usecase.GetDirectionUseCase 12 | import novalogics.android.bitemap.location.domain.usecase.GetLocationUpdateUseCase 13 | import novalogics.android.bitemap.location.domain.usecase.InsertPlacesToDbUseCase 14 | import javax.inject.Inject 15 | 16 | @HiltViewModel 17 | class GoogleMapViewModel @Inject constructor( 18 | private val getLocationUpdateUseCase: GetLocationUpdateUseCase, 19 | private val getDirectionUseCase: GetDirectionUseCase, 20 | private val insertPlacesToDbUseCase: InsertPlacesToDbUseCase, 21 | ) : BaseViewModel( 22 | GoogleMapContract.GoogleMapUiState() 23 | ) { 24 | 25 | override fun handleIntent(intent: GoogleMapContract.Intent) { 26 | when (intent) { 27 | is GoogleMapContract.Intent.GetLocationUpdates -> getLocationUpdates(intent.destination) 28 | is GoogleMapContract.Intent.InsertPlaceDetails -> insertPlaceDetails(intent.placeDetails) 29 | is GoogleMapContract.Intent.GetDirections-> getDirections(intent.start, intent.destination) 30 | } 31 | } 32 | 33 | private fun getDirections(start: LatLng, destination: LatLng) { 34 | viewModelScope.launch { 35 | getDirectionUseCase(start, destination).collectLatest { 36 | when (it) { 37 | is UiEvent.Loading -> { 38 | updateState { copy(isLoading= true) } 39 | } 40 | is UiEvent.Error -> { 41 | updateState { copy(isLoading= false, error = it.message) } 42 | } 43 | is UiEvent.Success -> { 44 | updateState { copy(isLoading= false, routePoints = it.data!!) } 45 | } 46 | } 47 | } 48 | } 49 | } 50 | 51 | private fun getLocationUpdates(destination: LatLng) { 52 | updateState { copy(isLoading= true) } 53 | viewModelScope.launch { 54 | getLocationUpdateUseCase(destination).collectLatest { 55 | updateState { copy(isLoading= false, currentLocation = it) } 56 | } 57 | } 58 | } 59 | 60 | private fun insertPlaceDetails(placeDetails: PlaceDetails) = viewModelScope.launch { 61 | insertPlacesToDbUseCase(placeDetails) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/location/presentation/screens/places/PlacesContract.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.location.presentation.screens.places 2 | 3 | import novalogics.android.bitemap.core.base.state.ViewIntent 4 | import novalogics.android.bitemap.core.base.state.ViewSideEffect 5 | import novalogics.android.bitemap.core.base.state.ViewState 6 | import novalogics.android.bitemap.core.navigation.events.PlacesResult 7 | import novalogics.android.bitemap.location.domain.model.PlaceDetails 8 | 9 | class PlacesContract { 10 | 11 | data class PlacesUiState( 12 | val isLoading: Boolean = false, 13 | val searchResult: PlacesResult = PlacesResult.Idle(), 14 | val searchQuery: String = "", 15 | val error: String? = null, 16 | ) : ViewState 17 | 18 | sealed class Intent : ViewIntent { 19 | data class UpdateQuery(val query: String) : Intent() 20 | data class SearchRestaurants(val query: String) : Intent() 21 | data class FetchDetails(val placeId: String, val result: (PlaceDetails) -> Unit) : Intent() 22 | } 23 | 24 | sealed class Effect : ViewSideEffect { 25 | data class NavigateToMaps(val placeDetails: PlaceDetails) : Effect() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/location/presentation/screens/places/PlacesSearch.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.location.presentation.screens.places 2 | 3 | import android.Manifest 4 | import android.content.res.Configuration.UI_MODE_NIGHT_NO 5 | import android.content.res.Configuration.UI_MODE_NIGHT_YES 6 | import androidx.compose.foundation.Image 7 | import androidx.compose.foundation.background 8 | import androidx.compose.foundation.clickable 9 | import androidx.compose.foundation.layout.Box 10 | import androidx.compose.foundation.layout.Column 11 | import androidx.compose.foundation.layout.Spacer 12 | import androidx.compose.foundation.layout.fillMaxSize 13 | import androidx.compose.foundation.layout.fillMaxWidth 14 | import androidx.compose.foundation.layout.height 15 | import androidx.compose.foundation.layout.padding 16 | import androidx.compose.foundation.layout.size 17 | import androidx.compose.foundation.lazy.LazyColumn 18 | import androidx.compose.foundation.lazy.items 19 | import androidx.compose.material.icons.Icons 20 | import androidx.compose.material.icons.filled.Search 21 | import androidx.compose.material3.CircularProgressIndicator 22 | import androidx.compose.material3.Icon 23 | import androidx.compose.material3.MaterialTheme 24 | import androidx.compose.material3.Text 25 | import androidx.compose.material3.TextField 26 | import androidx.compose.runtime.Composable 27 | import androidx.compose.runtime.LaunchedEffect 28 | import androidx.compose.runtime.collectAsState 29 | import androidx.compose.runtime.getValue 30 | import androidx.compose.ui.Alignment 31 | import androidx.compose.ui.Modifier 32 | import androidx.compose.ui.platform.LocalContext 33 | import androidx.compose.ui.res.painterResource 34 | import androidx.compose.ui.res.stringResource 35 | import androidx.compose.ui.text.font.FontWeight 36 | import androidx.compose.ui.text.style.TextAlign 37 | import androidx.compose.ui.tooling.preview.Preview 38 | import androidx.compose.ui.unit.dp 39 | import androidx.compose.ui.unit.sp 40 | import androidx.constraintlayout.compose.ConstraintLayout 41 | import androidx.constraintlayout.compose.Dimension 42 | import androidx.hilt.navigation.compose.hiltViewModel 43 | import androidx.navigation.NavHostController 44 | import com.google.accompanist.permissions.ExperimentalPermissionsApi 45 | import com.google.accompanist.permissions.rememberMultiplePermissionsState 46 | import com.google.android.gms.maps.model.LatLng 47 | import novalogics.android.bitemap.R 48 | import novalogics.android.bitemap.app.ui.theme.BiteMapTheme 49 | import novalogics.android.bitemap.core.navigation.DashboardRoute 50 | import novalogics.android.bitemap.core.navigation.LocationRoute 51 | import novalogics.android.bitemap.core.navigation.events.PlacesResult 52 | import novalogics.android.bitemap.location.domain.model.PlaceDetails 53 | 54 | @Composable 55 | fun RestaurantFinderScreen( 56 | navHostController: NavHostController, 57 | viewModel: PlacesSearchViewModel = hiltViewModel(), 58 | goToGoogleMap: (PlaceDetails) -> Unit 59 | ) { 60 | val uiState by viewModel.uiState.collectAsState() 61 | 62 | PermissionHandler(navController = navHostController) 63 | 64 | ScreenUiContent( 65 | uiState = uiState, 66 | navHostController = navHostController, 67 | updateQuery = { viewModel.handleIntent(PlacesContract.Intent.UpdateQuery(it)) }, 68 | goToGoogleMap = goToGoogleMap, 69 | fetchDetails = { id, result -> 70 | viewModel.handleIntent( 71 | PlacesContract.Intent.FetchDetails( 72 | placeId = id, 73 | result = result 74 | ) 75 | ) 76 | } 77 | ) 78 | } 79 | 80 | @Composable 81 | fun ScreenUiContent( 82 | uiState: PlacesContract.PlacesUiState, 83 | navHostController: NavHostController, 84 | updateQuery: (String) -> Unit, 85 | goToGoogleMap: (PlaceDetails) -> Unit, 86 | fetchDetails: (placeId: String, result: (PlaceDetails) -> Unit) -> Unit 87 | ) { 88 | ConstraintLayout( 89 | modifier = Modifier 90 | .fillMaxSize() 91 | .background(MaterialTheme.colorScheme.background) 92 | ) { 93 | val (searchCons, listCons) = createRefs() 94 | 95 | TextField( 96 | value = uiState.searchQuery, 97 | onValueChange = { updateQuery(it) }, 98 | maxLines = 1, 99 | leadingIcon = { 100 | Icon( 101 | imageVector = Icons.Default.Search, 102 | tint = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.5F), 103 | contentDescription = stringResource(id = R.string.search_icon_description) 104 | ) 105 | }, 106 | modifier = Modifier 107 | .fillMaxWidth() 108 | .constrainAs(searchCons) { 109 | start.linkTo(parent.start) 110 | end.linkTo(parent.end) 111 | top.linkTo(parent.top) 112 | }) 113 | 114 | when (uiState.searchResult) { 115 | is PlacesResult.Idle -> { 116 | Box( 117 | modifier = Modifier.fillMaxSize(), 118 | contentAlignment = Alignment.Center 119 | ) { 120 | Column( 121 | modifier = Modifier, 122 | horizontalAlignment = Alignment.CenterHorizontally, 123 | ) { 124 | Text( 125 | text = stringResource(id = R.string.search_restaurants), 126 | color = MaterialTheme.colorScheme.onSurface, 127 | style = MaterialTheme.typography.displayMedium.copy( 128 | fontWeight = FontWeight.Normal, 129 | fontSize = 24.sp, 130 | ), 131 | textAlign = TextAlign.Center, 132 | modifier = Modifier.padding(12.dp) 133 | ) 134 | Spacer(modifier = Modifier.height(16.dp)) 135 | Image( 136 | painter = painterResource(id = R.drawable.bitemap_logo_main), 137 | contentDescription = "BiteMap Logo", 138 | modifier = Modifier 139 | .size(180.dp) 140 | .align(Alignment.CenterHorizontally) 141 | ) 142 | } 143 | } 144 | } 145 | is PlacesResult.Loading -> { 146 | Box( 147 | modifier = Modifier.fillMaxSize(), 148 | contentAlignment = Alignment.Center 149 | ) { 150 | CircularProgressIndicator() 151 | } 152 | } 153 | 154 | is PlacesResult.Success -> { 155 | LazyColumn(modifier = Modifier.constrainAs(listCons) { 156 | start.linkTo(parent.start) 157 | end.linkTo(parent.end) 158 | top.linkTo(searchCons.bottom) 159 | bottom.linkTo(parent.bottom) 160 | height = Dimension.fillToConstraints 161 | }) { 162 | items(uiState.searchResult.list) { place-> 163 | Text( 164 | text = place.getFullText(null).toString(), 165 | color = MaterialTheme.colorScheme.onPrimary, 166 | modifier = Modifier 167 | .fillMaxWidth() 168 | .padding(12.dp) 169 | .clickable { 170 | fetchDetails(place.placeId) { 171 | it.origin = LatLng( 172 | uiState.searchResult.location?.latitude!!, 173 | uiState.searchResult.location.longitude, 174 | ) 175 | goToGoogleMap.invoke(it) 176 | navHostController.navigate(LocationRoute.GOOGLE_MAPS.route) 177 | } 178 | } 179 | ) 180 | } 181 | } 182 | } 183 | 184 | is PlacesResult.Error -> { 185 | Box( 186 | modifier = Modifier.fillMaxSize(), 187 | contentAlignment = Alignment.Center 188 | ) { 189 | Text( 190 | text = uiState.searchResult.message.toString(), 191 | color = MaterialTheme.colorScheme.onError, 192 | modifier = Modifier.padding(24.dp) 193 | ) 194 | } 195 | } 196 | } 197 | } 198 | } 199 | 200 | @OptIn(ExperimentalPermissionsApi::class) 201 | @Composable 202 | fun PermissionHandler( 203 | navController: NavHostController, 204 | permissionList: List = listOf( 205 | Manifest.permission.ACCESS_COARSE_LOCATION, 206 | Manifest.permission.ACCESS_FINE_LOCATION 207 | ) 208 | ) { 209 | val permissionState = rememberMultiplePermissionsState(permissions = permissionList) 210 | 211 | LaunchedEffect(Unit) { 212 | if (!permissionState.allPermissionsGranted) { 213 | navController.navigate(DashboardRoute.PERMISSION_SCREEN.route) 214 | } 215 | } 216 | } 217 | 218 | @Preview( 219 | name = "Light Mode", 220 | showBackground = true, 221 | uiMode = UI_MODE_NIGHT_NO 222 | ) 223 | @Preview( 224 | name = "Dark Mode", 225 | showBackground = true, 226 | uiMode = UI_MODE_NIGHT_YES 227 | ) 228 | @Composable 229 | fun PlaceSearchPreview() { 230 | 231 | val uiState = PlacesContract.PlacesUiState( 232 | isLoading = false, 233 | searchQuery = "", 234 | searchResult = PlacesResult.Idle(), 235 | error = null, 236 | ) 237 | val context = LocalContext.current 238 | BiteMapTheme { 239 | ScreenUiContent( 240 | uiState = uiState, 241 | navHostController = NavHostController(context = context), 242 | fetchDetails = { _, _ ->}, 243 | updateQuery = {}, 244 | goToGoogleMap = {} 245 | ) 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /app/src/main/java/novalogics/android/bitemap/location/presentation/screens/places/PlacesSearchViewModel.kt: -------------------------------------------------------------------------------- 1 | package novalogics.android.bitemap.location.presentation.screens.places 2 | 3 | import androidx.lifecycle.viewModelScope 4 | import dagger.hilt.android.lifecycle.HiltViewModel 5 | import kotlinx.coroutines.FlowPreview 6 | import kotlinx.coroutines.flow.collectLatest 7 | import kotlinx.coroutines.flow.debounce 8 | import kotlinx.coroutines.flow.distinctUntilChanged 9 | import kotlinx.coroutines.flow.map 10 | import kotlinx.coroutines.launch 11 | import novalogics.android.bitemap.core.base.BaseViewModel 12 | import novalogics.android.bitemap.location.domain.model.PlaceDetails 13 | import novalogics.android.bitemap.location.domain.usecase.FetchRestaurantDetailUseCase 14 | import novalogics.android.bitemap.location.domain.usecase.SearchRestaurantUseCase 15 | import javax.inject.Inject 16 | 17 | @OptIn(FlowPreview::class) 18 | @HiltViewModel 19 | class PlacesSearchViewModel @Inject constructor( 20 | private val searchRestaurantUseCase: SearchRestaurantUseCase, 21 | private val fetchRestaurantDetailUseCase: FetchRestaurantDetailUseCase, 22 | ) : BaseViewModel( 23 | PlacesContract.PlacesUiState() 24 | ) { 25 | 26 | override fun handleIntent(intent: PlacesContract.Intent) { 27 | when (intent) { 28 | is PlacesContract.Intent.UpdateQuery -> updateQuery(intent.query) 29 | is PlacesContract.Intent.SearchRestaurants -> searchRestaurants(intent.query) 30 | is PlacesContract.Intent.FetchDetails -> fetchDetails(intent.placeId, intent.result) 31 | } 32 | } 33 | 34 | init { 35 | viewModelScope.launch { 36 | uiState 37 | .map { it.searchQuery } 38 | .distinctUntilChanged() 39 | .debounce(1000) 40 | .collectLatest { query -> 41 | if (query.isNotEmpty()) { 42 | searchRestaurants(query) 43 | } 44 | } 45 | } 46 | } 47 | 48 | private fun updateQuery(query: String) { 49 | updateState { copy(searchQuery = query) } 50 | } 51 | 52 | private fun searchRestaurants(query: String) = viewModelScope.launch { 53 | searchRestaurantUseCase(query = query).collectLatest { places -> 54 | updateState { copy(searchResult = places) } 55 | } 56 | } 57 | 58 | private fun fetchDetails(placeId: String, result: (PlaceDetails) -> Unit) = 59 | viewModelScope.launch { 60 | fetchRestaurantDetailUseCase(placeId = placeId).collectLatest { 61 | result.invoke(it) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bitemap_logo_main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NovaLogics/bite-map-android-app/5c4ded795bf1084e654aae1a66dd0a12c8f896b5/app/src/main/res/drawable/bitemap_logo_main.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_restaurant_menu.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/font/montserrat_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NovaLogics/bite-map-android-app/5c4ded795bf1084e654aae1a66dd0a12c8f896b5/app/src/main/res/font/montserrat_bold.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/montserrat_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NovaLogics/bite-map-android-app/5c4ded795bf1084e654aae1a66dd0a12c8f896b5/app/src/main/res/font/montserrat_regular.ttf -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NovaLogics/bite-map-android-app/5c4ded795bf1084e654aae1a66dd0a12c8f896b5/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NovaLogics/bite-map-android-app/5c4ded795bf1084e654aae1a66dd0a12c8f896b5/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NovaLogics/bite-map-android-app/5c4ded795bf1084e654aae1a66dd0a12c8f896b5/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NovaLogics/bite-map-android-app/5c4ded795bf1084e654aae1a66dd0a12c8f896b5/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NovaLogics/bite-map-android-app/5c4ded795bf1084e654aae1a66dd0a12c8f896b5/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NovaLogics/bite-map-android-app/5c4ded795bf1084e654aae1a66dd0a12c8f896b5/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NovaLogics/bite-map-android-app/5c4ded795bf1084e654aae1a66dd0a12c8f896b5/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NovaLogics/bite-map-android-app/5c4ded795bf1084e654aae1a66dd0a12c8f896b5/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NovaLogics/bite-map-android-app/5c4ded795bf1084e654aae1a66dd0a12c8f896b5/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NovaLogics/bite-map-android-app/5c4ded795bf1084e654aae1a66dd0a12c8f896b5/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NovaLogics/bite-map-android-app/5c4ded795bf1084e654aae1a66dd0a12c8f896b5/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NovaLogics/bite-map-android-app/5c4ded795bf1084e654aae1a66dd0a12c8f896b5/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NovaLogics/bite-map-android-app/5c4ded795bf1084e654aae1a66dd0a12c8f896b5/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NovaLogics/bite-map-android-app/5c4ded795bf1084e654aae1a66dd0a12c8f896b5/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NovaLogics/bite-map-android-app/5c4ded795bf1084e654aae1a66dd0a12c8f896b5/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #E7EDF3 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | BiteMap 3 | Recently Visited Restaurants 4 | Nearby Restaurants 5 | An unexpected error occurs. Please try again later 6 | Restaurant Search 7 | Search Restaurants 8 | Checking permissions… 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |