20.
Networking
Written by Kevin D Moore
In this section, you’re going to utilize many of the skills you’ve already learned and dive into some more advanced areas of Android development. You’ll build a full-featured podcast manager and player app named PodPlay. This app will allow searching and subscribing to podcasts from iTunes and provide a playback interface with speed controls.
The following new topics are covered:
- Android networking.
- Retrofit REST API library.
- XML Parsing.
- Search activity.
- MediaPlayer library.
Getting started
PodPlay will contain these main features:
- Quick searching of podcasts by keyword or name.
- Display for previewing podcast episodes.
- Playback of audio and video podcasts.
- Subscribing to your favorite podcasts.
- Playback at various speeds.
Project set up
You’ll start by creating a project with a single empty Activity. This app uses the same structure as PlaceBook, but it will also add a new services layer.
Open Android Studio and close any open projects so that the “Welcome to Android Studio” dialog is displayed.
Select Start a new Android Studio project.
Click the Empty Activity project type in the Phone and Tablet tab, and click Next.
Fill out the Configure your project dialog:
- Name: PodPlay
- Package name: com.raywenderlich.podplay
- Save Location: Select your own location
- Language: Kotlin
- Minimum API level: API 22: Android 5.1 (Lollipop)
- Leave everything else unchecked.
Click Finish.
The new project is created and the MainActivty.kt file is shown.
Select MainActivity.kt in the Project navigator and press Shift-F6 to rename the activity. In the rename dialog, change the name to PodcastActivity, leave the other options to their default values and click Refactor.
Select activity_main.xml in the Project navigator inside res/layout and press Shift-F6 to rename the Layout. In the rename dialog, change the name to activity_podcast.xml, leave the other options to their default values, and click Refactor.
Next, in the Project navigator, right-click on com.raywenderlich.podplay and select New ▸ Package. Enter the name ui and click OK. You should see a new package named ui under com.raywenderlich.podplay.
In the Project navigator, move PodcastActivity.kt into ui using drag-and-drop. In the Move refactor dialog, leave all of the options set to their defaults and click Refactor.
If you have the option to Compact middle packages turned on in the Android Project view, then you’ll see something like this:
If you have the option to Compact middle packages turned off, you’ll see this:
Where are the podcasts?
Before you get to the fun part of podcast playback, you need to answer a fundamental question: Where do podcasts come from? The answer is just about anywhere. Podcasts are distributed using a standard format called RSS (Rich Site Summary, commonly referred to as Really Simple Syndication).
RSS feeds are based on a standard XML format and are used by websites to deliver a variety of content feeds. Most podcast feeds are found on the main website that promotes or produces the podcast. There’s normally a feed button that provides a URL to the podcast feed.
In the XML returned by the RSS feed, you have access to a lot of information regarding the podcast, which includes the title of the podcast, the date it was published, associated artwork, a descriptive summary of the podcast, and a link to the audio file where the podcast is hosted.
For a podcast management app like PodPlay, it would great if there was a consolidated listing of the podcast feeds spread throughout the internet. As it turns out, just about every podcast in existence is available through the iTunes podcast directory. Apple provides an API that you can use to allow users to search for podcasts by keywords, making it easy to subscribe to a podcast.
Android networking
So far, all of the apps you’ve built during your apprenticeship have been self-contained. They have not had to access any remote or network-based services directly. Although PlaceBook did access Google Places and download place photos, that was all handled by the Places library. That’s about to change with PodPlay.
PodPlay requires direct access to the iTunes podcast directory, as well as the ability to download individual RSS feeds. As with database access operations, network access operations are required to run in the background on Android. If you attempt to perform network operations on the UI thread, you’ll be shamed with a NetworkOnMainThreadException error.
There are several built-in ways to handle network access in the background, including:
- Handler
- IntentService
- AsyncTaskLoader
- Executor
- JobScheduler
- Coroutines
Each of these options has a different level of complexity and its own benefits and drawbacks. The alternative is a third-party library that handles the details and lets you concentrate on building app functionality.
There are a few choices available:
- Volley: Google provides a library with a simple interface for accessing network resources asynchronously.
- OkHttp: Similar to Volley, and developed by Square Engineering.
- Retrofit: Also developed by Square Engineering, it builds on top of OkHttp.
You’ll be using Retrofit for PodPlay. It’s a popular library that makes it easy to do asynchronous network calls and process JSON data into model objects.
Note: Although RSS feeds are formatted using an XML structure, iTunes returns a list of these feeds with a JSON structure.
PodPlay architecture
Continuing with the layered architecture, you’ll create a service layer that handles all network access to iTunes and hides the details of that communication. This will make it easy to swap out different methods for network access, without affecting any other parts of the code.
You’ll start by creating a single service to search the iTunes podcast directory. This will be called when the user searches for podcasts in the app.
iTunes search service
If you regularly listen to or have ever created a podcast, you’re probably familiar with the iTunes podcast directory. This provides a single place to find almost any podcast from a variety of categories.
Apple also provides an API to allow searching the podcast directory. You can find the full API documentation here:
https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/
There are a variety of options when calling the API, and it supports many types of media besides podcasts. The method you’ll use here allows searching for podcasts by titles or keywords. It looks like this:
https://itunes.apple.com/search?term=Android+Developer&media=podcast
The media=podcast part tells iTunes to only search for podcasts. term=Android+Developer is the search term. The plus sign is used because the search term must be URL-encoded. URL-encoding replaces all spaces with plus symbols and encodes all other special characters except letters, numbers, periods (.), dashes (-), underscores (_), and asterisks (*).
You can plug this URL into your browser and get back the search results, but a better way to explore web APIs is to use the excellent open-source Postman app. You can find Postman at https://www.getpostman.com. Download and install Postman for your OS and launch the app.
Using the default GET method, put in the search URL from above and click Send.
In the search results, set the output type to JSON and turn off line-wrapping. You’ll end up with a nicely formatted JSON display:
Scroll through the results array in the JSON output. There’s a lot of information for each found podcast, but you’ll only use a small number of items to display the search results to the user.
Introducing Retrofit
Now that you know how to get search results, the next step is to turn them into data models.
If you manually perform the steps to download and convert to a model, it would look something like this:
- Initiate a network request to the iTunes search URL in a background process.
- Capture the response to the network request as a JSON formatted string.
- Parse the string based on JSON formatting rules.
- Create a
PodcastResponseobject for each podcast item, and set the properties from the JSON data.
Here’s a visual picture of mapping the JSON response to a PodcastResponse data model:
This is where Retrofit swoops in and makes your development life much more comfortable! Retrofit lets you define a Kotlin interface that is a direct representation of the API you’re accessing. An interface allows you to define a class with abstract methods that don’t require a body. Once you have defined the interface, you use the Retrofit Builder to create a concrete implementation of the interface, and Retrofit supplies the method bodies. With the implementation in hand, you can make calls to the API and get back ready-to-use response objects.
Retrofit performs this magic with the help of Annotations. Annotations allow you to attach metadata to code. Retrofit uses the annotation data to determine how to call the API endpoints and parse the returned data into model objects.
You’ll create a simple service that encapsulates everything needed to define the service interface, and build the service implementation with Retrofit.
Defining Retrofit dependencies
First, you need to define the Retrofit dependency.
Open the project build.gradle file and replace the ext.kotlin_version line with the following:
ext {
kotlin_version = '1.4.21'
coroutines_version = '1.4.2'
retrofit_version = '2.9.0'
}
Open the app build.gradle file and add the following lines to the dependencies section:
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
The retrofit dependency is the core Retrofit library. The converter-gson dependency adds support for JSON parsing and the last two are for using coroutines.
This version of Retrofit uses some features of Java 8, and therefore requires Java 8 source compatibility.
If Android Studio did not add these lines, add the following lines to the android section:
compileOptions {
sourceCompatibility = 1.8
targetCompatibility = 1.8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
This tells Android Studio to target Java 8 when compiling the project source code.
A warning about changing Gradle files is shown at the top of the editor. Click on Sync Now.
Creating the podcast response model
Now you’ll create the model that represents a response from the iTunes service.
Create a new package named service inside the project root as you did previously with the ui package.
Note: To create the package inside com.raywenderlich.podplay, you may need to change the settings in the Project navigator to disable “Compact Middle Packages”.
Once you add the service package, you can re-enable the Compact Middle Packages, and your project structure will look like this:
In the service package, create a new Kotlin file named PodcastResponse.kt, and then replace the contents with the following:
data class PodcastResponse(
val resultCount: Int,
val results: List<ItunesPodcast>) {
data class ItunesPodcast(
val collectionCensoredName: String,
val feedUrl: String,
val artworkUrl30: String,
val releaseDate: String
)
}
This defines a data class that directly mirrors the layout and hierarchy of the JSON data returned by the iTunes search API. Notice the variable names exactly match the keys in the iTunes search JSON data. While it’s possible to use Annotations to allow different variable names than the JSON keys, this way is the most compact method to define the model. Also, it’s not a problem to leave out the fields you don’t need because the JSON parser used by Retrofit ignores extra fields.
Note: You may be wondering why the
PodcastResponsemodel was created in the service package instead of a separate model package. This is a matter of personal preference, but this particular model is limited to handling responses from the iTunes Service, so it makes sense to keep it in the service package.
In the service package, create a new Kotlin file named ItunesService.kt, and then replace the contents with the following:
interface ItunesService {
// 1
@GET("/search?media=podcast")
// 2
suspend fun searchPodcastByTerm(@Query("term") term: String): Response<PodcastResponse>
// 3
companion object {
// 4
val instance: ItunesService by lazy {
// 5
val retrofit = Retrofit.Builder()
.baseUrl("https://itunes.apple.com")
.addConverterFactory(GsonConverterFactory.create())
.build()
// 6
retrofit.create(ItunesService::class.java)
}
}
}
Note: If you have any unresolved references, with multiple resolutions, make sure to resolve them from the retrofit library.
This defines an interface with a single method searchPodcastByTerm. This interface also contains a companion object that returns an instance of the interface as a singleton. This ensures that the interface is only instantiated once during the app’s lifetime.
Time to go through this in detail:
-
This is your first encounter with a Retrofit annotation. Annotations always start with the
@symbol. This annotation is a “function” annotation, meaning that it applies to a function.Retrofit defines several function annotations that represent standard HTTP requests such as GET, POST, and PUT. The
@GETannotation takes a single parameter: The path of the endpoint that should be called. The annotation applies to the function that immediately follows. -
The method
searchPodcastByTermis a suspending method and takes a single parameter that has a Retrofit@Queryannotation. This annotation tells Retrofit that this parameter should be added as a query term in the path defined by the@GETannotation. The annotation takes a single parameter that represents the name of the query term. The return type is a retrofitResponseclass that will let you know if the request was successful.
When you call searchPodcastByTerm(), it must be called in a coroutine method and will run asynchronously to return a Response object containing the PodcastResponse.
As an example, calling searchPodcastByTerm(“Android Developer”) results in Retrofit using a final URL of /search?media=podcast&term=Android+Developer. Retrofit automatically URL-Encodes the parameter names and values when constructing the URL.
-
You define a companion object in the
ItunesServiceinterface. -
The
instanceproperty of the companion object holds the only application-wide instance of theItunesService. This property looks a little different than the ones you’ve defined in the past — and for good reason.This definition allows the instance property to return a Singleton object. When the application needs to use
ItunesService, it simply referencesItunesService.instance.Singleton objects are objects that have a single instance for the lifetime of the application. No matter how many times the instance property is accessed, it only performs the initialization one time and will always return the same
ItunesServiceobject.This is accomplished by using a Kotlin concept known as property delegation. As the name implies, property delegation allows you to delegate the property setters and getters to a class.
You specify a property delegate with the keyword
by, followed by a delegate class instance. Here’s a simple example (don’t type in this code):class SomeClass: { val someProperty: String by SomeDelegateClass() }SomeDelegateClassmust providesetValue()andgetValue().get()andset()forsomePropertyis delegated tosetValue()andgetValue(). Here’s a simple implementation ofSomeDelegateClass(don’t type in this code):class SomeDelegateClass { operator fun getValue(thisRef: Any?, property: KProperty<*>): String { return "A delegated return value" } operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { // No body required } }You won’t be using a custom delegate class for PodPlay, but if you want to learn more, refer to https://kotlinlang.org/docs/reference/delegated-properties.html.
Kotlin provides some standard delegates that also come in handy. The one used for the instance property is the Lazy<T> delegate, and it’s accompanied by the built-in lazy method. The lazy method takes a lambda and returns an instance of Lazy<T>.
The result of using the lazy method is that the first time the instance property is accessed, it executes the lambda and stores the result (an instance of ItunesService). All subsequent calls to the instance property return the original result.
- This is the first part of the lazy lambda method.
Retrofit.Builder()is used to create a retrofit builder object.Retrofit.Builderallows you to specify several options that let Retrofit know how it should ultimately create the concrete implementation of theItunesServiceinterface. In this case, you specify the following options:
-
baseUrl: Sets the base URL for the service. This is prepended to the path specified in the function annotations.
-
addConverterFactory: Adds a converter factory to handle the translation of the JSON data to the
PodcastResponsemodel object. A number of converter factories are available, but you’ll useGsonConverterFactoryto create an instance of the Gson Converter to handle the JSON parsing and conversion. Gson is a library developed by Google used to convert between Java objects and JSON.
- Finally, you call
create<ItunesService>()on theretrofitbuilder object to create theItunesServiceinstance. Since this is the last line evaluated in the lambda, it’s used as the value assigned to theinstanceproperty.
The next step is to hide the service behind a repository as you did with the database in PlaceBook. The repository is the only part of the app that touches the ItunesService.
Create a new package named repository inside the project root. Inside that package, create a new file named ItunesRepo.kt, and replace the contents with the following:
// 1
class ItunesRepo(private val itunesService: ItunesService) {
// 2
suspend fun searchByTerm(term: String) = itunesService.searchPodcastByTerm(term) // 3
}
Note: If you have any unresolved references with multiple choices for resolving, make sure to resolve them from the retrofit library.
- You define the primary constructor for
ItunesRepoto require an existing instance of theItunesServiceinterface. This is an example of the Dependency Injection principle. By passing anItunesServicetoItunesRepo, it makes it possible for the calling code to pass a different implementation forItunesService.ItuneRepodoesn’t care about the implementation, as long as it conforms to theItunesServiceinterface. -
ItunesRepocontains a single method namedsearchByTerm. This method takes a search term as the parameter. - You call
searchPodcastByTerm()and pass in the search term. This returns a RetrofitResponseobject ofPodcastResponse.
This gets rid of the extra objects from the raw PodcastResponse object that aren’t needed and returns only the resulting ItunesPodcast object.
To test if the service is working, you can use ItunesRepo to search for a podcast and log the results.
Open PodcastActivity.kt and add the following to the top of the class:
val TAG = javaClass.simpleName
Add the following to onCreate() after the setContentView() call:
val itunesService = ItunesService.instance
val itunesRepo = ItunesRepo(itunesService)
GlobalScope.launch {
val results = itunesRepo.searchByTerm("Android Developer")
Log.i(TAG, "Results = ${results.body()")
}
This code uses ItunesRepo to search for the podcast and prints the results to the Logcat window.
ItunesService.instance is called to get an instance of the ItunesService and it’s passed to a new ItunesRepo instance. searchByTerm() is called with the search term inside of a coroutine scope and is passed an anonymous method to receive the results.
Before you run the app for the first time, you need to give it permission to use the internet.
Open AndroidManifest.xml and add the following before the <Application> section:
<uses-permission android:name="android.permission.INTERNET"/>
Build and run the app, and you’ll see the default “Hello World” screen.
Check your Logcat window for the following results:
I/PodcastActivity: Results = PodcastResponse(resultCount=4, results=[ItunesPodcast(collectionCensoredName=Android Developers Backstage, feedUrl=http://feeds.feedburner.com/blogspot/AndroidDevelopersBackstage, artworkUrl30=https://is5-ssl.mzstatic.com/image/thumb/Podcasts113/v4/27/04/86/2704860b-686b-99a8-d97e-c475983bc904/mza_2743413905807513331.png/30x30bb.jpg, releaseDate=2020-12-23T14:30:00Z), ItunesPodcast(collectionCensoredName=Droid Dev Talk, feedUrl=https://anchor.fm/s/f117d30/podcast/rss, artworkUrl30=https://is1-ssl.mzstatic.com/image/thumb/Podcasts113/v4/d2/3b/62/d23b62c7-746d-32e0-a070-845e750522c0/mza_6742547218733609407.jpg/30x30bb.jpg, releaseDate=2020-09-19T07:21:00Z), ItunesPodcast(collectionCensoredName=Menjadi Android Developer Expert, feedUrl=https://anchor.fm/s/c5be3dc/podcast/rss, artworkUrl30=https://is1-ssl.mzstatic.com/image/thumb/Podcasts123/v4/99/b5/f7/99b5f713-e6e0-794b-a888-b4bd67b6b548/mza_3930316646477617026.jpg/30x30bb.jpg, releaseDate=2019-08-25T05:00:00Z), ItunesPodcast(collectionCensoredName=How to pick a android app developer in India : The Insider’s Guide, feedUrl=https://anchor.fm/s/1c33bac8/podcast/rss, artworkUrl30=https://is3-ssl.mzstatic.com/image/thumb/Podcasts113/v4/0a/9f/73/0a9f73fa-9bf3-cc13-7902-51e2f2e8dbd7/mza_16007747104321932097.jpg/30x30bb.jpg, releaseDate=2020-07-14T07:27:00Z)])
Congratulations, the service is working! The response displays the list of ItunesPodcast objects based on the search term.
Key Points
In this chapter you learned:
- Podcasts come from RSS feeds and feeds are based on an XML format.
- There are different ways to handle networking on another thread and you used coroutines.
- You used Retrofit for networking calls.
- You used the iTunes search service to search for podcasts.
- Retrofit uses annotations and interfaces for creating services.
Where to go from here?
The term dependency injection was mentioned briefly when you created the iTunesRepo class. You used a simple form of dependency injection when you passed in the ItunesService instance to the iTunesRepo constructor.
As your projects get more complicated it can be useful to have objects created and managed by a dependency injection library. Two of the most popular libraries for Android are Dagger and Koin.
Dagger is a Java-based library that has been around for many years. Koin is a newer library written in Kotlin that takes advantage of Kotlin features.
You can learn more about Dagger in the following tutorials:
- https://www.raywenderlich.com/265010-getting-started-with-dagger
- https://www.raywenderlich.com/265117-dagger-network-injection
You can learn more about Koin in the following tutorial:
In the next chapter, you’ll start building out the user interface to allow the user to search for podcasts.