他人のブログを読んだり、勉強会等のLTを聴いて感じること

こんにちは。

他人のブログを読んでいたり、勉強会や某ABCに参加してLTとか聴いていて感じたことを書きます。

1. お前のブログはオリジナリティがあるのか?
既知の内容なのに、そういった内容をブログに書いている人をよく見かけます。参考サイトのリンクを張っていればまだ許せますが、そういったものがないブログもあります。また、本に載っていることを書いているブログも見かけます。例えば、最近見かけたのでは、iOSでのUITableViewの使い方とか。これはひどいと思いました。お前が考案したAPIなのか?w

2. お前のブログは、どこかのサイトのページの翻訳ではないか?
Androidに関して、キーワードで日本語サイトをググると、Googleのドキュメントリファレンスのページをほぼ翻訳したページを見かけることがあります。ブログを書いている本人は、日本人のために翻訳してあげているんだとか言いそうですが、ブログ中に出てくるサンプルコードも、元のドキュメントリファレンスのページと同じことが多々あります。真っ先に日本語に訳して、日本で最先端を行っているとでも思っているのでしょうか。技術的にとんがりたいなら、自分で作ったサンプルアプリのコードでも載せて欲しいですね。また、最近見かけたブログでは、英語圏のブログのほぼ翻訳で、コード断片もコピペで、元のコードの、作者独自定義の定数を、定義もせずに(定義を引用もせずに)使用している例がありました。元の英語圏のブログでは、githubにサンプルコードがあったので、定数の定義を見ることができましたが、翻訳サイトでは、githubへのリンクは張られていませんでした。

3. 勉強会や某ABCのLTで笑いを取ることしかしない
勉強会や某ABCに参加してLTを聞いていると、次のようなことがあります。

a. アニメの画像をスライドに入れる。
面白くないですし、あなたは、そのアニメの作者に、画像の使用許諾を取ったのですか?

b. 奇声を上げて笑いをとろうとする
勉強会では見かけないですが、某ABCのLTではよく見かけます。ちっとも面白くないです。ビジネス、技術アピールで注目を浴びるようにして下さい。

c. その他
某ABCのLTで、当時流行っていた某ゲームアプリを、某テストツールで動かすことをやったLTがありました。はっきり言って、技術的に面白くないです。自分のプロダクトをテストツールで動かすのならいいですが、そうでないなら、単に笑いを取るだけの内容ですね。

iPhone 2Dゲームプログラミングを読んだ

STUDIO SHIN著の「本気でゲーム開発力をつけたい人のための! iPhone 2Dゲームプログラミング」を読んだので、書評を書きます。

 この本では、はじめに書いてある通り、iOSアプリの開発経験がない場合は、iOSアプリ開発の入門書を読むように書いてあるが、その通りであろう。また、SPriteKitのAPIについては、あまり説明なく使われているので、この本を読む前に、

SpriteKitではじめる 2Dゲームプログラミング Swift対応

などを読んだ方がいいだろう。Swiftについては、第2.2章で簡単な説明があるが、これについても、Swiftについての入門書を読んだ方がいいだろう。
 この本には次のような特徴がある。

1. 他のiOSアプリ開発本のSpriteKitの章や、SpriteKitアプリ開発本とは違い、1冊の本で、画面遷移のある本格的な1つのアプリの作り方を順を追って説明している。
2. 広告の貼り方やアプリ内課金のやり方にも説明をさいている。
3. アプリ内では、マップ、ステージを導入して、ゲームの進行に従って、遊ぶゲーム面を変える仕組みがあったり、弓という武器を購入することにより攻撃力を増したり、敵キャラ、ボスキャラ、障害物、アイテムの配置を自由に行う配置ツールの作り方も説明されているので、App Storeに出せるようなゲームアプリの開発をするのに役立つであろう。
4. アクションゲームというと、小さい画面のスマホでプレイヤーの操作をどうやるかが課題である。この本では、プレイヤーが矢を発射しないときは、マップを左に自動で移動させ(プレイヤーは通常画面内で固定位置で、マップが画面を左に流れていく)、武器である弓は、コントローラとして画面の一部に操作しやすい大きさに独立させ、弓を指で引くことにより、矢を発射するように工夫している。

 この本は、本のタイトルの通り、本格的なゲーム開発力をつけることができるが、次のようなマイナスな点がある。
1. 本に書かれているコードと、ダウンロードしたサンプルのコードとに食い違いが多い。一方では関数に引数があったのに、他方では引数がないとか。また、ボスキャラを導入したのに、本では、ItemPutViewControllerクラスの更新を説明していないとか、ItemBuyViewControllerクラスで、UITableViewDataSourceの記述がないとか。誤記が多い、本のコードとダウンロードしたコードに食い違いが多い点は、同じ著者の

SpritekIt iPhone 2Dゲームプログラミング

について、Amazonでのレビューで指摘されていたので、レビュー(点検)があまり行われなかったのであろう。
2. 本のコードでは、どこを書き換えていけばいいか分かりにくいところが多い。
3. 関数の中身が長すぎる。ifのネストが深すぎる。ifのブロックが長すぎる。Xcodeでコーディングしているとき、どのifのブロックにコードを書き加えたらいいかわかりにくく、バグが発生しやすい。こういうことを書くと、アプリ開発本は、順を追ってアプリの作り方を説明しているので仕方ない、リファクタリングを行うと、本のページ数が極端に多くなるという反論があるであろうが、例えば、ifのブロックの中身をはじめから関数として記述するなどの方法はあるであろう。
4. 配置ツールなしで、敵キャラ、障害物の位置がコードにじかに書かれていると、敵キャラが障害物に近接しすぎていて、ゲーム開始でゲームが即終了する。著者は、途中途中の開発過程のゲームの動作確認をやっているのだろうか。
5. サンプルコードが完成したものしかなく、章ごとのソースコードがなく、写経ミスがあった場合、原因を探りにくい。
6. 必要なコードが、本で説明されていないか、本を先まで読まないと書かれていないことが多い。例えば、4.2章で、矢を発射する操作は、gameStateが、.gamePlayingでないと、touchesBegan関数にある最初のif文内のreturnが実行され、touchBegan内の残りの処理が実行されない。gameStateのセッターで、shootOKをtrueに、gameStateを.gamePlayingにするなどの処理を書かないといけない。この説明がないので、4.2章で矢を発射できることを確かめることができない。
 以上の通り、マイナスな点が多いのではあるが、本格的なゲームアプリを開発するには、いい本であると思うので、評価4を付ける。

iOS 8 + Swiftで、performSelector:withObject:がない!!

國居貴浩氏のiPhoneアプリ開発のコツとツボ35のサンプルを、Swiftで書き換えることを試みています。

「Q05 UIViewにボタンの機能を追加するには?」 の「3 ターゲット/アクションデザインパターの実装」のサンプルを書き換えようとしました。ところが、Swiftでは、performSelector:withObject:メソッドがないんですね。メモリーリークを起こすことがあるとかで廃止になったようです。ググると、How to implement callback/selector with performSelector in swift?が引っかかりました。dispatch_afterを使って遅延実行を行うといいようです。参考にして実装してみました。

Button.swift

import UIKit

class Button: UIView {

var target:NSObject?
var action: Selector?

func myMethod(firstParam: String, setCallbackObject obj: AnyObject, withMySelector selector: Selector) {
if obj.respondsToSelector(selector) {
var myTimer: NSTimer = NSTimer.scheduledTimerWithTimeInterval(0.0, target: obj, selector: selector, userInfo: nil, repeats: false)
myTimer.fire()
} else {
println("Warning: does not respond to given selector")
}
}

/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func drawRect(rect: CGRect) {
// Drawing code
}
*/

override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
self.myMethod("thisfirstis", setCallbackObject: target!, withMySelector: action!);
}

そして、AppDelegate.swift


func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
self.window = UIWindow(frame: UIScreen.mainScreen().bounds)
// Override point for customization after application launch.
self.window?.backgroundColor = UIColor.whiteColor()

let button = Button(frame: CGRectMake(100, 100, 100, 100));
button.backgroundColor = UIColor.blueColor();
self.window?.addSubview(button);

button.target = self
button.action = "touched";

self.window?.makeKeyAndVisible()
return true
}

func touched() {
self.window?.backgroundColor = UIColor.blackColor()
}

これでうまくいきました。しかし、次の「4 ターゲット/アクションデザインパターンで色パレットを作る」の実装をやろうとすると、上の方法ではうまくいきません。push:メソッドは、Buttonインスタンスを引数に取るからです。いろいろ調べたら、Alternative to performSelector in Swift?にヒントがあり、アクションメソッドが引数を持つ場合は、dispatch_afterとNSThreadのdetachNewThreadSelectorを使うといいようです。そこで実装しました。

Button.swift

import UIKit

class Button: UIView {

var target: NSObject?
var action: Selector?

/*
* iOS 8 + Swiftでは、performSelector:withObjectが使えないので、dispatch_afterを使用。
* 引数を持つアクションのケースでは、NSThread.detachNewThreadSelector を使用。
* http://stackoverflow.com/questions/24158427/alternative-to-performselector-in-swift
* Buttonクラスは、AppDelegateクラスの詳細(push:のこと)を知らない。
*/
func myMethod(aSelector: Selector, anObject: NSObject, sender: Button) {
if (anObject.respondsToSelector(aSelector)) {
let delegateObj = anObject as AppDelegate
let delay = 0.02 * Double(NSEC_PER_SEC)
var time = dispatch_time(DISPATCH_TIME_NOW, Int64(delay))
dispatch_after(time, dispatch_get_main_queue(), {
NSThread.detachNewThreadSelector(aSelector, toTarget:delegateObj, withObject: sender)
})
} else {
println("Warning: does not respond to given selector")
}

}

/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func drawRect(rect: CGRect) {
// Drawing code
}
*/

override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
self.myMethod(action!, anObject: target!, sender: self)
}
}

そして、AppDelegate.swift

var window: UIWindow?
var indicator: UIView?

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
self.window = UIWindow(frame: UIScreen.mainScreen().bounds)
// Override point for customization after application launch.
self.window?.backgroundColor = UIColor.whiteColor()

var r: CGRect = CGRectMake(100, 100, 44, 44)
indicator = UIView(frame: r)
if (indicator != nil) {
self.window?.addSubview(indicator!)
indicator?.backgroundColor = UIColor.blackColor()
}

r = CGRectOffset(r, r.size.width + 10, 0)
for var i = 0; i < 7; i++ { var bt: Button = Button(frame: r) bt.backgroundColor = UIColor(hue: CGFloat(i)/7.0, saturation: 1.0, brightness: 1.0, alpha: 1.0) bt.target = self bt.action = "push:" self.window?.addSubview(bt) r = CGRectOffset(r, 0, r.size.height) } self.window?.makeKeyAndVisible() return true } func push(sender: Button) { indicator?.backgroundColor = sender.backgroundColor }

引用ページでは、detachNewThreadSelectorのtoTargetの引数は、selfですが、今の場合、ターゲットがAppDelegateなので、Buttun.swiftで、anObject をAppDelegateにキャストして、引数に入れるのがキモです。この実装方法にたどり着くまで、Buttonクラスは、AppDeleagteクラスのpushメソッドの引数のことをどうやって知るのだろうと疑問に思っていました。http://stackoverflow.com/questions/24985716/in-swift-how-to-call-method-with-parameters-on-gcd-main-thread などを参考にすると、anObjectをAppDelegateにキャストすると、Buttonクラスで、AppDelegateクラスにあるpush:メソッドを呼ぶことはできます。しかし、これはButtonクラスがAppDelegateクラスの実装に依存するので、アカンやつです。替わりに、ここで採用した実装方法だと、Buttonクラスは、AppDelegateクラスの詳細な実装、push:メソッドのことを知らないので、ターゲット/アクションデザインパターンになっています。

AsyncTaskLoaderとListFragmentを組み合わせたサンプル

Androidアプリ開発逆引きレシピに、リスト表示するサンプルアプリ「043 最後まで表示したら自動で項目が追加されるようにしたい」があります。このサンプルは、Activity、ListViewを使って実装しており、追加読み込みには、OnScrollListener と非同期処理を扱うAsyncTaskを使っています。

最近は、AsyncTaskではなく、AsyncTaskLoaderを使うのが流行りのようです。そこで、ListFragmentを組み合わせて、「text数字」という項目からなるリストを表示し、上にプルアップすると、さらにデータを読み込み表示するサンプルアプリを作ってみました。AsyncTaskLoaderでは、非同期処理を逐次的に行うのはそれほど自明ではないようで、ソースを下手に書くと、前に表示されていたリストデータと新たに追加されたリストデータが混在して表示されてしまいます。この点、AsyncTaskを使う場合、追加読み込みは、AsyncTaskのインスタンスを新たに生成し、executeメソッドを呼べばいいだけで、楽でした(※ 注: 非同期処理が走っているかのチェックをして、走っている場合は、追読み込みをしない処理は必要でした)。AsyncTaskLoaderを逐次的に呼べるようにした方法は以下の通りです。

* AsyncTaskLoaderを逐次的に行うようにするには、非同期処理を実行中かのmIsLoadingというboolean変数を導入し、非同期処理開始時、mIsLoadingをtrueにして、リストプルアップで呼ばれる additionalReadingメソッド内で、mIsLoadingの値をチェックをし、trueの場合、returnして、非同期処理を新たに呼ばないようにしました。1つの非同期処理が終了したとき呼ばれるonLoadFinished メソッド内で、mIsLoadingをfalseにしました。
* AsyncTaskLoaderを(逐次的に)複数回呼べるようにするために、コンストラクタに、非同期処理が何回目に呼ばれたかの引数を加え、AsyncTaskLoaderの初期化の際、initLoaderの第1引数に非同期処理が呼ばれる回数を入れました。

以下は実装したソースです。

ローダーのアイテム用データクラス

public class Entry {
    private String mLabel;

    public String getLabel() {
        return mLabel;
    }

    public void setLabel(String label) {
        mLabel = label;
    }

    @Override
    public String toString() {
        return mLabel;
    }
}

アダプター

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;

import java.util.List;


public class CustomAdapter extends ArrayAdapter<Entry> {
    private LayoutInflater mLayoutInflater;

    public CustomAdapter(Context context) {
        super(context, android.R.layout.simple_list_item_1);
        mLayoutInflater = (LayoutInflater)context
                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    }

    public void setData(List<Entry> data) {
        clear();
        if (data != null) {
//            addAll(data); // addAll is usable from API level 11.
            for (Entry entry: data) {
                add(entry);
            }
        }
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        // 特定の行(position)のデータを取得
        Entry item = (Entry)getItem(position);

        // 同じ行に表示されるViewは使い回しされるため初回だけ生成
        if (null == convertView) {
            convertView = mLayoutInflater.inflate(
                    R.layout.list_item, null);
        }

        // データをViewの各Widgetにセット
        TextView textView = (TextView)convertView.findViewById(R.id.text);
        textView.setText(item.toString());

        return convertView;
    }
}

AsyncTaskLoader

import android.content.Context;
import android.support.v4.content.AsyncTaskLoader;
import android.util.Log;

import java.text.Collator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class CustomLoader extends AsyncTaskLoader<List<Entry>> {

    List<Entry> list;
    static List<Entry> sOldList;
    static List<Entry> sData;
    int mCount;
    int m;

    private static final String TAG = CustomLoader.class.getSimpleName();

    public CustomLoader(Context context, int count) {
        super(context);
        mCount = count;
    }

    /**
     * バックグラウンドでローダ用のデータを読み込む
     */
    @Override
    public List<Entry> loadInBackground() {

        // 2秒止める
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        if (mCount == 0) {
            sData = new ArrayList<Entry>();
            m = 0;
        } else {
            // それまでのリスト
            sData = sOldList;
            m = sData.size();
        }
        Log.d(TAG, "m = " + m);

        for (int i = 0; i < 30; i++) {
            Entry entry = new Entry();
            entry.setLabel("text" + (i + m));
            sData.add(entry);
        }

       // Entry用のリストを作成
        List<Entry> entries = new ArrayList<Entry>(sData.size());

        for (int i = 0; i < sData.size(); i++) {
            Entry entry = sData.get(i);
            entries.add(entry);
        }

        // このソートは不要。AsyncTaskLoaderを何回も呼ぶ時、逐次的に、前の処理が終わったら
        // 次の処理を呼ぶ場合は、データ順序が正しくなるが、このアプリの実装初期、
        // 築時的に正しく呼ばれていなく、データの順序が正しくならなかったので、
        // リストのソートを実装した。
        // リストをソート
        Collections.sort(entries, CUSTOM_COMPARATOR);

        // entriesの内容をsOldListにコピー
        sOldList = new ArrayList<Entry>();
        for (Entry entry: entries) {
            sOldList.add(entry);
        }

        return entries;
    }

    /**
     * 提供する新しいデータがあるときに呼び出される
     */
    @Override
    public void deliverResult(List<Entry> data) {
        if (isReset()) {
            // リセット時(または最初に読み込みが開始されていない、もしくは
            // reset()が呼び出された後)現在の非同期クエリを解放
            if (data != null) {
                onReleaseResources(data);
            }
            return;
        }

        List<Entry> oldEntries = data;

        sData = data;

        if (isStarted()) {
            // 読み込みが開始されている (startLoading()が呼び出されているが
            // stopLoading()やreset()はまだ呼び出されていない)場合いn、その結果を返す
            super.deliverResult(data);
        }

        // この時点で、必要であれば oldEntries に関連するリソースを解放できる
        if (oldEntries != null && oldEntries != data) {
            onReleaseResources(oldEntries);
        }
    }

    @Override
    protected void onStartLoading() {
        forceLoad();
    }

    @Override
    protected void onStopLoading() {
        cancelLoad();
    }

    @Override
    public void reset() {
        super.reset();
        onStopLoading();
    }

    /**
     * 読み込んだデータ・セットに関連するリソースを解放するヘルパーメソッド
     */
    protected void onReleaseResources(List<Entry> apps) {
        // Cursorの場合は閉じる
        // 単純なリストList<>の場合は特に何もしない
    }

    /**
     * Entry用のComparator
     */
    public static final Comparator<Entry> CUSTOM_COMPARATOR =
            new Comparator<Entry>() {
//                private final Collator sCollator = Collator.getInstance();

                @Override
                public int compare(Entry object1, Entry object2) {
// 文字順だと、数字の順にならない
//                    return sCollator.compare(object1.getLabel(), object2.getLabel());
                    String str1 = object1.toString();
                    String str2 = object2.toString();
                    int length1 = str1.length();
                    int length2 = str2.length();
                    int num1 = Integer.valueOf(str1.substring(4, length1));
                    int num2 = Integer.valueOf(str2.substring(4, length2));
// 整数引数をとるCollator#compareはない
//                    return sCollator.compare(num1, num2);
                    if (num1 > num2) {
                        return +1;
                    } else if (num1 == num2) {
                        return 0;
                    } else {
                        return -1;
                    }
                }
            };
}

ListFragment

import android.os.Bundle;
import android.support.v4.app.ListFragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.util.Log;
import android.widget.AbsListView;

import java.util.List;

public class CustomListFragment extends ListFragment
        implements LoaderManager.LoaderCallbacks<List<Entry>>,
        AbsListView.OnScrollListener {

    private CustomAdapter mCustomAdapter;
    private boolean mIsLoading = false;
    private int mCount = 0;

    private final static String TAG = CustomListFragment.class.getSimpleName();

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        mCustomAdapter = new CustomAdapter(getActivity());
        setListAdapter(mCustomAdapter);

        // スクロールリスナーを設定
        getListView().setOnScrollListener(this);

        setListShown(false);

        mIsLoading = true;

        getLoaderManager().initLoader(0, null, this);
    }

    @Override
    public Loader onCreateLoader(int id, Bundle args) {
        return new CustomLoader(getActivity(), mCount);
    }

    @Override
    public void onLoadFinished(Loader<List<Entry>> loader, List<Entry> data) {
        mCustomAdapter.setData(data);
        mIsLoading = false;
        setListShown(true);
        mCount++;
    }

    @Override
    public void onLoaderReset(Loader<List<Entry>> loader) {
        mCustomAdapter.setData(null);
    }

    @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        if (totalItemCount == firstVisibleItem + visibleItemCount) {
            additionalReading();
        }
    }

    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {

    }

    private void additionalReading() {
        Log.d(TAG, "mIsLoading = " + mIsLoading);
        // 既に読み込み中ならスキップ
        if (mIsLoading) {
            return;
        }
        setListShown(false);
        mIsLoading = true;
        getLoaderManager().initLoader(mCount, null, this);
    }
}

MainActivity

import android.support.v4.app.FragmentActivity;
import android.os.Bundle;
import android.support.v4.app.FragmentTransaction;
import android.view.Menu;
import android.view.MenuItem;


public class MainActivity extends FragmentActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        CustomListFragment frag = new CustomListFragment();
        FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
        ft.add(R.id.main, frag, "CustomListFragment");
        ft.commit();
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();
        if (id == R.id.action_settings) {
            return true;
        }
        return super.onOptionsItemSelected(item);
    }
}

リストのレイアウトファイル list_item.xml

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="match_parent">

    <TextView
            android:id="@+id/text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text=""/>

</LinearLayout>

Fragmentのレイアウトファイル fragment_sample.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="fill_parent"
              android:layout_height="fill_parent"
              android:orientation="vertical">

    <ListView
            android:id="@+id/ListView"
            android:layout_width="fill_parent"
            android:layout_height="0dip"
            android:layout_weight="1" />

</LinearLayout>

Activityのレイアウトファイル activity_main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              xmlns:tools="http://schemas.android.com/tools"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="vertical"
              android:id="@+id/main"
              tools:context=".MainActivity">

</LinearLayout>

問題点
* データ読み込み開始時、setListShown(false)を呼ばないと、ずっとプログレスバーが表示され続ける。
* プログレスバーが画面中央に表示される。フッターの位置に表示したいのだけど、方法が不明。Activity + AsyncTaskでは簡単なんだけど。

GitHubに置いたソースコード
andropenguin/CustomListFragementSample4

参考
* Androidアプリ開発逆引きレシピ
* Android UI Cookbook for 4.0 ICS アプリ開発術

追記(Aug 20, 2014)
本記事中のコードには反映していないが、GitHubで、ソースコードに以下の変更を行う。上記問題点は解消された。
GitHubに置いたソースコード
andropenguin/CustomListFragementSample4
* プログレスバーの位置を画面下部に変える。
* レイアウトファイルで、ListViewのidを@id/android:listに変える。
* setListShownを使うとアプリが落ちるし、データ読み込み中、リストが非表示になるのは見栄えが悪いので、使用をやめる。

GreenDaoを使ったAndroidアプリでquery系のメソッドのテスト

GreenDaoを使ったAndroidアプリのテストを行っています。DBUtilitiesクラスに、queryBuilderを使ったquery系のメソッドを実装していて、そのメソッドをテストしたいです。

greenrobot/greenDAOのサンプルアプリ DaoExample には、 greenDAO /DaoExample/src-test/de/greenrobot/daoexample/CustomerOrderTest.java というテストファイルがあり、insertで、実データベースではなく、メモリー上にデータを挿入しています。このサンプルでは、query系のメソッドのテストはやっていません。ググると、https://github.com/greenrobot/greenDAO/tree/master/DaoTest を参考にしろとかの情報があるのですが、そのプロジェクトの内容はかなり難解です。考えなおして、さっきのテストファイルを見なおしてみます。データを実データベースではなくメモリー上に保存するようにしているのは、どうやってかと見ます。データ挿入は、

daoSession.insert(customer);

で行っています。だけど、daoSessionは、ファイル CustomerOrderTest.java にかかれていませんね。ちょっと困惑しましたが、このクラスは、抽象クラス AbstractDaoSessionTest を継承していました。そのクラスファイルを開くと、ちゃんと、daoSessionが定義されていました。それでは、query系のメソッドレシーバーをどうやって得ればいいでしょうか。GreenDaoのドキュメントの
greenDAO – Android ORM for SQLite Introduction
 を読むと、次の例があります。

helper = new DaoMaster.DevOpenHelper(this, "notes-db", null);
db = helper.getWritableDatabase();
daoMaster = new DaoMaster(db);
daoSession = daoMaster.newSession();
noteDao = daoSession.getNoteDao();

この例では、NoteDaoインスタンスは、DaoSessionインスタンスから、getNoteDaoメソッドで得ています。そうすると、EntityがFooの場合、FooDaoインスタンスは、daoSession.getFooDao()で得られるでしょう。

さて、daoインスタンスが得られましたが、query系のメソッドのテストをどう行うでしょうか。DBUtilitesクラスのインスタンスを、AbstractDaoSessionTest を継承したテストクラスのコンストラクタで生成するとしましょう。この場合、DBUtilites のコンストラクタで、SQLiteDatabase、DaoMaster、DaoSession、CommentDao の各インスタンスを生成するような実装をしている場合、テスト実行で、実データベースにデータベースに作成されてしまいます。仕方ないので、回避策として、テストクラスファイル内に、テスト対象クラスのquery系のメソッドの実装をコピーして、fooDaoをdaoSession.getFooDao()に置き換えます。この方法で、query系のメソッドのテストが出来るようになりました。しかも、データは実データベースには保存されず、メモリー上に保存されます。

RailsのオリジナルRakeタスクのテスト rspec 3.0.x編

モリジュン(@zyunnosuke)氏が、RailsでオリジナルRakeタスク作成からRSpecテストまでの記事において、Rakeタスクの作り方、RSpecによるテストの仕方を解説してます。その記事では、RSpecは2.x系で、テストコードで、shouldを使っています。今回、RSpec 3.0.xで、expectを使いたいので、挑戦してみました。

1. プロジェクトの作成

$ rails new rake-test-sample
$ cd rake-test-sample
$ bundle update
$ bundle install

2. モデルの作成

rakeのタスクの内容をクラスに抽出したものとして、モデル ReportGeneratorを作成する。

$ rails generate model report_generator
$ bundle exec db:migrate

モデルクラス ReportGeneratorには、次の内容を記述。

class ReportGenerator < ActiveRecord::Base
  def self.generate
    puts "ReportGenerator.generate called."
  end
end
&#91;/ruby&#93;

クラスメソッドで、ダミーな処理を記述している。

3. Rakeタスクファイルの作成

&#91;bash&#93;
$ rails generate task reports
&#91;/bash&#93;

生成されたlib/tasks/repots.rakeに次の内容を記述。元記事のまんまです。

&#91;ruby&#93;
namespace :reports do
  # descの記述は必須
  desc "Generate report"

  # :environmentはモデルにアクセスするのに必須
  task :generate => :environment do
    # 処理を記述
    ReportGenerator.generate
  end
end

タスクの確認は

$ rake -vT |grep reports
rake reports:generate                   # Generate report

タスクの実行は

$ rake reports:generate

4. Gemfileにrspecとrspec-its の追加

元記事では、specファイルで、itsを使っていますが、itsはrspec 3.0.xではサポートされなくなりました。RSpec の入門とその一歩先へ、第3イテレーション ~RSpec 3バージョン~によると、gem rspec-itsを導入すると、itsが使えるとのことです。そこで、元記事に書かれていたrake_shared_context のgemと共に、Gemfileに以下を追加。

group :development, :test do
  gem 'rake_shared_context'
  gem 'rspec-rails', '3.0.0'
  gem 'rspec-its', '1.0.1'
end
$ bundle update
$ bundle install

を実行。

5. RakeタスクのRSpecの作成

rspecによるテストが出来るように次のコマンドを実行します。

$ bundle exec rails generate rspec:install

元記事では、spec/lib/tasks/reports_rake_spec.rbファイルは、次の内容でした。

require 'spec_helper'

describe 'reports:generate' do
  include_context 'rake'

  its(:prerequisites) { should include('environment') }

  it 'generates the report' do
    ReportGenerator.should_receive(:generate)
    subject.invoke
  end
end

これは、rspec 2.x向けの記述なので、次のように書き換えます。

require 'spec_helper'

describe 'reports:generate' do
  include_context 'rake'

  its(:prerequisites) { is_expected.to include('environment') }

  it 'generate the report' do
    expect(ReportGenerator).to receive(:generate)
    subject.invoke
  end
end

記述の仕方は、RSpec の入門とその一歩先へ、第3イテレーション ~RSpec 3バージョン~ や、RSpec 3の重要な変更を参考にしました。

6. Rakeテストの実行

$ bundle exec rspec spec/lib/tasks/reports_rake_spec.rb

warningが多いので、プロジェクトディレクトリの .rspecファイルで、–warningsの行を削除しておきます。

`find_and_eval_shared’: Could not find shared context “rake” (ArgumentError)

というエラーが出ますが、spec/spec_helper.rbの冒頭に次の記述をします。

# This file is copied to spec/ when you run 'rails generate rspec:install'
ENV["RAILS_ENV"] ||= 'test'
require File.expand_path("../../config/environment", __FILE__)
require 'rspec/rails'
require 'rspec/autorun'

この記述は、パーフェクトRuby on Railsを参考にしました。

再度、テストを実行。

$ bundle exec rspec spec/lib/tasks/reports_rake_spec.rb
...
Finished in 0.01324 seconds (files took 1.22 seconds to load)
2 examples, 0 failures

テストが通りました。

7. ソースの公開場所

andropenguin/rake-test-sample

8. Special Thanks

RedmineでGmailにメール送信するための設定

RedmienでGmailにメール送信するための設定について、以前ハマったので、メモ。Remdineのバージョンは、2.5.1。

config/configuration.ymlで次のようにする。user_nameとpaaswordを適宜変更する。

production:

# specific configuration options for development environment
# that overrides the default ones
# —————————————————————————-
email_delivery:
delivery_method: :smtp
smtp_settings:
#tls: true
enable_starttls_auto: true
address: “smtp.gmail.com”
port: 587
domain: “smtp.gmail.com” # ‘your.domain.com’ for GoogleApps
authentication: :plain
user_name: “hoge@gmail.com”
password: “himitsu”
# ————————————————————————–
development:

WebブラウザでのRedmineの設定は、ググって。あと、メールがスパムメールボックスに入っていることがあるので、注意する。
(フォーマッタがうまく機能しないorz。ymlの書式に従って、行頭にスペースを入れる必要がある。)

八木俊広氏のAndroidオープンソースライブラリ徹底活用のサンプルのbuild.gradle集を作った

八木俊広氏のAndroidオープンソースライブラリ徹底活用のサンプルをIntelliJ IDEAでビルドしました。build.gradleファイル集を公開します。アプリのソースファイルは原則的に含めませんが、一部のアプリが動かないケースがあったので、その場合は、ソースファイルを改変したものも含めました。また、flickrサービスが、最近、SSL接続を要求するようになったのか、flickrサービスにアクセスするアプリが全く動きませんでした。それらのアプリでは、build.gradleを含めないことがあります。

StyledDialogsを使って、Customized List Dialog Fragementを作った

Androidライブラリ inmite/android-styled-dialogs を使って、リストを表示するDialog Fragmentを作ってみました。

特徴は、以下の通りです。

  • Android 3.1以前にも対応する、DialogFragmentベースのリスト表示するダイアログを実装した。
  • リストの項目を選択すると、どの項目が選択されたか検知できる。
  • ダイアログを表示中に画面回転をやった後、リストの項目やCancelをタップしても、アプリが落ちることはない。

ライブラリのソースファイルに添付のデモには、リストを表示するダイアログの例があるのですが、getTargetFragment()の返り値がnullになっていて、ダイアログのリストの項目をタップした時、どの項目がタップされたか拾えませんでした。本アプリではダイアログを開く画面を、Fragmentにして、そこで、setTagetFragmentでセットすることによって、この問題を解決しました。また、デモでは、Fragmentの実装にはよく見られるシングルトンパターンに関して、似たコーディングはあったのですが、あらわにはコーディングされていなかったので、ソースを書き換えました。

サンプルプログラムのソースファイルを GitHub andropenguin/CustomStyledDialogsSample
に置きます。

参考サイト