MójDroid.pl

Tworzymy czytnik kanałów RSS – Pobieramy dane z przykładowego adresu

2012-10-21
|
Damian P.

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