-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.html
360 lines (324 loc) · 22.6 KB
/
index.html
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<meta name="theme-color" content="#4F7DC9">
<meta charset="UTF-8">
<title>Vehicles on Google Maps Challenge</title>
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Source+Code+Pro:400|Roboto:400,300,400italic,500,700|Roboto+Mono">
<link rel="stylesheet" href="//fonts.googleapis.com/icon?family=Material+Icons">
<link rel="stylesheet" href="https://storage.googleapis.com/codelab-elements/codelab-elements.css">
<style>
.success {
color: #1e8e3e;
}
.error {
color: red;
}
</style>
</head>
<body>
<google-codelab-analytics gaid="UA-49880327-14"></google-codelab-analytics>
<google-codelab codelab-gaid="UA-52746336-1"
id="vehicles-on-maps"
title="Vehicles on Google Maps Challenge"
environment="web"
feedback-link="https://github.com/echorebel/codelab-vehicles-on-maps/issues">
<google-codelab-step label="Introduction" duration="5">
<p class="image-container"><img style="width: 200.50px" src="img/5039599e55d22d7f.png"> <img style="width: 178.50px" src="img/c5dfd645b9f6e5f6.png"> <img style="width: 523.00px" src="img/924a36dd7ebe18eb.png"></p>
<p><strong>Last Updated:</strong> 2019-06-30</p>
<p><a href="https://bit.ly/2YxRJWY" target="_blank">https://bit.ly/2YxRJWY</a></p>
<p><a href="https://echorebel.github.io/vehicles-maps-challenge/" target="_blank">https://echorebel.github.io/vehicles-maps-challenge/</a></p>
<h2 is-upgraded><strong>How quickly can we build an app that shows Vehicles on the Map?</strong></h2>
<p>For mytaxi it's a basic feature to show taxis on the map so the user is assured there are some available taxis around at his service. If one applies one will face a challenge to quickly implement a map and fetch vehicle data to show them on the map.</p>
<h3 is-upgraded><strong>Easy to use - easy to maintain</strong></h3>
<p>Two special challenges exist in most apps.It should be easy to use for the User as it should be easy to maintain and modify by <strong>other developers</strong>. </p>
<p>Often developers have to deal with code that they didn't wrote - or don't remember after years that they wrote this particular code. The simpler it's written the easier it is for anyone to pick up.</p>
<h3 is-upgraded><strong>Choose the right tools</strong></h3>
<p>To build any app you can use a lot of different existing building blocks. Have a reason to choose the right tool for the job instead of just following blindly what everyone else is doing. It doesn't matter in the end if you use Dagger or Koin, RxJava, Coroutines or LiveData. Choose them wisely and try to follow the YAGNI (You ain't gonna need it) principle. There is an awesome talk from <a href="https://www.infoq.com/presentations/Simple-Made-Easy/" target="_blank">Rich Hickey "Simple Made Easy"</a> - simple is reliable but it's not easy.</p>
<h3 is-upgraded><strong>Have a basic architecture in mind</strong></h3>
<p>Try to separate parts of you app so you can change and improve parts later. There are many ways to do it. One simple approach is to separate your apps into layers:</p>
<ul>
<li>Data (with IO and Network)</li>
<li>Domain (Models and Business Logic)</li>
<li>View (User facing representation)</li>
</ul>
<p class="image-container"><img style="width: 173.50px" src="img/c61acef37124ec7c.png"></p>
<h2 is-upgraded><strong>What you'll build</strong></h2>
<p>In this codelab, you're going to build a google maps app as a challenge. Your app will:</p>
<ul>
<li>Grab vehicle data from an API.</li>
<li>Show them on maps</li>
</ul>
<p class="image-container"><img style="width: 272.62px" src="img/33f4a277e159ecd2.png"></p>
<aside class="warning"><p><strong>Note:</strong> This is a challenge, so try to move fast and keep things simple. Choose the structure, libs and patterns you feel comfortable with. Doesn't matter if you want to use a different solution than this codelab suggests as example - it's actually appreciated to deviate from the path!</p>
</aside>
<aside class="special"><p><strong>Bonus:</strong> Pair up with someone on one Computer and hack together (any experience level)!</p>
<p>Experienced guys should try/use <a href="https://en.wikipedia.org/wiki/Extreme_programming" target="_blank">XP</a> style and <a href="https://technologyconversations.com/2013/12/20/test-driven-development-tdd-example-walkthrough/" target="_blank">TDD</a> :)</p>
</aside>
<h2 class="checklist" is-upgraded><strong>What you'll learn</strong></h2>
<ul class="checklist">
<li>How to embed google maps into your app</li>
<li>How to read data from a remote API</li>
<li>How to show the queried vehicle data on maps</li>
</ul>
<p>This codelab is a challenge and you should focus on getting things done so. We will provide some snippets to get you rolling. You are still required to build all the glue code and wiring yourself. Non-relevant concepts and left out and you are free to apply patterns or libs to make you go fast.</p>
<h2 is-upgraded><strong>What you'll need</strong></h2>
<ul>
<li>Android Studio and empty project</li>
<li>Kotlin Plugin is recommended</li>
<li>Google Developer Account to create a Google Maps API key</li>
<li>Emulator or Device</li>
</ul>
</google-codelab-step>
<google-codelab-step label="Setting up Maps" duration="5">
<h2 is-upgraded><strong>Create new Android Studio project</strong></h2>
<p>Create a new project with the project wizard and select Google Maps Activity.</p>
<p class="image-container"><img style="width: 592.50px" src="img/fd6b356d3138a3a0.png"></p>
<p>Enter Name and define a package name.</p>
<p class="image-container"><img style="width: 587.64px" src="img/ef54403752a45fa6.png"></p>
<h2 is-upgraded><strong>Get a key for the Google Maps API</strong></h2>
<p>To show maps we use the Google Maps SDK. You need to create a key in the developer console.</p>
<p>Easiest way is to just click the link provided in res/values/google_maps_api.xml as it will provide already SHA-1 certificate for the Api Key.</p>
<p><a href="https://developers.google.com/maps/documentation/android-sdk/get-api-key" target="_blank"><paper-button class="colored" raised>How to Register Manually for API Key</paper-button></a></p>
<aside class="warning"><p><strong>Note:</strong> Your API Key will be embedded to the Manifest. The default Android Studio Project will already support to use different keys for Debug and Release Variant. If you want to use the API key with release you will need to create and add your Release Certificate Fingerprint(SHA-1) to the key in the cloud project too.</p>
</aside>
<h3 is-upgraded><strong>Verify your API key is working properly</strong></h3>
<p>Instead of showing Sydney on the map, set the Marker to the CityCube where Droidcon Berlin takes place with nicer zoom level!</p>
<pre><code>val cityCube = LatLng(52.5002212, 13.2685643)
...
map.moveCamera(CameraUpdateFactory.newLatLngZoom(cityCube, 14f))</code></pre>
<p>Now just try to run the debug app and you should see a map like this:</p>
<p class="image-container"><img style="width: 395.24px" src="img/fc23f9305eb06689.png"></p>
</google-codelab-step>
<google-codelab-step label="Getting data from network" duration="10">
<p>In this section we will discuss how to grab the data we need to display the vehicles on the map from a demo API. The demo will just return random location within a window you specify in the request.</p>
<h2 is-upgraded><strong>What kind of data do we expect?</strong></h2>
<p>Our simplified Demo API will return a list of vehicles (named Points of Interest here).</p>
<p>Every item contains an id, a coordinate, which type of fleet it belongs to - either TAXI or POOLING - and a heading: the direction facing relative to North in degrees in a cartesian plane.</p>
<p>Example Response</p>
<pre><code>{
"poiList": [{
"id": 439670,
"coordinate": {
"latitude": 53.46036882190762,
"longitude": 9.909716434648558
},
"fleetType": "POOLING",
"heading": 344.19529122029735
},
{
"id": 739330,
"coordinate": {
"latitude": 53.668806556867445,
"longitude": 10.019908942943804
},
"fleetType": "TAXI",
"heading": 245.2005654202569
},
{
"id": 145228,
"coordinate": {
"latitude": 53.58500747958201,
"longitude": 9.807045083858156
},
"fleetType": "POOLING",
"heading": 71.63840043828377
}
]
}</code></pre>
<p>Now you need to generate data class from the json example, either by</p>
<ol type="1" start="1">
<li><a href="https://plugins.jetbrains.com/plugin/9960-json-to-kotlin-class-jsontokotlinclass-" target="_blank">JsonToKotlinClass</a> IntelliJ plugin or a similar one</li>
<li>A online tool like <a href="https://www.json2kotlin.com/" target="_blank">json2kotlin.com</a></li>
<li>Write from scratch manually - but why not be lazy?</li>
</ol>
<p>Depending on the generated classes do the tweaks like you would like to do, e.g. change class namings and/or create enum for fleet types.</p>
<aside class="warning"><p><strong>Pro-tip:</strong> Refactoring/Renaming of attributes can cause missing or wrong deserialization!</p>
<p>Always use serialization annotations so field names are properly mapped to data class attribute!</p>
<p>Example with gson: @SerializedName("poiList") val vehicles: List<Vehicle></p>
</aside>
<p>Eventually you will end up with something similar to this:</p>
<pre><code>data class Response(
@SerializedName("poiList") val vehicles: List<Vehicle>
)
data class Vehicle(
@SerializedName("coordinate") val coordinate: Coordinate,
@SerializedName("fleetType") val fleetType: FleetType,
@SerializedName("heading") val heading: Double,
@SerializedName("id") val id: Int
)
data class Coordinate(
@SerializedName("latitude") val latitude: Double,
@SerializedName("longitude") val longitude: Double
)
enum class FleetType {
TAXI,
POOLING
}</code></pre>
<h2 is-upgraded><strong>How to retrieve data from the API?</strong></h2>
<p>Now you need to implement a call to the API endpoint to retrieve the vehicle data from the network via HTTPS. Don't hurt yourself with AsyncTask - there are well tried & tested http clients available that do the major lifting for you, choose one:</p>
<ol type="1" start="1">
<li><a href="https://square.github.io/retrofit/" target="_blank">Retrofit</a></li>
<li><a href="https://ktor.io/" target="_blank">Ktor</a></li>
<li><a href="https://github.com/google/volley" target="_blank">Volley</a></li>
</ol>
<p>Now implement a http call to this endpoint to get the vehicles:</p>
<p><strong>https://fake-poi-api.mytaxi.com/?p1Lat={Latitude1}&p1Lon={Longitude1}&p2Lat={Latitude2}&p2Lon={Longitude2}</strong></p>
<p>The parameters in { } are the geographic bounds (northeast and southwest) for the request. For testing purposes you can use the area around droidcon::</p>
<p>Northeast: 52.51570426234859, 13.287037834525108</p>
<p>Southwest: 52.48417497476959, 13.251724876463415</p>
<h3 is-upgraded>Example: <strong>How to accomplish it with Retrofit</strong></h3>
<p>Retrofit is used by many Android devs and it's pretty neat to just write a interface definition of the endpoint. </p>
<aside class="warning"><p>The latest version 2.6.0 also supports coroutines natively - how awesome is this!</p>
<p><strong>But be aware the support for null values is not working yet!</strong></p>
</aside>
<p><code>Add dependencies to</code></p>
<pre><code>implementation 'com.squareup.retrofit2:retrofit:2.6.0'
implementation 'com.squareup.retrofit2:converter-gson:2.5.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0-M2'</code></pre>
<p>Let's have a look:</p>
<pre><code>interface VehicleService {
@GET(".")
suspend fun listVehicles(
@Query("p1Lat") northEastLat: Double,
@Query("p1Lon") northEastLon: Double,
@Query("p2Lat") southWestLat: Double,
@Query("p2Lon") southWestLon: Double
): Response
}</code></pre>
<p>To create a instance of this service you need to specify the base url, a converter so retrofit knows how to deserialize the response of the api and of course the interface you just created before. The new Version 2.6.0 can use a suspend function and return the result directly instead of passing the Call.</p>
<pre><code>Retrofit.Builder()
.baseUrl("https://fake-poi-api.mytaxi.com/")
.addConverterFactory(
GsonConverterFactory.create(
GsonBuilder().create()
)
)
.build()
.create(VehicleService::class.java)</code></pre>
<aside class="warning"><p><strong>Tip: </strong>Instead of creating instance of GsonConverter and GsonBuilder in place (for sake of understanding) it's better to provide and inject it via any IoC/DI solution (e.g. Dagger, Koin) - this way you can configure/provide it on an application level and it's also easier to read.</p>
</aside>
<h2 is-upgraded><strong>Try it out</strong></h2>
<p>Now go ahead - implement and test the call.</p>
<pre><code>CoroutineScope(Dispatchers.IO).launch {
val response = VehicleRepository.getVehicles()
}</code></pre>
</google-codelab-step>
<google-codelab-step label="Create a Repository" duration="5">
<p>Remember the introduction where we talked about app layer separation?</p>
<p>We don't want the a direct dependency from our view layer to our network layer. Separation will benefit us with easier maintenance, testability and being future proof - to be able to modernize layer by layer.</p>
<p>The Repository pattern is an abstraction of your domain data storage - it can be just in memory, realtime data, cached network data, or even a database or any combination of them.</p>
<p>It acts as a interface to your domain layer.</p>
<p class="image-container"><img style="width: 201.35px" src="img/e005cc286c420b73.png"></p>
<p>Your task is now to think about the interface the repository will need so we can display vehicles on the map and go ahead and implement it!</p>
<p>One simple implementation could be:</p>
<pre><code>object VehicleRepository {
suspend fun getVehicles() = VehicleApi.listVehicles()
}</code></pre>
<p>The cool thing about this is you really can follow the YAGNI principle here - in the beginning you keep is simple and direct - any request to the repository will call the API directly. If there is need later you can change the implementation to add persistence as a cache or database or even some other logic without changing the use in the view layer.</p>
</google-codelab-step>
<google-codelab-step label="Wire domain and view layer" duration="10">
<p>Now that we have the data available in the Repository let's do one more step to prepare using that data in the view. Good practice is to contain view data in a separate container no matter if you use MVVM or MVP.</p>
<aside class="warning"><p><strong>Avoid common mistakes:</strong></p>
<ul>
<li>Don't let ViewModels or Presenters know about Android framework classes</li>
<li>If you use ViewModel - never let in know a view, context, activity or subscribe to LiveData - Leaks will happen! If you need really need context use the AndroidViewModel instead.</li>
<li>Don't put all the logic in View/Activity/Fragments - keep it to a minimum and move it to ViewModel / Presenter / Interactors instead</li>
</ul>
</aside>
<p>Now it's your turn to implement a ViewModel and/or Presenter to provide data from the repository to the view!</p>
<h2 is-upgraded><strong>LiveData and Coroutines</strong></h2>
<p>Our example will use an alpha version of the lifecycle extensions where Coroutine support was added. The first dependency will add ViewModel and LiveData. The second dependency will add a extension builder function to call suspend function and return LiveData.</p>
<aside class="warning"><p>Ideally you would even introduce another layer between ViewModel and Repository with an Interactor / Use Case - but here it would potentially identical to our Repository implementation.</p>
</aside>
<p>Add to <code>app/build.gradle</code></p>
<pre><code>implementation "androidx.lifecycle:lifecycle-extensions:2.2.0-alpha01"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha01"</code></pre>
<p>A simple ViewModel implementation could be:</p>
<pre><code>class VehicleViewModel(vehicleRepository: VehicleRepository): ViewModel(){
// use IO for network, live data will auto switch to UI thread
val vehicles = liveData(Dispatchers.IO) {
emit(vehicleRepository.getVehicles())
}
}</code></pre>
<p>This is much shorter than the usual way:</p>
<pre><code>private val vehicles: MutableLiveData<List<String>> by lazy {
MutableLiveData<List<Vehicles>>().also {
loadVehicles()
}
}
fun getVehicles(): LiveData<List<String>> {
return vehicles
}
private fun loadVehicles() {
// Do an async operation to fetch vehicles...
}</code></pre>
<p>Now you need to access your data in your view. If you use our LiveData approach it could be as easy as:</p>
<pre><code>vehicleViewModel.vehicles.observe(this, Observer<Response> { response ->
// TODO add markers to map
})</code></pre>
</google-codelab-step>
<google-codelab-step label="Adding Markers on Map" duration="10">
<p>Now we can jump to the final step to add markers on the map in our view.</p>
<p>Our goal is to place the vehicles to their proper location and also let them face the direction they are reporting to the system. To do that we can create new <a href="https://developers.google.com/maps/documentation/android-sdk/marker" target="_blank">MarkerOptions</a>.The only mandatory property on MarkerOptions is position, but we will need more.</p>
<h2 is-upgraded><strong>Using custom icons</strong></h2>
<p class="image-container"><img style="width: 54.00px" src="img/bf75a19a7d0f0406.png"> <img style="width: 54.00px" src="img/878c6926819ca488.png"></p>
<p>Download the icons from <a href="https://www.dropbox.com/s/4jz9zn8bia9aoa0/batmobile.zip?dl=0" target="_blank">here</a> or pick/create your own and put the Icons to your src/main/res/ folder.</p>
<p>Since our API specify different fleet types let's apply the icon correspondingly:</p>
<pre><code>markerOptions.icon(
BitmapDescriptorFactory.fromResource(
when {
vehicle.fleetType == FleetType.TAXI -> R.drawable.batmobil
else -> R.drawable.unlicensed
}
)
)</code></pre>
<h2 is-upgraded><strong>Apply custom rotation to show heading of the car.</strong></h2>
<p>To be able to rotate the car properly you need to center the anchor point of the icon - so it does rotate around its own axis rather around one corner of the image.</p>
<pre><code>markerOptions.anchor(0.5f, 0.5f)
.rotation(vehicle.heading.toFloat())</code></pre>
<h2 is-upgraded><strong>Try it out</strong></h2>
<p>Now build your app and run it, you should end up with something like this:</p>
<p class="image-container"><img style="width: 439.87px" src="img/39d8b50ff552e6bf.png"></p>
</google-codelab-step>
<google-codelab-step label="Bonus Challenge: Error Handling and Refreshing data" duration="10">
<p>To improve the app we should be able to refresh data from the API and deal with errors.</p>
<p>You should refactor the app and ideally decouple the refresh logic into an Interactor / Use Case.</p>
<p>We won't give any hints now.</p>
</google-codelab-step>
<google-codelab-step label="Congratulations" duration="0">
<p>Congratulations, you've made it through the challenge!</p>
<p>You went through building and wiring basic building blocks of any app. You integrated Google Maps and your app is capable of loading and showing "live" data.</p>
<p>You already went halfway through the critical steps to pass through the mytaxi coding challenge!</p>
<h2 is-upgraded><strong>What's next?</strong></h2>
<p>Challenge yourself by:</p>
<ul>
<li>Add the user location and show vehicle data around the user</li>
<li>Use same view model to display a list with cars sorted by distance to the user</li>
<li>Refresh the data periodically</li>
<li>Get your solution reviewed by friends or colleagues / or send it to <a href="mailto:m.paech@mytaxi.com" target="_blank">m.paech@mytaxi.com</a></li>
<li>Feel free to <a href="https://mytaxi.com/uk/career" target="_blank">apply for mytaxi</a>, but make sure you are using DI and write Unit Tests ;)</li>
</ul>
<h2 is-upgraded><strong>Further reading / viewing</strong></h2>
<ul>
<li><a href="https://medium.com/corouteam/exploring-kotlin-coroutines-and-lifecycle-architectural-components-integration-on-android-c63bb8a9156f" target="_blank">Exploring new Coroutines and Lifecycle Architectural Components integration on Android</a></li>
<li><a href="https://proandroiddev.com/suspend-what-youre-doing-retrofit-has-now-coroutines-support-c65bd09ba067" target="_blank">Suspend what you're doing: Retrofit has now Coroutines support!</a></li>
<li><a href="https://mytaxi.com/uk/career/" target="_blank">Design the future of mobility with mytaxi!</a></li>
</ul>
<h2 is-upgraded><strong>Reference docs</strong></h2>
<ul>
<li><a href="https://developers.google.com/maps/documentation/android-sdk/intro" target="_blank">Google Maps Android SDK</a></li>
<li><a href="https://square.github.io/retrofit/" target="_blank">Retrofit</a></li>
<li><a href="https://github.com/square/retrofit/tree/master/retrofit-converters/gson" target="_blank">Gson Converter</a></li>
<li><a href="https://github.com/Kotlin/kotlinx.coroutines" target="_blank">kotlinx.coroutines</a></li>
<li><a href="https://developer.android.com/topic/libraries/architecture/livedata" target="_blank">LiveData Overview</a></li>
</ul>
</google-codelab-step>
</google-codelab>
<script src="https://storage.googleapis.com/codelab-elements/native-shim.js"></script>
<script src="https://storage.googleapis.com/codelab-elements/custom-elements.min.js"></script>
<script src="https://storage.googleapis.com/codelab-elements/prettify.js"></script>
<script src="https://storage.googleapis.com/codelab-elements/codelab-elements.js"></script>
</body>
</html>