Home Android & Kotlin Books Android Apprentice

19
Finishing Touches Written by Tom Blankenship

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

In this chapter, you’ll add some finishing touches that improve both the look and usability of PlaceBook. Even though PlaceBook is perfectly functional as-is, it’s often the subtle enhancements that make an app go from good to great. With that in mind, you’ll wrap things up by making the following changes:

  • Adding categories for bookmarks.
  • Displaying category specific icons on the map.
  • Adding place search.
  • Adding ad-hoc bookmark creation.
  • Adding bookmark deletions.
  • Adding bookmark sharing.
  • Updating the color scheme.
  • Displaying progress using indicators.

Getting started

The starter project for this chapter includes additional resources and an updated app icon. You can either begin this chapter with the starter project or copy the following resources from the starter project into your project:

  • src/main/ic_launcher_round-web.png
  • src/main/ic_launcher-web.png
  • src/main/res/drawable/ic_gas.png
  • src/main/res/drawable/ic_lodging.png
  • src/main/res/drawable/ic_restaurant.png
  • src/main/res/drawable/ic_search_white.png
  • src/main/res/drawable/ic_shopping.png
  • src/main/res/mipmap/ic_launcher_round.png
  • src/main/res/mipmap/ic_launcher.png

Make sure to copy the files from all of the drawable folders, including everything with the .hdpi, .mdpi, .xhdpi and .xxhdpi extensions.

If you’re using the starter project, remember to replace the key in google_maps_api.xml and in the method setupPlacesClient() in MapsActivity.kt.

Bookmark categories

Assigning categories to bookmarks gives you the opportunity to show different icons on the map for each type of place. Google already provides category information for Places, so you’ll use this to set a default category, and let the user assign a different category if they choose.

Update the model

Start by adding a new category property to Bookmark.

data class Bookmark(
    @PrimaryKey(autoGenerate = true) var id: Long? = null,
    var placeId: String? = null,
    var name: String = "",
    var address: String = "",
    var latitude: Double = 0.0,
    var longitude: Double = 0.0,
    var phone: String = "",
    var notes: String = "",
    var category: String = ""
)
@Database(entities = arrayOf(Bookmark::class), version = 3)

Converting place types

If you examine Place defined by the Google Play Services, you’ll notice that it provides a fairly long list of place types:

int TYPE_OTHER = 0;
int TYPE_ACCOUNTING = 1;
int TYPE_AIRPORT = 2;
int TYPE_AMUSEMENT_PARK = 3;
int TYPE_AQUARIUM = 4;
int TYPE_ART_GALLERY = 5;
...
private fun buildCategoryMap() : HashMap<Place.Type, String> {
  return hashMapOf(
	Place.Type.BAKERY to "Restaurant",
	Place.Type.BAR to "Restaurant",
	Place.Type.CAFE to "Restaurant",
	Place.Type.FOOD to "Restaurant",
	Place.Type.RESTAURANT to "Restaurant",
	Place.Type.MEAL_DELIVERY to "Restaurant",
	Place.Type.MEAL_TAKEAWAY to "Restaurant",
	Place.Type.GAS_STATION to "Gas",
	Place.Type.CLOTHING_STORE to "Shopping",
	Place.Type.DEPARTMENT_STORE to "Shopping",
	Place.Type.FURNITURE_STORE to "Shopping",
	Place.Type.GROCERY_OR_SUPERMARKET to "Shopping",
	Place.Type.HARDWARE_STORE to "Shopping",
	Place.Type.HOME_GOODS_STORE to "Shopping",
	Place.Type.JEWELRY_STORE to "Shopping",
	Place.Type.SHOE_STORE to "Shopping",
	Place.Type.SHOPPING_MALL to "Shopping",
	Place.Type.STORE to "Shopping",
	Place.Type.LODGING to "Lodging",
	Place.Type.ROOM to "Lodging"
  )
}
private var categoryMap: HashMap<Place.Type, String> = buildCategoryMap()
fun placeTypeToCategory(placeType: Place.Type): String {
  var category = "Other"
  if (categoryMap.containsKey(placeType)) {
    category = categoryMap[placeType].toString()
  }
  return category
}
private fun buildCategories() : HashMap<String, Int> {
  return hashMapOf(
      "Gas" to R.drawable.ic_gas,
      "Lodging" to R.drawable.ic_lodging,
      "Other" to R.drawable.ic_other,
      "Restaurant" to R.drawable.ic_restaurant,
      "Shopping" to R.drawable.ic_shopping
  )
}
private var allCategories: HashMap<String, Int> =
    buildCategories()
fun getCategoryResourceId(placeCategory: String): Int? {
    return allCategories[placeCategory]
}

Updating the view model

You’re ready to update the map’s view model to support bookmark categories.

private fun getPlaceCategory(place: Place): String {
  // 1
  var category = "Other"
  val placeTypes = place.types

  placeTypes?.let { placeTypes ->
    // 2
    if (placeTypes.size > 0) {
      // 3
      val placeType = placeTypes[0]
      category = bookmarkRepo.placeTypeToCategory(placeType)
    }
  }
  // 4
  return category
}
bookmark.category = getPlaceCategory(place)
data class BookmarkView(val id: Long? = null,
                        val location: LatLng = LatLng(0.0, 0.0),
                        val name: String = "",
                        val phone: String = "",
                        val categoryResourceId: Int? = null) {
private fun bookmarkToBookmarkView(bookmark: Bookmark):
    MapsViewModel.BookmarkView {
  return MapsViewModel.BookmarkView(
      bookmark.id,
      LatLng(bookmark.latitude, bookmark.longitude),
      bookmark.name,
      bookmark.phone,
      bookmarkRepo.getCategoryResourceId(bookmark.category))
}

Updating the UI

You can now update the user interface to show the category icons.

val placeFields = listOf(Place.Field.ID,
	Place.Field.NAME,
	Place.Field.PHONE_NUMBER,
	Place.Field.PHOTO_METADATAS,
	Place.Field.ADDRESS,
	Place.Field.LAT_LNG,
	Place.Field.TYPES)
val marker = map.addMarker(MarkerOptions()
    .position(bookmark.location)
    .title(bookmark.name)
    .snippet(bookmark.phone)
    .icon(bookmark.categoryResourceId?.let {
        BitmapDescriptorFactory.fromResource(it)
     })
    .alpha(0.8f))

bookmarkViewData.categoryResourceId?.let {
    holder.categoryImageView.setImageResource(it) 
}

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal">
  <TextView
      android:id="@+id/textViewCategoryLabel"
      style="@style/BookmarkLabel"
      android:layout_weight='0.4'
      android:text="Category"/>
  <ImageView
      android:id="@+id/imageViewCategory"
      android:layout_width="24dp"
      android:layout_height="24dp"
      android:src="@drawable/ic_other"
      android:layout_marginStart="16dp"
      android:layout_marginLeft="16dp"
      android:layout_gravity="bottom"
      />
  <Spinner
      android:id="@+id/spinnerCategory"
      android:layout_width="0dp"
      android:layout_height="wrap_content"
      android:layout_weight='1.4'
      android:layout_marginStart="8dp"
      android:layout_marginEnd="8dp"
      android:layout_marginTop="16dp"
      />
</LinearLayout>
data class BookmarkDetailsView(var id: Long? = null,
                               var name: String = "",
                               var phone: String = "",
                               var address: String = "",
                               var notes: String = "",
                               var category: String = "") {
return BookmarkDetailsView(
    bookmark.id,
    bookmark.name,
    bookmark.phone,
    bookmark.address,
    bookmark.notes,
    bookmark.category
)
bookmark.category = bookmarkDetailsView.category
fun getCategoryResourceId(category: String): Int? {
  return bookmarkRepo.getCategoryResourceId(category)
}
val categories: List<String>
  get() = ArrayList(allCategories.keys)
fun getCategories(): List<String> {
  return bookmarkRepo.categories
}
private fun populateCategoryList() {
  // 1
  val bookmarkView = bookmarkDetailsView ?: return
  // 2
  val resourceId =
      bookmarkDetailsViewModel.getCategoryResourceId(
          bookmarkView.category)
  // 3
  resourceId?.let { imageViewCategory.setImageResource(it) }
  // 4
  val categories = bookmarkDetailsViewModel.getCategories()
  // 5
  val adapter = ArrayAdapter(this,
      android.R.layout.simple_spinner_item, categories)
  adapter.setDropDownViewResource(
      android.R.layout.simple_spinner_dropdown_item)
  // 6
  spinnerCategory.adapter = adapter
  // 7
  val placeCategory = bookmarkView.category
  spinnerCategory.setSelection(
      adapter.getPosition(placeCategory))
}
populateCategoryList()

// 1
spinnerCategory.post {
  // 2
  spinnerCategory.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
    override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
      // 3
      val category = parent.getItemAtPosition(position) as String
      val resourceId = bookmarkDetailsViewModel.getCategoryResourceId(category)
      resourceId?.let {
          imageViewCategory.setImageResource(it) }
    }
    override fun onNothingSelected(parent: AdapterView<*>) {
      // NOTE: This method is required but not used.
    }
  }
}
bookmarkView.category = spinnerCategory.selectedItem as String

Searching for places

What if the user is looking for a specific place and can’t find it on the map? No worries! The Google Places API provides a powerful search feature that you’ll take advantage of next. You’ll add a new search button overlay on the map to trigger the search feature.

Use PlaceAutocomplete search

Open MapsActivity.kt and the following property to the companion object:

private const val AUTOCOMPLETE_REQUEST_CODE = 2
private fun searchAtCurrentLocation() {

  // 1
  val placeFields = listOf(
      Place.Field.ID, 
      Place.Field.NAME, 
      Place.Field.PHONE_NUMBER, 
      Place.Field.PHOTO_METADATAS, 
      Place.Field.LAT_LNG, 
      Place.Field.ADDRESS,
      Place.Field.TYPES)

  // 2
  val bounds = RectangularBounds.newInstance(map.projection.visibleRegion.latLngBounds)
  try {
    // 3
    val intent = Autocomplete.IntentBuilder(
        AutocompleteActivityMode.OVERLAY, placeFields)
        .setLocationBias(bounds)
        .build(this)
    // 4
    startActivityForResult(intent, AUTOCOMPLETE_REQUEST_CODE)
  } catch (e: GooglePlayServicesRepairableException) {
    //TODO: Handle exception
  } catch (e: GooglePlayServicesNotAvailableException) {
    //TODO: Handle exception
  }
}
override fun onActivityResult(requestCode: Int, resultCode: Int,
                              data: Intent?) {
  super.onActivityResult(requestCode, resultCode, data)
  // 1
  when (requestCode) {
    AUTOCOMPLETE_REQUEST_CODE ->
      // 2
      if (resultCode == Activity.RESULT_OK && data != null) {
        // 3
        val place = Autocomplete.getPlaceFromIntent(data)
        // 4
        val location = Location("")
        location.latitude = place.latLng?.latitude ?: 0.0
        location.longitude = place.latLng?.longitude ?: 0.0
        updateMapToLocation(location)
        // 5
        displayPoiGetPhotoStep(place)
      }
  }
}

Update the UI

Next, you’ll surround the main map view with a frame Layout and add a floating search button on top of the map.

<FrameLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
  <com.google.android.material.floatingactionbutton.FloatingActionButton
    android:id="@+id/fab"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="bottom|end"
    android:layout_margin="16dp"
    app:srcCompat="@drawable/ic_search_white"/>

</FrameLayout>
fab.setOnClickListener {
  searchAtCurrentLocation()
}

Create ad-hoc bookmarks

Google’s database of places is impressive, but it’s not perfect. What if the user wants to add a bookmark for a place that doesn’t show up on the map? You can make this possible by allowing the user to drop a pin at any location on the map.

fun addBookmark(latLng: LatLng) : Long? {
  val bookmark = bookmarkRepo.createBookmark()
  bookmark.name = "Untitled"
  bookmark.longitude = latLng.longitude
  bookmark.latitude = latLng.latitude
  bookmark.category = "Other"
  return bookmarkRepo.addBookmark(bookmark)
}
private fun newBookmark(latLng: LatLng) {
  GlobalScope.launch {
    val bookmarkId = mapsViewModel.addBookmark(latLng)
    bookmarkId?.let {
      startBookmarkDetails(it)
    }
  }
}
map.setOnMapLongClickListener { latLng ->
  newBookmark(latLng)
}

Deleting bookmarks

Any full featured app needs to account for user mistakes. In PlaceBook, this means letting the user remove a bookmark that’s no longer needed or one that was added by accident. For this, you’ll add a trashcan action bar icon to the detail Activity to let the user delete a bookmark.

<item
    android:id="@+id/action_delete"
    android:icon="@android:drawable/ic_menu_delete"
    android:title="Delete"
    app:showAsAction="ifRoom"/>
object FileUtils {
  fun deleteFile(context: Context, filename: String) {
    val dir = context.filesDir
    val file = File(dir, filename)
    file.delete()
  }
}
fun deleteImage(context: Context) {
  id?.let {
    FileUtils.deleteFile(context, generateImageFilename(it))
  }
}
fun deleteBookmark(bookmark: Bookmark) {
  bookmark.deleteImage(context)
  bookmarkDao.deleteBookmark(bookmark)
}
fun deleteBookmark(bookmarkDetailsView: BookmarkDetailsView) {
  GlobalScope.launch {
    val bookmark = bookmarkDetailsView.id?.let {
      bookmarkRepo.getBookmark(it)
    }
    bookmark?.let {
      bookmarkRepo.deleteBookmark(it)
    }
  }
}
private fun deleteBookmark()
{
  val bookmarkView = bookmarkDetailsView ?: return

  AlertDialog.Builder(this)
      .setMessage("Delete?")
      .setPositiveButton("Ok") { _, _ ->
        bookmarkDetailsViewModel.deleteBookmark(bookmarkView)
        finish()
      }
      .setNegativeButton("Cancel", null)
      .create().show()
}
R.id.action_delete -> {
  deleteBookmark()
  return true
}
fun mapBookmarkToBookmarkView(bookmarkId: Long) {
  val bookmark = bookmarkRepo.getLiveBookmark(bookmarkId)
  bookmarkDetailsView = Transformations.map(bookmark) { repoBookmark ->
    repoBookmark?.let { repoBookmark ->
      bookmarkToBookmarkView(repoBookmark)
    }
  }
}

Sharing bookmarks

Your users have painstakingly bookmarked some fantastic places, so why not let them share their good finds with friends?

data class BookmarkDetailsView(var id: Long? = null,
                               var name: String = "",
                               var phone: String = "",
                               var address: String = "",
                               var notes: String = "",
                               var category: String = "",
                               var longitude: Double = 0.0,
                               var latitude: Double = 0.0,
                               var placeId: String? = null) {
return BookmarkDetailsView(
    bookmark.id,
    bookmark.name,
    bookmark.phone,
    bookmark.address,
    bookmark.notes,
    bookmark.category,
    bookmark.longitude,
    bookmark.latitude,
    bookmark.placeId
)
private fun sharePlace() {
  // 1
  val bookmarkView = bookmarkDetailsView ?: return
  // 2
  var mapUrl = ""
  if (bookmarkView.placeId == null) {
    // 3
    val location = URLEncoder.encode("${bookmarkView.latitude},"
        + "${bookmarkView.longitude}", "utf-8")
    mapUrl = "https://www.google.com/maps/dir/?api=1" +
        "&destination=$location"
  } else {
    // 4
    val name = URLEncoder.encode(bookmarkView.name, "utf-8")
    mapUrl = "https://www.google.com/maps/dir/?api=1" +
        "&destination=$name&destination_place_id=" +
        "${bookmarkView.placeId}"
  }
  // 5
  val sendIntent = Intent()
  sendIntent.action = Intent.ACTION_SEND
  // 6
  sendIntent.putExtra(Intent.EXTRA_TEXT,
      "Check out ${bookmarkView.name} at:\n$mapUrl")
  sendIntent.putExtra(Intent.EXTRA_SUBJECT,
      "Sharing ${bookmarkView.name}")
  // 7
  sendIntent.type = "text/plain"
  // 8
  startActivity(sendIntent)
}
<FrameLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

<LinearLayout
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
  <com.google.android.material.floatingactionbutton.FloatingActionButton
      android:id="@+id/fab"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_margin="16dp"
      android:layout_gravity="bottom|end"
      app:srcCompat="@android:drawable/ic_dialog_email"/>

</FrameLayout>
private fun setupFab() {
  fab.setOnClickListener { sharePlace() }
}
setupFab()

Color scheme

It’s a minor change, but updating the color scheme to match the bookmark icon colors will make the app look much better.

<color name="colorPrimary">#3748AC</color>
<color name="colorPrimaryDark">#2A3784</color>
<color name="colorAccent">#E3A60B</color>

Progress indicator

It’s always good practice to let the user know when a potentially long-running operation is in progress. It also makes sense to prevent user interaction during this time. You’ll accomplish both of these tasks next.

<ProgressBar
    android:id="@+id/progressBar"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:visibility="gone"/>
private fun disableUserInteraction() {
  window.setFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
      WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE)
}

private fun enableUserInteraction() {
  window.clearFlags(
      WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE)
}
private fun showProgress() {
  progressBar.visibility = ProgressBar.VISIBLE
  disableUserInteraction()
}

private fun hideProgress() {
  progressBar.visibility = ProgressBar.GONE
  enableUserInteraction()
}
showProgress()
showProgress()
hideProgress()
hideProgress()
hideProgress()

Where to go from here?

Congratulations! You made it through the entire PlaceBook app section. You built a useful map-based app and learned a lot of new concepts along the way.

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.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2021 Razeware LLC

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.