Chapters

Hide chapters

Android Apprentice

Fourth Edition · Android 11 · Kotlin 1.4 · Android Studio 4.1

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

22. Podcast Details
Written by Kevin D Moore

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 find their favorite podcasts, you’re ready to add a podcast detail screen. In this chapter, you’ll complete the following:

  1. Design and build the podcast detail Fragment.
  2. Expand on the app architecture.
  3. Add a podcast detail Fragment.

Getting started

If you’re following along with your own project, open it and keep using it with this chapter. If not, don’t worry. Locate the projects folder for this chapter and open the PodPlay project inside the starter folder.

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

You’ll start by designing a Layout for the podcast detail screen. The purpose of the detail screen is to give the user a quick overview of the podcast, including the title, description, album art, and a list of recent episodes. It will also provide a subscribe action.

The Layout will contain the album art and title at the top, a scrollable description below that, and a list of episodes below the description. Each episode will contain the title, description, published date, and length. The final Layout will look like this:

Rather than define a new Activity for the podcast detail, you’ll use a Fragment to swap out the main podcast listing View with the podcast detail View. The advantage of using Fragments will become more evident as you build out the full user interface in later chapters.

Defining the Layouts

Create a new Layout and name it fragment_podcast_details.xml. Then replace the contents with the following:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

  <androidx.constraintlayout.widget.ConstraintLayout
      android:id="@+id/headerView"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:background="#eeeeee"
      android:maxHeight="300dp"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent">

    <ImageView
        android:id="@+id/feedImageView"
        android:layout_width="60dp"
        android:layout_height="60dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        android:src="@android:drawable/ic_menu_report_image"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>

    <TextView
        android:id="@+id/feedTitleTextView"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginEnd="8dp"
        android:layout_marginStart="8dp"
        android:maxHeight="100dp"
        android:text=""
        android:textSize="14sp"
        android:textStyle="bold"
        app:layout_constraintBottom_toBottomOf="@+id/feedImageView"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/feedImageView"
        app:layout_constraintTop_toTopOf="@+id/feedImageView"/>

    <TextView
        android:id="@+id/feedDescTextView"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginEnd="8dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="4dp"
        android:maxHeight="100dp"
        android:paddingBottom="8dp"
        android:scrollbars="vertical"
        android:text=""
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/feedImageView"/>

  </androidx.constraintlayout.widget.ConstraintLayout>

  <androidx.recyclerview.widget.RecyclerView
      android:id="@+id/episodeRecyclerView"
      android:layout_width="0dp"
      android:layout_height="0dp"
      android:layout_marginEnd="8dp"
      android:layout_marginStart="8dp"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toBottomOf="@+id/headerView"/>

</androidx.constraintlayout.widget.ConstraintLayout>
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    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:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginBottom="8dp"
    android:layout_marginTop="8dp">
  <TextView
      android:id="@+id/titleView"
      android:layout_width="0dp"
      android:layout_height="wrap_content"
      android:layout_gravity="top"
      android:layout_marginEnd="0dp"
      android:textStyle="bold"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintHorizontal_chainStyle="spread"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent"
      tools:text="Title"/>
  <TextView
      android:id="@+id/releaseDateView"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_gravity="top"
      android:layout_marginTop="4dp"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toBottomOf="@+id/descView"
      tools:text="01/01/18"/>
  <TextView
      android:id="@+id/durationView"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_gravity="top"
      android:layout_marginTop="4dp"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintTop_toBottomOf="@+id/descView"
      tools:text="00:00"/>
  <TextView
      android:id="@+id/descView"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:layout_gravity="top"
      android:layout_marginTop="4dp"
      android:maxLines="3"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toBottomOf="@+id/titleView"
      tools:text="Description"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<FrameLayout
    android:id="@+id/podcastDetailsContainer"
    android:layout_width="0dp"
    android:layout_height="0dp"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@id/app_bar"/>

Basic architecture

As in previous chapters, you need to define the basic architecture components consisting of a repository, a service, and a view model to display the podcast detail. There’s no need for any database layer at this point.

Podcast models

To store the podcast data, you need two models: one defines the detail for a single podcast episode, and the other is the podcast detail containing a list of episode models.

data class Episode (
    var guid: String = "",
    var title: String = "",
    var description: String = "",
    var mediaUrl: String = "",
    var mimeType: String = "",
    var releaseDate: Date = Date(),
    var duration: String = ""
)
data class Podcast(
    var feedUrl: String = "",
    var feedTitle: String = "",
    var feedDesc: String = "",
    var imageUrl: String = "",
    var lastUpdated: Date = Date(),
    var episodes: List<Episode> = listOf()
)

Podcast repository

You’ll use a repo for retrieving the podcast details and returning it to the view model.

class PodcastRepo {
  fun getPodcast(feedUrl: String): Podcast? {
        return Podcast(feedUrl, "No Name","No description", "No image")
  }
}

Podcast view model

Inside viewmodel, create a new file and name it PodcastViewModel.kt. Replace the contents with the following:

class PodcastViewModel(application: Application) : AndroidViewModel(application) {

  var podcastRepo: PodcastRepo? = null
  var activePodcastViewData: PodcastViewData? = null

  data class PodcastViewData(
      var subscribed: Boolean = false,
      var feedTitle: String? = "",
      var feedUrl: String? = "",
      var feedDesc: String? = "",
      var imageUrl: String? = "",
      var episodes: List<EpisodeViewData>
  )

  data class EpisodeViewData (
      var guid: String? = "",
      var title: String? = "",
      var description: String? = "",
      var mediaUrl: String? = "",
      var releaseDate: Date? = null,
      var duration: String? = ""
  )
}
private fun episodesToEpisodesView(episodes: List<Episode>): List<EpisodeViewData> {
  return episodes.map {
      EpisodeViewData(
          it.guid, 
          it.title, 
          it.description, 
          it.mediaUrl, 
          it.releaseDate, 
          it.duration
      )
  }
}
private fun podcastToPodcastView(podcast: Podcast): PodcastViewData {
  return PodcastViewData(
      false,
      podcast.feedTitle,
      podcast.feedUrl,
      podcast.feedDesc,
      podcast.imageUrl,
      episodesToEpisodesView(podcast.episodes)
  )
}
// 1
  fun getPodcast(podcastSummaryViewData: PodcastSummaryViewData): PodcastViewData? {
  // 2
  val repo = podcastRepo ?: return null
  val feedUrl = podcastSummaryViewData.feedUrl ?: return null
  // 3
  val podcast = repo.getPodcast(feedUrl)
    // 4
  podcast?.let {
      // 5    
      it.feedTitle = podcastSummaryViewData.name ?: ""
      // 6
      it.imageUrl = podcastSummaryViewData.imageUrl ?: ""
      // 7
      activePodcastViewData = podcastToPodcastView(it)
      // 8
      return activePodcastViewData
  }
  // 9
  return null
}

Details Fragment

The detail Fragment is responsible for displaying the podcast details and it gets its data from PodcastViewModel. This is also where the user can subscribe to a podcast. First, you need to add an action menu with a single Subscribe item.

<string name="subscribe">Subscribe</string>
<?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">
  <item
      android:id="@+id/menu_feed_action"
      android:title="@string/subscribe"
      app:showAsAction="ifRoom"/>
</menu>
class PodcastDetailsFragment : Fragment() {
  private lateinit var databinding: FragmentPodcastDetailsBinding

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    // 1
    setHasOptionsMenu(true)
  }

  override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                            savedInstanceState: Bundle?): View {
    databinding = FragmentPodcastDetailsBinding.inflate(inflater, container, false)
    return databinding.root
  }

  override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
  }

  // 2  
  override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
    super.onCreateOptionsMenu(menu, inflater)
    inflater.inflate(R.menu.menu_details, menu)  
  }
}
implementation "androidx.fragment:fragment-ktx:1.3.0"
private val podcastViewModel: PodcastViewModel by activityViewModels()
private fun updateControls() {
    val viewData = podcastViewModel.activePodcastViewData ?: return
    databinding.feedTitleTextView.text = viewData.feedTitle
    databinding.feedDescTextView.text = viewData.feedDesc
    activity?.let { activity ->
      Glide.with(activity).load(viewData.imageUrl).into(databinding.feedImageView)
    }
}
updateControls()
companion object {
  fun newInstance(): PodcastDetailsFragment {
    return PodcastDetailsFragment()
  }
}

Displaying details

Now it’s time to show the Fragment. Jump over to PodcastActivity and wire it up.

companion object {
  private const val TAG_DETAILS_FRAGMENT = "DetailsFragment"
}
private fun createPodcastDetailsFragment(): PodcastDetailsFragment {
  // 1
  var podcastDetailsFragment = supportFragmentManager
      .findFragmentByTag(TAG_DETAILS_FRAGMENT) as PodcastDetailsFragment?

  // 2
  if (podcastDetailsFragment == null) {
    podcastDetailsFragment = PodcastDetailsFragment.newInstance()
  }

  return podcastDetailsFragment
}
private lateinit var searchMenuItem: MenuItem
searchMenuItem = menu.findItem(R.id.search_item)
val searchView = searchMenuItem.actionView as SearchView
private fun showDetailsFragment() {
  // 1
  val podcastDetailsFragment = createPodcastDetailsFragment()
  // 2
  supportFragmentManager.beginTransaction().add(
      R.id.podcastDetailsContainer,
      podcastDetailsFragment, TAG_DETAILS_FRAGMENT)
          .addToBackStack("DetailsFragment").commit()
  // 3
  databinding.podcastRecyclerView.visibility = View.INVISIBLE
  // 4
  searchMenuItem.isVisible = false
}
if (databinding.podcastRecyclerView.visibility == View.INVISIBLE) {
  searchMenuItem.isVisible = false
}
private fun showError(message: String) {
  AlertDialog.Builder(this)
      .setMessage(message)
      .setPositiveButton(getString(R.string.ok_button), null)
      .create()
      .show()
}
<string name="ok_button">OK</string>
private val podcastViewModel by viewModels<PodcastViewModel>()
podcastViewModel.podcastRepo = PodcastRepo()
override fun onShowDetails(podcastSummaryViewData:
    SearchViewModel.PodcastSummaryViewData) {
  // 1
  val feedUrl = podcastSummaryViewData.feedUrl ?: return
  // 2
  showProgressBar()
  // 3
  val podcast = podcastViewModel.getPodcast(podcastSummaryViewData)
  // 4
  hideProgressBar()
  if (podcast != null) {
      // 5
    showDetailsFragment()
  } else {
      // 6
    showError("Error loading feed $feedUrl")
  }
}

private fun addBackStackListener() {
  supportFragmentManager.addOnBackStackChangedListener {
    if (supportFragmentManager.backStackEntryCount == 0) {
      databinding.podcastRecyclerView.visibility = View.VISIBLE
    }
  }
}
addBackStackListener()

if (supportFragmentManager.backStackEntryCount > 0) {
  databinding.podcastRecyclerView.visibility = View.INVISIBLE
}

Key Points

Where to go from here?

Congratulations, you made a lot of progress! :] However, the detail screen is still missing some key information, including the list of podcast episodes and the ability to subscribe to the podcast.

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