RSSリーダーの概要
サンプルとして作成したRSSリーダーは、8つのパッケージから構成されています。それぞれは、表のような役目のクラスが入っています。
jp.co.impress.dekiru | アプリケーション自体に相当するメインクラス |
jp.co.impress.dekiru.data | 通信やデータを扱うクラス |
jp.co.impress.dekiru.data.config | アプリケーションの設定を扱うクラス |
jp.co.impress.dekiru.data.connector | 通信やデータを扱うクラス(DataAccessクラスなどから呼ばれる) |
jp.co.impress.dekiru.ui.animation | RSSを取得するときのアニメーション |
jp.co.impress.dekiru.ui.header | アプリケーション画面上部のヘッダー関連 |
jp.co.impress.dekiru.ui.screen | アプリケーションの画面関連 |
jp.co.impress.dekiru.ui.title | タイトル部分の描画関連 |
このRSSリーダーのプログラムは、フルセットの機能を持っていますが、今回は、できるネットプラスのRSSだけを読み込むようになっています。自分で、改造することで、任意のサイトを対象にしたり、機能を追加したりすることができます。そのため、実際に使える機能に比べてソースコードが大きく、サンプルプログラム内では、呼び出されないクラスもあります。
ここでは、おおまかな動きを解説します。RSSリーダーが動作するとき、図のようなクラスが実行されていきます。
図 RSSリーダーのクラス呼び出し関係
RssReaderApp LoadingScreen Setup CommunicationThread InterfaceCommunication DataAccess RssConnector StrageManager DataAccess AnimatedGIFFField ItemScreen FeedData TreeScreen TitleBar HeaderBar Parameters UpdateScrren UrlDataManager BookmarkScreen ChannelScreen DescriptionScreen
RSSリーダーのおおまかな動作
ここでは、ソースコードを見ながら、RSSリーダーのおおまかな動きを解説します。前述したようにプログラム自体は比較的大きなものなので、起動からRSSリーダーのメイン画面が表示されるまでとします。ソースコードは、サンプルから抜粋したものですが、見やすさなどを考慮して一部省略などしています。
RssReaderAppクラス
RSSリーダーで最初に動き出すのがこのクラスです。このクラスは、BlackBerryのSDKで定義されているUiApplicationというクラスを継承しています。これは、画面を持つBlackBerryのアプリケーションです。メイン関数では、RssReaderAppクラスのインスタンスを生成し、その後、イベントのディスパッチループに入ります。
RssReaderAppのコンストラクタでは、LoadingScreenクラスを生成し、これを最初の自分自身の画面とするためにpushScreenメソッドを実行しています。LoadingScreenクラスは、impress.co.jp.dekiru.ui.screenパッケージにあります。
public class RssReaderApp extends UiApplication {
public RssReaderApp(){
this.pushScreen(new LoadingScreen());
}
public static void main(String args[]){
RssReaderApp bbApp = new RssReaderApp();
bbApp.enterEventDispatcher();
}
}
LoadingScreenクラス
このクラスでは、setupなどの実行に必要なクラスなどを初期化したあと、画面を作ります。 LoadingScreenクラスのコンストラクタでは、画面の配置を行うMainManagerを生成し、そこにスペースを空けるための空白行(makeSpaceField()で作成)、タイトル(makeTitleField())、空白行、ロゴ(makeLogoField())を追加しています。
最後にupdateDialogを実行して、RSS情報のダウンロードを行い、メイン画面を表示します。
画面の実際の描画などは、このupdateDialogの中でオブジェクトを作ってキューに登録し、別途メインのイベントループの中で行います。このメソッドは大きいのですが、大半は、作成するオブジェクトの定義です。必要なオブジェクトをキューに入れたら、updateDialogからコンストラクタへ戻ってきます。その後、メイン関数のイベント処理ループに制御が移ります。
public LoadingScreen(){
makeMainManager();
makeSpeceField();
makeTitleField();
makeSpeceField();
makeLogoField();
add(mainManager);
updateDialog(getFeedUrls(), "最新の情報を取得しますか?", UPDATE_DIALOG_INIT);
}
makeMainManager
mainManagerは、BlackBerryのライブラリにあるVerticalFieldManagerクラスのインスタンスです。これは、追加されたフィールドを縦に並べていくクラスです。また、ここで背景部分(デフォルトの色Setup.LOADINGBGCOLOR)を指定しています。
タイトルを表示するフィールドは、makeTitleFieldで作成しています。ここでは、ラベルフィールドに文字列やスタイル、フォントなどを指定してmainMangerに追加しています。他のmake〜もだいたい同じような手順です。
private void makeMainManager(){
mainManager = new VerticalFieldManager();
Background background = BackgroundFactory.createSolidBackground(Setup.LOADING_BG_COLOR);
mainManager.setBackground(background);
}
private void makeTitleField(){
LabelField titleField = new LabelField("\nRSS Reader Ver "+ Setup.VERSION,
DrawStyle.HCENTER |
DrawStyle.BOTTOM |
LabelField.FIELD_HCENTER |
LabelField.USE_ALL_WIDTH |
LabelField.NON_FOCUSABLE);
titleField.setFont(VER_FONT);
mainManager.add(titleField);
}
getFeedUrls
このupdateDialogの引数の1つは、メソッドgetFeedUrlsの戻り値です。これは、フィードに対応したURLのリストをSetupクラスの配列からコピーしてきます。
private Vector getFeedUrls(){
int feed_count = Setup.getInstance().FeedUrlKeyHash.size();
Vector urls = new Vector();
for(int i=0; i<feed_count; i++){
urls.addElement((String)( Setup.PERSISTENT_ARRAY[i][0] ));
}
return urls;
}
updateDialog
updateDialogメソッドですが、大きく以下のような構造になっています。この中で、このあと必要な処理が記述されており、これをオブジェクトとしてシステムのイベント処理キューに入れて、updateDialogが終了したあとに起動します。具体的にそれを行っているのがinvokeLaterです。invokeLaterは、引数で指定されたオブジェクトをキューに入れ、メイン関数にあるメインループでは、これを取り出したら、オブジェクトのrunメソッドを実行します。この場合には、run()の後ろ部分が実行されることになります。
private void updateDialog(final Vector urls, final String message, final int order){
:
:
呼び出し後に実行される部分
:
:
UiApplication.getUiApplication().invokeLater(new Runnable() {
public void run() {
この部分があとから実行される
}
});
}
あとから実行されるオブジェクトの動作
最初に行うのは、メッセージボックスで「最新の情報を取得しますか?」を表示させて、ユーザーに、RSSの更新を行うかどうかを確認することです。
int select = Dialog.ask(Dialog.D_YES_NO, message, 0);
//ダイアログを表示してユーザーに「はい」、「いいえ」を選択してもらう。
if( select == Dialog.YES ) {
//ダイアログの戻りがYESなら
if (order == UPDATE_DIALOG_INIT){
//フィード交信中の画面を作成(初回のみ)
makeSpeceField();
makeAnimaField();
makeSpeceField();
makeMsgField("RSSを更新中...(0/"+feedCount+")");
} else {
//2回目以降のフィード更新の場合、メッセージフィールドだけ変更
// ステータス表示フィールドを更新する。
msgField.setText("RSSを更新中...(0/"+feedCount+")");
}
通信スレッドを作る
次にスレッドを作り、これを使ってフィードを取得します。ただし、通信なのでエラーが発生する可能性があり、エラーが発生したURLを記録します。スレッドを作成したあと、start()でこれを起動します。
Thread bootThread = new Thread(new Runnable(){
public void run(){
通信スレッドの準備
フィードの取得
エラーになったURLの記録
});
bootThread.start();
ここは、ユーザーが「最新の情報を取得しますか?」に対して「いいえ」と答え、かつ、それがリトライのあとだった場合。つまり、これ以上やってもエラーとなるだけで、フィードが取得できない可能が高いため、プログラムを終了させます。
} else if (select == Dialog.NO && order == UPDATE_DIALOG_RETRY){
// ダイアログがあった場合閉じる
popDialogs();
Dialog.alert("アプリケーションを終了します。");
// アプリを終了
getScreen().close();
ここのelseは、「最新の情報を取得しますか?」に対してNOですが、リトライはしていない状態、つまり、ユーザーが最新の情報の取得を希望せず、NOのみを押したり、起動時に更新のチェックを行わない場合などに相当します。 画面の作成は、別のクラス(ChannelScreen、ItemScreen)で行っています。RSSは、チャンネルを複数含むことができ、その中にアイテムが複数あります。それぞれに画面が用意されているのですが、Setupクラスが持つ値でどちらを呼ぶかを決めています。できるネットプラスの場合、チャンネルは1つだけなので、ItemScreenだけが呼ばれるようになっています。
}else{
if (order == UPDATE_DIALOG_INIT){
makeSpeceField();
makeAnimaField();
makeSpeceField();
makeMsgField("RSSを読み込んでいます...");
}
Thread loadThread = new Thread(new Runnable(){
public void run(){
パーシステントストアからデータを持ってきて追加
フィードを表示する画面を作成
作成した画面から戻ってきたら画面を閉じてプログラムを終了
エラーであれば、プログラムを終了させる
}
});
loadThread.start();
//最後のスレッドを実行
}
通信スレッド
通信は、urlごとにスレッドを作って並行して行います。通信は、相手の状態や通信状態で実行時間が違うため、このように並列化することで処理時間を短くすることができます。また、場合によっては、通信ができないこともあり、順番に行うと非常に長い時間がかかってしまうこともあります。
複数のスレッドを作るため、まず、スレッドを入れておく配列を作ります。なお、URLの数feedCountは、dialogUpdateの引数として渡されるurlsベクターの要素の数と同じで、updateDialogの先頭でこれがfeedCountとしてセットされています。
CommunicationThread[] thread = new CommunicationThread[feedCount];
// 通信用スレッドをURLの数分用意します。
String strUrl;
String category;
final Vector errorUrls = new Vector();
作成した通信スレッドそれぞれに対して処理を行います。中心になるのは、try{...} catch () {...}で囲まれた部分です。あらかじめ作成しておいた配列threadに通信スレッドを生成して格納し、スレッドを起動(start)させ、メイン側では、スレッドの終了を待ち(join)ます。スレッドが終了したとき、ステータスを見て、エラーならば、そのURLをerrorUrlsに保存しておきます。
for(int i=0; i<feedCount; i++){
if (!stopCommunication){
// フィードURL取得
strUrl = (String)urls.elementAt(i);
// カテゴリ取得
category = DataAccess.getInstance().getSetupCategoryByURL(strUrl);
try {
// Feedを取得する。
thread[i] = new CommunicationThread(selfScreen, category, strUrl);
thread[i].start();
thread[i].join(); // スレッドの処理が終了するまで待ちます。
// 取得できなかったURLを保持する。
if (!thread[i].getStatus()){
errorUrls.addElement(strUrl);
}
} catch (Exception e){
e.printStackTrace();
}
}
}
エラーがなかったら、チャンネルまたはアイテムの画面を表示します。このプログラムでは、Setup.ROOTINFOにROOTID_ITEMが設定されているので、常にアイテム画面のみになります。
if (errorUrls.size() == 0){
// チャンネル又はアイテムの画面を表示する
UiApplication.getUiApplication().invokeLater(new Runnable(){
public void run(){
// ダイアログがあった場合閉じる
popDialogs();
// 未読・既読のクリーンアップを行う
Vector vector = DataAccess.getInstance().getAllItemLinkVector();
UrlDataManager.getInstance(UrlDataManager.TYPE_READ).cleanupUrlData(vector);
if(Setup.ROOT_INFO == Setup.ROOT_ID_CHANNEL) {
UiApplication.getUiApplication().pushScreen(new ChannelScreen());
}else{
UiApplication.getUiApplication().pushScreen(new ItemScreen());
}
getScreen().close();
}
});
ItemScreenクラス
ここが、RSSリーダーのメイン画面となります。また、このクラスに、メニューやトラックボールをクリックしたときの処理が記述されています。
ItemScreenクラスのコンストラクタは複数ありますが、LoadingScreenからは引数なしで呼ばれているので、引数なしのコンストラクタが使われます。Setup.ROOTIDITEMの値を引き継いだら、DataAccessクラスを生成します。DataAccessクラスのインスタンスは1つだけしか存在しないシングルトンオブジェクトなので、インスタンスを得るには、getInstanceが使われます。このDataAccessを使い、カテゴリ名と接続先のペアになったリストをvectorへとコピーします。
RSSを実際に表示するのはTreeScreenですが、これにFeedDataを渡して表示を行います。
public ItemScreen(){
super(Setup.ROOT_ID_ITEM);
DataAccess access = DataAccess.getInstance();
Vector vector = access.getFeedDataVector(Setup.CONFIG_ARRAY[0][0]);
Enumeration e = vector.elements();
while(e.hasMoreElements()){
feed = (FeedData) e.nextElement();
}
setStackTree(feed.getFeedTitle(), feed);
flush();
setCurrentFocus();
}
メニューの作成
メニューは、makeMenuメソッドで作成します。ItemScreenは、TreeScreenを継承しており、TreeScreenは、BlackBerryのSDKで定義するMainScreenを継承しています。makeMenuは、MainScreenの同名のメソッドをオーバーライドしていて、画面の表示が行われるときに自動的に呼び出されます。
protected void makeMenu(Menu menu, int instance) {
if(menu.getSize() > 0){
menu.deleteItem(0);
}
menu.add(menu1);
menu.add(menu2);
if(Setup.ROOT_INFO == Setup.ROOT_ID_CHANNEL){
menu.deleteItem(1);
}
menu.addSeparator();
menu.add(menu3);
menu.add(menu4);
super.makeMenu(menu, instance);
}
トラックボールのクリックイベントの処理
フィードを選択してトラックボールをクリックすれば、該当のページがブラウザで開きます。このイベントの処理もItemScreen内で行っています。これも、上位クラス(BlackBerrySDKのScreenクラス)で定義された名前を付けておくことで、イベントが発生したときに自動的に呼び出されます。
アプリケーション内からブラウザを起動するには、Browser.getDefaultSession()で、ブラウザセッションオブジェクトを得て、displayPageメソッドの引数にURLを指定して実行させます。
protected boolean navigationClick(int status, int time){
if(isTreeFocus()){
if(isRootCheck()){
return true;
}else{
if(Setup.USE_DESCRIPTION){
:
:
詳細画面を表示するとき(サンプルでは未使用)
:
:
}else{
int select = Dialog.ask(Dialog.D_YES_NO, "ブラウザを起動しますか?", 0);
if( select == Dialog.YES ){
FeedItem item = (FeedItem) getCurrentObject();
BrowserSession bs = Browser.getDefaultSession();
bs.displayPage(item.getLink());
UrlDataManager udm = UrlDataManager.getInstance(UrlDataManager.TYPE_READ);
try{udm.addUrlData(item);} catch(IllegalArgumentException iae){}
}
}
}
}else{
int select = Dialog.ask(Dialog.D_YES_NO, "ブラウザを起動しますか?", 0);
if( select == Dialog.YES ){
BrowserSession bs = Browser.getDefaultSession();
bs.displayPage(Setup.BANNER_URL);
}
}
return super.navigationClick(status, time);
}
次回は、アプリケーションの署名と実機へのインストールについて解説します。