Android TV: Getting Started
Learn how to create your first Android TV app! In this tutorial, you’ll create an Android TV app for viewing RayWenderlich Youtube channel videos! By Ivan Kušt.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Android TV: Getting Started
25 mins
- Getting Started
- Netderlix Code Structure
- Setting up Android TV Emulator
- The Leanback Library
- Meeting Android TV App Criteria
- Creating Video Catalog
- Using BrowseSupportFragment from Leanback
- Implementing a Presenter for Showing Video Thumbnails
- Creating Video Catalog With Playlists
- Setting up Click Listeners
- Presenting Video Details
- Creating Details View for Selected Playlist
- Previewing Recommendations
- Previewing Recommendation Video Details
- Setting Details View Background
- Playing a Video
- Introducing the Glue Mechanisms
- Where to Go From Here?
Presenting Video Details
Leanback provides DetailsSupportFragment
, which makes setting up video details screen easy.
Creating Details View for Selected Playlist
Open VideoDetailsFragment.kt and notice it already extends DetailsSupportFragment
. Its view contains two data rows, which you’ll implement now.
First, add a function that creates an adapter for actions available for the video:
private fun getActionAdapter() = ArrayObjectAdapter().apply {
add(
Action(
ACTION_WATCH,
resources.getString(R.string.watch_action_title),
resources.getString(R.string.watch_action_subtitle)
)
)
}
The function creates an instance of ArrayObjectAdapter
with a single Action
item. The item represents action to watch the video that also passes the respective title and subtitle.
Now, create a function that creates a details row:
private fun createDetailsOverviewRow(
selectedVideo: Video,
detailsAdapter: ArrayObjectAdapter
): DetailsOverviewRow {
val context = requireContext()
// 1
val row = DetailsOverviewRow(selectedVideo).apply {
imageDrawable = ContextCompat.getDrawable(context, R.drawable.default_background)
actionsAdapter = getActionAdapter()
}
// 2
val width = resources.getDimensionPixelSize(R.dimen.details_thumbnail_width)
val height = resources.getDimensionPixelSize(R.dimen.details_thumbnail_height)
// 3
loadDrawable(requireActivity(), selectedVideo.cardImageUrl, R.drawable.default_background, width, height)
{ resource ->
row.imageDrawable = resource
// 4
detailsAdapter.notifyArrayItemRangeChanged(0, detailsAdapter.size())
}
return row
}
This function creates the first data row. It contains details for the video: thumbnail, description and action for playing the video.
Here, you:
- Create a new instance of
DetailsOverviewRow
and set default video thumbnail and play action. - Get the width and height for the thumbnail drawable.
- Load a drawable using
loadDrawable()
from Util.kt. If loading is successful, you show the loaded image. - Notify the details adapter that held data has changed.
Then, add a function that handles actions:
private fun onActionClicked(action: Action, videoItem: VideoItem) {
if (action.id == ACTION_WATCH) {
val intent = VideoPlaybackActivity.newIntent(requireContext(), videoItem)
startActivity(intent)
}
}
It checks whether the action has ID ACTION_WATCH
and, if it does, starts VideoPlaybackActivity
.
Now, create a function that creates a presenter for the details row:
private fun createDetailsOverviewRowPresenter(
videoItem: VideoItem,
actionHandler: (Action, VideoItem) -> Unit
): FullWidthDetailsOverviewRowPresenter =
// 1
FullWidthDetailsOverviewRowPresenter(DetailsDescriptionPresenter()).apply {
// 2
backgroundColor =
ContextCompat.getColor(requireContext(), R.color.selected_background)
// 3
val sharedElementHelper = FullWidthDetailsOverviewSharedElementHelper()
sharedElementHelper.setSharedElementEnterTransition(
activity,
VideoDetailsActivity.SHARED_ELEMENT_NAME
)
setListener(sharedElementHelper)
isParticipatingEntranceTransition = true
// 4
onActionClickedListener = OnActionClickedListener {
actionHandler(it, videoItem)
}
}
Through these steps, you:
- Create an instance of
FullWidthDetailsOverviewRowPresenter
and configure it inapply
below - Set the background color
- Set up shared transition with Catalog screen
- Set a click listener
Finally, create a function:
private fun createPresenterSelector(videoItem: VideoItem) =
ClassPresenterSelector().apply {
// 1
addClassPresenter(
DetailsOverviewRow::class.java,
createDetailsOverviewRowPresenter(videoItem, ::onActionClicked)
)
// 2
addClassPresenter(
ListRow::class.java,
ListRowPresenter()
)
}
This function returns ClassPresenterSelector
with a proper presenter instance depending on the class of the rendering item:
-
DetailsOverviewRowPresenter created by
createDetailsOverviewRowPresenter()
forDetailsOverivewRow
items. -
ListRowPresenter for
ListRow
items.
Now, override onActivityCreated()
and add the following code:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
val videoItem = arguments?.getSerializable(ARGUMENT_VIDEO) as VideoItem
}
Here, you get VideoItem
passed from arguments.
Then, add the code at the end of onActivityCreated()
:
adapter = ArrayObjectAdapter(createPresenterSelector(videoItem)).apply {
add(createDetailsOverviewRow(videoItem.video, this))
}
With these lines, you create and assign an adapter for VideoDetailsFragment
.
Build and run. Select a video, and you’ll see the new details screen.
Previewing Recommendations
Add a new function for creating recommendations to VideoDetailsFragment.kt:
private fun createRelatedVideosRow(videoItem: VideoItem): ListRow {
// 1
val selectedVideo = videoItem.video
val playlistVideos = videoItem.playlist
// 2
val recommendations = ArrayList(playlistVideos.filterNot { it == selectedVideo })
// 3
val listRowAdapter = ArrayObjectAdapter(CatalogCardPresenter())
val header = HeaderItem(0, getString(R.string.related_videos))
if (recommendations.isNotEmpty()) {
for (i in 0 until NUM_RECOMMENDATIONS) {
listRowAdapter.add(
VideoItem(recommendations.random(), playlistVideos)
)
}
}
// 4
return ListRow(header, listRowAdapter)
}
Here is the explanation. You:
- Get the selected video and list of all videos in the corresponding playlist from
VideoItem
- Construct recommendations by filtering out selected video from the playlist videos
- Create an adapter that holds all the recommendations and fill it with items that represent recommendations
- Wrap the new adapter in
ListRow
and return it
Add the following line at the end of the apply block in onActivityCreated()
:
add(createRelatedVideosRow(videoItem))
Build and run. Open details for a video and navigate down to see recommended videos.
Previewing Recommendation Video Details
Set an item click listener by adding the following code to the end of onActivityCreated()
:
onItemViewClickedListener = OnItemViewClickedListener { itemViewHolder, item, _, _ ->
if(item is VideoItem) {
showVideoDetails(requireActivity(), itemViewHolder, item)
}
}
In this code, you present video details just as you did for catalog videos.
Build and run. Select a video recommendation to see the details of that video.
Setting Details View Background
To add a final touch to the details screen, call initializeBackground()
at the end of onActivityCreated()
:
initializeBackground(videoItem.video)
Build and run. You’ll see the background on the details screen now.
Select any video from the catalog and open its details. Then, select Watch Now For Free.
You’ll see an empty screen for now.
Playing a Video
And now for the most important part — playing the video.
Leanback provides two useful Fragment
s for implementing a playback screen:
- PlaybackSupportFragment: A fragment for displaying playback controls and video
-
VideoSupportFragment: A subclass of
PlaybackSupportFragment
that providesSurfaceView
for rendering the video
The only supported way of playing YouTube videos currently is using its IFrame player from WebView
.
You’ll use a library that wraps the IFrame player into View
.
Introducing the Glue Mechanisms
Leanback has a specific way of separating UI video playback controls from the video player. The following diagram shows the classes:
PlaybackSupportFragment
provides the basic functionality of a video playback screen. It uses PlaybackTransportControlGlue
and PlaybackSupportFragmentGlueHost
to communicate with PlayerAdapter
. PlayerAdapter
is a wrapper for a class (or more classes) for playing a video. PlaybackTransportControlGlue
handles displaying playback controls and communicating with PlayerAdapter
.
This setup looks complicated at first, but it just separates video player logic from Fragment
that displays video and controls.
To add support for your video player, you extend PlayerAdapter
and PlaybackTransportControlGlue
. For playing YouTube videos, you’ll use EmbeddedPlayerAdapter
, which is provided in the project.
First, you have to finish implementing VideoPlaybackControlGlue
, which communicates with EmbeddedPlayerAdapter
.
Open VideoPlaybackControlGlue.kt and add fields that hold playback actions:
private lateinit var skipPreviousAction: PlaybackControlsRow.SkipPreviousAction
private lateinit var skipNextAction: PlaybackControlsRow.SkipNextAction
private lateinit var fastForwardAction : PlaybackControlsRow.FastForwardAction
private lateinit var rewindAction : PlaybackControlsRow.RewindAction
Then, initialize actions in onCreatePrimaryActions()
. Add this function at the end of the class:
override fun onCreatePrimaryActions(primaryActionsAdapter: ArrayObjectAdapter?) {
super.onCreatePrimaryActions(primaryActionsAdapter)
// 1
skipPreviousAction = PlaybackControlsRow.SkipPreviousAction(context)
rewindAction = PlaybackControlsRow.RewindAction(context)
fastForwardAction = PlaybackControlsRow.FastForwardAction(context)
skipNextAction = PlaybackControlsRow.SkipNextAction(context)
// 2
primaryActionsAdapter?.apply {
add(skipPreviousAction)
add(rewindAction)
add(fastForwardAction)
add(skipNextAction)
}
}
Leanback calls this function when it creates a row of primary video controls. Here’s what it’s doing:
- Creates new instances of Skip Previous, Rewind, Fast Forward and Skip Next actions.
- Adds created actions to an adapter for controls row.
Add the following line at the end of next()
:
playerAdapter.next()
This calls next()
from EmbeddedPlayerAdapter
once the user clicks the Skip Next action.
And add the following at the end of previous()
:
playerAdapter.previous()
This line calls previous()
once the user clicks the Skip Previous action.
Add the following to onActionClicked()
:
when(action) {
rewindAction -> playerAdapter.rewind()
fastForwardAction -> playerAdapter.fastForward()
else -> super.onActionClicked(action)
}
This handles the remaining actions. If the action is not recognized, let the super implementation handle it.
Now, open VideoPlaybackFragment.kt and add this code to the end of onCreate()
:
//1
playerGlue = VideoPlaybackControlGlue(
requireActivity(),
EmbeddedPlayerAdapter(lifecycleScope)
).apply {
//2
host = PlaybackSupportFragmentGlueHost(this@VideoPlaybackFragment)
isControlsOverlayAutoHideEnabled = true
//3
title = videoItem.video.title
subtitle = videoItem.video.description
}
With this code, you:
- Pass activity reference and instance of
EmbeddedPlayerAdapter
toVideoPlaybackControlGlue
. This is the wrapper for the YouTube IFrame player thatVideoPlaybackControlGlue
uses to link video controls with IFrame player. - Set
PlaybackSupportFragmentGlueHost
as host and controls to be auto-hidden. - Set a video title and subtitle.
onCreateView()
to see initialization of the YouTubePlayerView. For more information on all the available properties, you can check android-youtube-player library.
Now, build and run and start a video!
That’s it! You’ve just created your first Android TV app. Congrats! :]