22.
Podcast Details
Written by Kevin D Moore
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:
- Design and build the podcast detail Fragment.
- Expand on the app architecture.
- 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>
This defines the main Layout for the detail Fragment as described earlier.
Create a new Layout and name it episode_item.xml. 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"
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>
This defines the layout for a single episode detail item.
Open activity_podcast.xml and add the following before the RecyclerView widget:
<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"/>
This is the container that holds the podcast detail Fragment. It’s configured to cover the entire Activity View below the app bar. Nothing displays in the container until you load the podcast detail Fragment, which happens after the user taps on a podcast row.
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.
You’ll start with a basic implementation to get the navigation working.
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.
Create a new package inside com.raywenderlich.podplay and name it model.
Inside model, create a new file and name it Episode.kt. Replace the contents with the following:
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 = ""
)
Note: Be sure to import Date(java.util) when resolving the
Dateclass.
This defines the data for a single podcast episode. These properties are required for display, management, or playback of an episode. Here’s an explanation for each property:
-
guid: Unique identifier provided in the RSS feed for an episode. -
title: The name of the episode. -
description: A description of the episode. -
mediaUrl: The location of the episode media. This is either an audio or video file. -
mimeType: Determines the type of file located atmediaUrl. -
releaseDate: Date the episode was released. -
duration: Duration of the episode as provided in the RSS feed.
Still inside model, create another file and name it Podcast.kt. Replace the contents with the following:
data class Podcast(
var feedUrl: String = "",
var feedTitle: String = "",
var feedDesc: String = "",
var imageUrl: String = "",
var lastUpdated: Date = Date(),
var episodes: List<Episode> = listOf()
)
Note: Be sure to import Date(java.util) when resolving the
Dateclass.
This defines the data for a single podcast. Here’s an explanation of each property:
-
feedUrl: Location of the RSS feed. -
feedTitle: Title of the podcast. -
feedDesc: Description of the podcast. -
imageUrl: Location of the podcast album art. -
lastUpdated: Date the podcast was last updated. -
episodes: List of episodes for the podcast.
Podcast repository
You’ll use a repo for retrieving the podcast details and returning it to the view model.
Inside repository, create a new file and name it PodcastRepo.kt. Replace the contents with the following:
class PodcastRepo {
fun getPodcast(feedUrl: String): Podcast? {
return Podcast(feedUrl, "No Name","No description", "No image")
}
}
PodcastRepo defines a single method, getPodcast(). This method has parameters for a feed URL and returns a Podcast or null. You’ll eventually add code to retrieve the feed from the URL and parse it into a Podcast object, but for now, a simple version of the Podcast object is created and returned.
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? = ""
)
}
Note: Be sure to import Date(java.util) when resolving the
Dateclass.
This defines the PodcastViewModel for the detail Fragment. The property podcastRepo is set by the caller. The property activePodcastViewData holds the most recently loaded podcast view data. PodcastViewData contains everything you need to display the details of a podcast.
The repo returns a list of Episode models, so you need a method to convert these models into EpisodeViewData view models.
Add the following method to the class:
private fun episodesToEpisodesView(episodes: List<Episode>): List<EpisodeViewData> {
return episodes.map {
EpisodeViewData(
it.guid,
it.title,
it.description,
it.mediaUrl,
it.releaseDate,
it.duration
)
}
}
This method uses map to do the following:
- Iterate over a list of
Episodemodels. - Convert
Episodemodels toEpisodeViewDataobjects. - Collect everything into a list.
You also need a method to convert Podcast models from the repo into PodcastViewData view objects.
Add the following method:
private fun podcastToPodcastView(podcast: Podcast): PodcastViewData {
return PodcastViewData(
false,
podcast.feedTitle,
podcast.feedUrl,
podcast.feedDesc,
podcast.imageUrl,
episodesToEpisodesView(podcast.episodes)
)
}
This method converts a Podcast model to a PodcastViewData object.
All that’s left to do is implement a method to retrieve a podcast from the repo.
Add the following method:
// 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
}
Here’s a closer look at what’s happening:
-
getPodcast()takes aPodcastSummaryViewDataobject and returns a PodcastViewData or null. -
Local variables are assigned to
podcastRepoandpodcastSummaryViewData.feedUrl. If either one isnull, the method returns early. -
Call
getPodcast()from the podcast repo with the feed URL. -
Check the podcast detail object to make sure it’s not
null. -
Set the podcast title to the podcast summary name. This line is required because you haven’t built out the full implementation of
repo.getPodcast(). In future chapters,repo.getPodcast()will fill in this item, and this line will be removed. -
Set the podcast detail image to match the podcast summary image URL if it’s not
null. -
Convert the
Podcastobject to aPodcastViewDataobject and assign it toactivePodcastViewData. -
Return the podcast view data.
-
Return null if no podcast is retrieved.
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.
Open strings.xml and add the following line:
<string name="subscribe">Subscribe</string>
Create a menu resource file and name it menu_details.xml. Replace the contents with the following:
<?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>
This defines the content of a menu that displays when the details Fragment is active. It contains a single item with the label “Subscribe”.
Inside ui, create a new file and name it PodcastDetailsFragment.kt. Replace the contents with the following:
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)
}
}
Note: Be sure to use
import androidx.fragment.app.Fragmentwhen resolving theFragmentclass.
This is the standard procedure for setting up a Fragment, except for a few important details:
-
The call to
setHasOptionsMenu()tells Android that this Fragment wants to add items to the options menu. This causes the Fragment to receive a call toonCreateOptionsMenu(). -
onCreateOptionsMenu()inflates themenu_detailsoptions menu so its items are added to the podcast Activity menu.
Next, you need to give the Fragment access to the main podcast view model.
Open the module’s build.gradle and the following new lines to the dependencies:
implementation "androidx.fragment:fragment-ktx:1.3.0"
A warning about changing Gradle files is shown at the top of the editor. Click on Sync Now.
This brings in support for activityViewModels() used in the next step.
Add the following property to the class:
private val podcastViewModel: PodcastViewModel by activityViewModels()
activitytViewModels() is an extension function that allows the fragment to access and share view models from the fragment’s parent activity.
In previous chapters, you used different techniques to communicate between Activities and Fragments. Using activitytViewModels() provides a convenient means to use shared view model data as the communication mechanism between a Fragment and its host Activity.
activityViewModels() provides the same instance of the PodcastViewModel that was created in PodcastActivity. When the fragment is attached to the activity it will automatically assign podcastViewModel to the already initialized parent activity’s podcastViewModel.
Note: The usage here illustrates a key benefit of using view models. You can seamlessly share view models with any Fragments managed by the Activity. View models can also survive configuration changes, so you don’t need to create them again when the screen rotates.
Now it’s time to fill out the user interface controls.
Still inside PodcastDetailsFragment.kt, add the following method:
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)
}
}
This first line checks to make sure there’s view data available (and that you have something in activePodcastViewData, which you defined earlier to hold the most recently loaded podcast view data). It then uses the view data to populate the title and description TextView elements, as well as load the podcast image using Glide.
Add the following to the end of onViewCreated():
updateControls()
This calls updateControls() after the View hierarchy is created. By placing this call here, you ensure that the podcast view data is already loaded by the main Activity.
The last thing you need is a method that the Activity can use to create an instance of the Fragment.
Add the following method:
companion object {
fun newInstance(): PodcastDetailsFragment {
return PodcastDetailsFragment()
}
}
This is a convenience method that returns an instance of PodcastDetailsFragment. This may seem unnecessary, but by allowing the Fragment to control its own creation, you’re giving your code more future flexibility.
Displaying details
Now it’s time to show the Fragment. Jump over to PodcastActivity and wire it up.
Much of the code you’re about to write should look familiar from your previous experience with managing Fragments. If you need a refresher, read Chapter 11, “Using Fragments”.
Open PodcastActivity.kt and add the following:
companion object {
private const val TAG_DETAILS_FRAGMENT = "DetailsFragment"
}
This defines a tag to uniquely identify the details Fragment in the Fragment Manager.
Add the following method to PodcastActivity:
private fun createPodcastDetailsFragment(): PodcastDetailsFragment {
// 1
var podcastDetailsFragment = supportFragmentManager
.findFragmentByTag(TAG_DETAILS_FRAGMENT) as PodcastDetailsFragment?
// 2
if (podcastDetailsFragment == null) {
podcastDetailsFragment = PodcastDetailsFragment.newInstance()
}
return podcastDetailsFragment
}
This method either creates the details Fragment or uses an existing instance if one exists. Here’s a closer look at how this works:
-
You use
supportFragmentManager.findFragmentByTag()to check if the Fragment already exists. -
If there’s no existing fragment, you create a new one using
newInstance()on the Fragment’s companion object. -
You return the Fragment object.
When the detail fragment is shown, it’s a good idea to hide the search icon. But first, you need to save a reference to the search icon menu item to allow you to hide/show the icon.
Add the following property to the top of the class:
private lateinit var searchMenuItem: MenuItem
In onCreateOptionsMenu(), remove the var keyword from the line that assigns searchMenuItem:
searchMenuItem = menu.findItem(R.id.search_item)
Update the next line in onCreateOptionsMenu() to remove the ? safe call operator:
val searchView = searchMenuItem.actionView as SearchView
You’re ready to add the method that displays the details Fragment:
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
}
Here’s a look at what’s going on with that method:
- The details fragment is created or retrieved from the fragment manager.
- The fragment is added to the
supportFragmentManager. TheTAG_DETAILS_FRAGMENTconstant you defined earlier is used to identify the fragment.addToBackStack()is used to make sure the back button works to close the fragment. - The main podcast
RecyclerViewis hidden so the only thing showing is the detail Fragment. - The
searchMenuItemis hidden so that the search icon is not shown on the details screen.
Note: Adding the Fragment to the back stack is essential for proper app navigation. If you don’t add the call to
addToBackStack(), then pressing the back button while the Fragment is displayed closes the app.
Add the following to the bottom of onCreateOptionsMenu() before the return true:
if (databinding.podcastRecyclerView.visibility == View.INVISIBLE) {
searchMenuItem.isVisible = false
}
This ensures that the searchMenuItem remains hidden if podcastRecyclerView is not visible.
You may be asking, “Why is this added to onCreateOptionsMenu()”?
Great question! onCreateOptionsMenu() is called a second time when the Fragment is added. Even though you hid the searchMenuItem in showDetailsFragment(), it gets shown again when the menu is recreated. This is because you requested that the Fragment adds to the options menu, so Android recreates the menu from scratch when adding the Fragment.
The next thing to do is replace onShowDetails() with code that loads PodcastViewModel and calls showDetailsFragment(). Before you do that, define the following helper method:
private fun showError(message: String) {
AlertDialog.Builder(this)
.setMessage(message)
.setPositiveButton(getString(R.string.ok_button), null)
.create()
.show()
}
Note: Make sure to use AlertDialog(androidx.appcompat.app) when resolving
AlertDialog.
This displays a generic alert dialog with an error message. You’ll show this dialog to handle all error cases.
To define the ok_button string, add the following line to strings.xml:
<string name="ok_button">OK</string>
Next, you need to create the PodcastViewModel that’s used to hold the podcast details view data.
Add the following property to PodcastActivity.kt:
private val podcastViewModel by viewModels<PodcastViewModel>()
You have used viewModels() in previous chapters. This initializes the podcastViewModel object when the Activity is created. If the Activity is being created for the first time, it creates a new instance of the PodcastViewModel object. If it’s just a configuration change, it uses an existing copy of the PodcastViewModel object instead.
Add the following to the bottom of setupViewModels():
podcastViewModel.podcastRepo = PodcastRepo()
A new instance of PodcastRepo is assigned to the podcastViewModel.podcastRepo property.
The podcastViewModel object is now ready to use when onShowDetails() is called in response to the user tapping on a podcast row. Time to code that!
Replace onShowDetails() with the following:
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")
}
}
This method is called when the user taps on a podcast. Here’s how it works:
- The
feedUrlis taken from thepodcastSummaryViewDataobject if it’s notnull; otherwise, the method returns without doing anything. - The progress bar is displayed to show the user that the app is busy loading the podcast data.
-
podcastViewModel.getPodcast()is called to load the podcast view data. - After the data is returned, the progress bar is hidden.
- If the data is not
null, thenshowDetailsFragment()is called to display the detail fragment. - If the data is
null, then the error dialog is displayed.
Build and run the app. Search for a podcast and then tap on one. The detail screen appears showing the podcast image, title, and the temporary description.
The SUBSCRIBE menu option is shown but not yet functional.
Tap the back button and the detail Fragment should go away. However, there’s something wrong: the search icon is missing and the display is blank. Where did the list of podcasts go?
The problem exists because the podcastRecyclerView was hidden before the details Fragment was displayed, but it was never made visible again. You need to make the podcastRecyclerView visible again, but how do you know when the details Fragment is closed?
One solution is to add a listener to supportFragmentManager so you’re notified when the back stack changes.
Back in PodcastActivity.kt, add the following method:
private fun addBackStackListener() {
supportFragmentManager.addOnBackStackChangedListener {
if (supportFragmentManager.backStackEntryCount == 0) {
databinding.podcastRecyclerView.visibility = View.VISIBLE
}
}
}
This adds a lambda method that can respond to changes in the Fragment back stack. This is called when items are added or removed from the stack. If the backStackEntryCount is 0, then all Fragments have been removed, and it’s safe to make the podcast RecyclerView visible again.
Add the following line to the end of onCreate():
addBackStackListener()
This adds the back stack listener to the Fragment Manager when the Activity is created.
Build and run the app. Bring up the detail screen and tap the back button. The screen now looks correct.
Before you call it a day, try to rotate the screen while viewing the podcast details. You’ll get an interesting mash-up of the search results and the podcast details. Whoops!
As this test demonstrates, the Android UI is not complete until you’ve tested it by rotating the screen. Fortunately, this is an easy fix. :] You need to hide the podcast RecyclerView after a configuration change.
In onCreateOptionsMenu(), after the line that calls searchView.setSearchableInfo(), add the following:
if (supportFragmentManager.backStackEntryCount > 0) {
databinding.podcastRecyclerView.visibility = View.INVISIBLE
}
Now, when the device rotates, the Activity gets created again. When onCreateOptionsMenu() is called — and if there are any fragments on the back stack — the podcastRecyclerView is hidden.
Build and run the app. For one last time in this chapter, bring up the detail screen for a podcast and rotate the device. The screen now looks as expected.
Key Points
-
Fragments can be used inside of an Activity instead of adding another Activity.
-
AndroidViewModels are useful when you need to share view model data.
-
The
addToBackStackmethod in a fragment transaction allows the fragment to be removed when going back. -
The
addOnBackStackChangedListenermethod is useful for showing/hiding items when popping a fragment.
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.
But don’t worry. You’ll fix this in the next chapter by fetching the actual RSS feed and using it to add these missing pieces.