Chapters

Hide chapters

Android Apprentice

Third Edition · Android 10 · Kotlin 1.3 · Android Studio 3.6

Before You Begin

Section 0: 4 chapters
Show chapters Hide chapters

Section II: Building a List App

Section 2: 7 chapters
Show chapters Hide chapters

Section III: Creating Map-Based Apps

Section 3: 7 chapters
Show chapters Hide chapters

16. Saving Bookmarks with Room
Written by Tom Blankenship

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

Now that the user can tap on places to get an info window pop-up, it’s time to give them a way to bookmark and edit a place.

In this chapter, you’ll:

  1. Learn about the Room Persistence Library and how it fits into the overall Android Component Architecture system.

  2. Create a Room database to manage bookmarks.

  3. Store bookmarks when the user taps on a map info window.

  4. Learn about LiveData and use it to update the View automatically.

Getting started

If you were following along with your own app, open it, and keep using it with this chapter. If not, don’t worry! Locate the projects folder for this chapter, and open the PlaceBook app in the starter folder. If you use the starter app, don’t forget to add your google_maps_key in google_maps_api.xml and in the method setupPlacesClient() in MapsActivity.kt. Read Chapter 13 for more details about the Google Maps key.

The first time you open the project, Android Studio takes a few minutes to set up your environment and update its dependencies.

In ListMaker, you used Shared Preferences to store data permanently. While Shared Preferences is a great way to manage simple key-value pairs, it’s not designed to store large amounts of structured data.

For PlaceBook, you’ll use the Room Persistence Library to store the bookmarks in a structured database. Room is built on top of SQLite and provides several advantages over Shared Preferences:

  • Works directly with Plain Java Objects (POJOs) with minimal effort.

  • Provides advanced search and sorting through SQL queries.

  • Manages relationships between different data types.

  • Efficiently stores large amounts of data.

Room overview

Before diving into the code, it’s important to understand the three basic components of Room.

Room and Android Architecture Components

Room is part of a larger set of libraries known as the Android Architecture Components. The other components are:

PlaceBook architecture

Before creating your first Room classes, you need to organize the app to achieve a clean overall architecture. You can separate the app into distinct areas of responsibility along these lines:

Development approach

Think about the architecture as a multi-layered cake. Have you ever seen somebody eat a cake one layer at a time? That would be a little odd! Likewise, you’re not going to build out the app one layer at a time. You’re going to take one slice at a time. Each slice may cut through all of the layers as you slowly build out the final product.

Adding the architecture components

The Architecture Components are provided as separate libraries from Google’s Maven repository. The gradle file is already set up to use this repository, but you’ll need to import the individual libraries.

lifecycle_version = '2.2.0'
room_version = '2.2.4'
apply plugin: 'kotlin-kapt'
// 1
implementation "androidx.activity:activity-ktx:1.1.0"
// 2
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
// 3
implementation "androidx.room:room-runtime:$room_version"
// 4
kapt "androidx.room:room-compiler:$room_version"

Room classes

Now you’re ready to add the basic classes required by Room. This includes the Entities, DAOs, and the Database. Behind the scenes, Room takes your class structure and does all of the hard work to create an SQLite database with tables and column definitions.

Entities

PlaceBook only requires a single entity type to store Bookmarks.

// 1
@Entity
// 2
data class Bookmark(
    // 3
    @PrimaryKey(autoGenerate = true) var id: Long? = null,
    // 4
    var placeId: String? = null,
    var name: String = "",
    var address: String = "",
    var latitude: Double = 0.0,
    var longitude: Double = 0.0,
    var phone: String = ""
)
import androidx.room.Entity
import androidx.room.PrimaryKey

DAOs

Next, you’ll define the data access object that reads and writes from the database.

// 1
@Dao
interface BookmarkDao {

  // 2
  @Query("SELECT * FROM Bookmark")
  fun loadAll(): LiveData<List<Bookmark>>

  // 3
  @Query("SELECT * FROM Bookmark WHERE id = :bookmarkId")
  fun loadBookmark(bookmarkId: Long): Bookmark

  @Query("SELECT * FROM Bookmark WHERE id = :bookmarkId")
  fun loadLiveBookmark(bookmarkId: Long): LiveData<Bookmark>

  // 4
  @Insert(onConflict = IGNORE)
  fun insertBookmark(bookmark: Bookmark): Long

  // 5
  @Update(onConflict = REPLACE)
  fun updateBookmark(bookmark: Bookmark)

  // 6
  @Delete
  fun deleteBookmark(bookmark: Bookmark)
}

Database

The last piece needed to complete the Room classes is the Database.

// 1
@Database(entities = arrayOf(Bookmark::class), version = 1)
abstract class PlaceBookDatabase : RoomDatabase() {
  // 2
  abstract fun bookmarkDao(): BookmarkDao
  // 3
  companion object {
    // 4
    private var instance: PlaceBookDatabase? = null
    // 5
    fun getInstance(context: Context): PlaceBookDatabase {
      if (instance == null) {
        // 6
        instance = Room.databaseBuilder(
            context.applicationContext,
            PlaceBookDatabase::class.java,
            "PlaceBook").build()
      }
      // 7
      return instance as PlaceBookDatabase
    }
  }
}

Creating the Repository

Your basic Room classes are ready to go, but let’s add one more layer of abstraction between Room and the rest of the application code. By doing this, you make it easy to change out how and where the app data is stored. This abstraction layer will be provided using a Repository pattern. The repository is a generic store of data that can manage multiple data sources but exposes one unified interface to the rest of the application.

// 1
class BookmarkRepo(context: Context) {
  // 2
  private var db = PlaceBookDatabase.getInstance(context)
  private var bookmarkDao: BookmarkDao = db.bookmarkDao()
  // 3
  fun addBookmark(bookmark: Bookmark): Long? {
    val newId = bookmarkDao.insertBookmark(bookmark)
    bookmark.id = newId
    return newId
  }
  // 4
  fun createBookmark(): Bookmark {
    return Bookmark()
  }
  // 5
  val allBookmarks: LiveData<List<Bookmark>>
    get() {
      return bookmarkDao.loadAll()
    }
}

The ViewModel

The ViewModel layer serves as the intermediary between your app Views and the data provided by the BookmarkRepo. The ViewModel drives the UI based on the repository data and updates the repository data based on user interactions.

// 1
class MapsViewModel(application: Application) :
    AndroidViewModel(application) {

  private val TAG = "MapsViewModel"
  // 2
  private var bookmarkRepo: BookmarkRepo = BookmarkRepo(
      getApplication())
  // 3
  fun addBookmarkFromPlace(place: Place, image: Bitmap?) {
    // 4
    val bookmark = bookmarkRepo.createBookmark()
    bookmark.placeId = place.id
    bookmark.name = place.name.toString()
    bookmark.longitude = place.latLng?.longitude ?: 0.0
    bookmark.latitude = place.latLng?.latitude ?: 0.0
    bookmark.phone = place.phoneNumber.toString()
    bookmark.address = place.address.toString()    
    // 5
    val newId = bookmarkRepo.addBookmark(bookmark)

    Log.i(TAG, "New bookmark $newId added to the database.")
  }
}

Adding bookmarks

You have everything in place for adding bookmarks to the database. Now you just need to detect when the user taps on a place info window.

compileOptions {
  sourceCompatibility = 1.8
  targetCompatibility = 1.8
}
kotlinOptions {
  jvmTarget = JavaVersion.VERSION_1_8.toString()
}
private val mapsViewModel by viewModels<MapsViewModel>()
private fun setupMapListeners() {
  map.setInfoWindowAdapter(BookmarkInfoWindowAdapter(this))
  map.setOnPoiClickListener {
    displayPoi(it)
  }
}
override fun onMapReady(googleMap: GoogleMap) {
  map = googleMap
  setupMapListeners()
  getCurrentLocation()
}
class PlaceInfo(val place: Place? = null,
    val image: Bitmap? = null)
marker?.tag = PlaceInfo(place, photo)
imageView.setImageBitmap((marker.tag as
    MapsActivity.PlaceInfo).image)
private fun handleInfoWindowClick(marker: Marker) {
  val placeInfo = (marker.tag as PlaceInfo)
  if (placeInfo.place != null) {
    mapsViewModel.addBookmarkFromPlace(placeInfo.place,
        placeInfo.image)
  }
  marker.remove()
}
map.setOnInfoWindowClickListener {
  handleInfoWindowClick(it)
}

java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time.

Coroutines

Coroutines make asynchronous programming easier by hiding many of the underlying complications. This frees you to think about your code in a more traditional sequential fashion that is easier to comprehend. You’ll learn more about Coroutines in future chapters, but for now, you only need to know about the launch coroutine builder.

Adding Coroutine libraries

Coroutine support is provided as a separate library and must be added to the project dependencies before being used.

coroutines_version = '1.3.0'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"

Creating a Coroutine

Open MapsActivity.kt and replace the call to addBookmarkFromPlace in handleInfoWindowClick() with the following:

GlobalScope.launch {
  mapsViewModel.addBookmarkFromPlace(placeInfo.place,
      placeInfo.image)
}

Observing database changes

You’ve made a huge step forward by saving bookmarks to the database, but the user has no way of identifying places that have been bookmarked. The goal is to have the UI automatically reflect the current state of the bookmark database. This is where your use of the ViewModel starts to pay off.

ViewModel changes

Remember that MapsViewModel is used to model the View seen by the user. You want to show the user a marker at each saved bookmark location, so you’ll create a class in MapsViewModel to hold the data for each visible bookmark marker.

data class BookmarkMarkerView(
    var id: Long? = null,
    var location: LatLng = LatLng(0.0, 0.0))
private var bookmarks: LiveData<List<BookmarkMarkerView>>?
    = null
private fun bookmarkToMarkerView(bookmark: Bookmark):
    MapsViewModel.BookmarkMarkerView {
  return MapsViewModel.BookmarkMarkerView(
      bookmark.id,
      LatLng(bookmark.latitude, bookmark.longitude))
}
private fun mapBookmarksToMarkerView() {
  // 1
  bookmarks = Transformations.map(bookmarkRepo.allBookmarks) { repoBookmarks ->
    // 2
    repoBookmarks.map { bookmark ->
      bookmarkToMarkerView(bookmark)
    }
  }
}
fun getBookmarkMarkerViews() :
    LiveData<List<BookmarkMarkerView>>? {
  if (bookmarks == null) {
    mapBookmarksToMarkerView()
  }
  return bookmarks
}

MapsActivity changes

Now you’re ready to update MapsActivity to listen for changes in the View model. First, you need a method to add a bookmark marker to the map.

private fun addPlaceMarker(
    bookmark: MapsViewModel.BookmarkMarkerView): Marker? {

  val marker = map.addMarker(MarkerOptions()
      .position(bookmark.location)
      .icon(BitmapDescriptorFactory.defaultMarker(
          BitmapDescriptorFactory.HUE_AZURE))
      .alpha(0.8f))

  marker.tag = bookmark

  return marker
}
private fun displayAllBookmarks(
    bookmarks: List<MapsViewModel.BookmarkMarkerView>) {
  for (bookmark in bookmarks) {
    addPlaceMarker(bookmark)
  }
}
private fun createBookmarkMarkerObserver() {
  // 1
  mapsViewModel.getBookmarkMarkerViews()?.observe(
      this, Observer<List<MapsViewModel.BookmarkMarkerView>> {
        // 2
        map.clear()
        // 3
        it?.let {
          displayAllBookmarks(it)
        }
      })
}
createBookmarkMarkerObserver()

Where to go from here?

There’s one problem with this new implementation: If you tap on any of the blue markers, the app will crash. Can you guess why? If not, don’t worry! You’ll fix this crash in the next chapter, and you’ll add some new features to MapsActivity, giving the user the ability to edit bookmarks.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now