Home Android & Kotlin Books Android Apprentice

17
Detail Activity 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 the ability to edit bookmarks. This involves creating a new Activity to display the bookmark details with editable fields.

Getting started

If you’re following along with your own app, open it and copy res/drawable/ic_action_done.png from the starter project into your project. Also, make sure to copy the files from all of the drawable folders, including everything with the .hdpi, .mdpi, .xhdpi and .xxhdpi extensions.

If you want to use the starter project instead, locate the projects folder for this chapter and open the PlaceBook app inside the starter folder. If you do use the starter app, don’t forget to add your google_maps_key in google_maps_api.xml. 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.

Fixing the info window

Before moving on, you need to track down and fix that pesky bug left over from the previous chapter where the app crashes when tapping on a blue marker. The desired behavior is:

com.raywenderlich.placebook.adapter.BookmarkInfoWindowAdapter.getInfoContents

imageView.setImageBitmap(
    (marker.tag as MapsActivity.PlaceInfo).image)
when (marker.tag) {
  // 1
  is MapsActivity.PlaceInfo -> {
    imageView.setImageBitmap(
        (marker.tag as MapsActivity.PlaceInfo).image)
  }
  // 2  
  is MapsViewModel.BookmarkMarkerView -> {
    var bookMarkview = marker.tag as
        MapsViewModel.BookmarkMarkerView
    // Set imageView bitmap here
  }
}

Saving an image

Although you can add an image directly to the Bookmark model class and let the Room library save it to the database, it’s not best practice to store large chunks of data in the database. A better method is to store the image as a file that’s linked to the record in the database.

// 1
object ImageUtils {
  // 2
  fun saveBitmapToFile(context: Context, bitmap: Bitmap,
      filename: String) {      
    // 3
    val stream = ByteArrayOutputStream()
    // 4
    bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
    // 5
    val bytes = stream.toByteArray()
    // 6
    ImageUtils.saveBytesToFile(context, bytes, filename)
  }
  // 7
  private fun saveBytesToFile(context: Context, bytes:
      ByteArray, filename: String) {
    val outputStream: FileOutputStream
    // 8
    try {    
      // 9
      outputStream = context.openFileOutput(filename,
          Context.MODE_PRIVATE)
      // 10
      outputStream.write(bytes)
      outputStream.close()
    } catch (e: Exception) {
      e.printStackTrace()
    }
  }
}
{
  // 1
  fun setImage(image: Bitmap, context: Context) {
    // 2
    id?.let {
      ImageUtils.saveBitmapToFile(context, image,
          generateImageFilename(it))
    }
  }
  //3
  companion object {
    fun generateImageFilename(id: Long): String {
      // 4
      return "bookmark$id.png"
    }
  }
}

Adding the image to the bookmark

Next, you need to set the image for a bookmark when it’s added to the database.

image?.let { bookmark.setImage(it, getApplication()) }

Simplifying the bookmark process

Before testing this new functionality there’s a small change you can make to simplify the process of adding a new bookmark.

marker?.showInfoWindow()

Using Device File Explorer

If you want to verify the image was saved and take a peek behind the scenes at how Android stores files, you can use the Device File Explorer in Android Studio. This is a handy tool for working directly with the Android file system.

Loading an image

It’s time to load the image from a file. This is considerably easier than saving an image because Android provides a method on BitmapFactory for loading images from files.

fun loadBitmapFromFile(context: Context, filename: String):
    Bitmap? {
  val filePath = File(context.filesDir, filename).absolutePath
  return BitmapFactory.decodeFile(filePath)
}

Updating BookmarkMarkerView

Now that you can load the image from where it’s stored, it’s time to update BookmarkMarkerView to provide the image for the View.

Loading images on-demand

Open MapsViewModel.kt and replace the BookmarkMarkerView data class definition with the following:

data class BookmarkMarkerView(var id: Long? = null,
                              var location: LatLng = LatLng(0.0, 0.0))
{
  fun getImage(context: Context): Bitmap? {
    id?.let {
      return ImageUtils.loadBitmapFromFile(context,
          Bookmark.generateImageFilename(it))
    }
    return null
  }
}
class BookmarkInfoWindowAdapter(val context: Activity) :
    GoogleMap.InfoWindowAdapter {
imageView.setImageBitmap(bookMarkview.getImage(context))

Updating the Info window

Open MapsViewModel.kt and update the BookmarkMarkerView declaration to match the following:

data class BookmarkMarkerView(
    var id: Long? = null,
    var location: LatLng = LatLng(0.0, 0.0),
    var name: String = "",
    var phone: String = "") {
private fun bookmarkToMarkerView(bookmark: Bookmark):
    MapsViewModel.BookmarkMarkerView {
  return MapsViewModel.BookmarkMarkerView(
      bookmark.id,
      LatLng(bookmark.latitude, bookmark.longitude),
      bookmark.name,
      bookmark.phone)
}
val marker = map.addMarker(MarkerOptions()
    .position(bookmark.location)
    .title(bookmark.name)
    .snippet(bookmark.phone)
    .icon(BitmapDescriptorFactory.defaultMarker(
        BitmapDescriptorFactory.HUE_AZURE))
    .alpha(0.8f))

Bookmark detail activity

You’ve waited patiently, and it’s finally time to build out the detail Activity for editing a bookmark. For that, you’ll add a new screen that allows the user to edit key details about the bookmark, along with a custom note. You’ll do this by creating a new Activity that displays when a user taps on an Info window.

Designing the edit screen

Before creating the Activity, let’s go over the screen layout and the main elements that will be incorporated.

The Bookmark Edit Layout
Bqo Xuuzdijg Iyod Tonoam

Defining styles

First, you need to define some standard styles that are required when using the support library version of the toolbar.

<style name="AppTheme.NoActionBar">
  <item name="windowActionBar">false</item>
  <item name="windowNoTitle">true</item>
</style>

<style name="AppTheme.AppBarOverlay"
    parent="ThemeOverlay.AppCompat.Dark.ActionBar"/>
<style name="AppTheme.PopupOverlay"
    parent="ThemeOverlay.AppCompat.Light"/>
<style name="BookmarkLabel">
  <item name="android:layout_width">0dp</item>
  <item name="android:layout_height">wrap_content</item>
  <item name="android:layout_weight">0.2</item>
  <item name="android:layout_gravity">bottom</item>
  <item name="android:layout_marginStart">8dp</item>
  <item name="android:layout_marginLeft">8dp</item>
  <item name="android:layout_marginBottom">4dp</item>
  <item name="android:gravity">bottom</item>
</style>

<style name="BookmarkEditText">
  <item name="android:layout_width">0dp</item>
  <item name="android:layout_weight">0.8</item>
  <item name="android:layout_height">wrap_content</item>
  <item name="android:layout_marginEnd">8dp</item>
  <item name="android:layout_marginRight">8dp</item>
  <item name="android:layout_marginStart">8dp</item>
  <item name="android:layout_marginLeft">8dp</item>
  <item name="android:ems">10</item>
</style>

Creating the details layout

Finally, you need to create the bookmark details Layout based on the design. The Activity will use all of the new styles you just added to the project.

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

<LinearLayout
    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"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/app_bar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:fitsSystemWindows="true"
        android:theme="@style/AppTheme.AppBarOverlay">

      <androidx.appcompat.widget.Toolbar
          android:id="@+id/toolbar"
          android:layout_width="match_parent"
          android:layout_height="?attr/actionBarSize"
          app:popupTheme="@style/AppTheme.PopupOverlay"/>

    </com.google.android.material.appbar.AppBarLayout>

    <ImageView
        android:id="@+id/imageViewPlace"
        android:layout_margin="0dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:maxHeight="300dp"
        android:scaleType="fitCenter"
        android:adjustViewBounds="true"
        app:srcCompat="@drawable/default_photo"/>

</LinearLayout>

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="8dp"
    android:orientation="horizontal">

  <TextView
      android:id="@+id/textViewName"
      style="@style/BookmarkLabel"
      android:text="Name"/>

  <EditText
      android:id="@+id/editTextName"
      style="@style/BookmarkEditText"
      android:hint="Name"
      android:inputType="text"
      />
</LinearLayout>

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal">

  <TextView
      android:id="@+id/textViewNotes"
      style="@style/BookmarkLabel"
      android:text="Notes"/>

  <EditText
      android:id="@+id/editTextNotes"
      style="@style/BookmarkEditText"
      android:hint="Enter notes"
      android:inputType="textMultiLine"/>
</LinearLayout>
<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal">

  <TextView
      android:id="@+id/textViewPhone"
      style="@style/BookmarkLabel"
      android:text="Phone"/>

  <EditText
      android:id="@+id/editTextPhone"
      style="@style/BookmarkEditText"
      android:hint="Phone number"
      android:inputType="phone"
      />
</LinearLayout>
<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal">

  <TextView
      android:id="@+id/textViewAddress"
      style="@style/BookmarkLabel"
      android:text="Address"/>

  <EditText
      android:id="@+id/editTextAddress"
      style="@style/BookmarkEditText"
      android:hint="Address"
      android:inputType="textMultiLine"
      />
</LinearLayout>

Details activity class

Now that the bookmark details Layout is complete, you can create the details Activity to go along with it.

class BookmarkDetailsActivity : AppCompatActivity() {
  override fun onCreate(savedInstanceState:
      android.os.Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_bookmark_details)
    setupToolbar()
  }

  private fun setupToolbar() {
    setSupportActionBar(toolbar)
  }
}

Support design library

To use setSupportActionBar() you need to include the material design library provided by Google.

implementation 'com.google.android.material:material:1.1.0'

Updating the manifest

Next, you need to make Android aware of the new BookmarkDetailsActivity class, so add the Activity to AndroidManifest.xml within the <application> section:

<activity
    android:name=
        "com.raywenderlich.placebook.ui.BookmarkDetailsActivity"
    android:label="Bookmark"
    android:theme="@style/AppTheme.NoActionBar"
    android:windowSoftInputMode="stateHidden">
</activity>

Starting the details Activity

You can now hook up the new details Activity to the main maps Activity. You’ll detect when the user taps on a bookmark Info window, and then start the details Activity.

private fun startBookmarkDetails(bookmarkId: Long) {
  val intent = Intent(this, BookmarkDetailsActivity::class.java)
  startActivity(intent)
}
private fun handleInfoWindowClick(marker: Marker) {
  when (marker.tag) {
    is MapsActivity.PlaceInfo -> {
      val placeInfo = (marker.tag as PlaceInfo)
      if (placeInfo.place != null && placeInfo.image != null) {
        GlobalScope.launch {
          mapsViewModel.addBookmarkFromPlace(placeInfo.place,
              placeInfo.image)
        }
      }
      marker.remove();
    }
    is MapsViewModel.BookmarkMarkerView -> {
      val bookmarkMarkerView = (marker.tag as
          MapsViewModel.BookmarkMarkerView)
      marker.hideInfoWindow()
      bookmarkMarkerView.id?.let {
        startBookmarkDetails(it)
      }
    }
  }
}

<ScrollView
    android:layout_width="match_parent"
    android:layout_height="match_parent">

  <LinearLayout
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:orientation="vertical">
  </LinearLayout>
</ScrollView>

Populating the bookmark

The Activity has the general look you want, but it lacks any knowledge about the bookmark. To fix this, you’ll pass the bookmark ID to the Activity so that it can display the bookmark data.

const val EXTRA_BOOKMARK_ID =
    "com.raywenderlich.placebook.EXTRA_BOOKMARK_ID"
intent.putExtra(EXTRA_BOOKMARK_ID, bookmarkId)
fun getLiveBookmark(bookmarkId: Long): LiveData<Bookmark> {
  val bookmark = bookmarkDao.loadLiveBookmark(bookmarkId)
  return bookmark
}
class BookmarkDetailsViewModel(application: Application) :
    AndroidViewModel(application) {    

  private var bookmarkRepo: BookmarkRepo =
      BookmarkRepo(getApplication())
}
data class BookmarkDetailsView(
    var id: Long? = null,
    var name: String = "",
    var phone: String = "",
    var address: String = "",
    var notes: String = ""
) {

  fun getImage(context: Context): Bitmap? {
    id?.let {
      return ImageUtils.loadBitmapFromFile(context,
          Bookmark.generateImageFilename(it))
    }
    return null
  }
}

Adding notes to the database

Before continuing, you need a way to store notes for a 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 = ""
)
@Database(entities = arrayOf(Bookmark::class), version = 2)
instance = Room.databaseBuilder(context.applicationContext,
    PlaceBookDatabase::class.java, "PlaceBook")
    .fallbackToDestructiveMigration()
    .build()

Bookmark view model

That’s all you need to support the revised Bookmark model in the database. Now you need to convert the database model to a view model.

private fun bookmarkToBookmarkView(bookmark: Bookmark): BookmarkDetailsView {
  return BookmarkDetailsView(
      bookmark.id,
      bookmark.name,
      bookmark.phone,
      bookmark.address,
      bookmark.notes
  )
}
private var bookmarkDetailsView: LiveData<BookmarkDetailsView>? = null
private fun mapBookmarkToBookmarkView(bookmarkId: Long) {
  val bookmark = bookmarkRepo.getLiveBookmark(bookmarkId)
  bookmarkDetailsView = Transformations.map(bookmark) { repoBookmark ->
    bookmarkToBookmarkView(repoBookmark)
  }
}
fun getBookmark(bookmarkId: Long): LiveData<BookmarkDetailsView>? {
  if (bookmarkDetailsView == null) {
    mapBookmarkToBookmarkView(bookmarkId)
  }
  return bookmarkDetailsView
}

Retrieving the bookmark view

You’re ready to add the code to retrieve the BookmarkDetailsView LiveData object in the View Activity.

private val bookmarkDetailsViewModel by 
    viewModels<BookmarkDetailsViewModel>()
private var bookmarkDetailsView:
    BookmarkDetailsViewModel.BookmarkDetailsView? = null
private fun populateFields() {
  bookmarkDetailsView?.let { bookmarkView ->
    editTextName.setText(bookmarkView.name)
    editTextPhone.setText(bookmarkView.phone)
    editTextNotes.setText(bookmarkView.notes)
    editTextAddress.setText(bookmarkView.address)
  }
}
private fun populateImageView() {
  bookmarkDetailsView?.let { bookmarkView ->
    val placeImage = bookmarkView.getImage(this)
    placeImage?.let {
      imageViewPlace.setImageBitmap(placeImage)
    }
  }
}

Using the intent data

When the user taps on the Info window for a bookmark on the maps Activity, it passes the bookmark ID to the details Activity.

private fun getIntentData() {
  // 1
  val bookmarkId = intent.getLongExtra(
      MapsActivity.Companion.EXTRA_BOOKMARK_ID, 0)  
  // 2    
  bookmarkDetailsViewModel.getBookmark(bookmarkId)?.observe(
      this, Observer<BookmarkDetailsViewModel.BookmarkDetailsView> {
    // 3
    it?.let {
      bookmarkDetailsView = it
      // Populate fields from bookmark
      populateFields()
      populateImageView()
    }
  })
}

Finishing the detail activity

You’re ready to pull everything together by adding the following call to the end of onCreate() in BookmarkDetailsActivity.

getIntentData()

Saving changes

The only major feature left is to save the user’s edits. For that, you’ll add a checkmark Toolbar item to trigger the save.

<?xml version="1.0" encoding="utf-8"?>
<menu
    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"
    tools:context=
    "com.raywenderlich.placebook.ui.BookmarkDetailsActivity">

  <item
      android:id="@+id/action_save"
      android:icon="@drawable/ic_action_done"
      android:title="Save"
      app:showAsAction="ifRoom"/>
</menu>
override fun onCreateOptionsMenu(menu: android.view.Menu):
    Boolean {
  val inflater = menuInflater
  inflater.inflate(R.menu.menu_bookmark_details, menu)
  return true
}
fun updateBookmark(bookmark: Bookmark) {
  bookmarkDao.updateBookmark(bookmark)
}

fun getBookmark(bookmarkId: Long): Bookmark {
  return bookmarkDao.loadBookmark(bookmarkId)
}
private fun bookmarkViewToBookmark(bookmarkView: BookmarkDetailsView):
    Bookmark? {
  val bookmark = bookmarkView.id?.let {
    bookmarkRepo.getBookmark(it)
  }
  if (bookmark != null) {
    bookmark.id = bookmarkView.id
    bookmark.name = bookmarkView.name
    bookmark.phone = bookmarkView.phone
    bookmark.address = bookmarkView.address
    bookmark.notes = bookmarkView.notes
  }
  return bookmark
}
fun updateBookmark(bookmarkView: BookmarkDetailsView) {
  // 1
  GlobalScope.launch {
    // 2
    val bookmark = bookmarkViewToBookmark(bookmarkView)
    // 3
    bookmark?.let { bookmarkRepo.updateBookmark(it) }
  }
}
private fun saveChanges() {
  val name = editTextName.text.toString()
  if (name.isEmpty()) {
    return
  }
  bookmarkDetailsView?.let { bookmarkView ->
    bookmarkView.name = editTextName.text.toString()
    bookmarkView.notes = editTextNotes.text.toString()
    bookmarkView.address = editTextAddress.text.toString()
    bookmarkView.phone = editTextPhone.text.toString()
    bookmarkDetailsViewModel.updateBookmark(bookmarkView)
  }
  finish()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
  when (item.itemId) {
    R.id.action_save -> {
      saveChanges()
      return true
    }
    else -> return super.onOptionsItemSelected(item)
  }
}

Where to go from here?

Congratulations! You can now edit bookmarks, but there’s still more work to do. The next chapter wraps things up by adding some additional features and putting the finishing touches on the app.

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.