|
| 1 | +# Exercise 2 |
| 2 | + |
| 3 | +Learn to compose layouts, call a web-service and integrate Google Maps |
| 4 | + |
| 5 | +The app goal is to display agencies of in-Tact and Atecna as a list and as markers on a map. Currently however, both views are replaced by placeholders. |
| 6 | + |
| 7 | +Initial | List | Map |
| 8 | +--------|------|------ |
| 9 | +|| |
| 10 | + |
| 11 | +## Goals |
| 12 | + |
| 13 | +### Agency list |
| 14 | + |
| 15 | +List items follows the [Material specs](https://material.io/components/lists#specs) with these elements: |
| 16 | + |
| 17 | + |
| 18 | + |
| 19 | +Take a look at the [Cookbook](https://flutter.dev/docs/cookbook/lists/long-lists) for some pointers on how to create list in Flutter. |
| 20 | + |
| 21 | +*Sideline stuff* |
| 22 | + |
| 23 | +`agency.company` is represented by an enum `Company`. Both `label` and `logo` properties have been added to this enum with extension properties. |
| 24 | + |
| 25 | +These could obviously have been represented by a proper class (it might even make more sense), but it is nonetheless interesting to see how extensions are done with Dart. |
| 26 | + |
| 27 | +Take a look at [agency.dart](lib/domain/agency.dart) to see how it is done. |
| 28 | + |
| 29 | +### Agency map |
| 30 | + |
| 31 | +The map displays all agencies at the coordinates provided by `agency.position` and use the logo of their company as an icon. Clicking an agency opens a popup with the name of the city (`agency.city`) and the name of the company (`agency.company.label`). |
| 32 | + |
| 33 | + |
| 34 | + |
| 35 | +API keys have been prepared for Google Maps on the different platforms, but you are free to use Mapbox or any other provider of your preference |
| 36 | + |
| 37 | +Platform| Google Maps API key |
| 38 | +--------|--------------------- |
| 39 | +Android | AIzaSyB0_z7TX7gwWptMi7CaPc5WMKyNtGzJO08 |
| 40 | +iOS | AIzaSyDork6umsMWcEecc4AbN27oKsYeztc2Hu4 |
| 41 | +Web | AIzaSyBQcpEMvCU4K9cD9OIxdDfDPwgr48fyiK4 |
| 42 | + |
| 43 | +*LatLng and other Maps related classes* |
| 44 | + |
| 45 | +To prevent having to convert our own `LatLng` (used by `agency.position`) to Google `LatLng`, you can simply comment all code from [map_domain.dart](lib/domain/map_domain.dart) and replace it by |
| 46 | + |
| 47 | +``` |
| 48 | +export 'package:google_maps_flutter/google_maps_flutter.dart'; |
| 49 | +``` |
| 50 | + |
| 51 | +This will import the classes provided by Google Maps in all files that import `map_domain.dart` |
| 52 | + |
| 53 | +*Resources* |
| 54 | + |
| 55 | +Helpful methods have been added to [map_utils.dart](lib/screen/dashboard/map/map_utils.dart): |
| 56 | + |
| 57 | + - `boundsFromLatLngList()` to create a `LatLngBounds` from a list of `LatLng`. This can be used to ensure all agencies are visible |
| 58 | + - `getBitmapDescriptorFromAssetBytes()` to prepare a marker icon from an asset image |
| 59 | + |
| 60 | +*Sideline stuff* |
| 61 | + |
| 62 | +Google Maps on Flutter is implemented as a [Platform View](https://flutter.dev/docs/development/platform-integration/platform-views) for Android and iOS. This means the view is rendered natively by the underlying platform, which comes with some caveats that you should learn about in the linked documentation. You can also take a look at the documentation of [`AndroidView`](https://api.flutter.dev/flutter/widgets/AndroidView-class.html) and [`UiKitView`](https://api.flutter.dev/flutter/widgets/UiKitView-class.html). |
| 63 | + |
| 64 | + |
| 65 | +:bangbang: Google Maps integration can be a bit messy, don't forget to check the [Troubleshooting](#Troubleshooting) sections if you get into an issue |
| 66 | + |
| 67 | +### Agency details |
| 68 | + |
| 69 | +Clicking on a list item opens the details screen of the agency. At first, this screen will only display the name of the agency. |
| 70 | + |
| 71 | + |
| 72 | + |
| 73 | +You will notice that the screen looks a bit broken. This is because no theme are provided to the screen, and the default style of a `TextField` is intentionaly ugly to make sure the developers fix that. This is easily fixed by wrapping the screen inside a Material widget like `Scaffold`. |
| 74 | + |
| 75 | +#### Navigation |
| 76 | + |
| 77 | +There is two [navigation and routing](https://flutter.dev/docs/development/ui/navigation) mechanisms in Flutter, and both can be used concurrently in different places of the same application. |
| 78 | + |
| 79 | +Navigator 1.0 is original mechanism, and uses an imperative approach that is usually easier to grasp. This is the mechanism explained in the Flutter Cookbook recipes: |
| 80 | + |
| 81 | +- [Navigate to a new screen and back](https://flutter.dev/docs/cookbook/navigation/navigation-basics) |
| 82 | +- [Navigate with named routes](https://flutter.dev/docs/cookbook/navigation/named-routes.html) |
| 83 | +- [Pass arguments to a named route](https://flutter.dev/docs/cookbook/navigation/navigate-with-arguments) |
| 84 | + |
| 85 | +Navigator 2.0 was introduced in 2020 and uses a declarative approach that is closer to the rest of Flutter philosophy, but can be a bit harder to grasp. With Navigator 2.0, navigation is part of the application state and we can manipulate the whole screen/page stack. This is not possible with Navigator 1.0 that can only push and pop one screen/page at a time. |
| 86 | + |
| 87 | +[Learning Flutter’s new navigation and routing system](https://medium.com/flutter/learning-flutters-new-navigation-and-routing-system-7c9068155ade) |
| 88 | + |
| 89 | +You are free to choose the approach, knowing that the corrections are based on Navigator 1.0 (I'll try to implement a Navigator 2.0 approach later). |
| 90 | + |
| 91 | +:medal_sports: Bonus point if you are able to manage urls like http://localhost:1234/agency/2 to open the agency details screen with Flutter Web :) |
| 92 | + |
| 93 | +#### Details layout |
| 94 | + |
| 95 | +Here are the design elements for the agency details screen |
| 96 | + |
| 97 | +Content |Blueprint |
| 98 | +----------------------------------------------|-------------------------------------------------- |
| 99 | +| |
| 100 | + |
| 101 | +`fetchLorem()` is provided by [lorem_fetcher.dart](lib/utils/lorem_fetcher.dart). This method returns a `Future<List<String>>`, each string representing a paragraph. The future has a small delay to simulate a remote call. The screen should display a loader until the text content is available. |
| 102 | + |
| 103 | +Text styles follows the [Material typography](https://material.io/design/typography/the-type-system.html#type-scale) |
| 104 | + |
| 105 | +Don't hesitate to experiment with the widgets provided by Flutter. Try different layouts, add animations, etc. |
| 106 | + |
| 107 | +*Resources* |
| 108 | + |
| 109 | + - [Layouts in Flutter](https://flutter.dev/docs/development/ui/layout) |
| 110 | + - [Basic Flutter layout concepts (Codelab)](https://flutter.dev/docs/codelabs/layout-basics) |
| 111 | + - [Widget catalog](https://flutter.dev/docs/development/ui/widgets) |
| 112 | + - [How to debug layout issues with the Flutter Inspector](https://medium.com/flutter/how-to-debug-layout-issues-with-the-flutter-inspector-87460a7b9db) |
| 113 | + |
| 114 | +#### Hero animation |
| 115 | + |
| 116 | +Add a Hero animation on the logo and agency label when switching between the list and the details screens. See the video below for an example: |
| 117 | + |
| 118 | + |
| 119 | + |
| 120 | +*Resources* |
| 121 | + |
| 122 | + - [Animate a widget across screens](https://flutter.dev/docs/cookbook/navigation/hero-animations) |
| 123 | + - [Hero animations](https://flutter.dev/docs/development/ui/animations/hero-animations) |
| 124 | + |
| 125 | +#### HTTP Call (Lorem Ipsum) |
| 126 | + |
| 127 | +Currently, [lorem_fetcher.dart](lib/utils/lorem_fetcher.dart) provides static content. |
| 128 | + |
| 129 | +The goal is to replace it with a call to the [Bacon Ipsum API](https://baconipsum.com/), like https://baconipsum.com/api/?type=meat-and-filler. |
| 130 | +(Or another similar API if you don't like bacon :wink: ) |
| 131 | + |
| 132 | +*Resources* |
| 133 | + |
| 134 | + - [Fetch data from the internet](https://flutter.dev/docs/cookbook/networking/fetch-data) |
| 135 | + - [Networking & HTTP](https://flutter.dev/docs/development/data-and-backend/networking) |
| 136 | + - [JSON and serialization](https://flutter.dev/docs/development/data-and-backend/json) |
| 137 | + |
| 138 | +## Troubleshooting |
| 139 | + |
| 140 | +I encountered some problems while preparing this exercise, and it is likely you will encounter them too. Here is how I resolved them: |
| 141 | + |
| 142 | +### Unable to run the project on the Web after adding Google Maps for the Web platform |
| 143 | + |
| 144 | + |
| 145 | + |
| 146 | +This happens because the exercise project use sound null-safety, while Google Maps for Flutter Web is not null-safe yet. |
| 147 | +As a result, the Dart compiler is unable to guarantee null-safety and fails with the above error. |
| 148 | + |
| 149 | +To indicate to the compiler that we are ok with *unsound* null-safety, the flag `--no-sound-null-safety` must be used. For example: |
| 150 | +`$ flutter run --no-sound-null-safety -d chrome` |
| 151 | + |
| 152 | +If you are using Visual Studio code, you can also run the project with "exo2 - disable sound null-safety". |
| 153 | + |
| 154 | + |
| 155 | +### Application crashes when opening the map in release mode |
| 156 | + |
| 157 | + |
| 158 | + |
| 159 | +In Android, a special tool named ProGuard is usually run on release mode. The purpose of this tool is to obfuscate and minify the code, respectively to make it harder to reverse-engineer the application and remove any unused methods/classes/etc. |
| 160 | + |
| 161 | +This tools is notoriously difficult to configure and in some cases a method will be removed or renamed that is actually used. This often happens when using reflection to call a method by its name, and I suppose the Google Maps plugin for Android has to rely on that. |
| 162 | + |
| 163 | +To fix this, follow these steps: |
| 164 | + |
| 165 | +1. Go to the `android/app/` folder |
| 166 | +2. Create a `proguard-rules.pro` file. This will be used to configure ProGuard |
| 167 | +3. In the `proguard-rules.pro` file, add the following line: |
| 168 | + |
| 169 | +`-keep class androidx.lifecycle.DefaultLifecycleObserver` |
| 170 | + |
| 171 | +This will instruct ProGuard to not rename nor remove the `DefaultLifecycleObserver` class that Google Maps relies on. |
| 172 | + |
| 173 | +4. Open `android/app/build.gradle` file (it should already exists) |
| 174 | +5. Locate the `release` configuration block inside the `buildTypes` block. It should be around line 45 |
| 175 | +6. Edit the `release` configuration block to provide ProGuard with the configuration file we just made: |
| 176 | + |
| 177 | +``` |
| 178 | + release { |
| 179 | + // TODO: Add your own signing config for the release build. |
| 180 | + // Signing with the debug keys for now, so `flutter run --release` works. |
| 181 | + signingConfig signingConfigs.debug |
| 182 | +
|
| 183 | + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' |
| 184 | + } |
| 185 | +``` |
| 186 | + |
| 187 | +To spot a ProGuard configuration issue in the future: |
| 188 | + |
| 189 | + - It only happens in Android release mode |
| 190 | + - You get a strange error message about a method not being accessible or not existing, while you are positive it should exist |
| 191 | + - The names of the class or method in the stacktrace are single letters like `io.flutter.plugins.googlemaps.h.a` (obfuscation) |
0 commit comments