├── .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 | [](#)
24 | [](#)
25 |
26 | [](#)
27 | [](#)
28 | [](#)
29 | [](#)
30 | [](./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 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
20 |
--------------------------------------------------------------------------------
/app/src/test/java/novalogics/android/bitemap/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package novalogics.android.bitemap
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | plugins {
3 | alias(libs.plugins.android.application) apply false
4 | alias(libs.plugins.kotlin.android) apply false
5 | alias(libs.plugins.compose.compiler) apply false
6 | alias(libs.plugins.kotlinx.serialization) apply false
7 | alias(libs.plugins.hilt.android) apply false
8 | alias(libs.plugins.ksp) apply false
9 | alias(libs.plugins.android.library) apply false
10 | alias(libs.plugins.kotlin.parcelize) apply false
11 | }
12 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. For more details, visit
12 | # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | android.enableJetifier=true
19 | # Kotlin code style for this project: "official" or "obsolete":
20 | kotlin.code.style=official
21 | # Enables namespacing of each library's R class so that its R class includes only the
22 | # resources declared in the library itself and none from the library's dependencies,
23 | # thereby reducing the size of the R class for that library
24 | android.nonTransitiveRClass=true
25 |
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | # Android Gradle Plugin and Kotlin versions
3 | agp = "8.5.2"
4 | kotlin = "2.0.20"
5 | kspVersion = "2.0.20-1.0.25"
6 | kotlinxSerialization = "1.6.0"
7 | kotlinParcelizeVersion = "1.8.10"
8 |
9 | # AndroidX versions
10 | androidxCoreKtx = "1.13.1"
11 | androidxAppcompat = "1.6.1"
12 | androidxConstraintlayout = "2.1.4"
13 | androidxLifecycle = "2.6.1"
14 | androidxPaging = "3.2.1"
15 | activityCompose = "1.9.2"
16 | composeBom = "2024.04.01"
17 | composeHiltNavigationVersion = "1.2.0"
18 | composeNavigationVersion = "2.8.2"
19 | constraintlayoutCompose = "1.0.1"
20 |
21 | # Google Material and Maps versions
22 | material = "1.11.0"
23 | gsmMaps = "18.2.0"
24 | gsmLocation = "21.2.0"
25 | mapsUtils = "3.6.0"
26 | mapsCompose = "3.1.0"
27 | places = "3.5.0"
28 |
29 | # Kotlin Coroutines
30 | kotlinxCoroutines = "1.7.3"
31 |
32 | # Networking & Dependency Injection
33 | retrofit = "2.9.0"
34 | retrofitGson = "2.2.0"
35 | okhttp = "4.12.0"
36 | hilt = "2.48"
37 | hiltCompilerVersion = "2.48"
38 |
39 | # Room Database
40 | room = "2.6.1"
41 |
42 | # Utility Libraries
43 | pluto = "2.0.6"
44 | coil = "2.6.0"
45 | timber = "5.0.1"
46 | junit = "4.13.2"
47 | junitVersion = "1.2.1"
48 | espressoCore = "3.6.1"
49 | androidxTestRunner = "1.5.2"
50 | androidxTestEspresso = "3.5.1"
51 | accompanistPermissions = "0.30.1"
52 |
53 | [libraries]
54 | # AndroidX
55 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidxCoreKtx" }
56 | androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidxAppcompat" }
57 | androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "androidxConstraintlayout" }
58 | androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidxLifecycle" }
59 | androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidxLifecycle" }
60 | androidx-paging = { module = "androidx.paging:paging-runtime-ktx", version.ref = "androidxPaging" }
61 | androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
62 | androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
63 | androidx-ui = { group = "androidx.compose.ui", name = "ui" }
64 | androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
65 | androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
66 | androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
67 | androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
68 | androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
69 | androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
70 | androidx-navigate-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "composeNavigationVersion" }
71 | androidx-hilt-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "composeHiltNavigationVersion" }
72 | constraintlayout-compose = { module = "androidx.constraintlayout:constraintlayout-compose", version.ref = "constraintlayoutCompose" }
73 |
74 | # Google Material and Maps
75 | material = { module = "com.google.android.material:material", version.ref = "material" }
76 | gsm-maps = { module = "com.google.android.gms:play-services-maps", version.ref = "gsmMaps" }
77 | gsm-location = { module = "com.google.android.gms:play-services-location", version.ref = "gsmLocation" }
78 | places = { module = "com.google.android.libraries.places:places", version.ref = "places" }
79 | maps-utils = { module = "com.google.maps.android:android-maps-utils", version.ref = "mapsUtils" }
80 | maps-compose = { module = "com.google.maps.android:maps-compose", version.ref = "mapsCompose" }
81 |
82 | # Kotlin Coroutines
83 | kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" }
84 | kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" }
85 | kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" }
86 |
87 | # Networking & DI
88 | retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
89 | okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
90 | okhttp-logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
91 | hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
92 | hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hiltCompilerVersion" }
93 | retrofit-converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofitGson" }
94 |
95 | # Room Database
96 | room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
97 | room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
98 | room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
99 |
100 | # Utility Libraries
101 | coil = { module = "io.coil-kt:coil", version.ref = "coil" }
102 | timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
103 | pluto = { module = "com.plutolib:pluto", version.ref = "pluto" }
104 | pluto-network = { module = "com.plutolib.plugins:network", version.ref = "pluto" }
105 | accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" }
106 |
107 | # Testing
108 | junit = { module = "junit:junit", version.ref = "junit" }
109 | androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidxTestRunner" }
110 | androidx-test-espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "androidxTestEspresso" }
111 | androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
112 |
113 | [plugins]
114 | android-application = { id = "com.android.application", version.ref = "agp" }
115 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
116 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
117 | kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
118 | hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
119 | ksp = { id = "com.google.devtools.ksp", version.ref = "kspVersion" }
120 | android-library = { id = "com.android.library", version.ref = "agp" }
121 | kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlinParcelizeVersion" }
122 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NovaLogics/bite-map-android-app/5c4ded795bf1084e654aae1a66dd0a12c8f896b5/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Feb 12 14:26:45 IST 2025
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/local.defaults.properties:
--------------------------------------------------------------------------------
1 | PLACES_API_KEY=DEFAULT_API_KEY
2 |
--------------------------------------------------------------------------------
/secrets.properties:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google {
4 | content {
5 | includeGroupByRegex("com\\.android.*")
6 | includeGroupByRegex("com\\.google.*")
7 | includeGroupByRegex("androidx.*")
8 | }
9 | }
10 | mavenCentral()
11 | gradlePluginPortal()
12 | }
13 | }
14 | dependencyResolutionManagement {
15 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
16 | repositories {
17 | google()
18 | mavenCentral()
19 | }
20 | }
21 |
22 | rootProject.name = "BiteMap"
23 | include(":app")
24 | include(":dashboard:presentation")
25 | include(":dashboard:data")
26 | include(":common:utils")
27 | include(":location:presentation")
28 | include(":location:data")
29 | include(":location:domain")
30 |
--------------------------------------------------------------------------------