読者です 読者をやめる 読者になる 読者になる

そーす

福岡在住のプログラマ。SwiftとかKotlinとかJavascriptとかSketchとか触ってます。

ReactNative+Redux環境で非同期アクションのテストを書く

Javascriptでちゃんとテスト書いてますか?

私は書いてないです。

テストはあんまり書いたことないです(ドン引き

Javascriptのテストは全く書いたことないです(ドンッ!!

今仕事で開発しているアプリはReactNativeなんですが、

やっぱスクリプト言語だと実行時までミスがわからないので怖いですね…。

もう怯えながらリリースするのは嫌なんだ…

というわけで、テストを導入してます。

ちなみにESLintflowは導入してます。

Jest

テストライブラリはJestを採用しました。

facebook.github.io

理由は、

  • ReactNativeは最初からJestの環境が整っている。
  • 構文がシンプルで簡単(テスト初心者でも使えそう。
  • Fluxの実装にReduxを使っているが、Jestでのテスト方法が公式にある。

という感じです。

プロジェクトを作る

とりあえず、新規RNプロジェクトを作って導入までをまとめます。

> react-native init redux_jest_sample

最初からJestがpackage.jsonに追加されていて、すぐにjestが動作する環境が整っています。 デフォルトで_test_にテストが書いてあるので実行してみます。

> npm test

> redux_jest_sample@0.0.1 test /Users/Ryohlan/Dev/react-native/redux_jest_sample
> jest

 PASS  __tests__/index.android.js
 PASS  __tests__/index.ios.js

Test Suites: 2 passed, 2 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        1.65s, estimated 10s
Ran all test suites.

テストがパスしました。

jestのテストは_test_ディレクトリ内か.text.js, .spec.jsが対象です。

通信のモック

nockというライブラリを使います。 github.com

nockは特定のリクエストが走ったときにその処理を横取りして任意のレスポンスを返してくれるライブラリです。

例えば、ReduxのHttp通信を行う非同期アクションのテストを行いたいときに、 通信のテストでは無いので特定の値を返してしまおうとういうものです。

 nock('http://hoge.com/api/v1')
        .get('/messages')
        .reply(200, { messages: ['hoge', 'fuga'] });

上記の例ではhttp://hoge.com/api/v1/messagesに対してGETメソッドでアクセスしたときに ステータスコード200で、レスポンスBodyを{ messages: ['hoge', 'fuga'] }で返すという書き方です。

ReduxのStoreのモック

redux-mock-storeというライブラリを使います github.com

非同期アクションが始まって終わるまでのアクションを保持してくれます。

非同期アクションのテスト

あとはアクションのテストを書くのみです。

//action
const REQUEST_MESSAGES = 'REQUEST_MESSAGES';
const SUCCESS_MESSAGES = 'SUCCESS_MESSAGES';
const requestMessges = () => ({ type: REQUEST_MESSAGES});
const successMessges = (messages) => ({ type: SUCCESS_MESSAGES , messages });
const fetchMessages = () => (dispatch) => {
 dispatch(requestMessges());
    return fetch('http://hoge.com/api/v1/messages')
               .then(response => response.json().then((json) => successMessages(json.message));
};


//reducer
const messages = (state = { messages: [], isFetching: false }, action )  => {
    switch(action.type) {
        case REQUEST_MESSAGES: 
            return Object.assign({}, state, { isFetching: true });
        case SUCCESS_MESSAGES: 
            return Object.assign({}, state, { messages: action.messages, isFetching: false };
        default: return state;
    }
};

//test
test('非同期テスト', () => {
    // Http通信のモック
  nock('http://hoge.com/api/v1')
        .get('/messages')
        .reply(200, { messages: ['hoge', 'fuga'] });

     const store = mockStore({}); // Reduxストアの初期化

      // ストアにアクションをディスパッチ
      return store.dispatch(Actions.fetchMessages())
        .then(() => expect(store.getActions()).toEqual(
          [
            { type: Actions.REQUEST_MESSAGES },
            { type: Actions.SUCCESS_MESSAGES, messages: ['hoge', 'fuga'] },
          ],
        ));
    });

重要なのは最後の

      return store.dispatch(Actions.fetchMessages())
        .then(() => expect(store.getActions()).toEqual(
          [
            { type: Actions.REQUEST_MESSAGES },
            { type: Actions.SUCCESS_MESSAGES, messages: ['hoge', 'fuga'] },
          ],
        ));
    });

です。

expect(A).toEqual(B)はAがBと同じであると言うJestのテスト構文です。

Actions.fetchMessages()でReduxの非同期アクションが始まります。

store.getActions()で非同期アクションの始まりから終わりまでのアクションが取得できるので、

期待されるアクションと比較することで非同期アクションのテストができるということです。

Javascriptの型チェック「Flow」まとめ

flow.org

個人的まとめ。 重要な所だけ読み返したい用です。 型名の和訳は適当です。 時間あったら追記する。

String

let text: string = 'hoge'; //OK
text = 'fuga'; // OK
text = 12;  // Error

let func = (value: string): string => value; // ok

Number

let num: number = 12; //OK
num = 12.3; //OK
num = 'hoge'; // Error

Boolean

let bool: boolean = false; //OK
bool = true; //OK
bool = 1223; //Error

Array

let array: Array<number> = [1, 3]; // OK
array = [3, 4, 4, 4]; // OK
array = ['hoge', 23]; // Error

Symbol

2017/05/18現在では未サポート

null, undefined

let hoge: null = null; // OK
hoge = undefined; // Error
hoge = 22e; // Error

let fuga: void = undefined; // OK
fuga = null; // Error
fuga = 124; // Error

Maybe型

KotlinとかSwiftとかのNullableとかOptionalとかですね

let maybe: ?string = 'text'; // OK
maybe = null; // OK
maybe = undefined; // OK
maybe = 123; // Error

maybe = `text ${maybe}`; // Error
if (maybe != null && maybe !== undefined) {
  maybe = `text ${maybe}`; // OK
}

Union

複数の型を許可します

let union: string | number = 'text'; // OK
union = 123; // OK
union = false; // Error

リテラル

特定の値のみ許可します。 ユニオン型と使うのが良さげ。Enum的な使い方かな?

let only2: 2 = 2; // OK
only2 = 3; // Error

let only2or3: 2 | 3 = 3; // OK
only2or3 = 3; // OK
only2or3 = 4; // Error

Another

i/oを統一したい時とかかな

const func = <T>(value: T): T => value;

Mixed

なんでもイケる。 サボりたい時(違うか

let mi: mixed = '23';
mi = false;
mi = 12;
mi = {};
mi = null;
mi = undefined;

関数のパラメータで使う時は注意が必要

let mFunc: func = (value: mixed) => (`text ${value}`); // Error

// OK
mFunc = (value: mixed) => {
  if (typeof value === 'string') {
    return `text ${value}`;
  }
  return value;
};

typeofなどで型をチェックした場合は以降はその型としてflowが型チェックを行う。 なので、以下はエラー。

mFunc = (value: mixed) => {
  if (typeof value === 'boolean') {
    return value + 'hoge'; // Error
  }
  return value;
};

詳しくは Type Refinements | Flow

Any

基本的に使っちゃ駄目なやつ。flowが完全に無視します。 mixedとの違いですが、

let mFunc: func = (value: any) => (`text ${value}`); // OK

先程mixedだった所を_anyに変えるとflowはエラーを出しません。 極力使用は避けましょう、との事。

関数


// OK
const f1: (value: string) => boolean = (value) => {
  return true;
};

// 実装ではパラメータ名が違っても良い
// OK
const f2: (value: string) => boolean = (v) => {
  return true;
};

// 変数の型はパラメータ無くても良い
// OK
const f3: (string) => boolean = (value) => {
  return true;
};

// 実装の方で型定義してもいい
// OK
const f4 = (value: string): boolean => {
  return true;
};

// どっちも書いても良い
// OK
const f5: (value: string) => boolean = (value: string): boolean => {
  return true;
};

// でも型は合わせないと駄目
// Error
const f6: (value: string) => boolean = (value: boolean): string => {
  return true;
};

// でも変数宣言のパラメータの型がMaybeじゃなくて実装のパラメータがMabyなら大丈夫っぽい。返り値はだめだった
// OK
const f7: (value: string) => boolean = (value: ?string): boolean => {
  return true;
};

// パラメータ名の後に?を付けるとオプションといして扱われる
// OK
const f8: (value?: string) => boolean = (value) => {
  console.log(value);
  return true;
};
f8(); // OK
f8(undefined); // OK
f8('hoge'); // OK
f8(null); // Error

Function

「関数である」ということだけをチェックする。なるべく利用は避けるべきとのこと。

const f9: (Function) => Function = (f: Function) => f(); // OK
f9(() => { })()()(); // OK

オブジェクト

const obj1: {
  foo: boolean,
  bar: ?string,
} =
  {
    foo: true,
    bar: null,
  };

obj1.foo = true; // OK
obj1.bar = 'bar'; // OK
obj1.piyo = null; // Error

厳密なオブジェクト

宣言したオブジェクト型以外のプロパティは許可しない。

const obj2: { foo: boolean, bar: ?string } = { foo: true, bar: null, bar2: 'hoge' }; // OK
const obj3: {|foo: boolean, bar: ?string|} = { foo: true, bar: null, bar2: 'hoge' }; // Error

タイプエイリアス

type NewType = {
  name: string,
  address: ?string,
  tel: number
}

const nType: NewType = {
  name: 'name',
  address: null,
  tel: 0,
};
nType.name = null; // Error
nType.name = 123; // Error
nType.hoge = ''; // Error


type Enum = 0 | 1 | 2;

let en: Enum = 2;
en = 0; // OK
en = 3; // Error

type State = 'state1' | 'state2' | 'state3';

let state: State = 'state1';
state = 'state3'; // OK
state = 'hoge'; // Error

interface

JavaのInterfaceっぽい。 デフォルトの実装は持てない。

interface Hello {
  name: string,
  say(): string
}

class HelloImpl {
  name = 'hogehoge'
  say() {
    return 'hello';
  }
}

class HelloImpl2 implements Hello {
  // nameとsayが実装してないとエラー
  name = 'fugafuga'
  say: () => ''
}

interface Address {
  address: string,
}

class HelloImpl3 implements Hello, Address {
  // nameとsayとaddressが実装してないとエラー
  name = 'fugafuga'
  address = ''
  say: () => ''
}

const h: Hello = new HelloImpl();
h.name
h.say();

const h2: Hello = new HelloImpl2();
h2.name;
h2.say();

const h3: Hello = new HelloImpl3();
h3.name;
h3.address; // Error

const h3_2: Address = new HelloImpl3();
h3_2.name; // Error

interfaceにはread/writeの制限がかけれる。 WriteOnlyの利点が理解出来ないので理解できたら追記する。

interface ReadOnly {
  +name: string
}

const ro: ReadOnly = { name: 'Ryohlan' };
ro.name; // Ok
ro.name = 'hoge'; // Error

interface WriteOnly {
  -name: string,
}

const wo: WriteOnly = { name: 'Ryohlan' };
wo.name = 'hoge'; // OK

ジェネリクス

interface Say<T> {
  name: T,
  say: () => T
}
const f: Say<string> = {
  name: 'hoge',
  say: () => 'hoge',
};
f.name;
f.say();
const f2: Say<number> = {
  name: 1, // OK
  say: () => 'hoge', // Error
};

class Item<T> {
  prop: T;

  constructor(param: T) {
    this.prop = param;
  }

  say(): T {
    return this.prop;
  }
}

const item: Item<string> = new Item('hoge'); /// OK
const item2: Item<string> = new Item(1); // Error


class Item<T: {name: string }> {
  prop: T;

  constructor(param: T) {
    this.prop = param;
  }

  sayName(): string {
    return this.prop.name; // OK
  }

  sayAddress(): string {
    return this.prop.address; // Error
  }
}

React Sketch.app で何ができるのか見てみた

f:id:saburesan:20170517103754p:plain

Introduction · react-sketchapp

ReactのシンタックスでSketchが動くらしい。

なんだかすごそう(小並感

インストー

公式のまんま

git clone https://github.com/airbnb/react-sketchapp.git
cd react-sketchapp/examples/basic-setup && npm install

basic-setupプロジェクトをエディタで開いてみると、

f:id:saburesan:20170517113237p:plain

おー、まんまReactですね。

package.jsonの中身は

{
  "name": "basic-setup",
  "version": "1.0.0",
  "description": "",
  "main": "basic-setup.sketchplugin",
  "manifest": "src/manifest.json",
  "scripts": {
    "build": "skpm build",
    "watch": "skpm build --watch",
    "render": "skpm build --watch --run",
    "render:once": "skpm build --run",
    "link-plugin": "skpm link"
  },
  "author": "Jon Gold <jon.gold@airbnb.com>",
  "license": "MIT",
  "devDependencies": {
    "skpm": "^0.9.0"
  },
  "dependencies": {
    "chroma-js": "^1.2.2",
    "prop-types": "^15.5.8",
    "react": "^15.4.2",
    "react-sketchapp": "^0.10.0",
    "react-test-renderer": "^15.4.2"
  }
}

ライブラリは特別なのはreact-sketchappくらいですね。Skech上でレンダリングできるコンポーネントライブラリでしょうか。

react-skechappの中身は

module.exports = {
  render: _render.render,
  renderToJSON: _render.renderToJSON,
  StyleSheet: _stylesheet2.default,
  Artboard: _Artboard2.default,
  Image: _Image2.default,
  RedBox: _RedBox2.default,
  Text: _Text2.default,
  TextStyles: _TextStyles2.default,
  View: _View2.default,
  Platform: _Platform2.default
};

使えるコンポーネントは多くはないようです。覚えることは少ないですね。

実行

> npm run render                                                      [master]

> basic-setup@1.0.0 render /Users/Ryohlan/dev/sketch-react/react-sketchapp/examples/basic-setup
> skpm build --watch --run

🖨  Copied src/manifest.json in 7ms


🔨  Built ./my-command.js in 2067ms

実行する前にsketchを立ち上げてプロジェクトを1つ開いておきましょう。 開かれてないと結果が描画されません。

f:id:saburesan:20170517113807p:plain

なるほど。

Hot Reaload動いてそうなのでコードを変更してみる。

f:id:saburesan:20170517114617p:plain

左上の色を黒に変更すると即座にSketchの方も変更されました。

まとめ

とりあえずちょっと触っただけ。 サンプルも結構あるので色々見てみます。

ReactNativeでJavascript側で起きたエラーを取得する

アプリのクラッシュレポートツールを大体入れているかと思うのですが、ReactNativeではどうするのが一般的なんでしょうかね。

FabricのCrashlyticsを入れたのですが、これはネイティブのエラーの箇所をレポートするので自前のネイティブ処理じゃない場合はライブラリ内のエラーの箇所が通知されます。

試しにApp.jsでエラーを投げてみると

f:id:saburesan:20170516161234p:plain

ワケガワカラナイヨ

これだと役に立たないですね…

というわけでJS側で起きるエラーを取得してそれを上手く通知できればいいなぁと思い調べてみました。

調べてみると、ErrorUtilsというオブジェクトがGlobalのプロパティに居るようで、こいつを使うとJS側のエラーをハンドリングできるようです。

github.com

試しにやってみました。一部だけ抜粋してます。

global.ErrorUtils.setGlobalHandler((e, isFatal) => {
  console.log(e);
});

export default class Sample extends Component {
  render() {
    return (
      <View style={styles.container}>
        <Button title="Invoke Error" onPress={() => { throw new Error('test'); }} />
      </View>
    );
  }
}

Chromeのログだとこんな感じ

f:id:saburesan:20170516144300p:plain

log-iosのログだと

line: 64
column: 28,
sourceURL: '/index.ios.bundleplatform=ios&runModule=false&entryModuleOnly=true&hot=true'

取得できてるみたいですね。

リリースビルドだとどうでしょうか。

line: 64,
column: 28,
sourceURL: index.android.bundle

なるほど(?)

index.android.bundleがデフォルトだと無いようなので作ります。

> react-native bundle --platform android --dev false --entry-file index.android.js --bundle-output android/app/build/outputs/index.android.bundle

これでAndroidのリリース時のバンドルされたJSファイルが取得出来ます。

f:id:saburesan:20170516155830p:plain

指定の行、列を探してみたらちゃんとthrow Error(‘text’)したコードが見つかりました。

この情報を元にCrashlyticsでレポート送るとかやればいいのかな???

あと調べたら、JavascriptのクラッシュレポートツールにSentryっていうのがあるらしいんですが、これとかって結構使われてるんですかね? AirbnbとかUberとかでの導入実績が在るらしいですが…

sentry.io

20代の振り返り

明日、5月9日に私は30歳になる。

20代を終えると言うのは一つの大きな区切りのような気がする。

20代はいろんなことがあった。

東京で就職

大学院を出て24歳で初就職。

渋谷駅近くのモバイル系広告営業の会社に新卒エンジニアとして入り、Webのフロントコーディングを学ぶ。

といってもWebのフロントは全くやったことなかったけど誰も教えてくれなかったので結局覚えながら仕事をすることに。

半年くらいでその部署が解散して、次の部署ではガラケーサイトの更新業務をしてました。

iOSアプリできまーす」って言って内定出たと思ってたけどiOSアプリの申請くらいしかやらなかったなぁ。

11ヶ月で(色々あって)退職しました。

東京で起業

新卒入社半年くらいたって同期Aと同期Bの友達のフリーランスと業務外でサービスを作る機会があって、そいつが 3人で起業したい、って言い出した。 同期Aは新卒なのに(色々あって)窓際だったし、起業はしたいと思ってた(というか大学の時にも起業してた)らしい。

私はガラケーの更新業務してたしでどうしようかなーと思っていたんだけど、

多分私単独で起業しないからそういうのやりたいやつとやれる機会は無いかなーと思って退職を決意。

結婚

入社10ヶ月で大学時代から付き合っていた今の妻と結婚。

つまりは結婚して1ヶ月後に私は会社を辞めたのだ。

妻は若干怒ってた(相談はしなかった)

不妊治療

ふたりとも子供がほしいということで妊活することに。

が、中々子供ができず、たまたま近くに不妊治療で有名な産婦人科があったので検査しに行くことに。

すると私にも妻にもちょっと問題があり子供ができにくいという結果に。

ちなみに私は精子の量が普通の人の半分以下だった。

1年タイミング法で頑張ったが出来ず、人工授精をすることにした。

(人工授精をするために専用の容器にオナニーして精子を出すのだが、あれはなんとも言えない気持ちになる。)

私が通っていた産婦人科では人工授精は最大でも5回までだった。

人工授精はできる人は1回でできるが出来ない人は5回やっても出来ないらしい。

精子卵子自体に異常があるわけではなかったのですぐにできると思った。

が、人生そんなに甘く無いもので4回やって駄目だった(10万近くかかったかな)

体外受精が頭をよぎる。

あれめっちゃ高い。

色んな治療費で結構お金がかかったので、体外受精をするかどうかはちょっと考えないとな、という感じだった。

そんな心配をよそに、5回目の人工授精でめでたく成功し子供を授かることになった。

こういう経験があるので友達とかには子作りのときは出来ないなーと思ったが早めに産婦人科で検査を受けるように言ってる。

ちなみに私がそういう話をしたからって事で病院に行った友達が子宮筋腫が見つかり手術した。

発見が早かったからなのか手術後に子供を授かったようだ。

なので私は不妊治療をしたことは特に隠さずに人に話すようにしている。

私もそうだったようにまさか入院も骨折も捻挫すらしたことがない自分がとは思わない。

けど、周りの友達の話なら案外ちゃんと聞いてくれるし検査に行かないデメリットも特に無いしそれで友達が事なきを得たのであればなおさらだ。

コレを隠して最悪手遅れで子供出来ないとかなったら後味最悪だ。

というわけでみんなさっさと産婦人科行こう。

退職、福岡で起業

起業した会社は4人になったが、(色々あって)社長以外の3人は辞めて福岡で起業することに。

色々あった。

結婚してすぐに会社を辞め、妊娠して会社を辞め、こう見ると本当に駄目な人間である。

出産、立ち会い

12月17日の21時位だったと思う。妻が気分が悪いと言ってきたので陣痛かもしれないと。

とりあえずちょっと吐き気がするくらいだったので可能なら明日電話してみようということになったが、18日の深夜2時くらに大分つらそうだったので

産婦人科に電話してみると

「電話できるならまだ大丈夫ですね」

と言われ諦めることに。

しかしながらかなりつらそうだったのと、そもそも19日が予定日で18日は診察に行く予定だったのもあって病院に行くことに。

生まれそうなので入院してくださいとのことだったので入院。

そこから24時間妻は陣痛と戦いました。

そして私もその間ずっと腰擦ったり、公式テニスボールを妻のお尻の穴に当てたりして頑張りました。←これが楽になるらしい

日付変わって19日0時ごろ、妻が分娩室に入り私も立ち会うので一緒に入りました。

「あ”ぁ”あ”あ”あ”あ”あ”あ”あ”あ”あ”あ”あ”あ”あ”あ”あ”あ”」

ってずっと言ってて痛そうだった(小並感

そして

「オンギャーオンギャーオンギャーオンギャーオンギャーオンギャー」

めっちゃ(うるさい)声とともに生まれました。

そこの産婦人科ではへその緒を切らせてくれるので、せっかくなので切らせてもらいました。

なんかゴムホースを切ってる感覚に近くて、

1回で切れなくて3回くらいでようやく切れました。

出産は感動しますよ、立ち会えるなら立ち会ったほうがいいです。(感動の描写無いですが

20代の総評

良い20代でした。

結婚、出産を20代のうちにできたのはよかったです。

結婚して家族増えて、出産で家族増えて、めっちゃ楽しいです。

もう家族が居ない生活は考えられないですね。

苦労もありますが、それ以上に楽しいことが多いです。

ちなみに子育ては完全体力勝負なので若い内が良いです、マジで。

30代の抱負

あと子供を一人欲しい。

そしたらあとはひたすらに金を稼ぐだけ。

あと月1でアプリをリリースしたい。

まだ個人アプリは1つだけしか出してないのでもっと出して行きたいし、

本業以外での収入をもっと増やしていかないと駄目だなぁと。

さて30歳も頑張ろう。

「ReactNativeってどうなの?」というタイトルでLTしてきました

www.slideshare.net

LTなんて初めてでした。

とりあえず、なんか友人からの質問やTwitterとか見てて疑問に思ってそうな事適当に並べて見ました。

なんというか、福岡でもReact/React Nativeをワイワイさせたいなーとか使ってる人と友達になりたいなーと

思ってLTしました。

とりあえずReactNativeの周知目的でザーッとReactNativeいいよ〜的な話をして最後に辛み述べた感じでなんとも微妙でした(反省

1人だけ、ReactNativeやってます!って人が話掛けてくれて嬉しかったです!!

しかし福岡だとまだまだなのかな…

ていうかアプリ界隈のコミュニティ無くね…

インフラとかサーバーサイドのコミュニティとか勉強会とかは多いんですがね…

初心者向けReact NativeハンズオンとかやってRN人口増やしたいとか思ってたけど、とりあえず興味持ってもらうためにLTもっと参加しないとなって思いました。

(解決)ReactNative+FirebaseではネイティブSDKを使ったほうが良いかも

(追記)解決しました。 やっぱりJavascriptCoreが原因でした。

github.com

f:id:saburesan:20170405095735p:plain

developers-jp.googleblog.com

ここで紹介されてたので、ネイティブSDK入れなくてもいけるのか〜

と思って色々試して見ました。

匿名ログインしてRealtimeDatabaseに書き込みは出来たのですが、Storageへのbase64形式での画像アップデートがぜーんぜん上手く行かなくて

エラーメッセージも

“Firebase Storage: String does not match format ‘base64

って出てるけどデコードしたら問題なかったし超悩みました。

んで、ひっさしぶりにDebug JS Remotelyを起動して見てみよう(基本的にiOSシミュレーターのログをtailするか, adb logcat でログ見てるので)と

起動してアップロードしてみたら、なんとアップロードが成功するではありませんか!!

これから分かるのはReactNativeのJSを実行する環境であるJavascriptCoreでは

FirebaseStorageのputStringの内部で使用される何かが実装されてない or バグがあるということでしょうか。

リファレンスにも書いてありますが、Debug JS Remotelyを実行するとReactNativeのJSはChrome上で動作します。

facebook.github.io

Debug JS Remotelyのオン/オフで成功/失敗が変わるのであればおそらくコレが原因でしょう…

FirebaseはAPIがたくさんあるので全て調べるのは面倒なので、おとなしくネイティブのSDKを入れることにします。

ReactNativeでiOS, Androidに最適化したUIを構成するには

最近は仕事でも個人でもReact Nativeでの開発がメインになっており、かなり知識が溜まってきました。

で、だいぶiOSAndroidに最適化したUIの構成ができるようになってきたのでメモ程度に残します。

OSごとのUIの分け方

ReactNativeは標準で幾つかiOS, Androidの処理を分ける機能があります。

リファレンス貼っときます。

facebook.github.io

ファイルごと分割する

ReactNativeの標準機能でXXX.ios.jsとXXX.android.jsという規則でファイル名を付けて

import XXX from './XXX';
const XXX = require('./XXX');

とするとiOSの時はXXX.ios.jsを、Androidの時はXXX.android.jsを自動でインポートしてくれます。

例えば、エラーをユーザに通知する機能を作るとします。

iOSではUIAlertControllerを使い、AndroidではToastを使うのが一般的だと思います。

その場合、

Alert.ios.jsは

'use strict';

import { AlertIOS } from 'react-native';

module.exports = {
  show: message => AlertIOS.alert(message),
 };

Alert.android.jsは

'use strict';

import { ToastAndroid } from 'react-native';

module.exports = { 
  show: message => ToastAndroid.show(message, ToastAndroid.LONG) 
};

呼び出し

import Alert from './Alert';

Alert.show('hoge');

気をつけることはファイル名とプロパティ名を揃えることです。

スタイルなどの値を分ける

処理自体を分ける場合はファイル分割が良いですが、スタイルを分けたい場合なども頻繁にあります。

そういうときはPlatform APIを使います。

例えばTextInputを使う場合、iOSAndroidではかなりあてるスタイルが異なります。

TextInputはiOSではheightの設定が必要だったり背景色やボーダーが無いとフォームが解りづらいので設定する必要があります。

Androidの方は元から適切なPaddingや高さ、入力フォームがわかるように下線が付いていたりしますので背景色やボーダーを追加する必要はありません。

そのときはPlatform.selectを使って

  height: Platform.select({ ios: 54 }),
  backgroundColor: Platform.select({ ios: '#EEE' }),
  borderColor: Platform.select({ ios: '#DDD' }),
  borderWidth: Platform.select({ ios: 0.5 }),

このようにすることで細かい分け方も可能です。

ios

f:id:saburesan:20170401134649p:plain

android

f:id:saburesan:20170401134746p:plain

特定のOSのみ適用されるプロパティを利用する

標準のコンポーネントのプロパティで特定のOSにしか意味を為さないプロパティがいくつかあります。

facebook.github.io

例えば上のリンクのViewコンポーネントにもいつくかあり

f:id:saburesan:20170401133855p:plain

上記のスクリーンショットのように[iOS][Android]という表記で書かれています。

また、表記が無くともプロパティに設定する値がOSごとに意味を為す/為さないがあるのでリファレンスはしっかり読みましょう。

ネイティブAPI/ネイティブViewの呼び出しで分ける。

標準に無いネイティブのAPIやViewを呼びたい場合などは必然的にXcodeとAndroidStudioを使ってコードを分ける必要がありますが、

この時にReactNative側から呼ぶインターフェースを統一しておけば

OSの違いを意識すること無く呼び出すことが出来ます。

ネイティブ連携はリファレンスを見ながら簡単に導入することが出来ます。

facebook.github.io

Imageコンポーネントは画像をキャッシュする

ReactNativeの標準のImageコンポーネントはキャッシュ機構があります。

なので、以前に取得したURLの画像の場合はキャッシュが読まれます(期限は無さそう)

同じURLの画像が更新される仕様のAPIなどではこれは厄介です…

ちなみにいくつかIssueが上がってました。

github.com

2017/03/20現在ではキャッシュの削除機構は存在せず、ワークアラウンドで対応するか、他のOSSライブラリで対応するしか無いようです。

ちなみに私はとりあえずMath.random()をパラメータに付加してキャッシュを回避する方法を取りました。

BackAndroidはremoveEventListenerではなくaddEventListenerの返り値のオブジェクトを使おう

facebook.github.io

Andoroidの戻るボタンが押された時のイベントをハンドリングするAPIです。 なんか公式ドキュメント見るとaddEventListerでセットしたハンドラの破棄はremoveEventListerを使わないといけないっぽいですが、 ソースを見てみる

addEventListener: function (
    eventName: BackPressEventName,
    handler: Function
  ): {remove: () => void} {
    _backPressSubscriptions.add(handler);
    return {
      remove: () => BackAndroid.removeEventListener(eventName, handler),
    };
  },

removeメソッドを持つオブジェクトを返してますね。

ちなみに現在のmasterではBackAndroidはDeprecatedになってます。

github.com

BackHandlerというAPIに変わるそうです。

github.com

NetInfo.isConnectedはiOSでは常にfalseを返すという罠

issue上がってます

github.com

再現方法

NetInfo.isConnectedを使うだけ。 Androidはちゃんとしたネットワークの接続状況を返してくれますが、iOSは常にfalse…

原因

github.com

ワークアラウンド

なんと公式ドキュメントのサンプルにまで書いてあります…直さないのかな…

facebook.github.io

NetInfo.isConnected.fetch().then(isConnected => {
  console.log('First, is ' + (isConnected ? 'online' : 'offline'));
});
function handleFirstConnectivityChange(isConnected) {
  console.log('Then, is ' + (isConnected ? 'online' : 'offline'));
  NetInfo.isConnected.removeEventListener(
    'change',
    handleFirstConnectivityChange
  );
}
NetInfo.isConnected.addEventListener(
  'change',
  handleFirstConnectivityChange
);

地味にめんどい…

そもそもiOSのfetchAPIがネットがつながってないときにエラー返さないのが問題なんだけど…

【追記】 fetchにもtimeout設定されてた。

iOSのみ、ModalとAlertを同時に使うとModalが消えないバグがある

issue上がってます

github.com

再現方法

例えば通信処理中にインジケータ(くるくるするやつ)を出して、通信終わったらインジケータ消してレスポンスに応じてダイアログでユーザに通知したいとかあるじゃないですか。

このインジケータの表示にModalを使って通知にAlertIOSを使ったりすると起きます。

原因

Modalはshow/hide時はアニメーションを付加することができるのですが、このアニメーションの終了前にAlertを追加してしまうのが原因のようです。これは困った。

ワークアラウンド

いくつか上がってます

Modalのアニメーションを切る

一番変更の少ない方法です。上手く行くことを確認しました。 ProgressViewコンポーネントを作ってしまっていて、animationプロパティを変更するだけだったのでこれでとりあえず対応してます。 ただ、やっぱ見た目が悪い…

setTimeoutで終了を待つ

これはちょっと…。変更の手間が多いですね。

DroidKaigiに出席した

3月9,10日に新宿で開催されたDroidKaigi2017に参加してきました。

スピーカーでは無いです。

スピーカーの皆様、スタッフの皆様、超お疲れ様でした。

あの規模のイベント回すのはとても大変だし準備も相当大変だっただろうとお察しします。

個人的にも大満足のイベントでした。

2日間座りっぱなしで話聞くのって体力いるなって思いました、鍛えます。エンジニアって体力いるのね。

(訳あって1日目終わった後に御茶ノ水から西新宿まで1時間30分歩いたのが一番キツかったです

元々は全然行く気無かったんですが、仕事仲間が行くって話をCEOが聞いて「行かないの?経費で行ってきていいよ」って行ってくれたので行きました。ありがとうございました。

2月末でそもそも売り切れてるんじゃないかなーと思ってみてみたらちょうど増員したあとで参加出来ました。

基本的に私は勉強会に対しては

  1. 登壇者として行き、自分を知ってもらう。外との繋がりを作る
  2. 自分が日頃アンテナを張ってる情報やトライしている事に対する答え合わせ

という考えがあり、今回は2が目的で行ってきました。

今回に関してはDroidKaigiが日本で一番大きいAndroidアプリ開発者向けのカンファレンスということで

雰囲気を感じてみたいと言うのが一番でしたが。

聞いたセッションを個別に感想とか書くとすごく長くなるので省略しますが、

2日間終わってみて答え合わせ的には70〜80点位でした。

アンテナ張ってる領域に話は結構自分がわかる話も多く、張っているアンテナや自分の行動が

あまりズレていないということがわかり少しホッとしました。

これで全然分からない話ばかりだったらかなり自信無くしてましたね(笑

結構ニッチな領域だったりの話は分からない事が多かったのですが、聞いてて思ったのはやはり業務で使ってみてどうだったかという実践を踏まえた話ばかりだったということです。

「業務では使えないけど、興味あるから調べてみよう」

というのは(私の経験上)結構時間をムダにすることが多く、業務で使える領域を調べるか業務で使える領域を拡大していくかがやはり重要だなぁとしみじみ感じました。

やっぱ業務時間使ったほうが時間使えるし集中できるし責任もあるし成長早いよね。

何はともあれDroidKaigi楽しかったです。

来年は発表者になれたら行きます。

DroidKaigi関係無いけど宿をAirbnbで取ったのですが、最悪でした。ビジネスホテルの方がマシでした。

ReactNative製アプリをデバッグするときのTips

実機デバッグはVysorを使おう

chrome.google.com

実機の画面をキャプチャしてPCに転送してくれます。

キャプチャ画面でクリックやスクロール、タイピングもできるので非常に便利です。

ReactNativeのRELOAD機能はR2回押しで実機だと出来ないのでコレを使うと非常に便利です。

実機でデバッグオプションを表示する

デバッグオプションはオプションキーを押すと出てきますが、最近の端末はオプションキーがありません。

なので、adbを使ってオプションキーイベントを送ると表示されます。

 adb shell input keyevent KEYCODE_MENU

【追記】 なお端末をシェイクしても出るようです。

f:id:saburesan:20170228105827p:plain

基本はChromeコンソール、重いようだったらlogcatで

chromeコンソールデバッグはオブジェクトの中身がプルダウンで見ることができるので非常に便利です。 しかしながら非常に重かったり、そのせいで動作がカクつくことがあります。 chromeコンソールデバッグで無くてもよいのであれば、adb logcatを使いましょう

adb logcat | grep React

ReactでgrepするとReactNativeのログのみ見ることが出来ます。

react-native run-XXXがうまくいかないことがある

ReactNativeのプロセスが複数立ち上がっていたりするのでpsコマンドで確認して不要なプロセスはkillしましょう

ps aux | grep React