├── .example.env
├── .github
└── ISSUE_TEMPLATE
│ └── Junior-AI.yml
├── .gitignore
├── .metadata
├── README.md
├── analysis_options.yaml
├── android
├── .gitignore
├── app
│ ├── build.gradle
│ └── src
│ │ ├── debug
│ │ └── AndroidManifest.xml
│ │ ├── main
│ │ ├── AndroidManifest.xml
│ │ ├── kotlin
│ │ │ └── com
│ │ │ │ └── example
│ │ │ │ └── travel_routes
│ │ │ │ └── MainActivity.kt
│ │ └── res
│ │ │ ├── drawable-v21
│ │ │ └── launch_background.xml
│ │ │ ├── drawable
│ │ │ └── launch_background.xml
│ │ │ ├── mipmap-hdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-mdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xhdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── values-night
│ │ │ └── styles.xml
│ │ │ └── values
│ │ │ └── styles.xml
│ │ └── profile
│ │ └── AndroidManifest.xml
├── build.gradle
├── gradle.properties
├── gradle
│ └── wrapper
│ │ └── gradle-wrapper.properties
└── settings.gradle
├── ios
├── .gitignore
├── Flutter
│ ├── AppFrameworkInfo.plist
│ ├── Debug.xcconfig
│ └── Release.xcconfig
├── Podfile
├── Podfile.lock
├── Runner.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ ├── IDEWorkspaceChecks.plist
│ │ │ └── WorkspaceSettings.xcsettings
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── Runner.xcscheme
├── Runner.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── WorkspaceSettings.xcsettings
├── Runner
│ ├── AppDelegate.swift
│ ├── Assets.xcassets
│ │ ├── AppIcon.appiconset
│ │ │ ├── Contents.json
│ │ │ ├── Icon-App-1024x1024@1x.png
│ │ │ ├── Icon-App-20x20@1x.png
│ │ │ ├── Icon-App-20x20@2x.png
│ │ │ ├── Icon-App-20x20@3x.png
│ │ │ ├── Icon-App-29x29@1x.png
│ │ │ ├── Icon-App-29x29@2x.png
│ │ │ ├── Icon-App-29x29@3x.png
│ │ │ ├── Icon-App-40x40@1x.png
│ │ │ ├── Icon-App-40x40@2x.png
│ │ │ ├── Icon-App-40x40@3x.png
│ │ │ ├── Icon-App-60x60@2x.png
│ │ │ ├── Icon-App-60x60@3x.png
│ │ │ ├── Icon-App-76x76@1x.png
│ │ │ ├── Icon-App-76x76@2x.png
│ │ │ └── Icon-App-83.5x83.5@2x.png
│ │ └── LaunchImage.imageset
│ │ │ ├── Contents.json
│ │ │ ├── LaunchImage.png
│ │ │ ├── LaunchImage@2x.png
│ │ │ ├── LaunchImage@3x.png
│ │ │ └── README.md
│ ├── Base.lproj
│ │ ├── LaunchScreen.storyboard
│ │ └── Main.storyboard
│ ├── Info.plist
│ └── Runner-Bridging-Header.h
└── RunnerTests
│ └── RunnerTests.swift
├── lib
├── main.dart
├── models
│ ├── destination.dart
│ ├── direction.dart
│ ├── geometry.dart
│ ├── location.dart
│ ├── opening_hours.dart
│ ├── photo.dart
│ ├── photo_image.dart
│ ├── place.dart
│ ├── place_autocomplete_prediction.dart
│ ├── point_of_interest.dart
│ ├── query_autocomplete_prediction.dart
│ ├── review.dart
│ └── viewport.dart
├── repositories
│ ├── destination_repository.dart
│ ├── destination_repository.g.dart
│ ├── maps_repository.dart
│ └── maps_repository.g.dart
├── screens
│ ├── destination_screen.dart
│ ├── error_screen.dart
│ ├── home_screen.dart
│ ├── loading_screen.dart
│ ├── map_screen.dart
│ └── points_of_interest_screen.dart
├── services
│ ├── api
│ │ ├── geocoding_api_client.dart
│ │ ├── places_api_client.dart
│ │ └── routes_api_client.dart
│ └── location_service.dart
└── state
│ ├── notifiers
│ ├── user_location_provider.dart
│ └── user_location_provider.g.dart
│ └── providers
│ ├── all_destinations_provider.dart
│ ├── all_destinations_provider.g.dart
│ ├── directions_provider.dart
│ ├── directions_provider.g.dart
│ ├── place_autocomplete_predictions_provider.dart
│ ├── place_autocomplete_predictions_provider.g.dart
│ ├── place_details_provider.dart
│ ├── place_details_provider.g.dart
│ ├── selected_destination_provider.dart
│ ├── selected_destination_provider.g.dart
│ ├── user_current_location_provider.dart
│ └── user_current_location_provider.g.dart
├── pubspec.lock
├── pubspec.yaml
└── screenshots
├── travel_routes_1.png
├── travel_routes_2.png
├── travel_routes_3.png
└── travel_routes_4.png
/.example.env:
--------------------------------------------------------------------------------
1 | API_KEY=YOUR_API_KEY
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/Junior-AI.yml:
--------------------------------------------------------------------------------
1 |
2 | name: Junior AI
3 | description: Create a new task for Junior AI
4 | title: "Junior AI: "
5 | labels: ["junior-ai"]
6 | body:
7 | - type: markdown
8 | attributes:
9 | value: |
10 | ### (Required) What is the task for Junior AI?
11 | - type: textarea
12 | id: task
13 | attributes:
14 | label: The scope of work for Junior AI
15 | description: Please provide a description of the task that you would like Junior AI to work on.
16 | render: shell
17 | validations:
18 | required: true
19 | - type: markdown
20 | attributes:
21 | value: "
"
22 | - type: markdown
23 | attributes:
24 | value: "### (Optional) Fill the information below to give Junior AI more detailed information"
25 | - type: textarea
26 | id: snippets
27 | attributes:
28 | label: Any relevant code snippet
29 | description: Please copy and paste any relevant code snippet for the task.
30 | render: shell
31 | - type: textarea
32 | id: file_references
33 | attributes:
34 | label: Any relevant file references
35 | description: Please copy and paste the path of any relevant files from the project repository
36 | render: shell
37 | - type: dropdown
38 | id: automated_workflows
39 | attributes:
40 | label: Automated Workflows
41 | description: Do you think any of these automated workflows fit well with your current request?
42 | options:
43 | - None of the options
44 | - API Client Workflow
45 | - BLoC Workflow
46 | - Cubit Workflow
47 | - Data Model Workflow
48 | - GoRouter Workflow
49 | - Hive Workflow
50 | - Localization Workflow
51 | - Widget Breakdown Workflow
52 | - Theme Workflow
53 | default: 0
54 | validations:
55 | required: false
56 | - type: dropdown
57 | id: code_or_test
58 | attributes:
59 | label: Code or Test
60 | description: Do you want Junior AI to generate code or test for the Flutter project?
61 | options:
62 | - Not sure
63 | - Code
64 | - Test
65 | default: 0
66 | validations:
67 | required: false
68 | - type: dropdown
69 | id: create_or_edit
70 | attributes:
71 | label: Create or Edit
72 | description: Do you want Junior AI to write new code or edit any existing part of your app?
73 | options:
74 | - Not sure
75 | - Create
76 | - Edit
77 | default: 0
78 | validations:
79 | required: false
80 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Miscellaneous
2 | *.class
3 | *.log
4 | *.pyc
5 | *.swp
6 | .DS_Store
7 | .atom/
8 | .buildlog/
9 | .history
10 | .svn/
11 | migrate_working_dir/
12 |
13 | # IntelliJ related
14 | *.iml
15 | *.ipr
16 | *.iws
17 | .idea/
18 |
19 | # The .vscode folder contains launch configuration and tasks you configure in
20 | # VS Code which you may wish to be included in version control, so this line
21 | # is commented out by default.
22 | #.vscode/
23 |
24 | # Flutter/Dart/Pub related
25 | **/doc/api/
26 | **/ios/Flutter/.last_build_id
27 | .dart_tool/
28 | .flutter-plugins
29 | .flutter-plugins-dependencies
30 | .packages
31 | .pub-cache/
32 | .pub/
33 | /build/
34 |
35 | # Symbolication related
36 | app.*.symbols
37 |
38 | # Obfuscation related
39 | app.*.map.json
40 |
41 | # Android Studio will place build artifacts here
42 | /android/app/debug
43 | /android/app/profile
44 | /android/app/release
45 |
46 |
47 | .env
--------------------------------------------------------------------------------
/.metadata:
--------------------------------------------------------------------------------
1 | # This file tracks properties of this Flutter project.
2 | # Used by Flutter tool to assess capabilities and perform upgrades etc.
3 | #
4 | # This file should be version controlled.
5 |
6 | version:
7 | revision: 84a1e904f44f9b0e9c4510138010edcc653163f8
8 | channel: stable
9 |
10 | project_type: app
11 |
12 | # Tracks metadata for the flutter migrate command
13 | migration:
14 | platforms:
15 | - platform: root
16 | create_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8
17 | base_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8
18 | - platform: android
19 | create_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8
20 | base_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8
21 | - platform: ios
22 | create_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8
23 | base_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8
24 |
25 | # User provided section
26 |
27 | # List of Local paths (relative to this file) that should be
28 | # ignored by the migrate tool.
29 | #
30 | # Files that are not part of the templates will be ignored by default.
31 | unmanaged_files:
32 | - 'lib/main.dart'
33 | - 'ios/Runner.xcodeproj/project.pbxproj'
34 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Travel Routes App with Flutter and Google Maps
2 |
3 | The Travel Routes Flutter app integrates with Google Maps Platform using 'google_maps_flutter'. It offers users details on travel locations, points of interest, and efficient routing. Real-time geolocation is facilitated through the 'geolocator' package and Google's Geocoding API, which converts coordinates into user-friendly addresses.
4 |
5 | The app is connected with three main APIs:
6 | * The Places API provides detailed insights into locations, including photos and operating hours.
7 | * The Routes API offers precise navigation directions.
8 | * The Geocoding API translates between geographical coordinates and addresses.
9 |
10 | State management is handled with Riverpod. The UI is animated with 'flutter_animate' library.
11 |
12 | ## App Screenshots:
13 | |  |  |
14 | |:---:|:---:|
15 | | Home Screen with City Selection | Destination Screen with City Information |
16 | |  |  |
17 | | Points of Interest Screen | Map Screen with Navigation Details |
18 |
19 |
20 | ## Features:
21 | * Flutter app integrates with:
22 | * Google Maps using the `google_maps_flutter` library.
23 | * Enables geolocation services with the `geolocator` package.
24 | * Supports geocoding using the Geocoding API from Google Maps Platform (e.g., converts coordinates to addresses).
25 | * API client to connect with:
26 | * Places API --> Get places information from photos and addresses to opening hours
27 | * Routes API --> Get directions from place A to place B and more.
28 | * Geocoding API --> Convert coordinates to addresses and vice versa.
29 | * The app uses Riverpod as a state management solution with the state stored and update through providers and notifiers.
30 | * The UI is enhanced with animations using the flutter_animate library.
31 |
32 | ## Prerequisites
33 | Before you start, make sure you have the following:
34 | * Flutter 3.10 (or newer) and Dart 3.0 (or newer version)
35 | * [Google Cloud Platform account](https://console.cloud.google.com/)
36 |
--------------------------------------------------------------------------------
/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | # This file configures the analyzer, which statically analyzes Dart code to
2 | # check for errors, warnings, and lints.
3 | #
4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled
5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
6 | # invoked from the command line by running `flutter analyze`.
7 |
8 | # The following line activates a set of recommended lints for Flutter apps,
9 | # packages, and plugins designed to encourage good coding practices.
10 | include: package:flutter_lints/flutter.yaml
11 |
12 | linter:
13 | # The lint rules applied to this project can be customized in the
14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml`
15 | # included above or to enable additional rules. A list of all available lints
16 | # and their documentation is published at
17 | # https://dart-lang.github.io/linter/lints/index.html.
18 | #
19 | # Instead of disabling a lint rule for the entire project in the
20 | # section below, it can also be suppressed for a single line of code
21 | # or a specific dart file by using the `// ignore: name_of_lint` and
22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file
23 | # producing the lint.
24 | rules:
25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule
26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
27 |
28 | # Additional information about this file can be found at
29 | # https://dart.dev/guides/language/analysis-options
30 |
--------------------------------------------------------------------------------
/android/.gitignore:
--------------------------------------------------------------------------------
1 | gradle-wrapper.jar
2 | /.gradle
3 | /captures/
4 | /gradlew
5 | /gradlew.bat
6 | /local.properties
7 | GeneratedPluginRegistrant.java
8 |
9 | # Remember to never publicly share your keystore.
10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
11 | key.properties
12 | **/*.keystore
13 | **/*.jks
14 |
--------------------------------------------------------------------------------
/android/app/build.gradle:
--------------------------------------------------------------------------------
1 | def localProperties = new Properties()
2 | def localPropertiesFile = rootProject.file('local.properties')
3 | if (localPropertiesFile.exists()) {
4 | localPropertiesFile.withReader('UTF-8') { reader ->
5 | localProperties.load(reader)
6 | }
7 | }
8 |
9 | def flutterRoot = localProperties.getProperty('flutter.sdk')
10 | if (flutterRoot == null) {
11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
12 | }
13 |
14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
15 | if (flutterVersionCode == null) {
16 | flutterVersionCode = '1'
17 | }
18 |
19 | def flutterVersionName = localProperties.getProperty('flutter.versionName')
20 | if (flutterVersionName == null) {
21 | flutterVersionName = '1.0'
22 | }
23 |
24 | apply plugin: 'com.android.application'
25 | apply plugin: 'kotlin-android'
26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
27 |
28 | android {
29 | namespace "com.example.travel_routes"
30 | compileSdkVersion flutter.compileSdkVersion
31 | ndkVersion flutter.ndkVersion
32 |
33 | compileOptions {
34 | sourceCompatibility JavaVersion.VERSION_1_8
35 | targetCompatibility JavaVersion.VERSION_1_8
36 | }
37 |
38 | kotlinOptions {
39 | jvmTarget = '1.8'
40 | }
41 |
42 | sourceSets {
43 | main.java.srcDirs += 'src/main/kotlin'
44 | }
45 |
46 | defaultConfig {
47 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
48 | applicationId "com.example.travel_routes"
49 | // You can update the following values to match your application needs.
50 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
51 | minSdkVersion 20
52 | targetSdkVersion flutter.targetSdkVersion
53 | versionCode flutterVersionCode.toInteger()
54 | versionName flutterVersionName
55 | }
56 |
57 | buildTypes {
58 | release {
59 | // TODO: Add your own signing config for the release build.
60 | // Signing with the debug keys for now, so `flutter run --release` works.
61 | signingConfig signingConfigs.debug
62 | }
63 | }
64 | }
65 |
66 | flutter {
67 | source '../..'
68 | }
69 |
70 | dependencies {
71 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
72 | }
73 |
--------------------------------------------------------------------------------
/android/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
12 |
13 |
21 |
25 |
29 |
30 |
31 |
32 |
33 |
34 |
36 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/android/app/src/main/kotlin/com/example/travel_routes/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.example.travel_routes
2 |
3 | import io.flutter.embedding.android.FlutterActivity
4 |
5 | class MainActivity: FlutterActivity() {
6 | }
7 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-v21/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxonflutter/Travel-App-with-Flutter-Google-Maps-Platform/50771634dc7f6161861048f3c47a4944bf0279ea/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxonflutter/Travel-App-with-Flutter-Google-Maps-Platform/50771634dc7f6161861048f3c47a4944bf0279ea/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxonflutter/Travel-App-with-Flutter-Google-Maps-Platform/50771634dc7f6161861048f3c47a4944bf0279ea/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxonflutter/Travel-App-with-Flutter-Google-Maps-Platform/50771634dc7f6161861048f3c47a4944bf0279ea/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxonflutter/Travel-App-with-Flutter-Google-Maps-Platform/50771634dc7f6161861048f3c47a4944bf0279ea/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/values-night/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/android/app/src/profile/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | ext.kotlin_version = '1.7.10'
3 | repositories {
4 | google()
5 | mavenCentral()
6 | }
7 |
8 | dependencies {
9 | classpath 'com.android.tools.build:gradle:7.3.0'
10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
11 | }
12 | }
13 |
14 | allprojects {
15 | repositories {
16 | google()
17 | mavenCentral()
18 | }
19 | }
20 |
21 | rootProject.buildDir = '../build'
22 | subprojects {
23 | project.buildDir = "${rootProject.buildDir}/${project.name}"
24 | }
25 | subprojects {
26 | project.evaluationDependsOn(':app')
27 | }
28 |
29 | tasks.register("clean", Delete) {
30 | delete rootProject.buildDir
31 | }
32 |
--------------------------------------------------------------------------------
/android/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx1536M
2 | android.useAndroidX=true
3 | android.enableJetifier=true
4 |
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | zipStoreBase=GRADLE_USER_HOME
4 | zipStorePath=wrapper/dists
5 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
6 |
--------------------------------------------------------------------------------
/android/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
4 | def properties = new Properties()
5 |
6 | assert localPropertiesFile.exists()
7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
8 |
9 | def flutterSdkPath = properties.getProperty("flutter.sdk")
10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
12 |
--------------------------------------------------------------------------------
/ios/.gitignore:
--------------------------------------------------------------------------------
1 | **/dgph
2 | *.mode1v3
3 | *.mode2v3
4 | *.moved-aside
5 | *.pbxuser
6 | *.perspectivev3
7 | **/*sync/
8 | .sconsign.dblite
9 | .tags*
10 | **/.vagrant/
11 | **/DerivedData/
12 | Icon?
13 | **/Pods/
14 | **/.symlinks/
15 | profile
16 | xcuserdata
17 | **/.generated/
18 | Flutter/App.framework
19 | Flutter/Flutter.framework
20 | Flutter/Flutter.podspec
21 | Flutter/Generated.xcconfig
22 | Flutter/ephemeral/
23 | Flutter/app.flx
24 | Flutter/app.zip
25 | Flutter/flutter_assets/
26 | Flutter/flutter_export_environment.sh
27 | ServiceDefinitions.json
28 | Runner/GeneratedPluginRegistrant.*
29 |
30 | # Exceptions to above rules.
31 | !default.mode1v3
32 | !default.mode2v3
33 | !default.pbxuser
34 | !default.perspectivev3
35 |
--------------------------------------------------------------------------------
/ios/Flutter/AppFrameworkInfo.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | App
9 | CFBundleIdentifier
10 | io.flutter.flutter.app
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | App
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1.0
23 | MinimumOSVersion
24 | 12.0
25 |
26 |
27 |
--------------------------------------------------------------------------------
/ios/Flutter/Debug.xcconfig:
--------------------------------------------------------------------------------
1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
2 | #include "Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/ios/Flutter/Release.xcconfig:
--------------------------------------------------------------------------------
1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
2 | #include "Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/ios/Podfile:
--------------------------------------------------------------------------------
1 | # Uncomment this line to define a global platform for your project
2 | # platform :ios, '12.0'
3 |
4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true'
6 |
7 | project 'Runner', {
8 | 'Debug' => :debug,
9 | 'Profile' => :release,
10 | 'Release' => :release,
11 | }
12 |
13 | def flutter_root
14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
15 | unless File.exist?(generated_xcode_build_settings_path)
16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
17 | end
18 |
19 | File.foreach(generated_xcode_build_settings_path) do |line|
20 | matches = line.match(/FLUTTER_ROOT\=(.*)/)
21 | return matches[1].strip if matches
22 | end
23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
24 | end
25 |
26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
27 |
28 | flutter_ios_podfile_setup
29 |
30 | target 'Runner' do
31 | use_frameworks!
32 | use_modular_headers!
33 |
34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
35 | target 'RunnerTests' do
36 | inherit! :search_paths
37 | end
38 | end
39 |
40 | post_install do |installer|
41 | installer.pods_project.targets.each do |target|
42 | flutter_additional_ios_build_settings(target)
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/ios/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - Flutter (1.0.0)
3 | - geolocator_apple (1.2.0):
4 | - Flutter
5 | - google_maps_flutter_ios (0.0.1):
6 | - Flutter
7 | - GoogleMaps (< 8.0)
8 | - GoogleMaps (5.2.0):
9 | - GoogleMaps/Maps (= 5.2.0)
10 | - GoogleMaps/Base (5.2.0)
11 | - GoogleMaps/Maps (5.2.0):
12 | - GoogleMaps/Base
13 |
14 | DEPENDENCIES:
15 | - Flutter (from `Flutter`)
16 | - geolocator_apple (from `.symlinks/plugins/geolocator_apple/ios`)
17 | - google_maps_flutter_ios (from `.symlinks/plugins/google_maps_flutter_ios/ios`)
18 |
19 | SPEC REPOS:
20 | trunk:
21 | - GoogleMaps
22 |
23 | EXTERNAL SOURCES:
24 | Flutter:
25 | :path: Flutter
26 | geolocator_apple:
27 | :path: ".symlinks/plugins/geolocator_apple/ios"
28 | google_maps_flutter_ios:
29 | :path: ".symlinks/plugins/google_maps_flutter_ios/ios"
30 |
31 | SPEC CHECKSUMS:
32 | Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
33 | geolocator_apple: cc556e6844d508c95df1e87e3ea6fa4e58c50401
34 | google_maps_flutter_ios: abdac20d6ce8931f6ebc5f46616df241bfaa2cfd
35 | GoogleMaps: 025272d5876d3b32604e5c080dc25eaf68764693
36 |
37 | PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796
38 |
39 | COCOAPODS: 1.15.2
40 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
37 |
38 |
39 |
40 |
43 |
49 |
50 |
51 |
52 |
53 |
63 |
65 |
71 |
72 |
73 |
74 |
80 |
82 |
88 |
89 |
90 |
91 |
93 |
94 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/ios/Runner.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import Flutter
3 | import GoogleMaps
4 |
5 | @UIApplicationMain
6 | @objc class AppDelegate: FlutterAppDelegate {
7 | override func application(
8 | _ application: UIApplication,
9 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
10 | ) -> Bool {
11 | GMSServices.provideAPIKey("YOU_API_KEY")
12 | GeneratedPluginRegistrant.register(with: self)
13 | return super.application(application, didFinishLaunchingWithOptions: launchOptions)
14 | }
15 | }
16 |
17 |
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "20x20",
5 | "idiom" : "iphone",
6 | "filename" : "Icon-App-20x20@2x.png",
7 | "scale" : "2x"
8 | },
9 | {
10 | "size" : "20x20",
11 | "idiom" : "iphone",
12 | "filename" : "Icon-App-20x20@3x.png",
13 | "scale" : "3x"
14 | },
15 | {
16 | "size" : "29x29",
17 | "idiom" : "iphone",
18 | "filename" : "Icon-App-29x29@1x.png",
19 | "scale" : "1x"
20 | },
21 | {
22 | "size" : "29x29",
23 | "idiom" : "iphone",
24 | "filename" : "Icon-App-29x29@2x.png",
25 | "scale" : "2x"
26 | },
27 | {
28 | "size" : "29x29",
29 | "idiom" : "iphone",
30 | "filename" : "Icon-App-29x29@3x.png",
31 | "scale" : "3x"
32 | },
33 | {
34 | "size" : "40x40",
35 | "idiom" : "iphone",
36 | "filename" : "Icon-App-40x40@2x.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "40x40",
41 | "idiom" : "iphone",
42 | "filename" : "Icon-App-40x40@3x.png",
43 | "scale" : "3x"
44 | },
45 | {
46 | "size" : "60x60",
47 | "idiom" : "iphone",
48 | "filename" : "Icon-App-60x60@2x.png",
49 | "scale" : "2x"
50 | },
51 | {
52 | "size" : "60x60",
53 | "idiom" : "iphone",
54 | "filename" : "Icon-App-60x60@3x.png",
55 | "scale" : "3x"
56 | },
57 | {
58 | "size" : "20x20",
59 | "idiom" : "ipad",
60 | "filename" : "Icon-App-20x20@1x.png",
61 | "scale" : "1x"
62 | },
63 | {
64 | "size" : "20x20",
65 | "idiom" : "ipad",
66 | "filename" : "Icon-App-20x20@2x.png",
67 | "scale" : "2x"
68 | },
69 | {
70 | "size" : "29x29",
71 | "idiom" : "ipad",
72 | "filename" : "Icon-App-29x29@1x.png",
73 | "scale" : "1x"
74 | },
75 | {
76 | "size" : "29x29",
77 | "idiom" : "ipad",
78 | "filename" : "Icon-App-29x29@2x.png",
79 | "scale" : "2x"
80 | },
81 | {
82 | "size" : "40x40",
83 | "idiom" : "ipad",
84 | "filename" : "Icon-App-40x40@1x.png",
85 | "scale" : "1x"
86 | },
87 | {
88 | "size" : "40x40",
89 | "idiom" : "ipad",
90 | "filename" : "Icon-App-40x40@2x.png",
91 | "scale" : "2x"
92 | },
93 | {
94 | "size" : "76x76",
95 | "idiom" : "ipad",
96 | "filename" : "Icon-App-76x76@1x.png",
97 | "scale" : "1x"
98 | },
99 | {
100 | "size" : "76x76",
101 | "idiom" : "ipad",
102 | "filename" : "Icon-App-76x76@2x.png",
103 | "scale" : "2x"
104 | },
105 | {
106 | "size" : "83.5x83.5",
107 | "idiom" : "ipad",
108 | "filename" : "Icon-App-83.5x83.5@2x.png",
109 | "scale" : "2x"
110 | },
111 | {
112 | "size" : "1024x1024",
113 | "idiom" : "ios-marketing",
114 | "filename" : "Icon-App-1024x1024@1x.png",
115 | "scale" : "1x"
116 | }
117 | ],
118 | "info" : {
119 | "version" : 1,
120 | "author" : "xcode"
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxonflutter/Travel-App-with-Flutter-Google-Maps-Platform/50771634dc7f6161861048f3c47a4944bf0279ea/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxonflutter/Travel-App-with-Flutter-Google-Maps-Platform/50771634dc7f6161861048f3c47a4944bf0279ea/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxonflutter/Travel-App-with-Flutter-Google-Maps-Platform/50771634dc7f6161861048f3c47a4944bf0279ea/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxonflutter/Travel-App-with-Flutter-Google-Maps-Platform/50771634dc7f6161861048f3c47a4944bf0279ea/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxonflutter/Travel-App-with-Flutter-Google-Maps-Platform/50771634dc7f6161861048f3c47a4944bf0279ea/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxonflutter/Travel-App-with-Flutter-Google-Maps-Platform/50771634dc7f6161861048f3c47a4944bf0279ea/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxonflutter/Travel-App-with-Flutter-Google-Maps-Platform/50771634dc7f6161861048f3c47a4944bf0279ea/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxonflutter/Travel-App-with-Flutter-Google-Maps-Platform/50771634dc7f6161861048f3c47a4944bf0279ea/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxonflutter/Travel-App-with-Flutter-Google-Maps-Platform/50771634dc7f6161861048f3c47a4944bf0279ea/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxonflutter/Travel-App-with-Flutter-Google-Maps-Platform/50771634dc7f6161861048f3c47a4944bf0279ea/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxonflutter/Travel-App-with-Flutter-Google-Maps-Platform/50771634dc7f6161861048f3c47a4944bf0279ea/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxonflutter/Travel-App-with-Flutter-Google-Maps-Platform/50771634dc7f6161861048f3c47a4944bf0279ea/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxonflutter/Travel-App-with-Flutter-Google-Maps-Platform/50771634dc7f6161861048f3c47a4944bf0279ea/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxonflutter/Travel-App-with-Flutter-Google-Maps-Platform/50771634dc7f6161861048f3c47a4944bf0279ea/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxonflutter/Travel-App-with-Flutter-Google-Maps-Platform/50771634dc7f6161861048f3c47a4944bf0279ea/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "LaunchImage.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "LaunchImage@2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "LaunchImage@3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxonflutter/Travel-App-with-Flutter-Google-Maps-Platform/50771634dc7f6161861048f3c47a4944bf0279ea/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxonflutter/Travel-App-with-Flutter-Google-Maps-Platform/50771634dc7f6161861048f3c47a4944bf0279ea/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxonflutter/Travel-App-with-Flutter-Google-Maps-Platform/50771634dc7f6161861048f3c47a4944bf0279ea/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md:
--------------------------------------------------------------------------------
1 | # Launch Screen Assets
2 |
3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory.
4 |
5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
--------------------------------------------------------------------------------
/ios/Runner/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/ios/Runner/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/ios/Runner/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleDisplayName
8 | Travel Routes
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | travel_routes
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | $(FLUTTER_BUILD_NAME)
21 | CFBundleSignature
22 | ????
23 | CFBundleVersion
24 | $(FLUTTER_BUILD_NUMBER)
25 | LSRequiresIPhoneOS
26 |
27 | UILaunchStoryboardName
28 | LaunchScreen
29 | UIMainStoryboardFile
30 | Main
31 | UISupportedInterfaceOrientations
32 |
33 | UIInterfaceOrientationPortrait
34 | UIInterfaceOrientationLandscapeLeft
35 | UIInterfaceOrientationLandscapeRight
36 |
37 | UISupportedInterfaceOrientations~ipad
38 |
39 | UIInterfaceOrientationPortrait
40 | UIInterfaceOrientationPortraitUpsideDown
41 | UIInterfaceOrientationLandscapeLeft
42 | UIInterfaceOrientationLandscapeRight
43 |
44 | UIViewControllerBasedStatusBarAppearance
45 |
46 | CADisableMinimumFrameDurationOnPhone
47 |
48 | UIApplicationSupportsIndirectInputEvents
49 |
50 | NSLocationWhenInUseUsageDescription
51 | We need access to your location when using the app to provide location-based features.
52 |
53 | NSLocationAlwaysUsageDescription
54 | We need access to your location all the time to provide location-based features.
55 |
56 | NSLocationAlwaysAndWhenInUseUsageDescription
57 | We need access to your location all the time and when using the app to provide location-based features.
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/ios/Runner/Runner-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | #import "GeneratedPluginRegistrant.h"
2 |
--------------------------------------------------------------------------------
/ios/RunnerTests/RunnerTests.swift:
--------------------------------------------------------------------------------
1 | import Flutter
2 | import UIKit
3 | import XCTest
4 |
5 | class RunnerTests: XCTestCase {
6 |
7 | func testExample() {
8 | // If you add code to the Runner application, consider adding tests here.
9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest.
10 | }
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/lib/main.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_dotenv/flutter_dotenv.dart';
3 | import 'package:hooks_riverpod/hooks_riverpod.dart';
4 |
5 | import 'screens/destination_screen.dart';
6 | import 'screens/home_screen.dart';
7 | import 'screens/map_screen.dart';
8 | import 'screens/points_of_interest_screen.dart';
9 |
10 | Future main() async {
11 | await dotenv.load(fileName: ".env");
12 | runApp(const ProviderScope(child: MyApp()));
13 | }
14 |
15 | class MyApp extends StatelessWidget {
16 | const MyApp({super.key});
17 |
18 | @override
19 | Widget build(BuildContext context) {
20 | return MaterialApp(
21 | title: 'Flutter Demo',
22 | theme: ThemeData(
23 | colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
24 | useMaterial3: true,
25 | ),
26 | home: const HomeScreen(),
27 | routes: {
28 | '/destination': (context) => const DestinationScreen(),
29 | '/points-of-interest': (context) => const PointsOfInterestScreen(),
30 | '/map': (context) => const MapScreen(),
31 | },
32 | );
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/lib/models/destination.dart:
--------------------------------------------------------------------------------
1 | import 'package:google_maps_flutter/google_maps_flutter.dart';
2 |
3 | import 'point_of_interest.dart';
4 |
5 | class Destination {
6 | final String id;
7 | final String name;
8 | final LatLng latLng;
9 | final String description;
10 | final String imageUrl;
11 | final List additionalImages;
12 | final double rating;
13 | final int numReviews;
14 | final List pointsOfInterest;
15 |
16 | const Destination({
17 | required this.id,
18 | required this.name,
19 | required this.latLng,
20 | required this.description,
21 | required this.imageUrl,
22 | required this.additionalImages,
23 | required this.rating,
24 | required this.numReviews,
25 | required this.pointsOfInterest,
26 | });
27 |
28 | @override
29 | String toString() {
30 | return 'Destination(id: $id, name: $name, latLng: $latLng, description: $description, imageUrl: $imageUrl, additionalImages: $additionalImages, rating: $rating, numReviews: $numReviews, pointsOfInterest: $pointsOfInterest)';
31 | }
32 |
33 | static const List sampleDestinations = [
34 | Destination(
35 | id: '1',
36 | name: 'New York',
37 | latLng: LatLng(40.7128, -74.0060),
38 | description: 'The Big Apple',
39 | imageUrl:
40 | 'https://images.unsplash.com/photo-1500916434205-0c77489c6cf7?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=687&q=80',
41 | additionalImages: [
42 | 'https://images.unsplash.com/photo-1492666673288-3c4b4576ad9a?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1632&q=80',
43 | 'https://images.unsplash.com/photo-1492666673288-3c4b4576ad9a?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1632&q=80',
44 | 'https://images.unsplash.com/photo-1492666673288-3c4b4576ad9a?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1632&q=80',
45 | 'https://images.unsplash.com/photo-1492666673288-3c4b4576ad9a?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1632&q=80',
46 | ],
47 | rating: 4.8,
48 | numReviews: 1589,
49 | pointsOfInterest: [
50 | PointOfInterest(
51 | id: 'p1',
52 | name: 'Statue of Liberty',
53 | latLng: LatLng(40.6892, -74.0445),
54 | description:
55 | 'World-famous statue gifted by France as a symbol of freedom.',
56 | ),
57 | PointOfInterest(
58 | id: 'p2',
59 | name: 'Central Park',
60 | latLng: LatLng(40.7829, -73.9654),
61 | description:
62 | 'Urban oasis with ball fields, a zoo, & a carousel, plus formal gardens & more.',
63 | ),
64 | PointOfInterest(
65 | id: 'p3',
66 | name: 'Times Square',
67 | latLng: LatLng(40.7580, -73.9855),
68 | description:
69 | 'Bustling destination in the heart of the Theater District known for bright lights, shopping & shows.',
70 | ),
71 | ],
72 | ),
73 | Destination(
74 | id: '2',
75 | name: 'Paris',
76 | latLng: LatLng(48.8566, 2.3522),
77 | description: 'City of Love',
78 | imageUrl:
79 | 'https://images.unsplash.com/photo-1499856871958-5b9627545d1a?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1420&q=80',
80 | additionalImages: [
81 | 'https://images.unsplash.com/photo-1431274172761-fca41d930114?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1470&q=80',
82 | 'https://images.unsplash.com/photo-1431274172761-fca41d930114?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1470&q=80',
83 | 'https://images.unsplash.com/photo-1431274172761-fca41d930114?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1470&q=80',
84 | 'https://images.unsplash.com/photo-1431274172761-fca41d930114?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1470&q=80',
85 | ],
86 | rating: 4.7,
87 | numReviews: 1390,
88 | pointsOfInterest: [
89 | PointOfInterest(
90 | id: 'p4',
91 | name: 'Eiffel Tower',
92 | latLng: LatLng(48.8584, 2.2945),
93 | description:
94 | 'Iconic wrought-iron lattice tower offering expansive views of Paris.',
95 | ),
96 | PointOfInterest(
97 | id: 'p5',
98 | name: 'Louvre Museum',
99 | latLng: LatLng(48.8606, 2.3376),
100 | description:
101 | 'Historic palace housing huge art collection, from Roman sculptures to Da Vinci\'s "Mona Lisa."',
102 | ),
103 | PointOfInterest(
104 | id: 'p6',
105 | name: 'Cathédrale Notre-Dame de Paris',
106 | latLng: LatLng(48.8530, 2.3499),
107 | description:
108 | 'Iconic, 13th-century cathedral with flying buttresses & gargoyles, setting for Hugo\'s novel.',
109 | ),
110 | ],
111 | ),
112 | Destination(
113 | id: '3',
114 | name: 'Tokyo',
115 | latLng: LatLng(35.6895, 139.6917),
116 | description: 'Land of the Rising Sun',
117 | imageUrl:
118 | 'https://images.unsplash.com/photo-1513407030348-c983a97b98d8?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1472&q=80',
119 | additionalImages: [
120 | 'https://images.unsplash.com/photo-1544885935-98dd03b09034?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=687&q=80',
121 | 'https://images.unsplash.com/photo-1544885935-98dd03b09034?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=687&q=80',
122 | 'https://images.unsplash.com/photo-1544885935-98dd03b09034?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=687&q=80',
123 | 'https://images.unsplash.com/photo-1544885935-98dd03b09034?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=687&q=80',
124 | ],
125 | rating: 4.9,
126 | numReviews: 1789,
127 | pointsOfInterest: [
128 | PointOfInterest(
129 | id: 'p7',
130 | name: 'Mount Fuji',
131 | latLng: LatLng(35.3606, 138.7274),
132 | description:
133 | 'Iconic, active stratovolcano known for its symmetrical snow-capped peak & climbing routes.',
134 | ),
135 | PointOfInterest(
136 | id: 'p8',
137 | name: 'Tokyo Tower',
138 | latLng: LatLng(35.6586, 139.7454),
139 | description:
140 | 'Prominent steel tower offering observation decks with panoramic city views.',
141 | ),
142 | PointOfInterest(
143 | id: 'p9',
144 | name: 'Senso-ji',
145 | latLng: LatLng(35.7148, 139.7967),
146 | description:
147 | 'Tokyo\'s oldest temple, offering shopping, dining & a pagoda with views.',
148 | ),
149 | ],
150 | ),
151 | Destination(
152 | id: '4',
153 | name: 'Sydney',
154 | latLng: LatLng(-33.8688, 151.2093),
155 | description: 'Harbour City',
156 | imageUrl:
157 | 'https://images.unsplash.com/photo-1506973035872-a4ec16b8e8d9?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1470&q=80',
158 | additionalImages: [
159 | 'https://images.unsplash.com/photo-1598948485421-33a1655d3c18?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1074&q=80',
160 | 'https://images.unsplash.com/photo-1598948485421-33a1655d3c18?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1074&q=80',
161 | 'https://images.unsplash.com/photo-1598948485421-33a1655d3c18?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1074&q=80',
162 | 'https://images.unsplash.com/photo-1598948485421-33a1655d3c18?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1074&q=80',
163 | ],
164 | rating: 4.6,
165 | numReviews: 1890,
166 | pointsOfInterest: [
167 | PointOfInterest(
168 | id: 'p10',
169 | name: 'Sydney Opera House',
170 | latLng: LatLng(-33.8568, 151.2153),
171 | description:
172 | 'Iconic Sydney opera house with a distinctive sail-like design.',
173 | ),
174 | PointOfInterest(
175 | id: 'p11',
176 | name: 'Sydney Harbour Bridge',
177 | latLng: LatLng(-33.8523, 151.2108),
178 | description:
179 | 'Large steel arch bridge known for panoramic views of the city skyline.',
180 | ),
181 | PointOfInterest(
182 | id: 'p12',
183 | name: 'Bondi Beach',
184 | latLng: LatLng(-33.8908, 151.2743),
185 | description:
186 | 'Well-known beach attracting surfers to its large, curling waves, and hipsters to its organic eateries.',
187 | ),
188 | ],
189 | ),
190 | Destination(
191 | id: '5',
192 | name: 'Cape Town',
193 | latLng: LatLng(-33.9249, 18.4241),
194 | description: 'Mother City',
195 | imageUrl:
196 | 'https://images.unsplash.com/photo-1580060839134-75a5edca2e99?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1471&q=80',
197 | additionalImages: [
198 | 'https://images.unsplash.com/photo-1576485290814-1c72aa4bbb8e?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1470&q=80',
199 | 'https://images.unsplash.com/photo-1576485290814-1c72aa4bbb8e?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1470&q=80',
200 | 'https://images.unsplash.com/photo-1576485290814-1c72aa4bbb8e?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1470&q=80',
201 | 'https://images.unsplash.com/photo-1576485290814-1c72aa4bbb8e?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1470&q=80',
202 | ],
203 | rating: 4.8,
204 | numReviews: 1523,
205 | pointsOfInterest: [
206 | PointOfInterest(
207 | id: 'p13',
208 | name: 'Table Mountain',
209 | latLng: LatLng(-33.9249, 18.4241),
210 | description:
211 | 'Flat-topped mountain forming a prominent landmark overlooking the city of Cape Town.',
212 | ),
213 | PointOfInterest(
214 | id: 'p14',
215 | name: 'Robben Island',
216 | latLng: LatLng(-33.8076, 18.3713),
217 | description:
218 | 'Iconic island prison where Nelson Mandela was held, with tours led by ex-prisoners.',
219 | ),
220 | PointOfInterest(
221 | id: 'p15',
222 | name: 'Cape of Good Hope',
223 | latLng: LatLng(-34.3568, 18.4969),
224 | description:
225 | 'Scenic headland marking the southeastern corner of the Cape Peninsula.',
226 | ),
227 | ],
228 | ),
229 | ];
230 | }
231 |
--------------------------------------------------------------------------------
/lib/models/direction.dart:
--------------------------------------------------------------------------------
1 | import 'package:google_maps_flutter/google_maps_flutter.dart';
2 |
3 | class Direction {
4 | final List route;
5 | final String distanceText;
6 | final String durationText;
7 |
8 | Direction({
9 | required this.route,
10 | required this.distanceText,
11 | required this.durationText,
12 | });
13 |
14 | factory Direction.fromJson(Map json) {
15 | // print(json.keys);
16 | // final steps = json['routes'][0]['legs'][0]['steps'] as List;
17 | final polylinePoints = json['routes'][0]['polyline']['encodedPolyline'];
18 |
19 | return Direction(
20 | route: _decodePoly(polylinePoints),
21 | distanceText:
22 | (json['routes'][0]['distanceMeters'] / 1000).toStringAsFixed(1),
23 | durationText: (int.parse(
24 | json['routes'][0]['duration'].replaceAll(RegExp(r'[^0-9]'), ''),
25 | ) /
26 | 60)
27 | .round()
28 | .toString(),
29 | );
30 | }
31 |
32 | // The decodePoly function decodes a polyline that has been encoded using Google's
33 | // polyline algorithm, which is used for efficiently encoding a series of coordinates
34 | // into a string. The function accepts a string parameter "encoded" which
35 | // represents the encoded polyline string.
36 | static List _decodePoly(String encoded) {
37 | // We start by creating an empty list of LatLng objects.
38 | List poly = [];
39 | // These variables will be used to hold the index we're currently at in the encoded string,
40 | // the length of the encoded string, and the current latitude and longitude.
41 | int index = 0, len = encoded.length;
42 | int lat = 0, lng = 0;
43 |
44 | // We keep looping until we've processed the entire encoded string.
45 | while (index < len) {
46 | // These variables will be used to hold the current character's decoded value,
47 | // the amount to shift by, and the resulting value after shifting.
48 | int b, shift = 0, result = 0;
49 |
50 | // This loop decodes the latitude value.
51 | do {
52 | // Get the ASCII value of the character and subtract 63.
53 | b = encoded.codeUnitAt(index++) - 63;
54 |
55 | // Bitwise-OR the result with the value of the character, bitwise-ANDed with 31, shifted to
56 | // the left by the shift amount.
57 | result |= (b & 0x1f) << shift;
58 | // We're dealing with a base-32 number system, so increase the shift amount by 5 for the next iteration.
59 | shift += 5;
60 | }
61 | // If the ASCII value of the character is 32 or more, that means the latitude isn't done yet and
62 | // we need to continue to the next character.
63 | while (b >= 0x20);
64 |
65 | // Convert the result into a signed integer.
66 | int dlat = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1));
67 |
68 | // Add the latitude difference to the current latitude.
69 | lat += dlat;
70 |
71 | // The longitude is decoded exactly the same way as the latitude, but starting from where the latitude left off.
72 | shift = 0;
73 | result = 0;
74 | do {
75 | b = encoded.codeUnitAt(index++) - 63;
76 | result |= (b & 0x1f) << shift;
77 | shift += 5;
78 | } while (b >= 0x20);
79 |
80 | // Convert the result into a signed integer.
81 | int dlng = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1));
82 | // Add the longitude difference to the current longitude.
83 | lng += dlng;
84 |
85 | // The latitude and longitude are encoded as integers multiplied by 10^5.
86 | // We divide by 10^5 to get the actual latitude and longitude.
87 | LatLng p = LatLng(lat / 1E5, lng / 1E5);
88 |
89 | // Add the decoded latitude and longitude to the list.
90 | poly.add(p);
91 | }
92 |
93 | // Return the list of decoded latitude and longitude pairs.
94 | return poly;
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/lib/models/geometry.dart:
--------------------------------------------------------------------------------
1 | import 'location.dart';
2 | import 'viewport.dart';
3 |
4 | class Geometry {
5 | final Location location;
6 | final ViewPort viewport;
7 |
8 | const Geometry({
9 | required this.location,
10 | required this.viewport,
11 | });
12 |
13 | factory Geometry.fromJson(Map json) {
14 | return Geometry(
15 | location: Location.fromJson(json['location']),
16 | viewport: ViewPort.fromJson(json['viewport']),
17 | );
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/lib/models/location.dart:
--------------------------------------------------------------------------------
1 | class Location {
2 | final double lat;
3 | final double lng;
4 |
5 | const Location({
6 | required this.lat,
7 | required this.lng,
8 | });
9 |
10 | factory Location.fromJson(Map json) {
11 | return Location(
12 | lat: json['lat'],
13 | lng: json['lng'],
14 | );
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/lib/models/opening_hours.dart:
--------------------------------------------------------------------------------
1 | class OpeningHours {
2 | final bool? openNow;
3 | final List? periods;
4 | final String? type;
5 | final List? weekdayText;
6 |
7 | const OpeningHours({
8 | this.openNow,
9 | this.periods,
10 | this.type,
11 | this.weekdayText,
12 | });
13 |
14 | factory OpeningHours.fromJson(Map json) {
15 | return OpeningHours(
16 | openNow: json['open_now'],
17 | periods: (json['periods'] as List?)
18 | ?.map((i) => OpeningHoursPeriod.fromJson(i))
19 | .toList(),
20 | type: json['type'],
21 | weekdayText:
22 | (json['weekday_text'] as List?)?.map((e) => e as String).toList(),
23 | );
24 | }
25 | }
26 |
27 | class OpeningHoursPeriodDetail {
28 | final int day;
29 | final String time;
30 | final String? date;
31 | final bool? truncated;
32 |
33 | const OpeningHoursPeriodDetail({
34 | required this.day,
35 | required this.time,
36 | this.date,
37 | this.truncated,
38 | });
39 |
40 | factory OpeningHoursPeriodDetail.fromJson(Map json) {
41 | return OpeningHoursPeriodDetail(
42 | day: json['day'],
43 | time: json['time'],
44 | date: json['date'],
45 | truncated: json['truncated'],
46 | );
47 | }
48 | }
49 |
50 | class OpeningHoursPeriod {
51 | final OpeningHoursPeriodDetail open;
52 | final OpeningHoursPeriodDetail? close;
53 |
54 | const OpeningHoursPeriod({
55 | required this.open,
56 | this.close,
57 | });
58 |
59 | factory OpeningHoursPeriod.fromJson(Map json) {
60 | return OpeningHoursPeriod(
61 | open: OpeningHoursPeriodDetail.fromJson(json['open']),
62 | close: json['close'] != null
63 | ? OpeningHoursPeriodDetail.fromJson(json['close'])
64 | : null,
65 | );
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/lib/models/photo.dart:
--------------------------------------------------------------------------------
1 | class Photo {
2 | final String photoReference;
3 | final int height;
4 | final int width;
5 |
6 | const Photo({
7 | required this.photoReference,
8 | required this.height,
9 | required this.width,
10 | });
11 |
12 | factory Photo.fromJson(Map json) {
13 | return Photo(
14 | photoReference: json['photo_reference'],
15 | height: json['height'],
16 | width: json['width'],
17 | );
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/lib/models/photo_image.dart:
--------------------------------------------------------------------------------
1 | import 'dart:typed_data';
2 |
3 | class PhotoImage {
4 | final Uint8List imageData;
5 |
6 | const PhotoImage({required this.imageData});
7 | }
8 |
--------------------------------------------------------------------------------
/lib/models/place.dart:
--------------------------------------------------------------------------------
1 | import 'geometry.dart';
2 | import 'opening_hours.dart';
3 | import 'photo.dart';
4 | import 'review.dart';
5 |
6 | class Place {
7 | final String placeId;
8 | final String name;
9 | final String formattedAddress;
10 | final Geometry geometry;
11 | final List photos;
12 | final List? reviews;
13 | final OpeningHours? openingHours;
14 |
15 | const Place({
16 | required this.placeId,
17 | required this.name,
18 | required this.formattedAddress,
19 | required this.geometry,
20 | required this.photos,
21 | this.reviews,
22 | this.openingHours,
23 | });
24 |
25 | factory Place.fromJson(Map json) {
26 | return Place(
27 | placeId: json['place_id'],
28 | name: json['name'],
29 | formattedAddress: json['formatted_address'],
30 | geometry: Geometry.fromJson(json['geometry']),
31 | photos: (json['photos'] as List).map((i) => Photo.fromJson(i)).toList(),
32 | reviews:
33 | (json['reviews'] as List?)?.map((i) => Review.fromJson(i)).toList(),
34 | openingHours: json['opening_hours'] != null
35 | ? OpeningHours.fromJson(json['opening_hours'])
36 | : null,
37 | );
38 | }
39 |
40 | @override
41 | String toString() {
42 | return 'Place{placeId: $placeId, name: $name, formattedAddress: $formattedAddress, geometry: $geometry, photos: $photos, reviews: $reviews, openingHours: $openingHours}';
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/lib/models/place_autocomplete_prediction.dart:
--------------------------------------------------------------------------------
1 | class PlaceAutocompletePrediction {
2 | final String placeId;
3 | final String description;
4 |
5 | const PlaceAutocompletePrediction({
6 | required this.placeId,
7 | required this.description,
8 | });
9 |
10 | factory PlaceAutocompletePrediction.fromJson(
11 | Map json,
12 | ) {
13 | return PlaceAutocompletePrediction(
14 | placeId: json['place_id'],
15 | description: json['description'],
16 | );
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/lib/models/point_of_interest.dart:
--------------------------------------------------------------------------------
1 | import 'package:google_maps_flutter/google_maps_flutter.dart';
2 |
3 | class PointOfInterest {
4 | final String id;
5 | final String name;
6 | final LatLng latLng;
7 | final String description;
8 |
9 | const PointOfInterest({
10 | required this.id,
11 | required this.name,
12 | required this.latLng,
13 | required this.description,
14 | });
15 |
16 | @override
17 | String toString() {
18 | return 'PointOfInterest(id: $id, name: $name, latLng: $latLng, description: $description)';
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/lib/models/query_autocomplete_prediction.dart:
--------------------------------------------------------------------------------
1 | class QueryAutocompletePrediction {
2 | final String description;
3 |
4 | const QueryAutocompletePrediction({
5 | required this.description,
6 | });
7 |
8 | factory QueryAutocompletePrediction.fromJson(
9 | Map json,
10 | ) {
11 | return QueryAutocompletePrediction(
12 | description: json['description'],
13 | );
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/lib/models/review.dart:
--------------------------------------------------------------------------------
1 | class Review {
2 | final String authorName;
3 | final int rating;
4 | final String text;
5 | final String? language;
6 |
7 | const Review({
8 | required this.authorName,
9 | required this.rating,
10 | required this.text,
11 | this.language,
12 | });
13 |
14 | factory Review.fromJson(Map json) {
15 | return Review(
16 | authorName: json['author_name'],
17 | rating: json['rating'],
18 | text: json['text'],
19 | language: json['language'],
20 | );
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/lib/models/viewport.dart:
--------------------------------------------------------------------------------
1 | import 'location.dart';
2 |
3 | class ViewPort {
4 | final Location northeast;
5 | final Location southwest;
6 |
7 | const ViewPort({
8 | required this.northeast,
9 | required this.southwest,
10 | });
11 |
12 | factory ViewPort.fromJson(Map json) {
13 | return ViewPort(
14 | northeast: Location.fromJson(json['northeast']),
15 | southwest: Location.fromJson(json['southwest']),
16 | );
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/lib/repositories/destination_repository.dart:
--------------------------------------------------------------------------------
1 | import '../models/destination.dart';
2 | import 'package:riverpod_annotation/riverpod_annotation.dart';
3 |
4 | part 'destination_repository.g.dart';
5 |
6 | @riverpod
7 | DestinationRepository destinationRepository(DestinationRepositoryRef ref) =>
8 | DestinationRepository();
9 |
10 | class DestinationRepository {
11 | Future> getDestinations() async {
12 | return Future.delayed(
13 | const Duration(milliseconds: 300),
14 | () => Destination.sampleDestinations,
15 | );
16 | }
17 |
18 | Future getDestinationByName(String name) async {
19 | return Future.delayed(
20 | const Duration(milliseconds: 300),
21 | () => Destination.sampleDestinations
22 | .firstWhere((destination) => destination.name == name),
23 | );
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/lib/repositories/destination_repository.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'destination_repository.dart';
4 |
5 | // **************************************************************************
6 | // RiverpodGenerator
7 | // **************************************************************************
8 |
9 | String _$destinationRepositoryHash() =>
10 | r'f16b5d8b3b66914d775156e64900aca0712521c3';
11 |
12 | /// See also [destinationRepository].
13 | @ProviderFor(destinationRepository)
14 | final destinationRepositoryProvider =
15 | AutoDisposeProvider.internal(
16 | destinationRepository,
17 | name: r'destinationRepositoryProvider',
18 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
19 | ? null
20 | : _$destinationRepositoryHash,
21 | dependencies: null,
22 | allTransitiveDependencies: null,
23 | );
24 |
25 | typedef DestinationRepositoryRef
26 | = AutoDisposeProviderRef;
27 | // ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions
28 |
--------------------------------------------------------------------------------
/lib/repositories/maps_repository.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_dotenv/flutter_dotenv.dart';
2 | import 'package:geolocator/geolocator.dart';
3 | import 'package:google_maps_flutter/google_maps_flutter.dart';
4 | import 'package:riverpod_annotation/riverpod_annotation.dart';
5 | import 'package:travel_routes/models/direction.dart';
6 | import 'package:travel_routes/models/photo_image.dart';
7 |
8 | import '../models/place.dart';
9 | import '../models/place_autocomplete_prediction.dart';
10 | import '../services/api/geocoding_api_client.dart';
11 | import '../services/api/places_api_client.dart';
12 | import '../services/api/routes_api_client.dart';
13 | import '../services/location_service.dart';
14 |
15 | part 'maps_repository.g.dart';
16 |
17 | @riverpod
18 | MapsRepository mapsRepository(MapsRepositoryRef ref) => MapsRepository();
19 |
20 | class MapsRepository {
21 | final PlacesApiClient _placesApiClient;
22 | final RoutesApiClient _routesApiClient;
23 | final GeocodingApiClient _geocodingApiClient;
24 | final LocationService _locationService;
25 |
26 | MapsRepository({
27 | PlacesApiClient? placesApiClient,
28 | RoutesApiClient? routesApiClient,
29 | GeocodingApiClient? geocodingApiClient,
30 | LocationService? locationService,
31 | }) : _placesApiClient = placesApiClient ??
32 | PlacesApiClient(
33 | apiKey: dotenv.env['API_KEY']!,
34 | ),
35 | _routesApiClient = routesApiClient ??
36 | RoutesApiClient(
37 | apiKey: dotenv.env['API_KEY']!,
38 | ),
39 | _geocodingApiClient = geocodingApiClient ??
40 | GeocodingApiClient(
41 | apiKey: dotenv.env['API_KEY']!,
42 | ),
43 | _locationService = locationService ?? const LocationService();
44 |
45 | /// Takes an address and latitude-longitude (LatLng) as input and
46 | /// returns detailed information about the place and a photo of it.
47 | /// It first finds the place_id of the address and then fetches
48 | /// detailed information about the place including a photo of it.
49 | Future<(Place, PhotoImage)> getDetailedPlaceFromAddress(
50 | String address,
51 | LatLng latLng,
52 | ) async {
53 | final locationBias = 'circle:10000@${latLng.latitude},${latLng.longitude}';
54 |
55 | // Get the place_id from the address
56 | final place = await _placesApiClient.findPlaceFromText(
57 | address,
58 | locationBias,
59 | );
60 |
61 | // Get detailed place information from the place_id
62 | final placeDetails = await _placesApiClient.getDetailedPlace(place.placeId);
63 |
64 | // Get one picture from the place
65 | final placePhoto = await _placesApiClient.getPlacePhoto(
66 | placeDetails.photos[0].photoReference,
67 | placeDetails.photos[0].height,
68 | placeDetails.photos[0].width,
69 | );
70 |
71 | return (placeDetails, placePhoto);
72 | }
73 |
74 | /// This method provides real-time suggestions as users type in a search box.
75 | /// It improves the user experience by reducing typing errors and speeding up the search process.
76 | Future> getPlaceAutocompletePredictions(
77 | String input,
78 | ) async {
79 | final predictions = await _placesApiClient.getAutocompletePredictions(
80 | input,
81 | );
82 | // print(predictions.map((e) => e.description).toList());
83 | return predictions;
84 | }
85 |
86 | /// This method fetches directions from origin to destination.
87 | Future getDirections(
88 | String origin,
89 | String destination,
90 | ) async {
91 | final directions = await _routesApiClient.getDirections(
92 | origin,
93 | destination,
94 | );
95 |
96 | return directions;
97 | }
98 |
99 | /// Fetches the current location of the user.
100 | Future getCurrentUserLocation() async {
101 | final location = await _locationService.getCurrentPosition();
102 | return location;
103 | }
104 |
105 | /// Fetches the current address of the user.
106 | Future getCurrentUserAddress() async {
107 | final location = await _locationService.getCurrentPosition();
108 | if (location != null) {
109 | final geoData = await _geocodingApiClient.getAddress(
110 | location.latitude,
111 | location.longitude,
112 | );
113 |
114 | String? address = geoData['results'][0]['formatted_address'];
115 | return address;
116 | } else {
117 | return null;
118 | }
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/lib/repositories/maps_repository.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'maps_repository.dart';
4 |
5 | // **************************************************************************
6 | // RiverpodGenerator
7 | // **************************************************************************
8 |
9 | String _$mapsRepositoryHash() => r'3f58424001e8a01070286d4900d8db3ac9ef0edd';
10 |
11 | /// See also [mapsRepository].
12 | @ProviderFor(mapsRepository)
13 | final mapsRepositoryProvider = AutoDisposeProvider.internal(
14 | mapsRepository,
15 | name: r'mapsRepositoryProvider',
16 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
17 | ? null
18 | : _$mapsRepositoryHash,
19 | dependencies: null,
20 | allTransitiveDependencies: null,
21 | );
22 |
23 | typedef MapsRepositoryRef = AutoDisposeProviderRef;
24 | // ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions
25 |
--------------------------------------------------------------------------------
/lib/screens/destination_screen.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:google_maps_flutter/google_maps_flutter.dart';
5 | import 'package:hooks_riverpod/hooks_riverpod.dart';
6 |
7 | import '../state/providers/selected_destination_provider.dart';
8 | import 'error_screen.dart';
9 | import 'loading_screen.dart';
10 |
11 | class DestinationScreen extends ConsumerStatefulWidget {
12 | const DestinationScreen({super.key});
13 |
14 | @override
15 | ConsumerState createState() => _DestinationScreenState();
16 | }
17 |
18 | class _DestinationScreenState extends ConsumerState {
19 | final Completer _controller =
20 | Completer();
21 |
22 | @override
23 | Widget build(BuildContext context) {
24 | final size = MediaQuery.sizeOf(context);
25 | final theme = Theme.of(context);
26 | final name = ModalRoute.of(context)!.settings.arguments as String;
27 |
28 | return ref.watch(selectedDestinationProvider(name)).maybeWhen(
29 | orElse: () => const ErrorScreen(),
30 | error: (Object error, StackTrace stackTrace) => const ErrorScreen(),
31 | loading: () => const LoadingScreen(),
32 | data: (destination) {
33 | return Scaffold(
34 | appBar: AppBar(
35 | title: Column(
36 | children: [
37 | Text(
38 | 'Destination',
39 | style: theme.textTheme.bodySmall,
40 | ),
41 | const SizedBox(height: 4),
42 | Text(
43 | destination?.name ?? '',
44 | style: theme.textTheme.headlineSmall,
45 | ),
46 | ],
47 | ),
48 | ),
49 | body: SafeArea(
50 | child: Padding(
51 | padding: const EdgeInsets.only(
52 | left: 16.0,
53 | right: 16.0,
54 | top: 16.0,
55 | ),
56 | child: Column(
57 | crossAxisAlignment: CrossAxisAlignment.start,
58 | children: [
59 | Image.network(
60 | destination?.imageUrl ?? '',
61 | fit: BoxFit.cover,
62 | width: size.width,
63 | height: size.height * 0.30,
64 | ),
65 | const SizedBox(height: 4.0),
66 | SizedBox(
67 | height: 75,
68 | child: Row(
69 | children: destination!.additionalImages.map((image) {
70 | return Expanded(
71 | child: Padding(
72 | padding: const EdgeInsets.only(
73 | left: 2.0,
74 | right: 2.0,
75 | ),
76 | child: Image.network(image, fit: BoxFit.cover),
77 | ),
78 | );
79 | }).toList(),
80 | ),
81 | ),
82 | const SizedBox(height: 8.0),
83 | Text(
84 | 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
85 | style: theme.textTheme.bodyMedium,
86 | maxLines: 5,
87 | ),
88 | const SizedBox(height: 8.0),
89 | Expanded(
90 | child: GoogleMap(
91 | mapType: MapType.normal,
92 | myLocationButtonEnabled: false,
93 | initialCameraPosition: CameraPosition(
94 | target: destination.latLng,
95 | zoom: 10,
96 | ),
97 | onMapCreated: (GoogleMapController controller) {
98 | _controller.complete(controller);
99 | },
100 | ),
101 | ),
102 | const SizedBox(height: 8.0),
103 | FilledButton(
104 | style: FilledButton.styleFrom(
105 | minimumSize: const Size.fromHeight(48.0),
106 | shape: RoundedRectangleBorder(
107 | borderRadius: BorderRadius.circular(0.0),
108 | ),
109 | ),
110 | onPressed: () {
111 | Navigator.pushNamed(
112 | context,
113 | '/points-of-interest',
114 | arguments: destination.name,
115 | );
116 | },
117 | child: const Text('Find Attractions'),
118 | )
119 | ],
120 | ),
121 | ),
122 | ),
123 | );
124 | },
125 | );
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/lib/screens/error_screen.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | class ErrorScreen extends StatelessWidget {
4 | const ErrorScreen({super.key});
5 |
6 | @override
7 | Widget build(BuildContext context) {
8 | return const Scaffold(
9 | body: Center(child: Text('Something went wrong!')),
10 | );
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/lib/screens/home_screen.dart:
--------------------------------------------------------------------------------
1 | import 'package:carousel_slider/carousel_slider.dart';
2 | import 'package:flutter/material.dart';
3 | import 'package:flutter_animate/flutter_animate.dart';
4 | import 'package:hooks_riverpod/hooks_riverpod.dart';
5 |
6 | import '../models/destination.dart';
7 | import '../state/providers/all_destinations_provider.dart';
8 |
9 | class HomeScreen extends ConsumerWidget {
10 | const HomeScreen({super.key});
11 |
12 | @override
13 | Widget build(BuildContext context, WidgetRef ref) {
14 | final size = MediaQuery.of(context).size;
15 | final theme = Theme.of(context);
16 | final destinations = ref.watch(allDestinationsProvider);
17 |
18 | return Scaffold(
19 | body: SafeArea(
20 | child: Column(
21 | crossAxisAlignment: CrossAxisAlignment.center,
22 | children: [
23 | const Spacer(),
24 | Text(
25 | 'Let\'s choose your travel route',
26 | textAlign: TextAlign.center,
27 | style: theme.textTheme.displaySmall!.copyWith(
28 | fontWeight: FontWeight.bold,
29 | ),
30 | )
31 | .animate(onPlay: (controller) => controller.repeat())
32 | .shimmer(
33 | duration: 2400.ms,
34 | color: theme.colorScheme.primary,
35 | )
36 | .animate()
37 | .fadeIn(duration: 1200.ms, curve: Curves.easeOutQuad)
38 | .slide(),
39 | const Spacer(),
40 | CarouselSlider(
41 | options: CarouselOptions(
42 | height: size.height * 0.66,
43 | enlargeCenterPage: true,
44 | ),
45 | items: destinations.when(
46 | data: (destinations) {
47 | return destinations.map(
48 | (destination) {
49 | return DestinationCard(
50 | destination: destination,
51 | );
52 | },
53 | ).toList();
54 | },
55 | loading: () => [
56 | const Center(child: CircularProgressIndicator()),
57 | ],
58 | error: (err, stack) => [Text('Error: $err')],
59 | ),
60 | ).animate().fadeIn(duration: 1200.ms, curve: Curves.easeOutQuad),
61 | const Spacer(),
62 | ],
63 | ),
64 | ),
65 | );
66 | }
67 | }
68 |
69 | class DestinationCard extends StatelessWidget {
70 | const DestinationCard({super.key, required this.destination});
71 |
72 | final Destination destination;
73 |
74 | @override
75 | Widget build(BuildContext context) {
76 | return InkWell(
77 | onTap: () {
78 | Navigator.pushNamed(
79 | context,
80 | '/destination',
81 | arguments: destination.name,
82 | );
83 | },
84 | child: Card(
85 | shape: RoundedRectangleBorder(
86 | borderRadius: BorderRadius.circular(0.0),
87 | ),
88 | child: Stack(
89 | children: [
90 | Positioned.fill(
91 | child: Image.network(
92 | destination.imageUrl,
93 | fit: BoxFit.cover,
94 | ),
95 | ).animate().shimmer(duration: 1200.ms),
96 | Align(
97 | alignment: Alignment.center,
98 | child: Text(
99 | destination.name,
100 | textAlign: TextAlign.center,
101 | style: Theme.of(context)
102 | .textTheme
103 | .displayLarge!
104 | .copyWith(color: Colors.white, fontWeight: FontWeight.bold),
105 | ),
106 | ),
107 | ],
108 | ),
109 | ),
110 | );
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/lib/screens/loading_screen.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | class LoadingScreen extends StatelessWidget {
4 | const LoadingScreen({super.key});
5 |
6 | @override
7 | Widget build(BuildContext context) {
8 | return const Scaffold(
9 | body: Center(child: CircularProgressIndicator()),
10 | );
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/lib/screens/map_screen.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:flutter_animate/flutter_animate.dart';
5 | import 'package:google_maps_flutter/google_maps_flutter.dart';
6 | import 'package:hooks_riverpod/hooks_riverpod.dart';
7 | import '../models/point_of_interest.dart';
8 | import '../state/providers/place_details_provider.dart';
9 |
10 | import '../state/providers/directions_provider.dart';
11 | import 'error_screen.dart';
12 | import 'loading_screen.dart';
13 |
14 | class MapScreen extends ConsumerStatefulWidget {
15 | const MapScreen({super.key});
16 |
17 | @override
18 | ConsumerState createState() => _MapScreenState();
19 | }
20 |
21 | class _MapScreenState extends ConsumerState {
22 | final Completer _controller =
23 | Completer();
24 |
25 | @override
26 | Widget build(BuildContext context) {
27 | final size = MediaQuery.sizeOf(context);
28 | final theme = Theme.of(context);
29 | final arguments =
30 | ModalRoute.of(context)!.settings.arguments as Map;
31 |
32 | final origin = arguments['origin'];
33 | final destination = arguments['destination'] as PointOfInterest;
34 |
35 | return ref.watch(directionsProvider(origin, destination.name)).maybeWhen(
36 | orElse: () => const ErrorScreen(),
37 | error: (Object error, StackTrace stackTrace) => const ErrorScreen(),
38 | loading: () => const LoadingScreen(),
39 | data: (directions) {
40 | return Scaffold(
41 | appBar: AppBar(
42 | backgroundColor: Colors.transparent,
43 | title: Text(
44 | destination.name,
45 | style: theme.textTheme.headlineSmall!
46 | .copyWith(fontWeight: FontWeight.bold),
47 | ),
48 | ),
49 | extendBodyBehindAppBar: true,
50 | body: Stack(
51 | children: [
52 | SizedBox(
53 | height: size.height,
54 | child: GoogleMap(
55 | mapType: MapType.normal,
56 | myLocationButtonEnabled: false,
57 | polylines: {
58 | Polyline(
59 | polylineId: const PolylineId('route1'),
60 | visible: true,
61 | points: directions.route,
62 | color: theme.colorScheme.secondary,
63 | width: 5,
64 | )
65 | },
66 | onMapCreated: (GoogleMapController controller) {
67 | _controller.complete(controller);
68 | },
69 | initialCameraPosition: CameraPosition(
70 | target: LatLng(
71 | directions.route[0].latitude,
72 | directions.route[0].longitude,
73 | ),
74 | zoom: 12,
75 | ),
76 | ),
77 | ),
78 | Positioned(
79 | bottom: 48.0,
80 | left: 16.0,
81 | right: 16.0,
82 | child: Column(
83 | crossAxisAlignment: CrossAxisAlignment.start,
84 | children: [
85 | Card(
86 | shape: RoundedRectangleBorder(
87 | borderRadius: BorderRadius.circular(0.0),
88 | ),
89 | child: Container(
90 | width: size.width * 0.33,
91 | padding: const EdgeInsets.all(8.0),
92 | child: Row(
93 | children: [
94 | const Icon(Icons.directions),
95 | const SizedBox(width: 8.0),
96 | Text(
97 | directions.distanceText,
98 | style: theme.textTheme.headlineMedium!
99 | .copyWith(fontWeight: FontWeight.bold),
100 | ),
101 | const SizedBox(width: 4.0),
102 | const Text('km')
103 | ],
104 | ),
105 | ),
106 | )
107 | .animate()
108 | .fadeIn(duration: 1200.ms, curve: Curves.easeOutQuad)
109 | .slide(
110 | begin: const Offset(1, 0),
111 | end: const Offset(0, 0),
112 | ),
113 | Card(
114 | shape: RoundedRectangleBorder(
115 | borderRadius: BorderRadius.circular(0.0),
116 | ),
117 | child: Container(
118 | width: size.width * 0.33,
119 | padding: const EdgeInsets.all(8.0),
120 | child: Row(
121 | children: [
122 | const Icon(Icons.schedule),
123 | const SizedBox(width: 8.0),
124 | Text(
125 | directions.durationText,
126 | style: theme.textTheme.headlineMedium!
127 | .copyWith(fontWeight: FontWeight.bold),
128 | ),
129 | const SizedBox(width: 4.0),
130 | const Text('mins')
131 | ],
132 | ),
133 | ),
134 | )
135 | .animate()
136 | .fadeIn(duration: 1200.ms, curve: Curves.easeOutQuad)
137 | .slide(
138 | begin: const Offset(1, 0),
139 | end: const Offset(0, 0),
140 | ),
141 | const SizedBox(height: 8),
142 | PointOfInterestDetails(
143 | size: size,
144 | theme: theme,
145 | destination: destination,
146 | )
147 | .animate()
148 | .fadeIn(duration: 1200.ms, curve: Curves.easeOutQuad)
149 | .slide(
150 | begin: const Offset(0, 1),
151 | end: const Offset(0, 0),
152 | ),
153 | ],
154 | ),
155 | ),
156 | ],
157 | ),
158 | );
159 | });
160 | }
161 | }
162 |
163 | class PointOfInterestDetails extends ConsumerWidget {
164 | const PointOfInterestDetails({
165 | super.key,
166 | required this.size,
167 | required this.theme,
168 | required this.destination,
169 | });
170 |
171 | final Size size;
172 | final ThemeData theme;
173 | final PointOfInterest destination;
174 |
175 | @override
176 | Widget build(BuildContext context, WidgetRef ref) {
177 | return ref
178 | .watch(placeDetailsProvider(destination.name, destination.latLng))
179 | .maybeWhen(
180 | orElse: () => const SizedBox(),
181 | data: (placeDetails) {
182 | return Card(
183 | shape: RoundedRectangleBorder(
184 | borderRadius: BorderRadius.circular(0.0),
185 | ),
186 | child: Padding(
187 | padding: const EdgeInsets.all(8.0),
188 | child: Row(
189 | children: [
190 | Image.memory(
191 | placeDetails.$2.imageData,
192 | fit: BoxFit.cover,
193 | width: size.width * 0.25,
194 | height: size.width * 0.25,
195 | ).animate().shimmer(duration: 1000.ms),
196 | const SizedBox(width: 8.0),
197 | Expanded(
198 | child: Column(
199 | crossAxisAlignment: CrossAxisAlignment.start,
200 | children: [
201 | Text(
202 | placeDetails.$1.name,
203 | style: theme.textTheme.bodyLarge!.copyWith(
204 | fontWeight: FontWeight.bold,
205 | ),
206 | ),
207 | Text(placeDetails.$1.formattedAddress),
208 | ],
209 | ),
210 | )
211 | ],
212 | ),
213 | ),
214 | );
215 | },
216 | );
217 | }
218 | }
219 |
--------------------------------------------------------------------------------
/lib/screens/points_of_interest_screen.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:google_maps_flutter/google_maps_flutter.dart';
5 | import 'package:hooks_riverpod/hooks_riverpod.dart';
6 |
7 | import '../models/place_autocomplete_prediction.dart';
8 | import '../models/point_of_interest.dart';
9 | import '../state/notifiers/user_location_provider.dart';
10 | import '../state/providers/place_autocomplete_predictions_provider.dart';
11 | import '../state/providers/selected_destination_provider.dart';
12 | import '../state/providers/user_current_location_provider.dart';
13 | import 'error_screen.dart';
14 | import 'loading_screen.dart';
15 |
16 | class PointsOfInterestScreen extends ConsumerStatefulWidget {
17 | const PointsOfInterestScreen({super.key});
18 |
19 | @override
20 | ConsumerState createState() =>
21 | _PointsOfInterestScreenState();
22 | }
23 |
24 | class _PointsOfInterestScreenState
25 | extends ConsumerState {
26 | final Completer _controller =
27 | Completer();
28 |
29 | @override
30 | Widget build(BuildContext context) {
31 | Size size = MediaQuery.sizeOf(context);
32 | final name = ModalRoute.of(context)!.settings.arguments as String;
33 |
34 | return ref.watch(selectedDestinationProvider(name)).maybeWhen(
35 | orElse: () => const ErrorScreen(),
36 | error: (Object error, StackTrace stackTrace) => const ErrorScreen(),
37 | loading: () => const LoadingScreen(),
38 | data: (destination) {
39 | final userLocation = ref.watch(userLocationProvider);
40 | final placePredictionsAsyncValue = ref.watch(
41 | placeAutocompletePredictionsProvider(userLocation),
42 | );
43 |
44 | final markers = destination!.pointsOfInterest.map(
45 | (pointOfInterest) {
46 | return Marker(
47 | markerId: MarkerId(pointOfInterest.name),
48 | position: pointOfInterest.latLng,
49 | );
50 | },
51 | ).toSet();
52 |
53 | return Scaffold(
54 | extendBodyBehindAppBar: true,
55 | body: SingleChildScrollView(
56 | child: Column(
57 | crossAxisAlignment: CrossAxisAlignment.start,
58 | children: [
59 | Stack(
60 | children: [
61 | SizedBox(
62 | height: size.height * 0.5,
63 | child: GoogleMap(
64 | mapType: MapType.normal,
65 | myLocationButtonEnabled: false,
66 | markers: markers,
67 | initialCameraPosition: CameraPosition(
68 | target: destination.latLng,
69 | zoom: 12,
70 | ),
71 | onMapCreated: (GoogleMapController controller) {
72 | _controller.complete(controller);
73 | },
74 | ),
75 | ),
76 | Positioned(
77 | top: 56.0,
78 | left: 16.0,
79 | right: 16.0,
80 | child: SizedBox(
81 | height: 64,
82 | child: AutocompleteSearchWidget(
83 | placePredictionsAsyncValue:
84 | placePredictionsAsyncValue,
85 | ),
86 | ),
87 | ),
88 | ],
89 | ),
90 | Container(
91 | padding: const EdgeInsets.all(16.0),
92 | child: Row(
93 | children: [
94 | Image.network(
95 | destination.imageUrl,
96 | fit: BoxFit.cover,
97 | height: 120,
98 | width: 160,
99 | ),
100 | const SizedBox(width: 16.0),
101 | Column(
102 | crossAxisAlignment: CrossAxisAlignment.start,
103 | children: [
104 | Text(
105 | destination.name,
106 | style:
107 | Theme.of(context).textTheme.headlineSmall,
108 | ),
109 | const SizedBox(height: 4),
110 | Text(
111 | destination.description,
112 | style: Theme.of(context).textTheme.bodyMedium,
113 | ),
114 | ],
115 | ),
116 | ],
117 | ),
118 | ),
119 | PointsOfInterestWidget(
120 | pointsOfInterest: destination.pointsOfInterest,
121 | userLocation: userLocation,
122 | mapController: _controller,
123 | ),
124 | ],
125 | ),
126 | ),
127 | );
128 | },
129 | );
130 | }
131 | }
132 |
133 | class AutocompleteSearchWidget extends ConsumerWidget {
134 | const AutocompleteSearchWidget({
135 | super.key,
136 | required this.placePredictionsAsyncValue,
137 | });
138 |
139 | final AsyncValue>
140 | placePredictionsAsyncValue;
141 |
142 | @override
143 | Widget build(BuildContext context, WidgetRef ref) {
144 | return RawAutocomplete(
145 | optionsBuilder: (TextEditingValue textEditingValue) {
146 | if (textEditingValue.text == '') {
147 | return const Iterable.empty();
148 | }
149 | return placePredictionsAsyncValue.when(
150 | data: (data) => data,
151 | loading: () => [],
152 | error: (error, stack) => [],
153 | );
154 | },
155 | displayStringForOption: (option) => option.description,
156 | fieldViewBuilder: (
157 | BuildContext context,
158 | TextEditingController textEditingController,
159 | FocusNode focusNode,
160 | VoidCallback onFieldSubmitted,
161 | ) {
162 | return TextFormField(
163 | controller: textEditingController,
164 | focusNode: focusNode,
165 | onFieldSubmitted: (String value) {
166 | onFieldSubmitted();
167 | },
168 | onChanged: (value) {
169 | ref.read(userLocationProvider.notifier).setUserLocation(value);
170 | },
171 | decoration: InputDecoration(
172 | border: InputBorder.none,
173 | fillColor: Theme.of(context).colorScheme.surface,
174 | filled: true,
175 | labelText: 'Choose location',
176 | prefixIcon: IconButton(
177 | onPressed: () async {
178 | final currentLocation = await ref.read(
179 | userCurrentLocationProvider.future,
180 | );
181 |
182 | if (currentLocation != null) {
183 | ref
184 | .read(userLocationProvider.notifier)
185 | .setUserLocation(currentLocation);
186 |
187 | textEditingController.text = currentLocation;
188 | }
189 | },
190 | icon: const Icon(Icons.location_on),
191 | ),
192 | suffixIcon: IconButton(
193 | onPressed: () {
194 | textEditingController.clear();
195 | },
196 | icon: const Icon(Icons.clear),
197 | ),
198 | ),
199 | );
200 | },
201 | optionsViewBuilder: (
202 | BuildContext context,
203 | AutocompleteOnSelected onSelected,
204 | Iterable options,
205 | ) {
206 | return Align(
207 | alignment: Alignment.topLeft,
208 | child: Container(
209 | color: Theme.of(context).colorScheme.surface,
210 | width: MediaQuery.of(context).size.width * 0.75,
211 | child: ListView.separated(
212 | shrinkWrap: true,
213 | padding: const EdgeInsets.all(16.0),
214 | itemCount: options.length,
215 | separatorBuilder: (context, index) {
216 | return const SizedBox(height: 16.0);
217 | },
218 | itemBuilder: (BuildContext context, int index) {
219 | final PlaceAutocompletePrediction option =
220 | options.elementAt(index);
221 | return GestureDetector(
222 | onTap: () {
223 | ref
224 | .read(userLocationProvider.notifier)
225 | .setUserLocation(option.description);
226 | return onSelected(option);
227 | },
228 | child: Text(
229 | option.description,
230 | style: Theme.of(context).textTheme.bodyMedium,
231 | ),
232 | );
233 | },
234 | ),
235 | ),
236 | );
237 | },
238 | );
239 | }
240 | }
241 |
242 | class PointsOfInterestWidget extends StatelessWidget {
243 | const PointsOfInterestWidget({
244 | super.key,
245 | required this.pointsOfInterest,
246 | required this.userLocation,
247 | required this.mapController,
248 | });
249 |
250 | final List pointsOfInterest;
251 | final String userLocation;
252 | final Completer mapController;
253 |
254 | @override
255 | Widget build(BuildContext context) {
256 | return Column(
257 | children: pointsOfInterest.map(
258 | (pointOfInterest) {
259 | return Container(
260 | margin: const EdgeInsets.only(bottom: 8.0),
261 | child: ListTile(
262 | tileColor: Colors.grey.shade100,
263 | leading: const Icon(Icons.attractions),
264 | title: Text(
265 | pointOfInterest.name,
266 | maxLines: 1,
267 | ),
268 | subtitle: Text(
269 | pointOfInterest.description,
270 | maxLines: 2,
271 | ),
272 | onTap: () async {
273 | if (userLocation.length > 5) {
274 | Navigator.pushNamed(
275 | context,
276 | '/map',
277 | arguments: {
278 | 'origin': userLocation,
279 | 'destination': pointOfInterest,
280 | },
281 | );
282 | } else {
283 | final GoogleMapController controller =
284 | await mapController.future;
285 | controller.animateCamera(
286 | CameraUpdate.newCameraPosition(
287 | CameraPosition(
288 | zoom: 14,
289 | target: pointOfInterest.latLng,
290 | ),
291 | ),
292 | );
293 | }
294 | },
295 | ),
296 | );
297 | },
298 | ).toList(),
299 | );
300 | }
301 | }
302 |
--------------------------------------------------------------------------------
/lib/services/api/geocoding_api_client.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 |
3 | import 'package:http/http.dart' as http;
4 |
5 | class GeocodingApiClient {
6 | final String _baseUrl;
7 | final http.Client _httpClient;
8 | final String _apiKey;
9 |
10 | GeocodingApiClient({
11 | http.Client? httpClient,
12 | required String apiKey,
13 | }) : this._(
14 | baseUrl: 'https://maps.googleapis.com/maps/api/geocode',
15 | httpClient: httpClient,
16 | apiKey: apiKey,
17 | );
18 |
19 | GeocodingApiClient._({
20 | required String baseUrl,
21 | http.Client? httpClient,
22 | required String apiKey,
23 | }) : _baseUrl = baseUrl,
24 | _httpClient = httpClient ?? http.Client(),
25 | _apiKey = apiKey;
26 |
27 | // https://developers.google.com/maps/documentation/geocoding/requests-geocoding
28 | Future