ボクココ

熱海で開発するブログ

Android の EventBus がめちゃくちゃ便利な件について

ライブラリで久々に感動した。これはマジックだ。今回は、greenrobot/EventBus · GitHub を紹介する。

よくアクティビティに独自のコールバックオブジェクトを実装させて、それを他のクラスで渡して実行させる処理がよくあると思う。以下は例。

interface Callback {
  void onFinished();
}

class SampleActivity extends Activity implements Callback {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
      HogeProcess process = new HogeProcess(this);
      process.start();
    }

  @Override
  public void onFinished() {
    Log.v("TAG", "finished");
  }
}

class HogeProcess {
  private Callback callback;

  public HogeProcess(Callback callback) {
    this.callback = callback;
  }

  public void start() {
    //.....
    callback.onFinished();
  }
}

概要としてはこんな感じ。割とライブラリとかでも一般的に使われている手法だ。

ただ、このインタフェースを使ったコールバックの方法は、以下のような欠点がある。

  • 毎回コールバックを作るごとにインタフェースを作らないといけない
  • インプリメントがどんどん増えて複雑になっちゃう
  • コールバックオブジェクトをわざわざ管理しないといけない

といった要は煩雑な処理が多くなってしまう。それを解決してくれるのが、このEventBus。

EventBus の使い方

本当に簡単。さっきのをEventBus を使って同じように作ってみる。

class SampleActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
      HogeProcess process = new HogeProcess();
      process.start();
    }

    @Override
    protected void onStart() {
        super.onStart();
        EventBus.getDefault().register(this);
    }

    @Override
    public void onStop() {
        EventBus.getDefault().unregister(this);
        super.onStop();
    }

    public void onEvent(HogeEvent event) {
      Log.v("TAG", "finished");
    }
}

class HogeEvent {
}

class HogeProcess {
  public HogeProcess() {
  }

  public void start() {
    //.....
   EventBus.getDefault().post(new HogeEvent())
  }
}

どうでしょう!インタフェースが消えました!それに伴ってHogeProcess内のコールバックオブジェクトも必要ないです。以下に要点をまとめよう。

  • onStart, onStop, 内にregister, unregister をそれぞれ呼ぶ
  • イベントを呼び出したい時に、 EventBus.getDefault().post(イベントクラス) を呼ぶ。この時のイベントクラスの中にフィールドを持たせる事ももちろん可能。
  • 受け取り側で public void onEvent(イベントクラス event) を定義してあげる。このイベントクラス名でかぶっていた場合だけそのイベントをキャッチすることができる。

これだけのルールだ。これだけでかなりソースが簡潔になる。そして無駄な処理が発生しなくなる。今後どのプロジェクトでも使っていきたいと思えるライブラリーだ。

Rails での Model.find(params[:id]) の恐怖

認証ありの Rails アプリケーションでのよくある話。

例えばこんなコードがあるとしよう。現在のユーザーの記事一覧を取得するコードだ。

class ArticlesController < ActionController::Base
  before_action :authenticate_user!
  
  def show
    @article = Article.find(params[:id])
  end
end

このコード、一見すると正しく動いて特に問題ないように見える。だがセキュリティ的に危険なところがある。それは、 他のログインしたユーザーなら誰でも指定の記事が見えてしまう という問題だ。 ActiveRecord を使っていれば、auto_increment なid 付与が一般的であるため、他人の記事が簡単に読めてしまう。記事であれば別に見られてもいいだろうけど、他のプライベートな情報をこのロジックでやるとアウト。

対応方法

こんな書き方で対応できる。

class ArticlesController < ActionController::Base
  before_action :authenticate_user!
  
  def show
    @article = current_user.articles.find(params[:id])
  end
end

自分の持つ記事の中でfind をかけるというやり方。こうすれば他人の記事はfindされなくなるので、安全なコードとなる。

サンプルではなかなかこの書き方が出回っていないように見受けられるので、知っておいたほうがいいかと思う。

Canvas を容易に扱えるプラグイン jCanvas

以下のようなハッカソンに参加した。

Salesforceハックチャレンジ2014 | Salesforce World Tour Tokyo

ここで、自動コールセンターを作成するコールクラウドを提出した。コールクラウドで分岐を手軽に作成できるようなユーザーインタフェースにこだわった。このようなインタフェースを作るために、 jCanvas | jQuery meets the HTML5 canvas を利用した。これを使うと、以下のようなグラフィック処理をウェブ上で実現できる。

こういうのって今まではかなりとっつきにくい感じでなかなか手が出なかったんだけど、意外とドキュメントを見ながら開発してなんとかなった。

特にjCanvas で重要な概念は「Layer」だ。これがHTMLでいう id の役割をしており、後からイベントが発生した時に getLayer でその要素を取ってくることができる。また「Group」の概念も重要だ。これはいわゆる class の役割をしており、ひとまとまりを一気に消したり、移動させたりするときに使える。

この2つさえそれなりに理解すれば、あとは線や四角形、テキストの表示方法をドキュメントを見ながら頑張れば、実現したいものが実現できるだろう。

今回のコールクラウドでは木構造を扱う割と複雑な描画だったため、さらに四角形のひとまとまりを「box」クラスとして別に定義し、それを木のノードとして扱うような作りにした。それらをどのx, y の位置に描画するのかいちいち計算して指定しなければならないため、普段のアプリ開発よりかなり頭を使う必要がある。ただそれも最初は数字でピクセル指定して場所を確認し、あとは動的に表示できるようにメソッドに切り分ければいいだけなので、頑張るだけだ。

今回は利用しなかったが、jCanvas を使えばドラッグも簡単だ。

所感

一歩先のユーザーエクスペリエンスを目指すウェブプログラマーの方は是非 jCanvas を使ってみることをおすすめする。やり始めると楽しいのでおすすめ。

言われてみればタイピング速度って重要だなと思った話

今日ふとしたきっかけでタイピング速度の話題になった。

自分は中学生の頃からタイピングゲームと出会い、そこから暇があれば定期的にやるようにしている。よくやるのは Goast Typing と寿司打の二つ。久々に見たら、GoastTyping の順位は527位だった。これで1秒7.764キー入力。普段は7.0くらい。合計が2万5千のうちの500位くらいだから、上位2%。まぁそんなもんか。

ちょっとした自慢話は置いておいて、このタイピング速度ってタイピングゲームとかで練習しないと意外と身につかないんじゃないかって思った。少なくとも今の自分のタイピング速度はタイピングゲームから身につけた。どうやったら早くなるのかを突き詰めていった結果、指の配置とかもちょっとずつ改善されていったし、変な癖も直してきた。

それなりのスピードで打てるようになったら、こういう記事を書く時とかもだいぶ時間の節約になるし、その分いろいろなことができる。もちろんプログラミングにも効果絶大だ。

時間短縮にもっとも効果的なものの一つにタイピング速度ってあるな〜と思った。

今回はただそれだけなんだけど、バカにできないことなので一応書いてみた。

Kinesiss のキーボードで高速でタイピングしている人を見たときは感動したな〜。お気に入りのキーボードを探すのも一つ手ですな。

iOS8 で画像をアルバム or カメラで取得し、S3 へアップロードする

iOS

だいぶお決まりな処理な気がするのでまとめてみる。 iOS8 からは Photos Framework という画像を扱うフレームワークがあり、これを利用していく。 UIImagePickerController を使えば、画像選択まではそれなりにうまくいくのだが、 S3 へアップロードとなるとそのファイルパスが必要になってきて、そのファイルパスをどうやってとってくればいいのかが難しいところ。

画像は特に、自前のサーバへアップロードするんじゃなくて、S3に直接アップロードしたほうが負荷的にも優しいし、マルチパートのPOSTを実装する必要もなくなるので、基本的には採用すべきだと思う。そのURLだけを自前サーバにポストすればいいだけにしよう。

画像をリサイズして色々なサイズの画像をS3に保存したいというようなケースも多くあると思う。その時は自前サーバのAPIで、アプリから渡したS3のURLにある画像を一度サーバに取り込んでからリサイズし、再度S3へアップロードという形の方がいいと思う。とにかくアプリから画像をアップロードする処理の実装はiOSでもAndroidでも大変だからオススメしないw

以下手順をまとめる。

UIImagePickerController を表示

タイプを指定して presentViewController すればいいだけ。とても簡単だ。

        var actionSheet = UIAlertController(title:"Image", message: "Select the image", preferredStyle: UIAlertControllerStyle.ActionSheet)
        var actionCancel = UIAlertAction(title: "Cancel", style: UIAlertActionStyle.Cancel, handler: {action in
            //nothing
        })
        var actionNormal1 = UIAlertAction(title: "From Album", style: UIAlertActionStyle.Default, handler: {action in
            let imagePickerVc = UIImagePickerController()
            imagePickerVc.sourceType = UIImagePickerControllerSourceType.PhotoLibrary
            imagePickerVc.delegate = self
            self.presentViewController(imagePickerVc, animated: true, completion: nil)
        })
        var actionNormal2 = UIAlertAction(title: "From Camera", style: UIAlertActionStyle.Default, handler: {action in
            let imagePickerVc = UIImagePickerController()
            imagePickerVc.sourceType = UIImagePickerControllerSourceType.Camera
            imagePickerVc.delegate = self
            self.presentViewController(imagePickerVc, animated: true, completion: nil)           
        })
        actionSheet.addAction(actionCancel)
        actionSheet.addAction(actionNormal1)
        actionSheet.addAction(actionNormal2)
        
        self.presentViewController(actionSheet, animated: true, completion: nil)

とりあえずこれだけでアルバムとカメラそれぞれ起動できる。

取得した画像のパスを取得

カメラで撮った場合、画像はまだどこにも保存されない状態で戻ってくるので、いったんどこかに保存して、そのパスを取得する必要がある。アルバムに保存してもいいんだけど、割と撮った写真はそのアプリだけのための一時的な画像であることが多いので、他の適当な場所に保存することにする。

UIImagePickerController が終わったタイミングで呼ばれるメソッドを定義してその中に処理を書く。

    func imagePickerController(picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [NSObject : AnyObject]) {
            self.dismissViewControllerAnimated(true, completion: nil)
        // from camaera
        if (info.indexForKey(UIImagePickerControllerOriginalImage) != nil) {
            let tookImage: UIImage = info[UIImagePickerControllerOriginalImage] as UIImage
            var imagePath = NSHomeDirectory()
            imagePath = imagePath.stringByAppendingPathComponent("Documents/face.png")
            var imageData: NSData = UIImagePNGRepresentation(tookImage)
            let isSuccess = imageData.writeToFile(imagePath, atomically: true)
            if isSuccess {
                let fileUrl: NSURL = NSURL(fileURLWithPath: imagePath)!
                uploadToS3(fileUrl)
            }
            return
        }
        
        // from album
        var pickedURL:NSURL = info[UIImagePickerControllerReferenceURL] as NSURL
        let fetchResult: PHFetchResult = PHAsset.fetchAssetsWithALAssetURLs([pickedURL], options: nil)
        let asset: PHAsset = fetchResult.firstObject as PHAsset
        
        PHImageManager.defaultManager().requestImageDataForAsset(asset, options: nil, resultHandler: {(imageData: NSData!, dataUTI: String!, orientation: UIImageOrientation, info: [NSObject : AnyObject]!) in
            let fileUrl: NSURL = info["PHImageFileURLKey"] as NSURL
            self.uploadToS3(fileUrl)
        })
    }

このパスを取得する方法だけど、かなり色々試行錯誤した結果の上でのコードなので、他にいいやり方があるのかもしれない。。

これが終わったら、いよいよアップロードだ。

S3 へアップロード

いくつか手順を踏まないといけない。

AWS SDK for iOS のインストール

CocoaPods で簡単に入れられる。

pod 'AWSiOSSDKv2'
pod 'AWSCognitoSync'

からの pod install

Bridging-Header に以下を追記。

#import "AWSCore.h"
#import "S3.h"

これで完了。

Amazon Cognito の登録

ではまずは準備。Amazon Cognito というAWSサービスをまずは登録する必要がある。

これを登録すると、データ同期の仕組みやユーザー認証の仕組みなども割と手軽に実装できるみたいだ。結構データ同期とか自前で実装すると同期に失敗したときとか戻しが効かなくなったりするので、今度機会があったら使ってみよう。

今回はとりあえずこれを登録して、S3を使える状態にする。登録完了後、AppDelegete に以下を追記。

    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
        // Override point for customization after application launch.
        let credentialsProvider = AWSCognitoCredentialsProvider.credentialsWithRegionType(
            AWSRegionType.USEast1,
            accountId: "You AccountId",
            identityPoolId: "your identityPoolId",
            unauthRoleArn: "Your UnauthRoleArn",
            authRoleArn: "Your AuthRoleArn")
        let defaultServiceConfiguration = AWSServiceConfiguration(
            region: AWSRegionType.USEast1,
            credentialsProvider: credentialsProvider)
        AWSServiceManager.defaultServiceManager().setDefaultServiceConfiguration(defaultServiceConfiguration)
        
        credentialsProvider.getIdentityId().continueWithBlock({ (task) -> AnyObject! in
            var myId = credentialsProvider.identityId
            println("myId is \(myId)")
            self.userDefault.setObject(myId, forKey: Conf.KEY_USER_ID)
            self.userDefault.synchronize()
            return nil
        })

        return true
    }

後半の setDefaultServiceConfiguration の呼び出しは、端末固有のIDを生成するのに使っている。これからアップロードする画像の名前を一意にするために、このIDと時刻を使って名前を生成し、S3へアップロードしよう。

S3 の設定

S3アップロード処理を書く前に、S3の設定がいる。そうしないとうまくいっても Permission Denied になってしまう。

まずはBucket を作成しよう。 ちなみにリージョンはus-east-1 がいいみたい? ここら辺は他のネット情報からなのでもしかしたら東京でも大丈夫。

作成したらそのBucketのパーミッションで EveryOne が UploadとRead できるようにしておく。それに追加して、Create Bucket Policy を選択し、以下のJSONを貼る。

{
    "Version": "2008-10-17",
    "Statement": [
        {
            "Sid": "",
            "Effect": "Allow",
            "Principal": "*",
            "Action": [
                "s3:AbortMultipartUpload",
                "s3:DeleteObject",
                "s3:GetObject",
                "s3:PutObjectAcl",
                "s3:PutObject"
            ],
            "Resource": "arn:aws:s3:::YOUR_BUCKET_NAME/*"
        }
    ]
}

ここのs3:PutObjectAcl がポイント。これをしないと読み込みの時にその画像を閲覧するということができなくなってしまう。

そしたら準備完了。

アップロード処理の記述

そしたらようやくコードが書ける。

    func uploadToS3(fileUrl: NSURL) {
         //make a timestamp variable to use in the key of the video I'm about to upload
        let date:NSDate = NSDate()
        var unixTimeStamp:NSTimeInterval = date.timeIntervalSince1970
        var unixTimeStampString:String = String(format:"%f", unixTimeStamp)
        println("this is my unix timestamp as a string: \(unixTimeStampString)")
        
        // set upload settings
        var myTransferManagerRequest:AWSS3TransferManagerUploadRequest = AWSS3TransferManagerUploadRequest()
        myTransferManagerRequest.bucket = "YOUR_BUCKET_NAME"
        var myId = userDefault.stringForKey(Conf.KEY_USER_ID)
        self.uploadedFileName = "\(myId!)_\(unixTimeStampString).jpg"
        myTransferManagerRequest.key = self.uploadedFileName
        myTransferManagerRequest.body = fileUrl
        myTransferManagerRequest.ACL = AWSS3ObjectCannedACL.PublicRead
        
        var myBFTask:BFTask = BFTask()
        var myMainThreadBFExecutor:BFExecutor = BFExecutor.mainThreadExecutor()
        var myTransferManager:AWSS3TransferManager = AWSS3TransferManager.defaultS3TransferManager()
        myTransferManager.upload(myTransferManagerRequest).continueWithExecutor(myMainThreadBFExecutor, withBlock: { (myBFTask) -> AnyObject! in
            if((myBFTask.result) != nil){
                println("Success!!")
                // send api?
                let s3Path = Conf.AWS_S3_URL + self.uploadedFileName
                println("uploaded s3 path is \(s3Path)")
                
            } else {
                println("upload didn't seem to go through..")
                var myError = myBFTask.error
                println("error: \(myError)")
            }
            return nil
        })       
    }

これで無事、対象のBucketに画像がアップロードされていることだろう。

終わりに

ちゃんとやるなら、たぶんこれにアップロード中は dispatch_asyncでバックグラウンドで処理させることが必要。あとuploading の表示も必要だ。

よくやるはずの処理なのに、まだSwiftでの記事が少なかったり、Photos フレームワークの資料が少なかったりでもっといいやり方があるのかもしれないので、その場合はシェアしてくださると嬉しいです。

UITableView の Accessory 系が全く表示されない

iOS

iOS アプリ開発で意外に詰まるところになるような気がしたのでメモ。UITableView を配置し、右側に矢印出したい。こんな感じ。

f:id:cevid_cpp:20141203105210p:plain

以下のようなコードを書いた。

    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("identifier", forIndexPath: indexPath) as UITableViewCell
        cell.textLabel.text = self.settings[indexPath.section][indexPath.row]
        
        cell.accessoryType = UITableViewCellAccessoryType.DisclosureIndicator
        return cell
    }

しかし、右側に矢印っぽいのは表示されない。

原因

UITableView の Widthが画面を超えてしまっていた。本当は表示されているのに、画面外に表示されていた。

ちゃんと UITableView の Width を画面に収まるようにしたら表示されました。。

やられたやられた。これはわっかりにくいw

AccessoryType, AccessoryView

AccessoryTypeはiOS SDK であらかじめ用意されたテンプレート。 AccessoryView は独自のUIView を継承したものならなんでも追加できるみたい。

この AccessoryView を使うことで、テーブルの右側に Switch を置いたり、 TextView を置いたりできる。これはなかなか便利なので覚えておこう。

爆速 Android アプリ開発をサポートする Bootroid をOSS化しました

Bootroid は、特に JSON API を通じてサーバとやりとりする Android アプリ開発を爆速化するフレームワークです。 一般的な Web サービスをAndroidアプリで実現しようとした際に有用です。

インストール方法などは上記リンク先を参照していただくとして、この記事ではBootroid の中で特に有用と思われる 3つの主要機能について解説します。

API 通信

HTTP リクエスト

Bootroid 内に Volley が埋め込まれていますので、わざわざ Volley をインストールする必要はありません。ほんの少しのセットアップを済ませるだけで、以下のように簡単にAPIアクセスができます。

        ApiRequest.get(URL, new ApiResponseHandler() {
            @Override
            public void onSuccess(JSONObject jsonObject) {
               // parse json
            }

            @Override
            public void onFailure(ApiException e) {
                // error handling
            }
        });

REST API アクセス

API のレスポンスに対応した Java エンティティを作成するだけで自動でそのフィールドに値が入るような REST API アクセスも可能です。

例えば、以下のようなJSONを返す APIがあったとします。

{
  article: {
    "id": 1,
    title: "Bootroid",
    "author": "honkimi",
    "content": "Bootroid supports your speedy android development."
  }
}

以下のように書くだけで、値の入ったArticleを取得できます!

class Article {
    public int id;
    public String title;
    public String author;
    public String content;
}

private void fetchRestRequest() {
    RestApi.baseKey = "article";
    RestApi.show(URL, Article.class, new ApiCallbackBase.ApiCallback<Article>() {
        @Override
        public void onSuccess(Article response) {
            // article
        }

        @Override
        public void onFailure(String message, int code) {
            // error handling
        }
    });
}

画像表示

HTTP での画像表示

レイアウトは以下のように記述します。

        <com.android.volley.toolbox.NetworkImageView
            android:id="@+id/network"
            android:layout_width="50dp"
            android:layout_height="50dp" />

アクティビティに読み込むURLを指定すればOKです。

NetworkImageView network = (NetworkImageView) findViewById(R.id.network);
String imageUrl = "https://assets-cdn.github.com/images/modules/open_graph/github-mark.png";
network.setImageUrl(imageUrl, ApplicationController.getInstance().getImageLoader());

Fontawesome のアイコン表示

Fontawesome のたくさんのアイコンをAndroidで利用することができます。

レイアウトは以下のように記述します。

    <TextView
        android:id="@+id/github_icon"
        android:text="&#xf092;"
        android:textSize="50sp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

&#xf092; は上記リンク先で使いたいアイコンから指定します。

アクティビティに以下を追加するだけです! thisActivity を指します。

IconUtil.setIcons(this, R.id.github_icon);

キャッシュの保存、取得

APIで取ってきたデータを一時的に保管し、次回起動時にそれを読み込ませてオフライン時の対策だったり、ローディングを回避したりといったところでキャッシュは多用されます。

Bootroid ではオブジェクトをそのままJSONに変換し、保存する機能を提供します。

例えば以下のようなエンティティがあるとします。

    class Sample {
        public int id;
        public String name;
        private Sample(int id, String name) {
            this.id = id;
            this.name = name;
        }
    }

ListでAPIから取得したとします。つまり、以下のフィールドに値が入っている状態です。

private ArrayList<Sample> samples;
private static final String CACHE_KEY = "sample_cache";

キャッシュの操作

// 保存
ObjectStorage.save(samples, CACHE_KEY);
// 読み込み
Sample[] cache = ObjectStorage.get(CACHE_KEY, Sample[].class);
// 削除
ObjectStorage.remove(CACHE_KEY);

取得した配列をリストに変換し、リスト操作した後また上書き保存することにより、簡単なデータ更新や一部削除なども簡単に実装できます。 複雑なデータ処理はSQLite を使う必要がありますが、シンプルなデータ操作であればキャッシュだけで実現できます。

その他機能

その他にも以下のようなよく使われる機能をまとめてあります。

  • GCM(プッシュ通知) の登録、受け取り処理
  • EditText のバリデーションのフレームワーク
  • Google Analytics 共通処理
  • Splunk 共通処理
  • その他 Androidアプリ開発でよく使う Util系

終わりに

今回公開したソースは全て、実際に作ったプロダクトで外出しにできる部分のうち、特に有用だと判断したもののみを公開しております。

もっと改善してより爆速な Android 開発を目指していきます。