コードレビューで気付きにくい言語仕様の話

この文章は日記として書きました。 うちの会社のアプリが急に使えなくなったという話が弊社slackの#generalチャンネルで話されていた。気になって会話を遡り状況を整理してみると、iOS版及びWebは問題ないがAndroid版だけが週末からずっと通信できなくなっているとのことだった。この不具合が報告されているのは一人で自分を含め他の人は問題なく動いていた。僕の所属部署はそのアプリを保守する立場にあり、自分はそのアプリのコードベースに詳しかったので原因を調査してみることにした。
まず調査のきっかけとして、一番手軽でかつ情報がありそうなサーバーログをKibanaを使って探した。しかし直近のログから該当ユーザーが送信したと思われるものは1件も見当たらなかった。そこでネットワークリクエストを投げる前にクライアント側でエラーが発生していると考え、モバイルアプリ側のエラー収集サービスであるCrashlyticsで該当のuser_idに関連するエラーを探した。こちらは多数見つかった。日時の新しいセッションを一つ選びスタックトレースを追ってみるとAPIリクエストをする際に呼ばれる認証系のコード内で「String から longへの変換」に失敗してクラッシュしていることがわかった。初歩的で単純な例外に思えたが該当の行を読んでもなぜNumberFormatException: For input string "null"となるのかまるで理解できなかった。重要なのは null.toLong() ではなく "null".toLong()であるところだった。周辺のコードを読む限りにおいて文字列の"null"なんて入る余地が無いように思えた。僕の頭では解決できそうになかったので、認証周りに詳しい同僚に調査の続きをお願いした。
結果から言うと問題は同僚が解決してくれた。原因はKolinのAny?.toString()の振る舞いで、このメソッドは直感に反してString?ではなくStringを返り型とする。ではレシーバーがnullだった時にどうするか、文字列の"null"を返すのだ。原因がわかり問題は既に修正された。

toString - Kotlin Programming Language

Returns a string representation of the object. 
Can be called with a null receiver, in which case it returns the string "null".

KotlinとAndroidStudioの優れた静的解析のおかげでNullableの扱いによるミスは減ったように思うけど、今回のケースでは逆にIDEの検査もPR時のlintも警告しなかったことで安心して不具合を入れてしまったのではないかとふと思った。
nullがレシーバーの場合に"null"文字列を返す仕様はどのようにして入ってしまったんだろうかと思いを馳せた出来事だった。

2019-01~2019-02のお気に入りのツイート

時系列昇順です。

キングダムハーツはストーリーがとにかく複雑で異常に長いです。 一時間くらいで全てまとめた総集編動画もありますが、ニコニコ動画に投稿されている蘭たんのシリーズが面白いので復習におすすめです。

「キングダムハーツまとめ」 蘭たん さんの公開マイリスト - ニコニコ動画

ういうくんがtwitterにあげてくれる旅の様子刺激的で心惹かれる。

うちの姉貴は典型的なマイルドヤンキーなんですが幸せそうです。(本人にバレたら怒られそう)

仲良さそう。

いい話。

ステレオタイプなイギリス人っぽいエピソードでこれも好きです。

完成度が絶妙で笑う。

僕も美容師さんによく心配されます。

勢いがあって好き。

以前には13歳の頃に初めて乱交パーティをしたという衝撃的な告白も話題になったA$AP Rocky。30歳を迎えた今もその好みは変わっていないようだ。

記事の出だしが最高にいいです。

文豪

他人へのリプライを勝手に覗いちゃったんですが、最高のオマージュですね

無職になるということが具体的にはどういうことなのか、僕にはまだよくわかりません。

でも、いつかずっと先にどこかで偶然に職に就いたとしても、恥ずかしくないような人間になっていたいと僕は思います。

そのことを僕は前職に約束したいです。

前職のことが、ずっと好きでした。

どうか どうか元気で。

さようなら。

これ結局どういう意味なんだろう。

上半期ツイートオブザイヤー候補

かわいい

さりげないHxHネタ芸術点が高いです

たまにかわいい娘がいる妄想をするんですが、詳細はきもすぎるので割愛します。

想像するとまあまあ嫌。

可愛らしいライフハック

これはリツイートから拾ったんですが発想が好き

ワンピースやワンピースが好きな人を避難する意図はないんですが、クソ笑った

僕も似たような経験あります(小声)

エンジニア相手に語学を教えるの大変そう

ReactNativeアプリの作り方::Props&State

このページの目標

  • [ ] Reactコンポーネントの状態管理の仕組みを理解する
  • [ ] PropsとStateの特性を理解する
  • [ ] 画面を越えた状態の受け渡し方を理解する(ReactNavigation)

Props

PropsはReactコンポーネントに渡す引数を指します

渡す側

class App extends React.Component{
  render(){
    //JSXリテラルで指定されるものがProps(name&iconUrl)
    <Person  name={name} iconUrl={icon.url} />
    {Person(name:name, iconUrl:icon.url)} //上と等価の表現
  }
}

受け取り側

Propsはconstrucorの引数かそれ以外の関数ではthis.propsで参照できます。
自分自身のPropsを更新することは出来ませんが、親コンポーネントが新しいPropsを再度受け渡すことがあります。その場合componentDidUpdateが呼ばれます。componentDidUpdateは初期化時には呼ばれないので注意が必要です。

interface Props {
 name: string;
 iconUrl: string;
}

class Person extends React.Component<Props> {
  constructor(props: Props){
    super(props);
    const name = props.name;
  }
  componentDidUpdate(prevProps: Props){
     if(this.props.name !== prevProps.name) { 
        // do something.
     }
  }
}

State

Stateは名前の通りコンポーネントの状態を管理するオブジェクトです。コンポーネントはStateの変更を検知すると再描画する仕組みになっています。

interface Props {
  id: number;
}
interface State {
 isLoading: boolean;
 name?: string;
 iconUrl?: string;
}

class Person extends React.Component<Props> {
  constructor(props: Props){
    super(props);
    this.state = { isLoading: true }; //初期化はコンストラクタで直接代入する
  }
  async componentDidMount() {
     const response = await fetch(`https:exmaple.com/users/${this.props.id}`);
     const {name, iconUrl} = await response.json();
     this.setState({name, iconUrl, isLoading: false}); //状態の更新はthis.setState経由で行う
  }
  render() {
    if(this.state.isLoading) { // Stateを参照する場合は直接アクセス出来る
      return <LoadingView>
    } else {
      const {name, iconUrl} = this.state;
      return <ProfileRow name={name} iconUrl={iconUrl} />
    }
  }
}

Navigation.State

画面間で値を受け渡したい時はNavigation.Stateの仕組みを利用します。これはReactNavigationの提供する機能です。 navigation.navigate する際にparamプロパティを渡すと遷移先のコンポーネントではprops.navigation.state.paramsからアクセスが可能です。

const params = {userName: this.state.userName}
this.props.navigation.navigate({ routeName: HomeScreen.routeName, params }); // 受け渡し側
type Navigation = NavigationScreenProp<NavigationRoute<any>, any>; //Props型が複雑なためaliasを作成
interface Props {
  navigation: Navigation; 
}

class HomeScreen extends React.Component<Props> {
  constructor(props: Props) {
    super(props);
    const userName = props.navigation.state.params.userName; //受け取り側
  }
}

ReactNativeアプリの作り方::画面遷移

このページの目標

  • [ ] StackNavigatorを使いこなせるようになる
  • [ ] StackActionに使って変則的な画面遷移も出来るようになる

画面遷移の実装

ReactNavigation

フルスクラッチでの画面遷移実装は大変なのでReactNavigationを利用します。

セットアップ

yarn add react-navigation
yarn add @types/react-navigation -D
yarn start

提供されているNavigator

ReactNavigationはiOSAndroidでよく使われるいくつかの画面切り替えのためのUIコンポーネントを提供しています。今回は一番シンプルなStackNavigatorについて説明します。

  • StackNavigator
  • SwitchNavigator
  • DrawerNavigator
  • TabNavigator
  • BottomTabNavigator
  • MaterialBottomTabNavigator
  • MaterialTopTabNavigator

余談ですが、上記のNavigatorの動作見たいときはExpoで配信されているのでそちらでチェックすると便利です。 https://expo.io/@react-navigation/NavigationPlayground

StackNavigatorについて

StackNavigatorは基本となるもので、画面切り替えと遷移スタックの管理を提供します。何も設定しなくてもヘッダーなどはそれっぽい見た目に合わせてくれます。

実装サンプル

"Hello world!"を表示するだけのアプリがあると仮定して、これをStackNavigator経由で起動します。

Before

//App.tsx ここがルートコンポーネントとする
export default class App extends React.Component<{}, {}> {
  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.text}>Hello world!!</Text>
      </View>
    );
  }
}

After

// App.tsx
import { createStackNavigator, createDrawerNavigator, NavigationScreenProp, NavigationRoute } from 'react-navigation';
import HomeScreen from './HomeScreen';

// ルートコンポーネントをStackNavigatorに切り替える
export default createStackNavigator({
  [HomeScreen.routeName]: { screen: HomeScreen }
});

各画面に1:1対応するrouteNameというstatic stringを定義します。

// HomeScreen.tsx
export default class HomeScreen extends React.Component<{}, {}> {
  static routeName = '/HomeScreen';
  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.text}>Hello world!!</Text>
      </View>
    );
  }
}

画面遷移

ReactNavigationを経由してマウントされたコンポーネントPropsnavigationが入っています。これを利用して他画面への遷移操作ができます。

基本の画面遷移:navigate

createStackNavigatorに登録したスクリーンであればnavigation.navigate(routeName: "hoge")で遷移することができます。

const navigation = this.props.navigation
const params = {userName: "ReactNative太郎"} // 遷移先の画面に値が渡す
navigation.navigate({ routeName: HomeScreen.routeName, params });

複雑な画面遷移: dispatch+StackAction

例えば アプリを起動->ログイン画面->(ログイン成功)->ホーム画面 という画面遷移があった場合、ホーム画面でエッジスワイプや戻るボタン押された際はアプリは終了するべきだと思われます。しかしnavigateを使ってログイン画面からホーム画面に遷移してしまうと戻ることが出来てしまいます。
このような特殊なケースはStackActionを使って詳細な制御する方法があるので紹介します。下記には画面遷移はするがスタックに積まない場合のStackActionの設定を載せました。

https://reactnavigation.org/docs/en/stack-actions.html

const resetAction = StackActions.reset({
  index: 0,
  actions: [NavigationActions.navigate({ routeName: HomeScreen.routeName })],
});
this.props.navigation.dispatch(resetAction);

ReactNativeアプリの作り方::Fetch APIとFlatList

このページの目標

  • [ ] Fetch APIを使ってHttpリクエストが扱えるようになる
  • [ ] FlatListを使ってリスト表示が出来るようになる
  • [ ] APIレスポンスをFlatListでリストを表示する実装が出来るようになる

Fetch API

Fetch APIとは

  • 現代のXMLHttpRequestのようなもの
  • WHATWGによって標準化され、ほとんどのモダンブラウザで利用可能
  • ReactNativeでも同様のインターフェースで使える

https://developer.mozilla.org/ja/docs/Web/API/Fetch_API/Using_Fetch

GET

const response = await fetch('https://example.com/images/1');

POST

const request = {
  method: 'POST',
  headers: {
    Accept: 'application/json',
    'content-type': 'application/json',
  },
  body: JSON.stringify({image: base64String}),
};
const response = await fetch('https://example.com/images', request)

Responseを扱う

エラー時(4xx,5xx)も例外を吐かないので正常なやり取りが出来たか始めに確認する必要があります。

if(response.ok) { //200系が返ってきていることを確認
  return await response.json(); //レスポンス内容は非同期で受け取る
}

https://developer.mozilla.org/ja/docs/Web/API/Response

TypeScriptで型をつけてwrapする

FetchAPIはそのまま使うとanyを扱う必要があったり、クエリパラメータのエンコード機能がなかったりするので簡易的にWrapしたAPIClientを経由して利用します。

const API_ENDPOINT = 'https://exmaple.com'
type Params = { [key: string]: string | number };

class ApiClient {
  get = async <T>(path: string, params: Params = {}): Promise<T> => {
    const url = this.createUrl(path, params);
    const response = await fetch(url);
    if (response.ok) { 
      return (await response.json()) as T; 
    } else {
      const status = response.status;
      const body = await response.text();
      throw new Error(`status: ${status}, body: ${body}`);
    }
  };

  private createUrl(path: string, params: Params = {}): string {
    let url: string;
    if (Object.keys(params).length == 0) {
      const encoded_params = this.encoded_params(params);
      url = `${API_ENDPOINT}/${path}?${encoded_params.join('&')}`;
    } else {
      url = `${API_ENDPOINT}/${path}`;
    }
    return url;
  }

  private encoded_params(params: Params): string[] {
    return Object.keys(params).map(
      key => `${encodeURIComponent(key)}=${encodeURIComponent(String(params[key]))}`,
    );
  }
}
export default new ApiClient();

型を指定してGET

intreface Image {
  id: number;
  url: string;
  width: number;
  height: number;
}
const image = await = ApiClient.get<Image>(`images/${imageId}`);
return image.url;

FlatList

FlatListとは

  • ReactNative公式のリストを表示するのコンポーネント
  • 多機能でかつインターフェースが扱いやすい
  • FlatListのためにReactNativeが使いたくなるレベル

https://facebook.github.io/react-native/docs/flatlist

最小実装

<FlatList
  data={[{key: 'a'}, {key: 'b'}]} //データセットの提供
  renderItem={({item}) => <Text>{item.key}</Text>} //描画方法の指定
/>

APIからデータを受け取り表示

架空のAPIから過去のメッセージの一覧を取得する実装例です。ポイントは3点あります。

  • componentDidMountAPIリクエストする
  • 結果をStateに保存する
  • FlatListのデータソースはStateを参照する
interface Props {}
interface State {
  messages: Message[];
}

interface Message {
  id: number;
  text: string;
}

class App extends React.Component<Props, State> {
  //コンポーネントが描画されるタイミングで呼ばれるイベント
  async componentDidMount() { 
    const messages = await ApiClient.get<Message[]>('messages/1');
    this.setState({ messages }); // コンポーネントの状態を差分更新
  }
  render() {
    return (
      <FlatList
        keyExtractor={item => { return String(item.id);}}
        data={this.state.messages}
        renderItem={({ item }: { item: Message }) => <Text>{item.text}</Text>}
      />
    );
  }
}

FlatListの便利機能

ネイティブ実装だとかなり苦労するPull-to-Refreshや無限スクロールも数行で実装できます。今回は省略しますがヘッダー,フッター,複数カラムや横向きのスクロールなどで手軽に作成できます。

Pull-to-Refresh

      <FlatList
        keyExtractor={item => { return String(item.id);}}
        data={this.state.messages}
        renderItem={({ item }: { item: Message }) => <Text>{item.text}</Text>}
+       onRefresh={() => { handleOnRefresh(); }} //再読込処理を呼び出す
+       refreshing={this.state.refreshing} //リフレッシュ処理が走っているか状態をセット
      />

無限スクロール

      <FlatList
        keyExtractor={item => { return String(item.id);}}
        data={this.state.messages}
        renderItem={({ item }: { item: Message }) => <Text>{item.text}</Text>}
+       onEndReached ={() => { handleonEndReached(); }} //次ページのデータを読み込む
      />

ReactNativeアプリの作り方::HelloWorld

このページの目標

クライアントアプリケーションの作り方

一般的にクライアントアプリケーションはUIコンポーネントの集合体なので、「コンポーネントの作り方」と「レイアウトの組み方」が理解すると静的なアプリケーションが作成出来ます。 前半はReactNativeでのコンポーネントの作り方とレイアウトの方法を説明していきます。

コンポーネントとは?

Cookpadアプリを例にすると、"ヘッダー"と"メイン"と"フッター"の大きく3つのコンポーネントに分類できます。 それぞれのコンポーネントは更に小さいコンポーネントに分類できます。例えば"ヘッダー"を作るなら下記のような手順になるでしょう。

他のコンポーネントも複雑度は違いますが手順が増えますが大枠は同じです。

f:id:kazy1991:20180724044849p:plain

HelloWorld!

ReactNativeではReactの作法でコンポーネントを作成します。

  • React.Component を継承したクラスを作る
  • render メソッドに表示したい内容を書く

WebのReactと異なる点はrender内で<div>要素などの要素を利用するかreact-nativeが用意したコンポーネントを利用するかの違いのみです。

公式ドキュメント上でReactNativeの振る舞いを試す事が出来るので、このページでHelloWorldを表示させましょう。 https://facebook.github.io/react-native/docs/tutorial.html#hello-world

import React from 'react';
import { StyleSheet, Text, View } from 'react-native';

export default class App extends React.Component {
  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.text}>Hello world!!</Text>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },
  text: {
    color: 'gray',
    fontSize: 32,
  },
});

カスタムコンポーネントを描画する

View, Text などと同様に自作コンポーネントも簡単に描画する事が出来ます。BuleTextComponentという青い文字列の"HelloWold!"を表示するコンポーネントを作って先程のコードに追加して自作コンポーネントが描画されることを確認しましょう。
ReactNativeの開発ではあるまとまったUIパーツを一つのコンポーネントに纏めることで見通しを良くしたり、再利用性を高めたりします。

import React from 'react';
import { StyleSheet, Text, View } from 'react-native';

export default class App extends React.Component {
  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.text}>Hello world!!</Text>
        <BlueTextComponent />
      </View>
    );
  }
}

class BlueTextComponent extends React.Component {
  render() {
    return (
      <Text style={{color:'blue'}}>Hello world!!</Text>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },
  text: {
    color: 'gray',
    fontSize: 32,
  },
});

Peril: GithubAppとして動くDanger

これは下記のブログざっと読んだメモです。

artsy.github.io

概要

PerilはDangerをホストするアプリケーションです。Dangerは通常CIサーバーで動かしますが、Perilを導入すると自らwebhookを受け取りPerilサーバー上でDangerスクリプトが実行されます。
PerilプロジェクトはCIサーバーからDangerの実行環境を独立させることを目的としています。これによってissueの更新などのイベントフックが可能になります。 PerilはGithubAppとして動きます。Herokuでの動作手順は下記に紹介されています。

peril/setup_for_org.md at master · danger/peril · GitHub

なぜPerilをつくったのか

作者のブログに色々と書いてありますが昔から構想はあったそうです。結局の所CIサーバー上で動かしている限り制約が多いので、独立したサーバーとして動かして多くの作業をDangerに任せたいからだと理解しました。 CocoaPods orgではissueとPRの管理で実際に運用されています。

github.com

Perilの設定ファイル

Artsy社で実際に使われているPerilの設定ファイルは下記のようです。Org単位で設定を書く形になります。任意のwebhookに任意のscriptを手軽に動かせるのがPerilの魅力のようです。

{
  "settings": {
    "modules": [
      "danger-plugin-spellcheck",
      "danger-plugin-yarn",
      "@slack/client"
    ],
    "env_vars": ["SLACK_RFC_WEBHOOK_URL"]
  },
  "rules": {
    "pull_request": "artsy/artsy-danger@org/all-prs.ts"
  },
  "repos" : {
    "artsy/reaction": {
      "pull_request": "danger/pr.ts"
    },
    "artsy/positron": {
      "pull_request": "dangerfile.ts"
    },
    "artsy/artsy-danger": {
      "issues.opened": "artsy/artsy-danger@danger/new_rfc.ts"
    }
  }
}

おわり

Perilはまだ開発途中で問題をいくつか抱えています。またPerilはDanger.jsで動くのでGithub以外に対応していないほかDaner(Ruby)と比べると安定性に欠けます。それでもこういうものを求めていたという人はぜひ導入してみると良さそうです。