MockWebServerを使ってAndroid通信をテストする - Re.Ra.Ku アドベントカレンダー day 16

Re.Ra.Ku アドベントカレンダー 16日目です。

こんにちは、安部です。

MockWebServerを使うと通信部分のモックが簡単にできるので、使い方を紹介したいと思います。

okhttp/mockwebserver at master · square/okhttp

(試してませんが、Android以外のJavaプロジェクトでも使えると思います。)

セットアップ

build.gradleに追加します。今回サンプルで使うOkHttpとRetrofitも追加してます。

testCompile 'com.squareup.okhttp3:mockwebserver:3.4.1'

compile 'com.squareup.okhttp3:okhttp:3.4.1'
compile 'com.squareup.retrofit2:retrofit:2.1.0'
compile 'com.squareup.retrofit2:adapter-rxjava:2.1.0'
compile 'com.squareup.retrofit2:converter-gson:2.1.0'

OkHttp

OkHttpを使った通信のテストのサンプルです。

テスト対象コード

URLをもらって通信して結果を返却するだけのコードです。実践的ではないですが、使い方をわかりやすくするために単純にしています。

モックのURLを渡せるようにエンドポイントのURLはコンストラクタで設定しています。

public class Connection {

    private String baseURL;

    public Connection(String baseURL) {
        this.baseURL = baseURL;
    }

    public String run() throws IOException {
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder().url(baseURL + "test").build();

        Response response = client.newCall(request).execute();
        return response.body().string();
    }
}

テストコード

簡単なテストコードです。

基本的な使い方はレスポンスを設定して、URLを取得して、そこにアクセスする感じです。

public class ConnectionTest {

    private MockWebServer server;

    @After
    public void tearDown() throws Exception {
        if (server != null) {
            // モックサーバーを停止
            server.shutdown();
        }
    }

    @Test
    public void run() throws Exception {
        server = new MockWebServer();
        // レスポンスで返したいものを設定
        MockResponse response = new MockResponse()
                // header
                .addHeader("Content-Type", "text/plain")
                // status code
                .setResponseCode(200)
                // body
                .setBody("レスポンス");

        server.enqueue(response);

        // モックサーバーのURL設定と取得
        HttpUrl url = server.url("/");

        // 実行
        Connection connection = new Connection(url.toString());
        String result = connection.run();

        // レスポンスを確認
        assertThat(result, is("レスポンス"));

        // リクエストを確認
        RecordedRequest request = server.takeRequest();
        assertThat(request.getPath(), is("/test"));
    }
}

Retrofit + RxJava

RetrofitとRxJavaを使ったサンプルです。

テスト対象コード

想定として、GitHubのユーザー情報を取得する感じのものです。

RxJavaのObservableを返す形になっています。使う側は取得したObservableをsubscribeする感じになるかと思います。

public interface GitHubService {

    @GET("users/{user}")
    Observable<User> getUser(@Path("user") String user);

}
public class GitHubAPI {

    private String baseURL;

    public GitHubAPI(String baseURL) {
        this.baseURL = baseURL;
    }

    public Observable<User> getUser(String user) {
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(baseURL)
                // Gson
                .addConverterFactory(GsonConverterFactory.create())
                // RxJava Adapter
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                .build();

        GitHubService service = retrofit.create(GitHubService.class);

        return service.getUser(user);
    }
}
public class User {

    public String id;

    public String name;
}

テストコード

MockWebServerを作るあたりは先程の例と変わりませんが、Observableをテストする方法として、TestSubscriberを使っています。TestSubscriberを使うとだいぶテストが書きやすくなると思います。

public class GitHubAPITest {

    private MockWebServer server;

    @After
    public void tearDown() throws Exception {
        if (server != null) {
            // モックサーバーを停止
            server.shutdown();
        }
    }

    @Test
    public void getUser() throws Exception {
        server = new MockWebServer();

        MockResponse response = new MockResponse()
                .addHeader("Content-Type", "application/json")
                .setResponseCode(200)
                .setBody("{\"id\":123456,\"name\":\"Kenji Abe\"}");

        server.enqueue(response);

        HttpUrl url = server.url("/");
        GitHubAPI api = new GitHubAPI(url.toString());

        // Test用のSubscriber
        TestSubscriber<User> testSubscriber = TestSubscriber.create();

        api.getUser("STAR-ZERO").subscribe(testSubscriber);

        // 終了するまで待つ
        testSubscriber.awaitTerminalEvent();
        // 完了を確認
        testSubscriber.assertCompleted();

        // onNextにあたるイベントからデータ取得
        User user = testSubscriber.getOnNextEvents().get(0);

        assertThat(user.id, is(123456));
        assertThat(user.name, is("Kenji Abe"));

        // リクエストを確認
        RecordedRequest request = server.takeRequest();
        assertThat(request.getPath(), is("/users/STAR-ZERO"));
    }

}

まとめ

MockWebServerを使うことで通信部分のテストが可能になります。使い方もそこまで難しくはないと思うのでオススメです。

プログラミングしながらできる、健康への近道講座:3回目 - Re.Ra.Ku アドベントカレンダー day 15

こんにちは!
Re.Ra.Kuの泉原です。

あっという間に3回目です。
前回もブックマークや星を下さった方々ありがとうございます。
毎日プログラミングをして社会が便利になる仕組みを作っているエンジニアの皆様が、
より元気でより健康的にプログラミングできるよう、微力ながら協力できればと思っております。
今回も宜しくお願い致します!

3回目の今回は、骨盤周りのストレッチ方法をお伝えします。
今回も、デスクから離れずに出来るストレッチのご紹介です。

骨盤の仕組み

前回同様、ストレッチに入る前にまずは体の仕組みについて簡単に触れます。

1. 体の基盤となる骨盤

私たちの体は、重力やご自身の骨・筋肉・脂肪等の重みを常に支えて生活しています。
建物の基盤が弱いと倒れてしまうのと同じように、
人間の体も基盤がぐらぐらしていると、上に乗っている骨や筋肉の位置がずれてきてしまいます。
人間の体の基盤となるのは、下半身(足、腰)であり、下半身にある筋肉・関節と関連性が高いのが骨盤です。
下半身の筋肉と関連性が強い骨盤の位置を整えると、全身のバランスが整いやすくなります。

骨盤を整えて安定した基盤を作り、肩甲骨で上半身のバランスを綺麗に保つことができると、疲れにくい体を作ることができます。
肩甲骨の仕組みについては前回記載していますので、こちらも確認いただけますと幸いです。
プログラミングしながらできる、健康への近道講座:2回目 - Re.Ra.Ku アドベントカレンダー day 9 - Re.Ra.Ku tech blog

2. 全身の代謝アップが期待できる部位

骨盤周りの筋肉は体を支えるための筋肉なので大きいものが多いのも特徴です。
筋肉の柔軟性が上がると使えていなかった筋肉が正しく働くようになるため、代謝も上がりやすくなります。
体型のお悩みがある方や、冷え症が気になる方にも、骨盤周りのケアはオススメです。

骨盤周りのストレッチ

それではストレッチに入っていきます。
前回同様、1回目の講座で出てきた姿勢の作ってストレッチしてみてください。
プログラミングしながらできる、健康への近道講座:1回目 - Re.Ra.Ku アドベントカレンダー day 3 - Re.Ra.Ku tech blog
綺麗な姿勢を保つだけでも腰・骨盤周りのストレッチと筋トレ代わりになります。
腰の疲れが気になる方は、無理の無い範囲で普段から姿勢を意識するようにしてみてください。

綺麗な姿勢を作ったら、息を吸って、左手で椅子の背もたれを持ち、左膝に右ひじを引っ掛けて、息を吐きながら椅子を回してください。
(回せない椅子の方は腰をひねってください。)
f:id:nozomiii:20161214113440j:plain
気持ち良く伸びたな、と実感できたら息を吸いながらゆっくり体を戻します。
戻した後は反対側も行います。
ひねることで腰の筋肉を伸ばしていきます。

腰を伸ばしたら、今度は股関節をストレッチします。
左足のふくらはぎを右足の太もも〜膝の間に乗せます。
乗せたら、胸を張って、頭を下げないようにして、おへそから体を倒します。 f:id:nozomiii:20161214113352j:plain

このストレッチも、気持ち良く伸びたなと実感できたところで息を吸いながらゆっくり体を戻します。
股関節を伸ばし、リンパが流れやすくなるストレッチです。

体の力が抜けていれば抜けているほど、疲れは溜まりにくくなります。
いつもプログラミングを頑張っている体にお礼を伝える気持ちを持って、
優しく心地の良い負荷でストレッチしてみてください。

最後に

弊社リラクでは、今回お伝えした骨盤周りのストレッチをしっかり行うコースもご用意しています。
肩甲骨ストレッチも骨盤ストレッチも同時に行うコースなので、体のバランスも整いやすくなります。
腰のお疲れが気になる方はぜひチェックしてみてください!
reraku.jp

次回は、目と首のストレッチ方法をお伝えします。
今後とも宜しくお願いいたします!

バリデーションライブラリ clarifyの紹介 - Re.Ra.Ku アドベントカレンダー day 14

Re.Ra.Ku アドベントカレンダー 14日目です。

こんにちは。ヘルステックチームの近藤です。

突然ですがみなさんバリデーションしていますか?病めるときも健やかなるときも、アプリケーションサービスを書くときもドメインモデルを書くときも、バリデーションはなんだかんだでせざるを得ないですよね?

当然私も日々バリデーションしているわけですが、いろいろと思うところがあり、自分でバリデーション用のライブラリを書きました。今回はそれを紹介します。

バリデーションライブラリ clarifyのコンセプト

そのバリデーションライブラリはclarifyという名前で、とあるコンセプトに沿って作られています。それは「値を与えたらバリデーション結果を返す」というものです。

まあ当たり前のことなんですが、じゃあ反対にやりたくなかったのは:

  • とあるtraitを継承し
  • DSLにてバリデーションの内容を定義したり
  • validateメソッドでそのオブジェクトが妥当かどうかを確かめる

みたいなやつです。具体的な例で言うとActiveModel::Validationsのようなものです。

何故それをやりたくなかったかと言うと、8日目のリラクのサーバサイド事情 with Scala / ドメイン編でも触れた「ドメインにフレームワークを持ち込みたくない」という事情に起因します。ドメインにおける表現の自由度をバリデーションによって奪いたくありません。

また、バリデーションは「外側の値(ユーザが入力したもの、APIにて受け取ったものなど)が内側のルールに即しているか」を確かめるものです。故にどこにでも現れる処理だと思います。アプリケーションサービスではクライアントから与えられた値が妥当かどうか、ドメインモデルではドメインに沿っているかどうか……。冒頭で言った「アプリケーションサービスを書くときもドメインモデルを書くときも」というのはただノリで言ったわけではなく、本当にそうだと思っています。

そういうことなので、取り回しが利きやすい「値を与えたらバリデーション結果を返す」という単なる「関数」である形を目指しています。

インストール

じゃあ実際にどう使うのかを紹介します。とは言えこのライブラリ、とりあえず私自身が使えるようにしたかったものなので、ドキュメントは雑ですし、テストもなく、配布もただ.jarファイルを置いただけという代物です。なのでまあこういうものがあるんだーーーぐらいのものです。

とりあえず.jarは下記の場所にあります。

https://takkkun.github.io/clarify/org/usagram/clarify_2.11/0.0.4/clarify_2.11-0.0.4.jar

sbtを使用している場合はlibraryDependenciesに下記を加えてあげればOKです。

"org.usagram" %% "clarify" % "0.0.4" from "https://takkkun.github.io/clarify/org/usagram/clarify_2.11/0.0.4/clarify_2.11-0.0.4.jar"

Validator

clarifyは「値を与えたらバリデーション結果を返す」に終始していると言いましたが、その最小単位はValidatorと呼ばれるものです。

NotBlank Validatorは文字列が空じゃないかどうか、GreaterThanOrEqualTo Validatorは数値がとある数値以上かどうかを確かめるものです。

import org.usagram.clarify.validator._

val nameValidator: Validator[String] = NotBlank

println(nameValidator.validate("Kondo")) // => no error
println(nameValidator.validate("")) // => CannotBeBlank error

val ageValidator: Validator[Int] = GreaterThanOrEqualTo(0) // GreaterThanOrEqualTo.zero とも書ける

println(ageValidator.validate(17)) // => no error
println(ageValidator.validate(-1)) // ShouldBeGreaterThan(0) error

コレクションのすべての要素が条件を満たしているかを確かめるEach Validatorなんてのもあります。

val tagsValidator: Validator[Set[String]] = Each(NotBlank)

println(tagsValidator.validate(Set("programmer", "scala"))) // => no error
println(tagsValidator.validate(Set("programmer", ""))) // => ErrorAt(1, CannotBeBlank) error

これらはすべてValidator[-V]トレイトのサブクラスで、 validate(value: V): Iterable[Error] (Errorはclarifyで定義しているtrait)を実装しているだけです。もちろんオリジナルのValidatorを作ることも出来ます。

import org.usagram.clarify.validator.Validator
import org.usagram.clarify.error.Error
import org.usagram.clarify.Tags

class StartWith(prefix: String) extends Validator[String] {
  def validate(value: String) =
    failIf(!value.startsWith(prefix)) { // 第1引数の条件を満たしたら第2引数のエラーを返すfailIfというヘルパー
      NotStartWith(prefix)
    }
}

// Validatorそれぞれに専用のエラーを設ける場合はErrorを継承し、
// そのエラーのメッセージを返す message(tags: Tags): String メソッドを実装する必要がある。
//
// Tagsはバリデーションした値に付与されたメタデータ。
// 値に名前を割り振るためのlabelなどがある。

case class NotStartWith(prefix: String) extends Error {
  def message(tags: Tags) = {
    val label = tags.label getOrElse "(no label)"
    s"$label is not start with $prefix"
  }
}

どうでしょう?十分にシンプルかなーと思います。

Validator同士を合成して大きなValidatorを作る

Validatorたったひとつでは現実世界の要望を満たせないことが多いと思います。文字列においても大体「空ではなく、かつ20文字以内」みたいなのがほとんどじゃないでしょうか?

というわけで、関数合成のようにValidator同士を合成することでその要望に応えています。

val nameValidator: Validator[String] = NotBlank && AtMostCharacters(20) // AtMost(20).characters とも書ける

println(nameValidator.validate("")) // => CannotBeBlank error
println(nameValidator.validate("very very very long name")) // => TooLongString(20) error

val tagsValidator: Validator[Set[String]] = Each(NotBlank) && AtLeast(1) // AtLeast.one とも書ける

println(tagsValidator.validate(Set("programmer", ""))) // => ErrorAt(1, CannotBeBlank) error
println(tagsValidator.validate(Set.empty)) // => RequireAtLeast(1) error

&& というメソッドがそれです。論理演算のそれをイメージしてもらえればと思います。一応「または」を意味する /\ というメソッドもあるのですが、個人的にあまり使う機会がないため動作をやや決め切れてなかったりします*1

ComplexValidatorN

「値を与えたらバリデーション結果を返す」についてはValidatorで十分実現できています。ですが、とある値の集合にたいしてバリデーションを行い、すべてのバリデーションを実行後、ひとつでもエラーとなった場合はバリデーション失敗にしたい、というのはよくあります。

Validatorだけでそれを行うことは不可能ではありませんが、ひとつひとつValidatorに値を与え、最後にまとめるのを何度も書くのはしんどいものがあります。

それを解決するのがComplexValidatorNというものです。Nには1から22までの数字が入ります。Validator同士を <-> というメソッドで繋ぐことでComplexValidatorNを作ることが出来ます。下記のケースではValidatorを3つ繋いでいるので、ComplexValidator3が作られます。

class Person(val name: String, val age: Int, val tags: Set[String])

val personValidator = nameValidator <-> ageValidator <-> tagsValidator

// valid case
{
  val name = "Kondo"
  val age = 17
  val tags = Set("programmer", "scala")

  val result = personValidator(
    Indefinite(name) label "name",
    Indefinite(age) label "age",
    Indefinite(tags) label "tags"
  )

  println(result.isValid) // => true
  
  // result.isValid が true の場合、 result.resolve[A] に
  // 与えた関数 (String, Int, Set[String]) => A が呼ばれ、
  // その実行結果を result.resolve[A] の結果として返す。
  val person = result.resolve { (n, a, t) => new Person(n, a, t) }
}

// invalid case
{
  val name = ""
  val age = 17
  val tags = Set("programmer", "")
  
  val result = personValidator(
    Indefinite(name) label "name",
    Indefinite(age) label "age",
    Indefinite(tags) label "tags"
  )
  
  println(result.isValid) // => false
  println(result.invalidValues) // => Invalid("", Tags("name"), CannotBeBlank), Invalid(Set("programmer", ""), Tags("tags"), ErrorAt(1, CannotBeBlank))
  
  // result.isValid が false の場合、 result.resolve[A] は
  // InvalidValueException 例外を発生させる。
  val person = result.resolve { (n, a, t) => new Person(n, a, t) }
}

ここで出てきたIndefiniteですが、これは値の状態を表しています。ComplexValidatorNにおいて値は必ず以下の4種類の状態を取る必要があります。

  • Indefinite: 値がまだバリデーションされていない状態
  • Valid: 値がバリデーションされ、有効だった状態
  • Invalid: 値がバリデーションされ、無効だった状態
  • Unknown: ComplexValidatorNによってバリデーションが実行されたが、バリデーションの結果が不明な状態

なぜComplexValidatorNにおいて値を直接与えず、状態を持たせる必要があるか。それはバリデーションを強制するためにあります。さきほどのComplexValidatorNによるバリデーション例を、Personのコンパニオンオブジェクトにて行うよう書き換えてみます。

class Person private (val name: String, val age: Int, val tags: Set[String])

object Person {
  val personValidator = nameValidator <-> ageValidator <-> tagsValidator
  
  def apply(name: Indefinite[String], age: Indefinite[Int], tags: Indefinite[Set[String]]): Person =
    personValidator(
      name label "name",
      age label "age",
      tags label "tags"
    ).resolve { (n, a, t) => new Person(n, a, t) }
}

val validPerson = Person(
  Indefinite("Kondo"),
  Indefinite(17),
  Indefinite(Set("programmer", "scala"))
)

val invalidPerson = Person(
  Indefinite(""),
  Indefinite(17),
  Indefinite(Set("programmer", ""))
)

ミソはPerson objectのapplyメソッドにおいてIndefiniteを指定している点です。こう指定されていてはapplyメソッドで受け取った値をそのままnew Personとすることが出来ません。Indefinite[String]とStringは当然異なる型ですからね。Indefinite[String]からStringを抜き出すにはComplexValidatorNによるバリデーションを通過させる必要があります。と、こんな感じでバリデーションを強制しています*2

Indefinite, Valid, Invalidの3つは想像が付きやすいと思うのですが、Unknownはなんでしょうか。これはComplexValidatorNを作るためのもうひとつのメソッド --> と関係があります。

値の集合をバリデーションするときにどうしても発生し得るのが、「値Bは値A以上であること」などのような、他の値に依存するケースです。まあ普通にValidatorで値Aを参照してあげれば良いのですが、バリデーション前の値はIndefiniteで包まれている以上、直接参照できません。そこで使うのが --> というメソッドです。

val aValidator: Validator[Int] = GreaterThanOrEqualTo(0)

val bValidator: Int => ComplexValidator1[Int] = { a =>
  GreaterThanOrEqualTo(a)
}

val validator = a --> b

// a: valid, b: invalid
{
  val result = validator(
    Indefinite(1) label "a",
    Indefinite(0) label "b"
  )
  
  println(result.isValid) // => false
  println(result.values) // => Valid(1, Tags("a")), Invalid(0, Tags("b"), ShouldBeGreaterThanOrEqualTo(1))
}

// a: invalid
{
  val result = validator(
    Indefinite(-1) label "a",
    Indefinite(2) label "b"
  )
  
  println(result.isValid) // => false
  println(result.values) // => Invalid(-1, Tags("a"), ShouldBeGreaterThanOrEqualTo(0)), Unknown(2, Tags("b"))
}

--> メソッドは左辺のComplexValidatorN (Validator)によってバリデーションされた結果がValidの場合、その値を右辺の関数( Int => ComplexValidator1[Int] )に与えます。そしてその関数から得られたComplexValidatorNを用いてさらにバリデーションを行う。といった具合に動作します。

もし左辺の結果がInvalidの場合はどうなるか。左辺がInvalidだとすると、当然Invalidな値からComplexValidatorNが作られることになるのですが、そんなComplexValidatorNでバリデーションしても意味がありません。ですので、右辺の結果はバリデーションの結果が不明ということでUnknownとなります。

まとめ

ということでclarifyでどんなことが出来るかを紹介しました。サンプル付きで長々と書いていますが、まとめると「値を与えたらバリデーション結果を返す」ということです。ので、比較的導入しやすいとは思います。まあ第三者が導入するためにはドキュメントを整え(この記事がドキュメントで良いのでは?)、なによりテストを書かなければいけませんが……。

ともかく、私は実際に使っているのですが、使い勝手はなかなか良く、自分で言うのもなんですが良いものが出来たと思っています。その上で「ビルトインのValidatorはさすがにもっとあっていいわ……」とか「Indefiniteで包むのさすがにめんどうだなー。ここに限ってはimplicit conversionがあってもいいな」とか考えています。

余談ですが2016年7月2日にあったYAP(achimon)C::Asia Hachioji 2016 mid in Shinagawaにて下記の発表をしました。

この発表で言っている内容自体はclarifyと直接関係がないのですが、この発表で言っていることをやるために軽量なバリデーションライブラリが欲しかったという経緯があったりします。興味がありましたら是非ご覧くださいませ。それでは。

*1:一応説明すると / は左辺および右辺のValidator双方とも条件を満たさない場合、右辺のエラーのみを全体のエラーとして採用する(左辺のエラーは結果に含めない)、というものです。 \ は左辺を採用するものです。ただ今思えば素直に両方ともエラーを返しても良い気がしますし、 And(leftError, rightError) という形で包んであげても良かったかな……

*2:まあapplyメソッドにIndefiniteを指定しない場合はどうしようもないのですが……

実況中継シリーズ Vue.jsで実現するMVVMパターン Fluxアーキテクチャとの距離 - Re.Ra.Ku アドベントカレンダー day 13

前説

丸山です。Re.Ra.Ku. アドベントカレンダー13日目の記事です。前日はiOSアプリのUIをコードで書いてみる話でした。明日はおそらくScalaの話になると思います。

さて、以前も話題にしましたが、builderscon2016が先日開催されました。チケットは3hでSOLD OUT。プラチナチケットと化した参加権ですが、発表する側ならば実質無料で参加し放題!これはいっそ申し訳ないレベルでは!?

というわけで、せっかく発表したのでその内容をなるべく多くの手段で共有したい。そう思い、今回も実況中継シリーズを弊社テックブログで行います。実況中継シリーズというのは、プレゼンをブログで再現するアレです。なお、実際のプレゼンは動画になってYoutubeにアップロードされております。builderscon公式サイトのセッション詳細ページからもご覧いただけますので、よろしければそちらも合わせてご覧ください。なお、先日行われたNDS#50でも再演を行いました。

続きを読む

iOSアプリのUIをコードで書いてみる話 - Re.Ra.Ku アドベントカレンダー day 12

Re.Ra.Ku アドベントカレンダー 12日目です。

こんにちは、磯貝です。前回は Sketch.app のプラグインについての記事でした。

今回は、iOS アプリの UI をコードで書く際の知見をいくつか紹介します。

UI をコードで書く利点

iOS アプリ開発において UI を構築する手段として一般的なのは、Xcode の Interface Builder を用いたものでしょう。優れた GUI ツールにより、実行時の見た目を即座に確認しながら、豊かな表現を行うことができます。

しかし現在弊社で開発されている iOS アプリは、ほぼ全てにおいて UI をコードにより実装しています。
この方法には、以下のような利点があります:

  • ファイルの変更点が確認しやすく、レビューを行いやすい
    • nib の xml のように、理不尽な diff が発生することもなく、conflict も起こりづらいです。
  • View の使い回しをしやすい
    • Xib として View を定義した場合、それを他の Xib や Storyboard から利用するには、結局コードによる初期化が必要です。最初からピュアな UIView のサブクラスとして View を定義すれば、オーバーヘッド無しで再利用可能です。
  • デザインや文言を一貫して管理できる
    • 定数やユーティリティクラスを用いて、色、フォント、マージンといったデザイン要素や、各種文言、そのローカライズを一貫して管理できます。
  • ViewController の初期化を厳密に行うことができる
    • Segue を用いた画面遷移を行ったり、Storyboard から ViewController を初期化する場合、値の受け渡しは新しい ViewController インスタンスのプロパティへ値をセットする形で行います。しかしこの方法では値の受け渡しを強制することはできず、すべての Optional なプロパティに対して何らかの対処をする必要があります。UI をコードで記述すると、ViewController の初期化はイニシャライザを用いて行うことができるため、そこで適切に値を渡せばこのような問題は起こりません。また、ViewController 同士が疎結合となり、再利用が容易になります。
// Storyboard から初期化する場合
class ArticleViewController: UIViewController {
    var id: ArticleId!
    var reader: Reader?
    
    ...
}

let articleVC = UIStoryboard(name: "Article", bundle: nil).instantiateViewController(withIdentifier: "Article")
articleVC.id = articleId
articleVC.reader = reader

// イニシャライザを用いる場合
class ArticleViewController: UIViewController {
    let id: ArticleId
    let reader: Reader?
    
    init(id: ArticleId, reader: Reader?) {
        // 実際は ApplicationService 等へ渡していく
        self.id = id
        self.reader = reader
        super.init(...)
        
        ...
    }
    
    ...
}
let articleVC = ArticleViewController(id: articleId, reader: reader)

一方、次のような欠点もあります:

  • 実際にビルドして実行するまで、見た目を確認できない
  • Size Classes への対応が難しい (すみません、調べきれませんでした)

特に Size Classes はどうしても必要となることもあるでしょう。要件に応じて利点欠点を天秤にかけつつやっていくしかなさそうです。
(余談ですが、Size Classes に関しては、Web におけるレスポンシブで行くか別テンプレートでいくか、みたいな判断にも似ているなーと感じます。)

UI の書き方 Tips

いくら利点があるとはいっても、View を愚直にコードで書き起こしていくのは骨が折れる作業です。

それを解決するために弊社で利用しているライブラリと、いくつかの実装を紹介します。

SnapKit

SnapKit は、Auto Layout をシンプルな DSL を用いて記述できるライブラリです。
例として、画面中央に緑の枠線を表示し、その中にラベルとボタンを表示するサンプルを用意しました。

class SampleViewController: UIViewController {
    weak var label: UILabel!
    
    override func loadView() {
        view = UIView()
        view.backgroundColor = UIColor.white
        
        let container = UIView()
        view.addSubview(container)
        
        container.layer.borderWidth = 5
        container.layer.borderColor = UIColor.green.cgColor
        // SnapKit
        container.snp.makeConstraints { make in
            make.centerY.equalTo(view)
            make.left.right.equalTo(view).inset(20)
            make.height.equalTo(view).multipliedBy(0.5)
        }
        
        
        let label = UILabel()
        container.addSubview(label)
        self.label = label
        
        label.font = UIFont.boldSystemFont(ofSize: 20)
        label.textColor = UIColor.red
        label.textAlignment = .center
        label.text = "Some Text!!"
        // SnapKit
        label.snp.makeConstraints { make in
            make.top.left.right.equalTo(container).inset(30)
        }
        
        
        let button = UIButton()
        container.addSubview(button)
        
        button.backgroundColor = UIColor.lightGray
        button.setTitle("OK", for: .normal)
        button.addTarget(self, action: #selector(self.buttonTapped(_:)), for: .touchUpInside)
        // SnapKit
        button.snp.makeConstraints { make in
            make.top.equalTo(label.snp.bottom).offset(30)
            make.left.right.equalTo(container).inset(20)
            make.bottom.equalTo(container).inset(20).priority(UILayoutPriorityDefaultLow)
            make.height.greaterThanOrEqualTo(44)
        }
    }
    
    @objc private func buttonTapped(_: UIButton) {
        label.text = "tapped!!"
    }
}

結果: f:id:y1soga1:20161212062156p:plain

あまりよろしくないレイアウトをしていますが、機能の例としてご覧ください。

コードの階層化

上記の例では、

  • view
    • container
      • label
      • button

という View の親子/兄弟関係がありますが、すべてベタ書きしているため、なんとも構造がわかりづらくなってしまいます。

そこで、以下のような Extension を導入します。

protocol ViewConstructable {}

extension ViewConstructable {
    func addSubview<Sub: UIView, Super: UIView>(_ subview: Sub, toSuperview superview: Super, initializer: (Sub, Super) -> Void) -> Sub {
        superview.addSubview(subview)
        initializer(subview, superview)
        return subview
    }
}

これを用いることで、先程のサンプルは以下のように書き直すことができます:

class SampleViewController: UIViewController, ViewConstructable {
    weak var label: UILabel!

    override func loadView() {
        constructView()
    }

    private func constructView() {
        view = UIView()
        view.backgroundColor = UIColor.white

        let /* container */ _ = addSubview(UIView(), toSuperview: view) { sb, sp in
            sb.layer.borderWidth = 5
            sb.layer.borderColor = UIColor.green.cgColor
            sb.snp.makeConstraints { make in
                make.centerY.equalTo(sp)
                make.left.right.equalTo(sp).inset(20)
                make.height.equalTo(sp).multipliedBy(0.5)
            }

            label = addSubview(UILabel(), toSuperview: sb) { sb, sp in
                sb.font = UIFont.boldSystemFont(ofSize: 20)
                sb.textColor = UIColor.red
                sb.textAlignment = .center
                sb.text = "Some Text!!"
                sb.snp.makeConstraints { make in
                    make.top.left.right.equalTo(sp).inset(30)
                }
            }

            let /* button */ _ = addSubview(UIButton(), toSuperview: sb) { sb, sp in
                sb.backgroundColor = UIColor.lightGray
                sb.setTitle("OK", for: .normal)
                sb.addTarget(self, action: #selector(self.buttonTapped(_:)), for: .touchUpInside)
                sb.snp.makeConstraints { make in
                    make.top.equalTo(label.snp.bottom).offset(30)
                    make.left.right.equalTo(sp).inset(20)
                    make.bottom.equalTo(sp).inset(20).priority(UILayoutPriorityDefaultLow)
                    make.height.greaterThanOrEqualTo(44)
                }
            }
        }
    }

    @objc private func buttonTapped(_: UIButton) {
        label.text = "tapped!!"
    }
}

クロージャ内で child view を生成することにより、コードのインデントと View の階層を一致させることができました。

ViewController と View のファイルを分割する

上の例では、ViewController 内で UI を構築しています。これではファイルが肥大化しますし、ViewController と View の責務を適切に分割できません。

そこで、以下のような ViewController の基底クラスを用意してみました。

class ViewController<ViewType: UIView>: UIViewController {
    private var tempView: ViewType?
    weak var v: ViewType!

    convenience init() {
        self.init(view: ViewType())
    }

    convenience init(view: ViewType) {
        self.init(nibName: nil, bundle: nil)
        tempView = view
    }

    override func loadView() {
        view = tempView
        v = tempView
        tempView = nil
    }
}

これを用いることで、先程のサンプルを以下のように書き直すことができます:

class SampleView: UIView, ViewConstructable {
    weak var label: UILabel!
    weak var button: UIButton!

    required init?(coder aDecoder: NSCoder) {
        fatalError()
    }

    init() {
        super.init(frame: CGRect.zero)
        constructView()
    }

    private func constructView() {
        backgroundColor = UIColor.white
        
        let /* container */ _ = addSubview(UIView(), toSuperview: self) { sb, sp in
            sb.layer.borderWidth = 5
            sb.layer.borderColor = UIColor.green.cgColor
            sb.snp.makeConstraints { make in
                make.centerY.equalTo(sp)
                make.left.right.equalTo(sp).inset(20)
                make.height.equalTo(sp).multipliedBy(0.5)
            }
            
            label = addSubview(UILabel(), toSuperview: sb) { sb, sp in
                sb.font = UIFont.boldSystemFont(ofSize: 20)
                sb.textColor = UIColor.red
                sb.textAlignment = .center
                sb.text = "Some Text!!"
                sb.snp.makeConstraints { make in
                    make.top.left.right.equalTo(sp).inset(30)
                }
            }
            
            button = addSubview(UIButton(), toSuperview: sb) { sb, sp in
                sb.backgroundColor = UIColor.lightGray
                sb.setTitle("OK", for: .normal)
                sb.snp.makeConstraints { make in
                    make.top.equalTo(label.snp.bottom).offset(30)
                    make.left.right.equalTo(sp).inset(20)
                    make.bottom.equalTo(sp).inset(20).priority(UILayoutPriorityDefaultLow)
                    make.height.greaterThanOrEqualTo(44)
                }
            }
        }
    }
}

class SampleViewController: ViewController<SampleView> {
    override func loadView() {
        super.loadView()
        v.button.addTarget(self, action: #selector(self.buttonTapped(_:)), for: .touchUpInside)
    }

    @objc private func buttonTapped(_: UIButton) {
        v.label.text = "tapped!!"
    }
}

View は UI の構築のみに注力し、イベントの受け取りや View の更新は ViewController が行っています。

まとめ

いかがでしたでしょうか。
UI をコードで書く手法は、弊社としてもまだまだ試行錯誤を続けている段階です。またアップデートがあれば記事にできればと思います。 それでは、今回もお読み頂きありがとうございました!

Swift ifとguardのエトセトラ - Re.Ra.Ku アドベントカレンダー day 11

Re.Ra.Ku アドベントカレンダー 11日目です。

こんにちは、神場です。 前回の記事 に引き続き、今回もSwift関連の記事となります。

概要

今回はif文とguard文に関するtipsです。 if文は一般の条件分岐に、guard文は想定外の値が来た場合などの早期リターンに使われるという違いはありますが、使い方が似ているためこの記事ではまとめて紹介したいと思います。

ifとguard両方で使えるもの

今回紹介するのはこの3つです。いずれもifguardの両方で使えます。

  • if let, guard letによるOptional Binding
  • if A, B, ..., guard A, ,B ...のような複数の条件
  • if case, guard caseによるパターンマッチ

順番に見て行きましょう。

if let, guard letによるOptional Binding

まずは基本的なところですが、if let, guard letでオプショナルな変数のバインディングを行うことが出来ます。

if let a = a {
    print(a)
} else {
    print("a is nil")
}

これはオプショナルな変数のUnwrapに使う基本的な方法なので、特に疑問はないかと思います。

if A, B, ..., guard A, ,B ...のような複数の条件

Swiftをやり始めの頃だと(自分も含めて)よくやってしまっていたと思うのですが、

if let a = a {
    if let b = b {
        ...
    }
}

のような文はまとめて

if let a = a, let b = b {
    ...
}

と書くことが出来ます。上記の例のように二つぐらいの条件であればまだ良いのですが、条件が多い時は基本的にカンマ区切りで書いたほうがコードがすっきりします。guard文を使う例を以下に挙げます。

indexが存在し、かつステータスがactiveなユーザーの名前を取得するサンプルコード

enum Status {
  case active, inactive
}
struct User {
  let name: String
  let status: Status
}

func getActiveUserName(users: [User], index: Int) -> String? {
    guard
        index < users.count,
        users[index].status == .active
        else { return nil }
    return users[index].name
}

if case, guard caseによるパターンマッチ

上記二つに比べて日本語ではあまり情報がない印象がありますが、実はifguardでもswitchで使うようなパターンマッチを使うことが出来ます。

enumのAssociated Valueの中身をパターンマッチで取り出すサンプルコード

enum Result<T> {
    case success(value: T)
    case failure(error: Error)
}

func getValue<T>(result: Result<T>) -> T? {
    guard case let .success(value) = result else {
        return nil
    }
    return value
}

上記のように、switchに書くcaseがほぼそのままguardの中に 入ります。

なおこれを応用(?)することによって、Optionalでない変数のバインドを行うことも出来ます。

indexが存在し、かつステータスがactiveなユーザーの名前を取得するサンプルコード(userを一旦変数に入れておくバージョン)

func getActiveUserName2(users: [User], index: Int) -> String? {
    guard
        index < users.count,
        case let user = users[index],
        user.status == .active
        else { return nil }
    return user.name
}

if case, guard caseのさらに詳しい情報については、こちらの方の記事 が非常に参考になるかと思います。

より実践的なサンプル

以上のいくつかを組み合わせて、より具体的なサンプルを見てみましょう。

guard文を使ったAPIレスポンスのハンドリングのサンプル

func parseResponse(result: Result<Any>) -> DTO? {
    guard case let .success(value) = result else {
        print("通信に失敗したよ")
        return nil
    }
    
    guard let json = data as? [String: Any] else {
        print("データがJSONじゃないみたいだよ")
        return nil
    }
    
    guard (200..<300) ~= statusCode else {
        print("ステータスコードが200番台以外だよ")
        return nil
    }
    
    guard let dto = parseJson(json) else {
        print("JSONが期待しているものと違うよ")
        return nil
    }
    
    print("正常なレスポンスが返ってきたよ")
    return dto
}

ただしエラー内容も取得したい場合はsuccessfailureの2つにパターンマッチを使いたいため、成功か失敗かの分岐部分をswitchで書くことになるかと思います。

まとめ

いかがでしょうか。以上の3つ程度があればifguardだけでも比較的読みやすい書き方が出来るのではないでしょうか。若干まとまりのない感じになってしまいましたが、もしご参考になれば幸いです。

参考

Android DataBinding Tips - Re.Ra.Ku アドベントカレンダー day 10

Re.Ra.Ku アドベントカレンダー 10日目です。

こんにちは、安部です。

今回はAndroidのDataBindingのTipsを少し紹介します。

DataBindingの変数に設定するなど基本的なところは省略しています。

includeしたレイアウトに変数を渡す

includeタグを使ったときに変数を渡す方法です。

includeされるレイアウト

通常のレイアウトと同じようにします。

<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>

        <variable
            name="name"
            type="java.lang.String" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{name}" />
    </LinearLayout>
</layout>

includeタグ

先程のレイアウトで定義した変数をincludeタグの属性としてapp:nameに渡したい変数を設定することでincludeされた側のほうでも変数が使用可能になります。

<include
    app:name="@{user.name}"
    layout="@layout/custom_view"/>

EditTextの入力を変数に反映する

two-way bindingの実現方法です。

入力を受け取るクラス/変数

EditTextの入力を受けて取るクラスと変数の例です。

public class User {
    public final ObservableField<String> name = new ObservableField<>();
}

レイアウト

分かりにくいのですが、android:textに設定している値の@{の間に=を入れます。こうするとEditTextに入力された値が変数に反映されます。

<EditText
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@={user.name}" />

制限事項としては、android:textにはStringのものしか設定できません。

EditTextの変更を監視する

変更されるたびに呼ばれるメソッド

この場合ですとonTextChangedが変更されるたびに呼ばれます。

public class Handlers {

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

    public void onTextChanged(CharSequence s, int start, int before, int count) {
        Log.d(TAG, "onTextChanged: " + s);
    }
}

レイアウト

android:onTextChangedに先程つくった処理を設定してあげます。これだけです。

<EditText
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:onTextChanged="@{handlers::onTextChanged}" />

補足

android:onTextChangedがどこで定義されてるかですが、ココ です。

他にもいくつか拡張であるので、ほしいのがないかをココで探すと良いと思います。

よく使いそうな、フォーカスイベントのandroid:onFocusChangeココで定義されてます。

ひとつ問題としては、xmlで属性が定義されてないみたいな警告が出てしまいます。気になる人はコメントで抑制しておくと良いと思います。(tools:ignoreを使った抑制がうまくできなかった…)

<!--suppress AndroidUnknownAttribute -->
<EditText
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:onTextChanged="@{handlers::onTextChanged}" />

注意としてはパッとどこで定義されてるのかが分からないので、何かしらコメントがあると良いかもしれないです。

定義されていないイベント処理する

例えば、SwipeRefreshLayout#setOnRefreshListenerはそのままではレイアウトファイルのみでイベントを設定することが出来ません。

カスタム属性を設定する

次のようなクラスを作って、属性を追加して、それを受け取れるようにします。引数の順番は対象のView、属性に渡す型になります。今回はSwipeRefreshLayout.OnRefreshListenerを受け取るようにします。

public class SwipeRefreshLayoutBinding {

    @BindingAdapter("onRefresh")
    public static void onRefresh(SwipeRefreshLayout view, SwipeRefreshLayout.OnRefreshListener listener) {
        view.setOnRefreshListener(listener);
    }
}

別のパターンとして、@BindingMethodsを使うことも出来ます。こちらはクラスのほうにアノテーションを追加します。

@BindingMethods({
        @BindingMethod(type = SwipeRefreshLayout.class, attribute = "onRefresh", method = "setOnRefreshListener")
})
public class SwipeRefreshLayoutBinding {

}

イベントを処理するメソッドを定義する

SwipeRefreshLayout.OnRefreshListenerで定義されているメソッドと同じシグネチャのメソッドを定義します。メソッド名は変更しても大丈夫です。

public class Handlers {

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

    public void onRefresh() {
        Log.d(TAG, "onRefresh");
    }
}

レイアウトに設定する

先程の作った属性(名前空間はappを使います)とメソッドを設定してあげると、コードを書かずにイベントを処理できるようになります。

<android.support.v4.widget.SwipeRefreshLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:onRefresh="@{handlers::onRefresh}">

    ...

</android.support.v4.widget.SwipeRefreshLayout>

まとめ

頑張ればDataBindingで色々できそうですが、あまりトリッキーなことをしすぎない感じのほうが良いとは思います。そこはうまくバランスを取りながらで。

うまくDataBindingを使って、コードを簡潔にしていきたいですね。