Nasza aplikacja do czytania wiadomości po RSS nie prezentuje się póki co zbyt dobrze - programowi bowiem brakuje tego, co w nim najważniejsze, czyli pobierania wiadomości i ich wyświetlania. Dziś szybko się tym zajmiemy, wykorzystując ostatnio poznane techniki dostępu do sieci. Kilka rzeczy zrobimy do przodu, przed głównym poradnikiem, ale za chwilkę i to nadrobimy.
Informacja: Nie radzisz sobie z kodem? Coś Ci nie wychodzi lub nie działa? Zgubiłeś się? W tym miejscu zobaczysz pełen kod aplikacji.
1. Budowa kanału RSS
Zanim zajmiemy się pisaniem kodu do czytania wybranego kanału, najpierw
będziemy musieli poznać budowę podstawowej wiadomości w tym formacie (XML). Nie jest ona trudna ani skomplikowana, bowiem w każdym źródle RSS wygląda tak samo, a całość ogranicza się do kilku tagów. Poszczególne kanały różnią się tylko jedną rzeczą - ilością dostępnej treści. My wykorzystamy (póki co) jedynie te podstawowe.
Naszym przykładowym źródłem będzie oczywiście RSS mójdroida. Znajduje się on
w tym miejscu, a jego kod z informacjami wygląda w części tak:

Możemy zauważyć prostą zależność -
każda wiadomość znajduje się w tagu o nazwie "item", ten tag z kolei posiada kilka kolejnych jak
tytuł ("title"), źródło ("link") czy opis ("description"). Wykorzystamy to, nazywając kolejne funkcje w naszych klasach podobnymi nazwami. Przez to każde dołączanie treści odbędzie się automatycznie (o tej sztuczce za chwilkę opowiem).
2. Przygotowanie nowych klas
Naszą aplikację rozbudujemy o
4 nowe klasy. Pierwsza będzie naszym własnym obiektem, który będzie posiadał odpowiednie pola i metody do przypisywania (
RSSItem), druga będzie służyć do automatycznego wypełniania pól (
RSSParser), trzecia do wywoływania czytnika kodu (
RSSReader) oraz czwarta, która będzie osobnym wątkiem w programie (AsyncTask,
RSSDownloader).
Od razu opiszę tutaj dosyć złożony proces czytania danych z pliku XML. Możecie tego nie zrozumieć, ale znowu z drugiej strony nic trudnego w tym nie ma i jeżeli przysiądziecie nad tym chwilę, to zrozumiecie zasadę działania. W aplikacji wykorzystam znany już w branży
SAXParser, który również dołączony jest i do androida (będzie nam łatwiej, bo to on zajmie się analizą składni i tagów a nie my). Jako podstawa służyła mi
biblioteka autorstwa matshofmana (zamiast moich klas możecie ją wykorzystać, ale wtedy za działanie odpowiadacie sami).
RSSItem, jak pisałem, ma posiadać pola wykorzystywane w pojedynczej wiadomości. Stworzyłem więc te podstawowe na początku (jako Stringi), po czym do każdej ze zmiennych stworzyłem metody do zapisu i odczytu (nie musicie ich tworzyć [bo są nieco wolniejsze niż operacja na samych zmiennych], jednak łatwiej nam będzie na nich operować w przyszłości). Całość dodatkowo rozszerza element
Parcelable z Androida, który w skrócie odpowiada za szybkie pakowanie danych i przesyłanie ich dalej wewnątrz aplikacji. Zaletę tego rozwiązania dostrzeżecie w kolejnych częściach poradnika, gdzie operować będziemy na jednej tablicy z danymi.
package pl.damianpiwowarski.parser;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
public class RSSItem implements Parcelable {
private String title;
private String link;
private String description;
private String content;
private String pubDate;
public RSSItem() {
// Parcel będzie potrzebny tylko Androidowi, nie bezpośrednio nam
}
public RSSItem(Parcel parcel) {
final Bundle data = parcel.readBundle();
title = data.getString("title");
link = data.getString("link");
description = data.getString("description");
content = data.getString("content");
pubDate = data.getString("pubDate");
}
@Override
public void writeToParcel(Parcel dest, int flags) {
final Bundle data = new Bundle();
data.putString("title", title);
data.putString("link", link);
data.putString("description", description);
data.putString("content", content);
data.putSerializable("pubDate", pubDate);
dest.writeBundle(data);
}
public static final Parcelable.Creator<RSSItem> CREATOR = new Parcelable.Creator<RSSItem>() {
public RSSItem createFromParcel(Parcel data) {
return new RSSItem(data);
}
public RSSItem[] newArray(int size) {
return new RSSItem[size];
}
};
@Override
public int describeContents() {
return 0;
}
// "SETTERS AND GETTERS"
// GETTERS
public String getTitle() {
return title;
}
public String getLink() {
return link;
}
public String getDescription() {
return description;
}
public String getContent() {
return content;
}
public String getPubDate() {
return pubDate;
}
// I SETTERS
public void setTitle(String title) {
this.title = title;
}
public void setLink(String link) {
this.link = link;
}
public void setDescription(String description) {
this.description = description;
}
public void setContent(String content) {
this.content = content;
}
public void setPubDate(String pubDate) {
this.pubDate = pubDate; // To będziemy musieli poprawić, bo data zwracana jest w takiej postaci jaka została wysłana
}
}
RSSParser natomiast czyta kolejne linijki kanału i dane, które pozna, przypisuje w odpowiednie miejsca. W kolejnych krokach tworzone są pojedyncze elementy RSSItem, po czym po pełnym odczytaniu gromadzone są w tablicy rssItems. Te następnie przekazujemy dalej do aplikacji.
package pl.damianpiwowarski.parser;
import java.lang.reflect.Method;
import java.util.ArrayList;
import org.xml.sax.Attributes;
import org.xml.sax.helpers.DefaultHandler;
public class RSSParser extends DefaultHandler {
private ArrayList<RSSItem> rssItems;
private RSSItem parsingItem; // Aktualnie "przerabiana" wiadomość
private StringBuilder stringBuilder; // Tutaj tworzone są tytuły, daty i tak dalej
public RSSParser() {
rssItems = new ArrayList<RSSItem>(); // Tworzymy tablicę składającą się z elementów RSSItem
// Tutaj będzie znajdować się więcej niż jeden element
}
@Override
public void characters(char[] ch, int start, int length) {
stringBuilder.append(ch, start, length); // To o czym mówiłem wyżej, odczytane znaki są łączone w całość
}
// Końcowa faza zapisu elementów
@Override
public void endElement(String uri, String localName, String qName) {
if (parsingItem != null) {
try {
// To musimy zmienić dla dobra naszych funkcji
if (qName.equals("content:encoded")) {
qName = "content";
}
// Nasza sztuczka, która zrobi za nas przypisywanie danych :>
// Nazwa funkcji-settera w RSSItem, np setTitle
String functionName = "set"
+ qName.substring(0, 1).toUpperCase() // Pierwszy znak musi być wielki
+ qName.substring(1);
// Szukamy metody
Method function = RSSItem.class.getMethod(functionName,
String.class);
// I zaczynamy działanie na niej!
function.invoke(parsingItem, stringBuilder.toString());
} catch (Exception e) {
e.printStackTrace();
// Obsługa błędu :(
}
}
}
// Tworzymy kolejny element RSSItem
@Override
public void startElement(String uri, String localName, String qName,
Attributes attributes) {
stringBuilder = new StringBuilder();
if (qName.equals("item") && rssItems != null) {
parsingItem = new RSSItem();
rssItems.add(parsingItem); // Reszta danych za chwilę sama się dopisze
}
}
public ArrayList<RSSItem> getResult(){
return rssItems; // Zwracamy elementy po fakcie
}
}
RSSReader jest prosto zbudowany, ale odpowiada za ważne czynności w programie - to on dostaje się do elementów takich jak paser XML czy RSSParser. To on również odpowiada za pobranie czystych danych z sieci i przekazanie ich do naszej klasy. W razie błędów kończy działanie całości.
package pl.damianpiwowarski.parser;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
public class RSSReader {
public static ArrayList<RSSItem> startReader(URL url) throws SAXException, IOException {
try {
// Musimy dostać się do dwóch rzeczy
// # Parsera XML i kodu kanału
// # Naszego czytnika, który odpowiednio umieści dane w RSSItem
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser parser = factory.newSAXParser();
XMLReader reader = parser.getXMLReader(); // Mamy nasz parser XML!
RSSParser rssParser = new RSSParser(); // A tutaj parser dla RSSItem
InputSource inputSource = new InputSource(url.openStream());
reader.setContentHandler(rssParser);
reader.parse(inputSource);
// Zwracamy dane
return rssParser.getResult();
} catch (Exception e) {
// Pokazujemy błąd w LogCacie
e.printStackTrace();
throw new SAXException();
}
}
}
Natomiast
RSSDownloader to klasa, która wszystko to łączy z samym wyświetlaniem.
package pl.damianpiwowarski.parser;
import java.net.URL;
import java.util.ArrayList;
import android.os.AsyncTask;
public class RSSDownloader extends AsyncTask<String, Void, ArrayList<RSSItem>> {
// Prosty AsyncTask, który czyta nam dane
@Override
protected ArrayList<RSSItem> doInBackground(String... params) {
try {
URL url = new URL(params[0]);
ArrayList<RSSItem> rssItems = RSSReader.startReader(url);
return rssItems;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
Na samym końcu należy do naszego AndroidManifestu dodać odpowiednie uprawnienie, które pozwoli nam korzystać z połączenia z internetem...
<uses-permission android:name="android.permission.INTERNET" />
... oraz zaktualizować sam adapter listy i główne Activity do przyjęcia nowych danych:
AdapterListViewMain:
package pl.damianpiwowarski.parser;
import java.util.ArrayList;
import android.content.Context;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;
public class AdapterListViewMain extends BaseAdapter {
// Podstawowe zmienne do wykorzystania
private ArrayList<RSSItem> data;
private Context listContext;
private LayoutInflater layoutInflater;
public AdapterListViewMain(Context context, ArrayList<RSSItem> data) {
this.listContext = context;
this.data = data;
layoutInflater = LayoutInflater.from(listContext);
}
// Ta funkcja liczy nam ile elementów ma pojawić się na liście
// Na testy dla tego przykładu damy ich 20
public int getCount() {
if (data != null) {
return data.size();
} else {
return 0;
}
}
// Pobranie danych dla jednego elementu
// To zostawiamy puste
public Object getItem(int position) {
return position;
}
// Jak wyżej, tylko tutaj występuje identyfikator
// To zostawiamy puste
public long getItemId(int position) {
return 0;
}
// Holder do cachowania elementów
// Poprawia znacząco płynność
private class CustomHolder {
TextView tvTitle;
TextView tvSite;
ImageView ivImage;
}
// Pojedynczy element na liście
public View getView(int position, View convertView, ViewGroup parent) {
CustomHolder viewCache;
RSSItem actualItem = data.get(position);
// ConvertView - czy da się wykorzystać ponownie ostatnio usunięty
// element na liście?
if (convertView == null) {
// Jest pusty, więc definiujemy podstawę
convertView = layoutInflater.inflate(
R.layout.item_view_main_listview, null);
viewCache = new CustomHolder();
// Cachujemy kolejne elementy
viewCache.tvTitle = (TextView) convertView
.findViewById(R.id.textView_title_item_view_main_listview);
viewCache.tvSite = (TextView) convertView
.findViewById(R.id.textView_site_item_view_main_listview);
viewCache.ivImage = (ImageView) convertView
.findViewById(R.id.imageView_photo_item_view_main_listview);
convertView.setTag(viewCache);
} else {
viewCache = (CustomHolder) convertView.getTag();
}
// Wypełnienie!
viewCache.tvTitle.setText(actualItem.getTitle());
return convertView;
}
}
ViewMain:
package pl.damianpiwowarski.parser;
import java.util.ArrayList;
import android.os.Bundle;
import android.app.Activity;
import android.view.Menu;
import android.widget.ListView;
import android.widget.Toast;
public class ViewMain extends Activity {
// Podstawowe wykorzystywane elementy
private ArrayList<RSSItem> data;
private ListView listViewParser;
private AdapterListViewMain adapterListViewMain;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.view_main);
// Nasza tablica z danymi
data = new ArrayList<RSSItem>();
// Przypisujemy każdy element View do zmiennej
listViewParser = (ListView) findViewById(R.id.listView_view_main_parser);
// Tutaj łaczymy dane z ich wyświetlaniem
adapterListViewMain = new AdapterListViewMain(this, data);
listViewParser.setAdapter(adapterListViewMain);
// I testujemy nasze pobieranie
new RSSDownloader() {
// Najprościej będzie wykorzystać jedną z funkcji AsyncTaska
// onPostExecute wykonuje się po wykonaniu wszystkich czynności
@Override
protected void onPostExecute(ArrayList<RSSItem> result) {
// Dodajemy do naszej tablicy nowe dane
data.addAll(result);
adapterListViewMain.notifyDataSetChanged(); // Powiadamiamy o zmianach w "łączniku"
// Wyświetlamy komunikat o liczbie pobranych wiadomości
Toast.makeText(ViewMain.this,
"Pobrano " + result.size() + " wiadomości",
Toast.LENGTH_SHORT).show();
}
}.execute("http://www.mojdroid.pl/feed/"); // Adres kanału RSS, to jeszcze zmienimy w przyszłości
}
// Menu na ActionBarze
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.view_main, menu);
return true;
}
}
Może wydawać się, że całość opisałem nieco za szybko, ale w tym wypadku
analiza należy do was. Sam kod starałem się odpowiednio komentować w locie, ale nieoceniona będzie również pomoc dokumentacji Androida i wyszukiwarki Google. Póki co z kodem zostawiam was samych, w razie problemów - możecie pisać do mnie w komentarzach...