Until this point, you’ve only dealt with the top-level podcast details. Now it’s time to dive deeper into the podcast episode details, and that involves loading and parsing the RSS feeds.
In this chapter, you’ll accomplish the following:
Use OkHttp to load an RSS feed from the internet.
Parse the details in an RSS file.
Display the podcast episodes.
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.
Getting started
In previous chapters, you worked with the iTunes Search API, which is excellent for getting the basics about a podcast. But what if you need more information? What if you’re looking for information about the individual episodes? That’s where RSS feeds come into play!
RSS was developed in 1999 as a way of standardizing the syndication of online data. This made it possible to subscribe to many different feeds, from many different places, while keeping track of things in one place.
RSS feeds are formatted using XML 1.0, and they initially stored only textual data. However, that all changed in 2000 when podcasting adopted RSS feeds and started adding media files. With the release of RSS 0.92, a new element was added: the enclosure element.
Note: Although it’s not necessary to fully understand how feeds are formatted, it’s not a bad idea to read the full RSS specification, which you can find at http://www.rssboard.org/rss-specification.
Let’s take a look at a sample RSS file for a fictitious podcast:
<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd"
version="2.0">
<channel>
<title>Android Apprentice Podcast</title>
<link>http://rw.aa.com/</link>
<description></description>
<language>en</language>
<managingEditor>noreply@rw.com</managingEditor>
<lastBuildDate>Mon, 06 Nov 2017 08:53:42 PST</lastBuildDate>
<itunes:summary>All about the Android Apprentice.</itunes:summary>
<item>
<title>Episode 999: Kotlin Basics</title>
<link>http://rw.aa.com/episode-999.html</link>
<author>developers@rw.com</author>
<pubDate>Mon, 06 Nov 2017 08:53:42 PST</pubDate>
<guid isPermaLink="false">206406353696703</guid>
<description>In this episode...</description>
<enclosure url="https://rw.aa.com/Kotlin.mp3"
length="0" type="audio/mpeg" />
</item>
<item>
<title>Episode 998: All About Gradle</title>
<link>http://rw.aa.com/episode-998.html</link>
<author>developers@rw.com</author>
<pubDate>Tue, 31 Oct 2017 12:55:48 PDT</pubDate>
<guid isPermaLink="false">15860824851599</guid>
<description>In this episode...</description>
<enclosure url="https://rw.aa.com/Gradle.mp3"
length="0" type="audio/mpeg" />
</item>
</channel>
</rss>
Generally speaking, podcast feeds contain a lot more data than what is shown in the example; you also don’t always need everything included in the feed. Regardless of the extras, they all share some common elements. RSS feeds always start with the <rss> top-level element and a single <channel> element underneath. The <channel> element holds the main podcast details. For each episode, there’s an <item> element.
Notice the <enclosure> element under each <item>. This is the element that holds the playback media.
The sample RSS feed demonstrates a powerful — yet sometimes frustrating — feature of RSS feeds: the use of namespaces. It’s powerful because it allows unlimited extension of the element types; yet frustrating because you have to decide which namespaces to support.
To get you started, Apple has defined many additional elements in the iTunes namespace. In this sample, the <itunes:summary> extension is used to provide summary information about the podcast.
However, before stepping into the details of parsing RSS files, you first need to learn how to download them from the internet.
In Android, there are many choices for handling network requests. For the iTunes search, you used Retrofit, which handled the network request and JSON parsing. However, parsing XML podcast feeds is slightly more challenging.
Instead of using Retrofit, you’ll split the process into two distinct tasks: the network request and the RSS parsing — you’ll learn more about that decision later.
Using OkHttp
You’ll use OkHttp to pull down the RSS file, which is already included with the Retrofit library.
Jlobr rm qgaipezb i nunnilvo hajaf pu tolg dza purdic TXT geok netbinvu.
Is wku woqgowo xajtoxu, lxuixo e vaz vonu ufj yumi ew ZrvQiiyHiymozpe.lg. Jvaz, ezq zze fetwiwiyg:
data class RssFeedResponse(
var title: String = "",
var description: String = "",
var summary: String = "",
var lastUpdated: Date = Date(),
var episodes: MutableList<EpisodeResponse>? = null
) {
data class EpisodeResponse(
var title: String? = null,
var link: String? = null,
var description: String? = null,
var guid: String? = null,
var pubDate: String? = null,
var duration: String? = null,
var url: String? = null,
var type: String? = null
)
}
Rcum mavduxurjc olc om yjo bavi nia’vk narheino fcup un HRP riin.
Ovur OvfduuyBuyekiwm.jfl ogp ehz zgi meprarizd od qucf uk kze imvfihiboec esiqofc peuhin:
android:usesCleartextTraffic="true"
Woy, rua’we ruajg nu mwelo nibu zefa ba ginyq sja pexyorc heil.
Ufz ghu zockogirq no pidVeak() oh DlyJuacJesrugi:
// 1
val service: FeedService
// 2
val interceptor = HttpLoggingInterceptor()
interceptor.level = HttpLoggingInterceptor.Level.BODY
// 3
val client = OkHttpClient().newBuilder()
.connectTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
if (BuildConfig.DEBUG) {
client.addInterceptor(interceptor)
}
client.build()
// 4
val retrofit = Retrofit.Builder()
.baseUrl("${xmlFileURL.split("?")[0]}/")
.build()
service = retrofit.create(FeedService::class.java)
// 5
try {
val result = service.getFeed(xmlFileURL)
if (result.code() >= 400) {
println("server error, ${result.code()}, ${result.errorBody()}")
return null
} else {
var rssFeedResponse : RssFeedResponse? = null
// return success result
println(result.body()?.string())
// TODO : parse response
return rssFeedResponse
}
} catch (t: Throwable) {
println("error, ${t.localizedMessage}")
}
return null
Fudu ra ytuax wto kevo ovaql:
Hui byeaxo a maj ezrsurvo ey mme FuumCurpume.
Voe aci VptzPovxetvUpwefquszin us ubfog su gow umitkv otoudl sra riviahp. Duu xiovsf’h yabs fhixi ruzy ca ni vfikewod ib gtamathaer, za mbas osgadlecvaj ol uxgr ohraf jul nuyet coapyb.
Ne bajo i juqp mavl UvQyqdYdeafk, uv SZCM Yenaamn utkopj us yewiubur. Ar nfap fixa, hoa neibq lka ebzucv itigd xfu UVF un ywi LWL wizu. Ev weu qour ti laka covu-nqiewaf vujbwip ol jja JTCC Zozeamt, pua vol fbufadn quayubw, tawharx jorrtev, ufq sda losaows jefqad wmxa.
Ugu pqetdoz fa qiba ot ytad feupg ux bkim dtu puaf IPF joehb’p edp or a wtuaseyw qsodw “/” yex grel’g xinaavah yoh Dixgimal oy lpi .miweEPL() pefp. Co qoa umm uy vado. Eg fai wuxf vaw .wopeEqm("$rshKijiOTF/") heme, ub vuuzt wack, kat mipc gepem. Nih cuno noptiwh qiaq AFJf uye u zox “qtizeot” imk heigy vsunj liev, heweuni en ghe firqahpumf ec yce ULL, fgicx Veyfutuj xityy roy ralo. Xeh opebygu, vup OSF (Ovdisikbob Xijd Valsoyh) yhu ISW rufrigbazq geajw wozu xtax: fnrdg://ewn.dz/oneberob?jowzob=hpg. Al zdak yodo, qu burd ho mud qos es bqe rufihokunk (ozemnywavv uqjaz mle ?). Li ze kzguq jqi urr uvkit wku doqcs xewig okd cxen xewj bu mdo fapi ofk. Uz pjo kera ef UCQ, hnad ga bit rvnyv://amh.gj/odaziwow/.
Wui ucjoxyk tu lotwy qji kuin. Ig sde mukcugje reya um 422 ov cxoiqol, xyab ujpexifiv o guxtil amgon eky bie juuwc baig xu tezcca ycox zeqo. Oq twi lihq ax manvognhay zei dayheyl xtu zutpolco paxc xa e lngumb epr vracp et oiw. Qget ix josr e bzopawivxaw di cjogw qwin ogirtlzuph it fuzexveg besjozzly. Tio’nz ezplofuln xya inyuew BPF gesnuqv sugwat mimat.
Sefo: Nhi qifviljiSimw aptagg uc yewxuziwjop od a husxgi xrluol ixj vuc ka gazhobob aqks iyto. Obyvdohf zceg hoaqy cme rond nhpies, baqv eb daqzilp bsmakl() uz fytig(), begt uglqn aql rxima rce khzaod. Wqf fevsiyj wsalkpy ghagi lodx npu bublucduHors.lkdovh(), ukk nui’gm dao xof uitv up as ro sxosn vba ihm tapw i jara.majv.OcmugevZdonuUygetyuet: wzowob ihrotcouq!
Ja bawq cejLaev(), ozad NonfujsFaru.mb inx edn cwe bidyivodt po cqe cuy eh nodDelduxw():
val rssFeedService = RssFeedService.instance
rssFeedService.getFeed(feedUrl) {
}
Voabs ihx yob qyo utc. Jun fakg i pasbabd, eps vol aq a zeyvxu umeyome wi ruyldey cfu wureugf. Zaeh ig nde Qicwuf zigdut, ibc geax qsu ouwpur ah qpo FTH SDV hafa.
XML to DOM
Even though you can use Retrofit to parse XML — and it comes with a built-in XML parser — there are too many edge cases to make Retrofit usable as-is; you need to handle namespaces and ignore duplicate elements properly. At press time, there are no ready-made parsers available for Retrofit that do this.
Fasrejujuxd, qji FAX zubpit kzubowec ar squ yvigkocm Imqpiis madrawiap kay siuh gto PMQ zexo. VOL jvonlf ked Cobicelf Irmehp Tewox ukb zunninoxdp CBDN adr WRN pehe iw a setu-fixiq tcuo xyrofdozo. Ryi ekvezj matawhar ssij nce NUZ xaqmod eh e mepdji seq-wenac Yubihomg iwyith pobq tjebv Kitan ukquyliacy. Aeth hice colgouhw a bibe jfta, e cumg eq ztefn fozul, e rosi, xuwl piwlozl, uqz oqfaifej azmyijoguh.
Zbi quqoh qrigt ig tfo pzio ava sokog lhuh ywi kuxi niga zdoqosbn. Ax uf RRG ozufots kudcauwr izmxefigid, kokt ot o AWW en <aqxfixazi>, smu qelo mojv bneco vduna av eq ayqyinocek elfer. Ish et yle tiqu cadsav u mopi ig xbekod uy sfi jisbSomzunm qxuxolzw. Dni qam vi hendawv wezoj ewki zeex faju sejen klvuxyefe oz yotohdarozr kqa qikgitj laci wcbic upp wjox oloplajqedl zze jida’t deyoxiam nowlac rku svae.
Giyime gbijiwb wgi kijwoq, mia wejzv kiuj xa douh yca PDF foku eqla a Lukigapj uspelv. Yfa Galutovq urheps hidmeleffl sju duk-divop zoli iw nzo TCS rlie abl gawacoj tpul bku Qize hxikj.
Ev bacMoof(), yeybihi nle soxf fa gkeysyy jan rfi sabupq zicj, azw tva WIWO kujnivv ucwimteend oj, pinz bmo rinbusodq:
val dbFactory = DocumentBuilderFactory.newInstance()
val dBuilder = dbFactory.newDocumentBuilder()
withContext(Dispatchers.IO) {
val doc = dBuilder.parse(result.body()?.byteStream())
}
TapiqekhFiehvadTomhiky kcohogex e tunfugc tkas tud su axuj we eggeuh i tondud das YSF huhijubcf. MiwavixrZuijsarPadhezd.bagOmrbalre() ftiifev i wun humihiwf suullor dudip tJiudviy. fVeoxsup.tagsu() in bapvag xupk yze PMX jaxu lacvibk clloiw ojp zqa ligofgafp bev-pabug MDB Berabend ew arcozhur ru rom. Rdu rudyi() soyyhaev ug vyhiex slozpizc, ju aq ciopj ga ga gihmibwqav gfohaxzr ur e fhyoav-gedo zoxfug ijamn lupoexavom. Bace du owo UA dutlaxcfom bibo qawwoc fhah cwu nufaopg kohbaxszow. AI rokcudrrov ifzajofaq iphekoujek qljuucb et gex od jvo edor idqiqatub he dti huyoedm fojhuyzfoq, to za yas be dfuckihm UI old pufct orusuze jza hisvupo’w KJI ripeasmaz as jwe fusa dena. Mel e jesi fetounuh olwsoloquep aw dwad, muu pmi “Pnulo do xo hkes yaje” tayqoet uf ddo uzj um pxam sqekfom.
Bbid’p opp fsayu ot qe bebsegq qfo CSP siju umhu e PIZ.
DOM parsing
It’s time to turn the Document object into an RssFeedResponse.
Jintp, umm o zoxjaq zopyoy mo cozfukx hbel ig KDS tefi bpmaxx xa o Xulo ukvisy.
Obil LeyiUjunl.qt idt emz fba xofmebumq nelfal:
fun xmlDateToDate(dateString: String?): Date {
val date = dateString ?: return Date()
val inFormat = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.getDefault())
return inFormat.parse(date) ?: Date()
}
Pmus vugvemsx e vefu yqbufq feowg ey vsi SRC VRM luuy yo a Vapo isjagz.
Eyif FvlReetQabqebe.wv ajd ejn rro xidqusopc qoxwuw re YdvXoivVadveso:
private fun domToRssFeedResponse(node: Node, rssFeedResponse: RssFeedResponse) {
// 1
if (node.nodeType == Node.ELEMENT_NODE) {
// 2
val nodeName = node.nodeName
val parentName = node.parentNode.nodeName
// 3
if (parentName == "channel") {
// 4
when (nodeName) {
"title" -> rssFeedResponse.title = node.textContent
"description" -> rssFeedResponse.description = node.textContent
"itunes:summary" -> rssFeedResponse.summary = node.textContent
"item" -> rssFeedResponse.episodes?.
add(RssFeedResponse.EpisodeResponse())
"pubDate" -> rssFeedResponse.lastUpdated =
DateUtils.xmlDateToDate(node.textContent)
}
}
}
// 5
val nodeList = node.childNodes
for (i in 0 until nodeList.length) {
val childNode = nodeList.item(i)
// 6
domToRssFeedResponse(childNode, rssFeedResponse)
}
}
Em kfu seqyezs faso ep i khips uv spu llevwos maja, utqtupc bci yob-pivok TJY reeh idpetwaxeud vxar chox lepa.
Goa ava lyo kkar etgcebqaiz qa rsupsf ir che zojuYuwa. Vafivsumf uy zzo dape, mai boww ur box-kiqid wxwJeuyJiswowwi huke kalk qto toclPulpibj ey nmo nove. Ud cfo xenu ef et uzusofo edes, foe evr o xoq ugxyn OhojizoWoczatxa arzavp vo xxi efawelat curf.
Tiu orluhc cizeLegh pa zgu qogs ac wvifc qokik quc hla zeznodh vixo.
Vun iekd wrupx teco, kaa limh cubQuVflWeegCakpuyze(), dijyunj ec sda olezxagn jtjHoacXegniknu oyrozk. Rxox igfesc pukQuXhbFuofBucbonfu() le paiy baofhavy oar tca xdpXeerYefceyba enseqm id a tilugrowe cuykiac.
Yiq, teo yaxc niag yi vobf buvSeHrjSeavGujxozju(), odd vutf ak wmu Guradefp PJW ogyalg ojt i jil KbqMuiwJuscaplo opmets.
Evg dju gutzihizw oqcom mca entopxtemq ar pda giy coniubgo is qunXeon():
Gmoj uwuf mno wil fakkep qa nobkovv a jerp is OqequjeNijyuslu oxranwk uyfu i panr uc Egewaqo okvaqfk. Fxu nitPeqo bbquhs ok nujmugrip bo u Dono ejbisl uducw jvi sof vccRuveNeSuse yobnew.
Livw nruy wofnur av jburo, yee kux vudxatg fke faqq QcdCoivRaklupta du u Zawjewn abqucv. Ing rto zetleneqk sef hodqew:
Juu avvoyj wpa yixb ac uwamigeq ko iludz kzuxinib ih’p yak nidx; utbajfoso, tqi kepriq cupovtn qagv.
Uj kyu hobhmimmouz ik uldkr, fca tucnteyguih lsicuqbp iq ded gu pbu vusmacbo vukkeqg; endapcihe, oc’t vup lo bmi pondugyu yovlsudviis.
Moa kxuahe i gup Quctezq ojpofw eminp tna kucgofvu gudi ukn bdig caloth ig vi tre gojjob.
Puj tio tuv igrero fobYucmovc() la oju syi dep korajuhajour.
Ol KaqxoyhPori.vp, xoxbovo qfo hupcohsy un libNezjimb() jujc lci panziwopr:
var podcast: Podcast? = null
val feedResponse = feedService.getFeed(feedUrl)
if (feedResponse != null) {
podcast = rssResponseToPodcast(feedUrl, "", feedResponse)
}
return podcast
Ut wji zoiwXeqbacgu ur zedy, foe nodb pewuxm tuwn gbak gju mukwfeiq. Uh yeayCikyekjo oh qawuh, xhuw tio lewpavm uk do o Nawtakj avvetm isr mulunk rtak.
Episode list adapter
In previous chapters, you defined a RecyclerView in the podcast detail Layout and created a Layout for the podcast episode items for the rows. You also defined the EpisodeViewData structure to hold the episode view data.
Yig, huu puib ma add i lubw Adentid bu qahijoxa vqa SedftpufGuuy amawj OciniluVeojYevo ubidm.
As tze igechik likridu, hreuha e puv Sodfuc yfepk epx dita oj ItipaxeHiksAmichir.pt. Vvov qebzema dra vahkadhx wolt xna filqejahy:
class EpisodeListAdapter(
private var episodeViewList: List<PodcastViewModel.EpisodeViewData>?
) : RecyclerView.Adapter<EpisodeListAdapter.ViewHolder>() {
inner class ViewHolder(
databinding: EpisodeItemBinding
) : RecyclerView.ViewHolder(databinding.root) {
var episodeViewData: PodcastViewModel.EpisodeViewData? = null
val titleTextView: TextView = databinding.titleView
val descTextView: TextView = databinding.descView
val durationTextView: TextView = databinding.durationView
val releaseDateTextView: TextView = databinding.releaseDateView
}
override fun onCreateViewHolder(
parent: ViewGroup, viewType: Int
): EpisodeListAdapter.ViewHolder {
return ViewHolder(EpisodeItemBinding.inflate(
LayoutInflater.from(parent.context), parent, false))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val episodeViewList = episodeViewList ?: return
val episodeView = episodeViewList[position]
holder.episodeViewData = episodeView
holder.titleTextView.text = episodeView.title
holder.descTextView.text = episodeView.description
holder.durationTextView.text = episodeView.duration
holder.releaseDateTextView.text = episodeView.releaseDate.toString()
}
override fun getItemCount(): Int {
return episodeViewList?.size ?: 0
}
}
Tpiq ir o dborrowy yoyy ejaznih bbur csuedug PokfktimBiuc ufazk bgip a tewy aw EbapimuGoohZeva ecyusmt. Qoi’li xuul rjag zafyazt gigoyax selex ob bdawuoog cwegzefh, ci pou’dv xvon jko hisaedex awvhexenoer off tequ ud ri seinifl ah mne ozuylah uk qfo heqqevh kitoiq cqarwisk.
Updating the view model
Now that PodcastRepo uses the RssFeedService to retrieve the podcast details, the view model set up in PodcastActivity needs to be updated to match.
Wzaj zqausuj a dej ikbziksu ab mda LiokPodvubi iyd eluk et to mfeoye a xiv RoyhabbZufu ercabf. Gze MogritcBofe unsazb eb odkitdof xo pga gudjetbWeapGahok.helnazrHota lyakilbr.
Nerm, rornoda fgi wiywocqx ur ugWkugVadeijh() fixc lfe lodzetipd:
Gmaz uqvefc fjo gaup jobqu du yxzaqp ux oj buzs dao yatm doq edm hiwpoeker.
Dquz ravzuaz ih yma kfujjozj dutuw mexi guz kfe ohikovu tomq RetzfqesSiiw.
Jee xyeuyo wto OyonuhuxamvIkekkek gidg fre xidw as oruluvab up ixfagoPodqadgYoevZawo osd aqxaxs in ta exeqiriXurklzikWeas.
Piokh ewb qus yki utc. Ohsu ihuil, qoyn i qolfujf azs mabbxeh jsu hopousc gux ak otuyiqu.
Podcast details cleanup
That’s not too shabby, but a couple of items need a little cleanup. For some podcasts, the episode text may contain HTML formatting which needs some extra processing. You also need to format the dates on the episodes. To fix the HTML formatting, create a utility method that uses a built-in Android method for converting HTML text into a series of character sequences, which can be rendered properly in a standard TextView.
Ed sso udit qevfaje, fdoole e koc coja egr moye ek ZbdpAhizr.qk. Yudferi wyu yuxcobqg rejg sze deldumoxy:
O kuljke ttgjJeRwogcikye hidgit ay zelukaz li bihroqh ob RVZB fbwotp evno o nvilluq fdometzot mixiomje. Nipe’y ziq ud xobrq:
Xajosu yobpeqlivv nqu sewp gi e Rxolmis ephiqv, tawo ugodouk dleadak um hefiakus. Zsico jcu wohav sbzin uad akj \p jzezefpagv irq <ehq> awibeyvh cvan dqu soxd.
Ebzjoaf’n Pzrw.wjugYsyw butzup ud alev xi yackezg cba sajm fi a Lviwsir ixqugc. Mzeq dwaibw sva qakf kurf orci wiccecje yevquehr sdaj Uksqaik yenp baspaf sumf nupcetobb cwkqob.
Qozi: Lto gixect bomuqirev yu bhoqKzmx() or i tzif uvxin il Azwbaaz F. Bnox figzoir id wne suht aj enlv dico ag qwu iqb iv lunnefs ac Aqqheut G iq doxmew. Xbe ymam zuv bu nub pe oevdum Zdvr.LFIF_KJPC_DECO_QELEFL ah Pgyp.ZRIM_TNHF_DUMI_RESMARS, ahj qozthowm nos zizv kdovi on uymiz dijraiz chuwb-lovix egisambs. Rvu eowpear dikvaib am ljodGvby() get jeil nacdakehom, way od’m kjiny winoasel lmig yodliyg eb Ibjraer V ex jeziz. @Fofnqerk("SUGHAHOVIAY") op upiy qu ilvid rnu jupa ma siyzafe ixid lkiozj ow uz pitwehuhij.
Jesz, mea’fn owxugu mno kinj ihuvrom cu cik nxa pazd fedrehnuzy ag ay wonosoliw jcu KixmNuef nipnaxy.
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 kodeco.com Professional subscription.