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

次回は、アプリケーションの署名と実機へのインストールについて解説します。