A Extensible Markup Language (XML) é um conjunto de regras para a codificação de documentos em um formato legível por máquina. XML é um formato conhecido para o compartilhamento de dados na Internet.
Sites que atualizam seu conteúdo com frequência, como sites de notícias ou blogs, geralmente fornecem um feed XML para que programas externos possam acompanhar as mudanças de conteúdo. Carregar e analisar dados XML é uma tarefa comum para apps conectados a uma rede. Este tema explica como analisar documentos XML e usar os dados deles
Para saber mais sobre a criação de conteúdo baseado na Web no seu app Android, consulte Conteúdo baseado na Web.
Escolher um analisador
Recomendamos o XmlPullParser
, que é uma forma eficiente e
sustentável de analisar XML no Android. O Android tem duas
implementações dessa interface:
KXmlParser
(link em inglês), usandoXmlPullParserFactory.newPullParser()
ExpatPullParser
, usandoXml.newPullParser()
As duas opções são adequadas. O
exemplo nesta seção usa ExpatPullParser
e
Xml.newPullParser()
.
Analisar o feed
A primeira etapa na análise de um feed é decidir quais os campos do seu interesse. O analisador extrai dados desses campos e ignora o restante.
Confira abaixo um trecho de um feed analisado no app de exemplo. Todas as
postagens no StackOverflow.com (link em inglês) aparecem no
feed como uma tag entry
que contém várias tags aninhadas:
<?xml version="1.0" encoding="utf-8"?> <feed xmlns="http://www.w3.org/2005/Atom" xmlns:creativeCommons="http://backend.userland.com/creativeCommonsRssModule" ..."> <title type="text">newest questions tagged android - Stack Overflow</title> ... <entry> ... </entry> <entry> <id>http://stackoverflow.com/q/9439999</id> <re:rank scheme="http://stackoverflow.com">0</re:rank> <title type="text">Where is my data file?</title> <category scheme="http://stackoverflow.com/feeds/tag?tagnames=android&sort=newest/tags" term="android"/> <category scheme="http://stackoverflow.com/feeds/tag?tagnames=android&sort=newest/tags" term="file"/> <author> <name>cliff2310</name> <uri>http://stackoverflow.com/users/1128925</uri> </author> <link rel="alternate" href="http://stackoverflow.com/questions/9439999/where-is-my-data-file" /> <published>2012-02-25T00:30:54Z</published> <updated>2012-02-25T00:30:54Z</updated> <summary type="html"> <p>I have an Application that requires a data file...</p> </summary> </entry> <entry> ... </entry> ... </feed>
O app de exemplo
extrai dados para a tag entry
e as tags aninhadas correspondentes:
title
, link
e summary
.
Instanciar o analisador
A próxima etapa é
instanciar um analisador e iniciar o processo de análise do feed. Este snippet
inicializa um analisador para não processar namespaces e usar o InputStream
fornecido como entrada. Ele inicia o processo de análise com uma chamada para
nextTag()
e invoca o método
readFeed()
, que extrai e processa os dados pelos quais o app se
interessa:
Kotlin
// We don't use namespaces. private val ns: String? = null class StackOverflowXmlParser { @Throws(XmlPullParserException::class, IOException::class) fun parse(inputStream: InputStream): List<*> { inputStream.use { inputStream -> val parser: XmlPullParser = Xml.newPullParser() parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false) parser.setInput(inputStream, null) parser.nextTag() return readFeed(parser) } } ... }
Java
public class StackOverflowXmlParser { // We don't use namespaces. private static final String ns = null; public List parse(InputStream in) throws XmlPullParserException, IOException { try { XmlPullParser parser = Xml.newPullParser(); parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false); parser.setInput(in, null); parser.nextTag(); return readFeed(parser); } finally { in.close(); } } ... }
Ler o feed
O método readFeed()
realiza o trabalho efetivo de processamento do
feed. Ele procura elementos marcados como "entrada" como ponto de partida para
processar o feed recursivamente. Se uma tag não for entry
, ela será ignorada. Depois de
processar recursivamente o feed todo, readFeed()
vai retornar uma
List
contendo as entradas (incluindo membros de dados aninhados)
extraídas do feed. Depois disso, essa List
é retornada pelo
analisador.
Kotlin
@Throws(XmlPullParserException::class, IOException::class) private fun readFeed(parser: XmlPullParser): List<Entry> { val entries = mutableListOf<Entry>() parser.require(XmlPullParser.START_TAG, ns, "feed") while (parser.next() != XmlPullParser.END_TAG) { if (parser.eventType != XmlPullParser.START_TAG) { continue } // Starts by looking for the entry tag. if (parser.name == "entry") { entries.add(readEntry(parser)) } else { skip(parser) } } return entries }
Java
private List readFeed(XmlPullParser parser) throws XmlPullParserException, IOException { List entries = new ArrayList(); parser.require(XmlPullParser.START_TAG, ns, "feed"); while (parser.next() != XmlPullParser.END_TAG) { if (parser.getEventType() != XmlPullParser.START_TAG) { continue; } String name = parser.getName(); // Starts by looking for the entry tag. if (name.equals("entry")) { entries.add(readEntry(parser)); } else { skip(parser); } } return entries; }
Analisar XML
As etapas para analisar um feed XML são estas:
- Conforme descrito em Analisar o feed, identifique as tags que você quer incluir no app. Este
exemplo extrai dados para a tag
entry
e as tags aninhadastitle
,link
esummary
. - Crie os métodos abaixo:
- Um método de "leitura" para cada tag que você quer incluir, como
readEntry()
ereadTitle()
. O analisador lê tags do fluxo de entrada. Quando ele encontra uma tag com nome, neste exemplo,entry
,title
,link
ousummary
, ele chama o método adequado para ela. Caso contrário, a tag é ignorada. - Métodos para extrair dados para cada tipo diferente de tag e avançar o analisador para a
tag seguinte. Neste exemplo, os métodos relevantes são estes:
- Para tags
title
esummary
, o analisador chamareadText()
. Esse método extrai dados para as tags chamandoparser.getText()
. - Para a tag
link
, o analisador extrai dados dos links determinando, primeiro, se o link é de interesse. Em seguida, ele usaparser.getAttributeValue()
para extrair o valor do link. - Para a tag
entry
, o analisar chamareadEntry()
. Esse método analisa as tags aninhadas da entrada e retorna um objetoEntry
com os membros de dadostitle
,link
esummary
.
- Para tags
- Um método auxiliar
skip()
recursivo. Para saber mais sobre esse assunto, consulte Ignorar as tags que não importam.
- Um método de "leitura" para cada tag que você quer incluir, como
O snippet abaixo mostra como o analisador processa entradas, títulos, links e resumos.
Kotlin
data class Entry(val title: String?, val summary: String?, val link: String?) // Parses the contents of an entry. If it encounters a title, summary, or link tag, hands them off // to their respective "read" methods for processing. Otherwise, skips the tag. @Throws(XmlPullParserException::class, IOException::class) private fun readEntry(parser: XmlPullParser): Entry { parser.require(XmlPullParser.START_TAG, ns, "entry") var title: String? = null var summary: String? = null var link: String? = null while (parser.next() != XmlPullParser.END_TAG) { if (parser.eventType != XmlPullParser.START_TAG) { continue } when (parser.name) { "title" -> title = readTitle(parser) "summary" -> summary = readSummary(parser) "link" -> link = readLink(parser) else -> skip(parser) } } return Entry(title, summary, link) } // Processes title tags in the feed. @Throws(IOException::class, XmlPullParserException::class) private fun readTitle(parser: XmlPullParser): String { parser.require(XmlPullParser.START_TAG, ns, "title") val title = readText(parser) parser.require(XmlPullParser.END_TAG, ns, "title") return title } // Processes link tags in the feed. @Throws(IOException::class, XmlPullParserException::class) private fun readLink(parser: XmlPullParser): String { var link = "" parser.require(XmlPullParser.START_TAG, ns, "link") val tag = parser.name val relType = parser.getAttributeValue(null, "rel") if (tag == "link") { if (relType == "alternate") { link = parser.getAttributeValue(null, "href") parser.nextTag() } } parser.require(XmlPullParser.END_TAG, ns, "link") return link } // Processes summary tags in the feed. @Throws(IOException::class, XmlPullParserException::class) private fun readSummary(parser: XmlPullParser): String { parser.require(XmlPullParser.START_TAG, ns, "summary") val summary = readText(parser) parser.require(XmlPullParser.END_TAG, ns, "summary") return summary } // For the tags title and summary, extracts their text values. @Throws(IOException::class, XmlPullParserException::class) private fun readText(parser: XmlPullParser): String { var result = "" if (parser.next() == XmlPullParser.TEXT) { result = parser.text parser.nextTag() } return result } ...
Java
public static class Entry { public final String title; public final String link; public final String summary; private Entry(String title, String summary, String link) { this.title = title; this.summary = summary; this.link = link; } } // Parses the contents of an entry. If it encounters a title, summary, or link tag, hands them off // to their respective "read" methods for processing. Otherwise, skips the tag. private Entry readEntry(XmlPullParser parser) throws XmlPullParserException, IOException { parser.require(XmlPullParser.START_TAG, ns, "entry"); String title = null; String summary = null; String link = null; while (parser.next() != XmlPullParser.END_TAG) { if (parser.getEventType() != XmlPullParser.START_TAG) { continue; } String name = parser.getName(); if (name.equals("title")) { title = readTitle(parser); } else if (name.equals("summary")) { summary = readSummary(parser); } else if (name.equals("link")) { link = readLink(parser); } else { skip(parser); } } return new Entry(title, summary, link); } // Processes title tags in the feed. private String readTitle(XmlPullParser parser) throws IOException, XmlPullParserException { parser.require(XmlPullParser.START_TAG, ns, "title"); String title = readText(parser); parser.require(XmlPullParser.END_TAG, ns, "title"); return title; } // Processes link tags in the feed. private String readLink(XmlPullParser parser) throws IOException, XmlPullParserException { String link = ""; parser.require(XmlPullParser.START_TAG, ns, "link"); String tag = parser.getName(); String relType = parser.getAttributeValue(null, "rel"); if (tag.equals("link")) { if (relType.equals("alternate")){ link = parser.getAttributeValue(null, "href"); parser.nextTag(); } } parser.require(XmlPullParser.END_TAG, ns, "link"); return link; } // Processes summary tags in the feed. private String readSummary(XmlPullParser parser) throws IOException, XmlPullParserException { parser.require(XmlPullParser.START_TAG, ns, "summary"); String summary = readText(parser); parser.require(XmlPullParser.END_TAG, ns, "summary"); return summary; } // For the tags title and summary, extracts their text values. private String readText(XmlPullParser parser) throws IOException, XmlPullParserException { String result = ""; if (parser.next() == XmlPullParser.TEXT) { result = parser.getText(); parser.nextTag(); } return result; } ... }
Ignorar as tags que não importam
O analisador precisa pular as tags que não são de interesse. Confira o método skip()
do analisador:
Kotlin
@Throws(XmlPullParserException::class, IOException::class) private fun skip(parser: XmlPullParser) { if (parser.eventType != XmlPullParser.START_TAG) { throw IllegalStateException() } var depth = 1 while (depth != 0) { when (parser.next()) { XmlPullParser.END_TAG -> depth-- XmlPullParser.START_TAG -> depth++ } } }
Java
private void skip(XmlPullParser parser) throws XmlPullParserException, IOException { if (parser.getEventType() != XmlPullParser.START_TAG) { throw new IllegalStateException(); } int depth = 1; while (depth != 0) { switch (parser.next()) { case XmlPullParser.END_TAG: depth--; break; case XmlPullParser.START_TAG: depth++; break; } } }
Veja como funciona:
- Gera uma exceção se o evento atual não for uma
START_TAG
. - Consome a
START_TAG
e todos os eventos, inclusive aEND_TAG
correspondente. - Monitora a profundidade do aninhamento para garantir que ele pare na
END_TAG
correta e não na primeira tag encontrada após aSTART_TAG
original.
Assim, se o elemento atual tiver elementos aninhados, o valor de
depth
não será 0 até que o analisador tenha consumido todos os eventos entre
a START_TAG
original e a END_TAG
correspondente. Por
exemplo, considere como o analisador ignora o elemento <author>
,
que tem dois elementos aninhados, <name>
e
<uri>
:
- Na primeira vez na repetição
while
, a próxima tag encontrada pelo analisador depois de<author>
éSTART_TAG
para<name>
. O valor dedepth
aumenta para 2. - Na segunda passagem da repetição
while
, a próxima tag que o analisador encontra é aEND_TAG
</name>
. O valor dedepth
diminui para 1. - Na terceira passagem da repetição
while
, a próxima a tag que o analisador encontra é aSTART_TAG
<uri>
. O valor dedepth
aumenta para 2. - Na quarta passagem da repetição
while
, a próxima tag que o analisador encontra é aEND_TAG
</uri>
. O valor dedepth
diminui para 1. - Na quinta e última passagem da repetição
while
, a próxima tag que o analisador encontra é aEND_TAG
</author>
. O valor dedepth
diminui para 0, indicando que o elemento<author>
foi ignorado.
Consumir dados XML
O aplicativo de exemplo busca e analisa o feed XML de forma assíncrona. Isso retira o processamento da linha de execução de interface principal. Quando
o processamento é concluído, o app atualiza a interface na atividade principal
(NetworkActivity
).
No trecho abaixo, o método loadPage()
faz o seguinte:
- Inicializa uma string variável com o URL para o feed XML.
- Se as configurações do usuário e a conexão de rede permitirem,
ele invoca o método
downloadXml(url)
. Esse método faz o download e analisa o feed, retornando um resultado de string para ser mostrado na interface.
Kotlin
class NetworkActivity : Activity() { companion object { const val WIFI = "Wi-Fi" const val ANY = "Any" const val SO_URL = "http://stackoverflow.com/feeds/tag?tagnames=android&sort=newest" // Whether there is a Wi-Fi connection. private var wifiConnected = false // Whether there is a mobile connection. private var mobileConnected = false // Whether the display should be refreshed. var refreshDisplay = true // The user's current network preference setting. var sPref: String? = null } ... // Asynchronously downloads the XML feed from stackoverflow.com. fun loadPage() { if (sPref.equals(ANY) && (wifiConnected || mobileConnected)) { downloadXml(SO_URL) } else if (sPref.equals(WIFI) && wifiConnected) { downloadXml(SO_URL) } else { // Show error. } } ... }
Java
public class NetworkActivity extends Activity { public static final String WIFI = "Wi-Fi"; public static final String ANY = "Any"; private static final String URL = "http://stackoverflow.com/feeds/tag?tagnames=android&sort=newest"; // Whether there is a Wi-Fi connection. private static boolean wifiConnected = false; // Whether there is a mobile connection. private static boolean mobileConnected = false; // Whether the display should be refreshed. public static boolean refreshDisplay = true; public static String sPref = null; ... // Asynchronously downloads the XML feed from stackoverflow.com. public void loadPage() { if((sPref.equals(ANY)) && (wifiConnected || mobileConnected)) { downloadXml(URL); } else if ((sPref.equals(WIFI)) && (wifiConnected)) { downloadXml(URL); } else { // Show error. } }
O método downloadXml
chama estes métodos no Kotlin:
lifecycleScope.launch(Dispatchers.IO)
, que usa corrotinas do Kotlin para iniciar o métodoloadXmlFromNetwork()
na linha de execução de E/S. Ele passa o URL do feed como um parâmetro. O métodoloadXmlFromNetwork()
busca e processa o feed. Quando termina, ele retorna uma string de resultados.- O
withContext(Dispatchers.Main)
, que usa corrotinas do Kotlin para retornar à linha de execução principal, usa a string retornada e a exibe na interface.
Na linguagem de programação Java, o processo é o seguinte:
- Um
Executor
executa o métodoloadXmlFromNetwork()
em uma linha de execução em segundo plano. Ele passa o URL do feed como um parâmetro. O métodoloadXmlFromNetwork()
busca e processa o feed. Quando termina, ele retorna uma string de resultados. - Um
Handler
chamapost
para retornar à linha de execução principal, seleciona a string retornada e a exibe na interface.
Kotlin
// Implementation of Kotlin coroutines used to download XML feed from stackoverflow.com. private fun downloadXml(vararg urls: String) { var result: String? = null lifecycleScope.launch(Dispatchers.IO) { result = try { loadXmlFromNetwork(urls[0]) } catch (e: IOException) { resources.getString(R.string.connection_error) } catch (e: XmlPullParserException) { resources.getString(R.string.xml_error) } withContext(Dispatchers.Main) { setContentView(R.layout.main) // Displays the HTML string in the UI via a WebView. findViewById<WebView>(R.id.webview)?.apply { loadData(result?: "", "text/html", null) } } } }
Java
// Implementation of Executor and Handler used to download XML feed asynchronously from stackoverflow.com. private void downloadXml(String... urls) { ExecutorService executor = Executors.newSingleThreadExecutor(); Handler handler = new Handler(Looper.getMainLooper()); executor.execute(() -> { String result; try { result = loadXmlFromNetwork(urls[0]); } catch (IOException e) { result = getResources().getString(R.string.connection_error); } catch (XmlPullParserException e) { result = getResources().getString(R.string.xml_error); } String finalResult = result; handler.post(() -> { setContentView(R.layout.main); // Displays the HTML string in the UI via a WebView. WebView myWebView = (WebView) findViewById(R.id.webview); myWebView.loadData(finalResult, "text/html", null); }); }); }
O método loadXmlFromNetwork()
, que é invocado por
downloadXml
, é mostrado no próximo snippet. Ele faz o seguinte:
- Instancia um
StackOverflowXmlParser
e cria variáveis para umaList
de objetos deEntry
(entries
),title
,url
esummary
para armazenar os valores extraídos do feed XML desses campos. - Ele chama
downloadUrl()
, que busca o feed e o retorna como umInputStream
. - Ele usa
StackOverflowXmlParser
para analisar oInputStream
.StackOverflowXmlParser
preenche umaList
deentries
com dados do feed. - Ele processa as
entries
List
e combina os dados do feed com a marcação HTML. - Ele retorna uma string HTML que é mostrada na interface da atividade principal.
Kotlin
// Uploads XML from stackoverflow.com, parses it, and combines it with // HTML markup. Returns HTML string. @Throws(XmlPullParserException::class, IOException::class) private fun loadXmlFromNetwork(urlString: String): String { // Checks whether the user set the preference to include summary text. val pref: Boolean = PreferenceManager.getDefaultSharedPreferences(this)?.run { getBoolean("summaryPref", false) } ?: false val entries: List<Entry> = downloadUrl(urlString)?.use { stream -> // Instantiates the parser. StackOverflowXmlParser().parse(stream) } ?: emptyList() return StringBuilder().apply { append("<h3>${resources.getString(R.string.page_title)}</h3>") append("<em>${resources.getString(R.string.updated)} ") append("${formatter.format(rightNow.time)}</em>") // StackOverflowXmlParser returns a List (called "entries") of Entry objects. // Each Entry object represents a single post in the XML feed. // This section processes the entries list to combine each entry with HTML markup. // Each entry is displayed in the UI as a link that optionally includes // a text summary. entries.forEach { entry -> append("<p><a href='") append(entry.link) append("'>" + entry.title + "</a></p>") // If the user set the preference to include summary text, // adds it to the display. if (pref) { append(entry.summary) } } }.toString() } // Given a string representation of a URL, sets up a connection and gets // an input stream. @Throws(IOException::class) private fun downloadUrl(urlString: String): InputStream? { val url = URL(urlString) return (url.openConnection() as? HttpURLConnection)?.run { readTimeout = 10000 connectTimeout = 15000 requestMethod = "GET" doInput = true // Starts the query. connect() inputStream } }
Java
// Uploads XML from stackoverflow.com, parses it, and combines it with // HTML markup. Returns HTML string. private String loadXmlFromNetwork(String urlString) throws XmlPullParserException, IOException { InputStream stream = null; // Instantiates the parser. StackOverflowXmlParser stackOverflowXmlParser = new StackOverflowXmlParser(); List<Entry> entries = null; String title = null; String url = null; String summary = null; Calendar rightNow = Calendar.getInstance(); DateFormat formatter = new SimpleDateFormat("MMM dd h:mmaa"); // Checks whether the user set the preference to include summary text. SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this); boolean pref = sharedPrefs.getBoolean("summaryPref", false); StringBuilder htmlString = new StringBuilder(); htmlString.append("<h3>" + getResources().getString(R.string.page_title) + "</h3>"); htmlString.append("<em>" + getResources().getString(R.string.updated) + " " + formatter.format(rightNow.getTime()) + "</em>"); try { stream = downloadUrl(urlString); entries = stackOverflowXmlParser.parse(stream); // Makes sure that the InputStream is closed after the app is // finished using it. } finally { if (stream != null) { stream.close(); } } // StackOverflowXmlParser returns a List (called "entries") of Entry objects. // Each Entry object represents a single post in the XML feed. // This section processes the entries list to combine each entry with HTML markup. // Each entry is displayed in the UI as a link that optionally includes // a text summary. for (Entry entry : entries) { htmlString.append("<p><a href='"); htmlString.append(entry.link); htmlString.append("'>" + entry.title + "</a></p>"); // If the user set the preference to include summary text, // adds it to the display. if (pref) { htmlString.append(entry.summary); } } return htmlString.toString(); } // Given a string representation of a URL, sets up a connection and gets // an input stream. private InputStream downloadUrl(String urlString) throws IOException { URL url = new URL(urlString); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setReadTimeout(10000 /* milliseconds */); conn.setConnectTimeout(15000 /* milliseconds */); conn.setRequestMethod("GET"); conn.setDoInput(true); // Starts the query. conn.connect(); return conn.getInputStream(); }