Phân tích cú pháp dữ liệu XML

Ngôn ngữ đánh dấu mở rộng (XML) là một bộ quy tắc để mã hoá các tài liệu ở dạng mà máy có thể đọc được. XML là một định dạng phổ biến để chia sẻ dữ liệu trên Internet.

Các trang web thường xuyên cập nhật nội dung, chẳng hạn như trang web tin tức hoặc blog, thường cung cấp một nguồn cấp dữ liệu XML để các chương trình bên ngoài có thể nắm bắt sự thay đổi về nội dung. Tải lên và phân tích cú pháp dữ liệu XML là một tác vụ phổ biến cho các ứng dụng kết nối mạng. Chủ đề này giải thích cách phân tích cú pháp tài liệu XML và sử dụng dữ liệu trong tài liệu đó.

Để tìm hiểu thêm về cách tạo nội dung dựa trên nền tảng web trong ứng dụng Android, hãy xem bài viết Nội dung dựa trên nền tảng web.

Chọn một trình phân tích cú pháp

Bạn nên sử dụng XmlPullParser. Đây là một cách hiệu quả và dễ bảo trì để phân tích cú pháp XML trên Android. Android có 2 cách triển khai giao diện này:

Cả hai lựa chọn đều được. Ví dụ trong phần này sẽ sử dụng ExpatPullParserXml.newPullParser().

Phân tích nguồn cấp dữ liệu

Bước đầu tiên trong việc phân tích cú pháp nguồn cấp dữ liệu là quyết định những trường mà bạn quan tâm. Trình phân tích cú pháp sẽ trích xuất dữ liệu cho những trường đó và bỏ qua các trường còn lại.

Hãy xem phần trích dẫn sau đây từ một nguồn cấp dữ liệu được phân tích cú pháp trong ứng dụng mẫu. Mỗi lệnh post đến StackOverflow.com đều xuất hiện trong nguồn cấp dữ liệu ở dạng thẻ entry chứa nhiều thẻ lồng nhau:

<?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>

Ứng dụng mẫu sẽ trích xuất dữ liệu cho thẻ entry và các thẻ lồng nhau title, linksummary.

Tạo thực thể cho trình phân tích cú pháp

Bước tiếp theo trong quá trình phân tích cú pháp nguồn cấp dữ liệu là tạo thực thể cho trình phân tích cú pháp và bắt đầu quá trình phân tích. Đoạn mã này khởi động một trình phân tích cú pháp để không xử lý không gian tên, và để dùng InputStream đã cung cấp làm dữ liệu đầu vào. Trình phân tích cú pháp này bắt đầu quá trình phân tích cú pháp bằng lệnh gọi đến nextTag() và gọi phương thức readFeed(). Phương thức này sẽ trích xuất và xử lý dữ liệu mà ứng dụng mong muốn:

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();
        }
    }
 ...
}

Đọc nguồn cấp dữ liệu

Phương thức readFeed() thực hiện công việc xử lý nguồn cấp dữ liệu thực tế. Phương thức này tìm các phần tử được gắn thẻ "entry" làm điểm xuất phát để xử lý đệ quy nguồn cấp dữ liệu. Nếu không phải là thẻ entry thì thẻ đó sẽ bị bỏ qua. Sau khi toàn bộ nguồn cấp dữ liệu được xử lý đệ quy, readFeed() sẽ trả về List chứa các mục nhập (bao gồm cả thành phần dữ liệu lồng nhau) được trích xuất từ nguồn cấp dữ liệu đó. Sau đó, trình phân tích cú pháp sẽ trả về List này.

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;
}

Phân tích cú pháp XML

Các bước phân tích cú pháp nguồn cấp dữ liệu XML như sau:

  1. Như mô tả trong phần Phân tích nguồn cấp dữ liệu, hãy xác định các thẻ mà bạn muốn đưa vào ứng dụng của mình. Ví dụ này sẽ trích xuất dữ liệu của thẻ entry cũng như các thẻ lồng nhau: title, linksummary.
  2. Tạo các phương thức sau:

    • Phương thức "read" cho từng thẻ mà bạn muốn thêm vào, chẳng hạn như readEntry()readTitle(). Trình phân tích cú pháp sẽ đọc các thẻ từ luồng đầu vào. Khi gặp một thẻ có tên entry, title, link hoặc summary như trong ví dụ này, trình phân tích cú pháp sẽ gọi phương thức thích hợp cho thẻ đó. Nếu không, thẻ đó sẽ bị bỏ qua.
    • Các phương thức trích xuất dữ liệu cho từng loại thẻ và chuyển trình phân tích cú pháp sang thẻ tiếp theo. Trong ví dụ này, các phương thức có liên quan như sau:
      • Đối với thẻ titlesummary, trình phân tích cú pháp sẽ gọi readText(). Phương thức này trích xuất dữ liệu cho các thẻ này bằng cách gọi parser.getText().
      • Đối với thẻ link, trình phân tích cú pháp sẽ trích xuất dữ liệu cho các đường liên kết bằng cách xác định trước xem đường liên kết có phải là loại mong muốn hay không. Sau đó, trình phân tích cú pháp sẽ dùng parser.getAttributeValue() để trích xuất giá trị của đường liên kết.
      • Đối với thẻ entry, trình phân tích cú pháp sẽ gọi readEntry(). Phương thức này phân tích cú pháp các thẻ lồng nhau của mục nhập và trả về một đối tượng Entry chứa các thành phần dữ liệu title, linksummary.
    • Một phương thức skip() của trình trợ giúp mang tính đệ quy. Để xem thêm nội dung thảo luận về chủ đề này, hãy tham khảo phần Bỏ qua các thẻ mà bạn không quan tâm.

Đoạn mã này cho thấy cách trình phân tích cú pháp phân tích cú pháp các mục nhập, tiêu đề, đường liên kết và thông tin tóm tắt.

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;
}
  ...
}

Bỏ qua các thẻ mà bạn không quan tâm

Trình phân tích cú pháp cần bỏ qua các thẻ không mong muốn. Sau đây là phương thức skip() của trình phân tích cú pháp:

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;
        }
    }
 }

Dưới đây là cách hoạt động:

  • Trình phân tích cú pháp trả về ngoại lệ nếu sự kiện hiện tại không phải là START_TAG.
  • Trình phân tích cú pháp sử dụng START_TAG và mọi sự kiện cho đến END_TAG trùng khớp.
  • Trình phân tích cú pháp theo dõi độ sâu lồng ghép để đảm bảo sẽ dừng ở đúng END_TAG, chứ không phải ở thẻ đầu tiên gặp phải sau START_TAG gốc.

Do đó, nếu phần tử hiện tại có các phần tử lồng nhau, thì giá trị của depth sẽ không bằng 0 cho đến khi trình phân tích cú pháp sử dụng mọi sự kiện trong khoảng từ START_TAG gốc đến END_TAG trùng khớp. Ví dụ: hãy xem xét cách trình phân tích cú pháp bỏ qua phần tử <author>, phần tử này có 2 phần tử lồng nhau là <name><uri>:

  • Lần đầu tiên thông qua vòng lặp while, thẻ tiếp theo mà trình phân tích cú pháp gặp phải sau <author>START_TAG cho <name>. Giá trị của depth tăng lên 2.
  • Lần thứ hai thông qua vòng lặp while, thẻ tiếp theo mà trình phân tích cú pháp gặp phải là END_TAG </name>. Giá trị của depth giảm xuống 1.
  • Lần thứ ba thông qua vòng lặp while, thẻ tiếp theo mà trình phân tích cú pháp gặp phải là START_TAG <uri>. Giá trị của depth tăng lên 2.
  • Lần thứ tư thông qua vòng lặp while, thẻ tiếp theo mà trình phân tích cú pháp gặp phải là END_TAG </uri>. Giá trị của depth giảm xuống 1.
  • Lần thứ năm và lần cuối cùng thông qua vòng lặp while, thẻ tiếp theo mà trình phân tích cú pháp gặp phải là END_TAG </author>. Giá trị của depth giảm xuống 0, cho biết rằng phần tử <author> đã được bỏ qua thành công.

Sử dụng dữ liệu XML

Ứng dụng mẫu tìm nạp và phân tích cú pháp nguồn cấp dữ liệu XML một cách không đồng bộ. Ứng dụng này sẽ đưa quá trình xử lý ra khỏi luồng giao diện người dùng chính. Khi quá trình xử lý hoàn tất, ứng dụng sẽ cập nhật giao diện người dùng trong hoạt động chính là NetworkActivity.

Trong phần trích dẫn sau đây, phương thức loadPage() thực hiện những việc sau:

  • Khởi động một biến chuỗi với URL của nguồn cấp dữ liệu XML.
  • Gọi phương thức downloadXml(url) nếu chế độ cài đặt của người dùng và kết nối mạng cho phép. Phương thức này tải xuống và phân tích cú pháp nguồn cấp dữ liệu, cũng như trả về kết quả dạng chuỗi sẽ hiển thị trong giao diện người dùng.

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.
        }
    }

Phương thức downloadXml gọi các phương thức sau đây trong Kotlin:

  • lifecycleScope.launch(Dispatchers.IO), sử dụng coroutine của Kotlin để chạy phương thức loadXmlFromNetwork() trên luồng IO. Phương thức này sẽ truyền URL nguồn cấp dữ liệu ở dạng tham số. Phương thức loadXmlFromNetwork() tìm nạp và xử lý nguồn cấp dữ liệu. Khi hoàn tất, phương thức này sẽ trả về một chuỗi kết quả.
  • withContext(Dispatchers.Main) sử dụng coroutine của Kotlin để trở về luồng chính, lấy chuỗi được trả về và hiển thị chuỗi trong giao diện người dùng.

Trong ngôn ngữ lập trình Java, quá trình này diễn ra như sau:

  • Executor thực thi phương thức loadXmlFromNetwork() trên một luồng trong nền. Phương thức này sẽ truyền URL nguồn cấp dữ liệu ở dạng tham số. Phương thức loadXmlFromNetwork() tìm nạp và xử lý nguồn cấp dữ liệu. Khi hoàn tất, phương thức này sẽ trả về một chuỗi kết quả.
  • Handler gọi post để trở về luồng chính, lấy chuỗi được trả về và hiển thị nó trong giao diện người dùng.

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);
        });
    });
}

Phương thức loadXmlFromNetwork() được gọi từ downloadXml sẽ hiển thị trong đoạn mã tiếp theo. Phương thức này thực hiện những việc sau:

  1. Tạo thực thể cho StackOverflowXmlParser. Thao tác này cũng tạo biến cho List của đối tượng Entry (entries) cũng như cho title, urlsummary để lưu giữ các giá trị được trích xuất từ nguồn cấp dữ liệu XML cho những trường đó.
  2. Gọi downloadUrl() để tìm nạp nguồn cấp dữ liệu và trả về ở dạng InputStream.
  3. Sử dụng StackOverflowXmlParser để phân tích cú pháp InputStream. StackOverflowXmlParser điền sẵn dữ liệu từ nguồn cấp dữ liệu vào List entries.
  4. Xử lý entries List và kết hợp dữ liệu của nguồn cấp dữ liệu với phần đánh dấu HTML.
  5. Trả về một chuỗi HTML hiển thị trong giao diện người dùng hoạt động chính.

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();
}