Google map, location service, and Firebase Realtime Database on Android — Advanced

Kai Xie
8 min readAug 22, 2021

--

Map and location service are very important features on mobile devices, and they are one of the major distinctions compared to traditional desktop systems. With map and location service, the users could get where they are, show their location on the map, and acquire the local stores and services nearby.

In the previous article

we have implemented a simple, but standard map app with location service

So in the article, we will explore some more advanced features, such as how to customize the marker on the map to make it more useful, and how to share the markers across all users with Firebase Realtime Database.

Display Notes on the map

In the previous article, we have shown the marker on the map, which is a standard red pin with a standard title if you click it. So we would customize the marker as a sticky note with some message. Let’s do it.

Add Note class and ViewModel accordingly

According to the MVVM pattern, we would create a data model to keep the notes, and split the data model from the view, so let’s create a new package, data, and a new file, Data.kt, in this package, and create a new data class as followed

data class Note(
val user: String = "",
val text: String = "",
val latLng: LatLng = LatLng(-33.8523341, 151.2106085)
)

It’s pretty straightforward that it will save the user, text, and location of this note.

And then, I would like to create another file, SampleData.kt, with the following Note objects as

val sampleData = listOf(
Note(user = "user1", text = "Manly Beach", latLng = LatLng(-33.78777149790135, 151.28657753901692)),
Note(user = "user2", text = "Sydney Harbour Bridge", latLng = LatLng(-33.84738608026124, 151.21035989009738)),
Note(user = "user3", text = "Taronga Zoo Sydney", latLng = LatLng(-33.83854635067856, 151.2405722914709)),
)

for testing.

And we also need to create another package, viewModel, and a new class, NotesViewModel in this package, and then implement this class with the following code

class NotesViewModel: ViewModel() {
private val _notes = MutableLiveData<MutableList<Note>>()

val notes: MutableLiveData<MutableList<Note>>
get() = _notes

init
{
_notes.value = sampleData.toMutableList()
}
}

It is a standard ViewModel class, which reads the data from the sampleData.

And then, let’s update the MapsActicity, which is the view of this app.

Firstly, we need to create an instance of NotesViewModel in the MapsActivity as followed

private val notesViewModel: NotesViewModel by lazy {
ViewModelProvider(this).get(NotesViewModel::class.java)
}

and create a new private function updateUI with following code

private fun updateUI() {
notesViewModel.notes.value?.forEach {
mMap
.addMarker(
MarkerOptions()
.position(it.latLng)
.title("${it.text} - ${it.user}")
)
.showInfoWindow()
}
}

in which, we would add all markers to the map.

And we also need to call this updateUI() in onMapReady function and remove the

mMap.addMarker(MarkerOptions().position(defaultLocation).title("Marker in Sydney"))

So if you build and run the app, zoom/move to the proper location, you would see the markers as

We could also add

try {
if (mMap.isMyLocationEnabled) {
mMap.uiSettings.isMyLocationButtonEnabled = true
} else {
mMap.uiSettings.isMyLocationButtonEnabled = false
lastKnownLocation
= null
}
} catch (e: SecurityException) {
Log.e("Exception: %s", e.message, e)
}

to the updateUI function to make sure the location button would be displayed correctly.

Customize the marker

According to the documentation, we are unable to customize the marker too much. The only thing we can do is to customize the icon of the marker, so we would convert the notes to an image and set it as the icon of the marker.

At first, we need a helper function as followed

fun buildBitmap(note: Note, textSize: Float, textColor: Int): Bitmap? {
val lines = "${note.text}\n\n - ${note.user}".split("\n")
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
paint.textSize = textSize
paint.color = textColor
paint.textAlign = Paint.Align.LEFT
var baseline: Float = -paint.ascent() // ascent() is negative
val width = (paint.measureText(lines.maxByOrNull{ it.length }) + 0.5f).toInt() + 16 // round
val height = lines.size * 44
val image = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(image).apply {
val
paint = Paint()
paint.color = Color.WHITE
paint.style = Paint.Style.FILL
drawRect(0.0f, 0.0F, width.toFloat(), height.toFloat(), paint)
paint.color = Color.BLACK
paint.style = Paint.Style.STROKE
drawRect(0.0f, 0.0F, (width-1).toFloat(), (height-1).toFloat(), paint)
}
lines.forEach {
canvas.drawText(it, 8.0f, baseline, paint)
baseline += 44
}
return
image
}

which converts the note object to an image.

And we need to update the updateUI function as followed

private fun updateUI() {
notesViewModel.notes.value?.forEach {
val bmp = BitmapDescriptorFactory.fromBitmap(buildBitmap(it, 36.0F, Color.BLACK))

map.addMarker(
MarkerOptions()
.position(it.latLng)
.icon(bmp)
)
.showInfoWindow()
}
...
}

And if you build and run the app, you would see all markers are customised as

Then, actually, you could customize the marker as any look as you prefer with the same method.

Add new note

In this chapter, we would implement the function that let the user can add a new note.

Firstly, let’s add the following function to the NotesViewModel class

fun addNote(note: Note) {
_notes.value?.add( note)
}

and update the init block with

init {
_notes.value = mutableListOf()
}

It is very simple that the LiveData notes is empty at the beginning, and the user can add a new note to it by invoking addNote function.

Secondly, we need to add the following code into android section in the build.gradle(Module)

dataBinding{
enabled = true
}

to support data binding.

And create a new layout file, dialog_input.xml

<?xml version="1.0" encoding="utf-8"?>

<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
>

<data>
<variable
name="user"
type="String"
/>
<variable
name="text"
type="String"
/>
</data>

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="300dp"
android:background="@color/white"
>

<EditText
android:id="@+id/user"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:singleLine="true"
android:hint="user"
android:text="@={user}"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/text"
/>

<EditText
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:lines="3"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:hint="write down the notes"
android:text="@={text}"
app:layout_constraintTop_toBottomOf="@id/user"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

It is just a layout to show the note.

And let’s create a private helper function showNoteDetail as

private fun showNoteDetail( note: Note?, latLng: LatLng): Boolean {
val dialogInputBinding: DialogInputBinding = DialogInputBinding.inflate(layoutInflater)
dialogInputBinding.setUser(note?.user)
dialogInputBinding.setText(note?.text)
val dialogBuilder = AlertDialog.Builder(this)
.setTitle("Note")
.setView( dialogInputBinding.root)
.setNegativeButton("Cancel") { dialog, _ -> dialog.cancel() }

if
(note == null) {
dialogBuilder.setPositiveButton("OK") { dialog, _ ->
val
user = dialogInputBinding.getUser() ?: ""
val
text = dialogInputBinding.getText() ?: ""
if
(user.isNotEmpty() and text.isNotEmpty()) {
notesViewModel.addNote(Note(user = user, text = text, latLng = latLng))
updateUI()
}
}
}
dialogBuilder.show()
return true
}

We would reuse this dialog for displaying existing notes or adding a new note for simplicity. If the parameter, note, is null, then the user is able to input the username and text to add a new note, and this note will be added into the view model by invoking notesViewModel.addNote, or else the user could browse the existing note only.

And then we need to add the following code into the onMapReady function

mMap.setOnMarkerClickListener { marker -> showNoteDetail(notesViewModel.notes.value?.firstOrNull { note -> note.latLng == marker.position }, marker.position)}
mMap.setOnMapLongClickListener { latLng -> showNoteDetail(null, latLng)}

As we see, we added two listeners to the GoogleMap object. Once the user clicks the marker, we will check the location of each note, if the marker with the same location is found, then we will show them this note.

Or else if the user long clicks the map, we would show an empty dialog for adding a new note.

Ok. let’s build and run the app again, and long click the map to add a new note like

So far, we have implemented the NOTE function. But you might notice that the notes are not added to any persistent storage, so they would be lost once we restart the app. Let’s fix it in the next chapter.

Integrate Firebase Realtime Database

The Firebase Realtime Database is a cloud-hosted database. Data is stored as JSON and synchronized in realtime to every connected client. When you build cross-platform apps with our iOS, Android, and JavaScript SDKs, all of your clients share one Realtime Database instance and automatically receive updates with the newest data.

It is not difficult to set up the Firebase Realtime Database according to the following document

Briefly speaking, we need to go to the firebase console, and go to the Project Setting of your project, and click Add app button to add a new app. And then you need to input the package name and download the google-services.json file as instructed step by step, and you also need to add

classpath ‘com.google.gms:google-services:4.3.10’

to the project-level build.gradle file, and add a new plugin

id 'com.google.gms.google-services'

to the App-level build.gradle, and add the following dependencies

implementation platform('com.google.firebase:firebase-bom:28.2.0')
implementation 'com.google.firebase:firebase-database-ktx'

to enable relevant APIs.

And let’s add a new helper function to the NotesViewModel as followed

private fun getDataFromFirebase() {
val database = Firebase.database
val myRef = database.getReference(PATH)
myRef.addChildEventListener(object : ChildEventListener {
override fun onChildAdded(snapshot: DataSnapshot, previousChildName: String?) {
val value = snapshot.getValue<HashMap<String, Any>>()
val latLng = value?.get("latLng") as HashMap< String, Double>
_notes.value = _notes.value?.apply { add(
Note(
user = value.get("user") as String, text = value.get("text") as String,
latLng = LatLng(
latLng.get("latitude") ?: 0.0, latLng.get("longitude") ?: 0.0,
),
),
)}
}

override fun onChildChanged(snapshot: DataSnapshot, previousChildName: String?) {
TODO("Not yet implemented")
}

override fun onChildRemoved(snapshot: DataSnapshot) {
TODO("Not yet implemented")
}

override fun onChildMoved(snapshot: DataSnapshot, previousChildName: String?) {
TODO("Not yet implemented")
}

override fun onCancelled(error: DatabaseError) {
TODO("Not yet implemented")
}

})
}

and you might find the PATH is undefined, so let’s add it to the companion object section as

companion object {
private const val PATH = "note"
}

This piece of code is the standard way to access Firebase Realtime Database by creating a reference. But we also added a listener to listen to the change of the data in this database, so we are able to update our LiveData, _notes, once the data in the database changed.

And we need to update the addNote function as following

fun addNote(note: Note) {
val database = Firebase.database
val myRef = database.getReference(PATH)

myRef.push().setValue(note)
}

which will add a new note to the database.

And let’s call getDataFromFirebase() in the init block to fetch the notes from the database at the beginning.

And we also need to observe the change of the LiveData, notesin the view by adding

notesViewModel.notes.observe(this, Observer {
if
(it != null) {
updateUI()
}
})

into the onMapReady function. It would observe the change of the notes and update the UI if the value of notes changes.

And let’s run the app and try to add a new note somewhere, kill the app and launch again. you will see the newly added note is still there.

You can also check the notes from the Firebase console by navigating to the Realtime Database at the left pane as

Another thing we need to pay attention to is the Rules of the database. It might expire after some days, and then you would get a Permission Denied error if you run the app. In this case, you just need to update the rules.

And because this Firebase Realtime Database could be accessed by this app running on all devices, so all users could see all notes from all users.

That is all for this tutorial. Thanks for reading it.

--

--