KotlinJS x ReactNativeにトライしてみた

前回でKotlinJSの使い方がチョットだけわかったので、ReactNativeで動くようにしてみたいと思います。

プロジェクト作成。

Intellij ideaで進めていきます。

KotlinJSプロジェクトを作ります。

次のそのプロジェクトのrootでReactNativeのプロジェクトを作ります。

react-native init rn

とりあえず図のような構成にします。

f:id:anect:20171207110148p:plain

コンパイル結果のディレクトリを調整する

Project Structure -> Project Settings -> Projectを開きます。

f:id:anect:20171206182309p:plain

Project compiler outputをReactNativeプロジェクトのnode_modulesにします

変更後

f:id:anect:20171207124119p:plain

次にProject Structure -> Project Settings -> Modulesを開きます。

f:id:anect:20171206180923p:plain

図のCompiler outputを作成したReactNativeプロジェクトのnode_modulesに変更します。

変更後

f:id:anect:20171207124203p:plain

次に、Preferences -> Build, Execution ... -> Compiler -> Kotlin Compilerを開きます。

f:id:anect:20171206181338p:plain

前回もありましたが、Destination directoryModule kindを以下のように変更します。

f:id:anect:20171207124244p:plain

これでプロジェクトの設定は完了です。

ReactNativeでHelloWorld

では、ハロワしましょう。

index.jsはReact Nativeが直接参照しているので変更してはいけません。

とりあえずはrnディレクトリにあるApp.jsを変更して今回目標とするハロワのコードを書いてみましょう。

App.js

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

module.exports = () =>
    <View style={{flex: 1, alignItems: 'center', justifyContent: 'center'}}>
        <Text>Hello World</Text>
    </View>;

これをiPhoneSimulatorで確認します。

react-native run-ios

f:id:anect:20171207104708p:plain

目標はこんな感じです。

App.ktを作成

src以下にApp.ktファイルを作成します

コンパイルが通る程度に適当にApp.jsっぽい感じに書いてみましょう

import kotlin.js.json

@JsModule("react")
external object React {
    class Component
    fun createElement(element:Component, props: dynamic, children: dynamic)
}

@JsModule("react-native")
external object ReactNative {
    val View: React.Component
    val Text: React.Component
}

fun App():dynamic = React.createElement(
        ReactNative.View,
        json(Pair("style", json(
                Pair("flex", 1),
                Pair("alignItems", "center"),
                Pair("justifyContent", "center")
        ))),
        React.createElement(ReactNative.Text, null, "Hello World"));

JSXでは書けないのでReact.createElementで書きます。

json

これはKotlinJSの機能でJavaScriptJSONに変換できる機能です。

json - Kotlin Programming Language

以下がコンパイル結果です

(function (_, Kotlin, $module$react, $module$react_native) {
  'use strict';
  var Pair = Kotlin.kotlin.Pair;
  var json = Kotlin.kotlin.js.json_pyyo18$;
  function App() {
    return $module$react.createElement($module$react_native.View, json([new Pair('style', json([new Pair('flex', 1), new Pair('alignItems', 'center'), new Pair('justifyContent', 'center')]))]), $module$react.createElement($module$react_native.Text, null, 'Hello World'));
  }
  _.App = App;
  Kotlin.defineModule('ReactNativeKotlin', _);
  return _;
}(module.exports, require('kotlin'), require('react'), require('react-native')));

コンパイル結果みるとイケそうな感じ

index.jsを少し変更します。

import { AppRegistry } from 'react-native';
import { App } from 'ReactNativeKotlin';

AppRegistry.registerComponent('rn', () => App);

実行

f:id:anect:20171207113751p:plain

...

...

...

(あれ、読み込みが遅い…)

...

...

f:id:anect:20171207113847p:plain

駄目だったか…

原因を探す

とりあえずbundleだけしてみましょう。

% react-native bundle --entry-file index.js --bundle-output ./ --dev false --platform ios
Scanning folders for symlinks in /Users/Ryohlan/dev/kotlin/ReactNativeKotlin/rn/node_modules (15ms)
Scanning folders for symlinks in /Users/Ryohlan/dev/kotlin/ReactNativeKotlin/rn/node_modules (14ms)
Loading dependency graph, done.

...

...

bundleも終わらない。

kotlin.jsがbundle出来ない

コンパイル結果を色々いじって見た結果、kotlin.jsをrequireするとbundleが上手く行きません。

ログにタイムアウトが出てたのでmetro-bundlerのタイムアウト時間を伸ばしてみたけど駄目でした。

とりあえず、kotlin.jsからコンパイル結果で使っているものを抜粋して動かしてみます。

function Pair(first, second) {
  this.first = first;
  this.second = second;
}
Pair.prototype.toString = function () {
  return '(' + this.first + ', ' + this.second + ')';
};
Pair.prototype.component1 = function () {
  return this.first;
};
Pair.prototype.component2 = function () {
  return this.second;
};

function json(pairs) {
  var tmp$;
  var res = {};
  for (tmp$ = 0; tmp$ !== pairs.length; ++tmp$) {
    var tmp$_0 = pairs[tmp$];
    var name = tmp$_0.component1(), value = tmp$_0.component2();
    res[name] = value;
  }
  return res;
}

var Kotlin = {
  defineModule: function(a, b) {},
  kotlin: {
    Pair: Pair,
    js: {
      json_pyyo18$: json,
    }
  }
};

(function (_, Kotlin, $module$react, $module$react_native) {
  'use strict';
  var Pair = Kotlin.kotlin.Pair;
  var json = Kotlin.kotlin.js.json_pyyo18$;
  function App() {
    return $module$react.createElement($module$react_native.View, json([new Pair('style', json([new Pair('flex', 1), new Pair('alignItems', 'center'), new Pair('justifyContent', 'center')]))]), $module$react.createElement($module$react_native.Text, null, 'Hello World'));
  }
  _.App = App;
  Kotlin.defineModule('ReactNativeKotlin', _);
  return _;
}(module.exports, Kotlin, require('react'), require('react-native')));

f:id:anect:20171207170945p:plain

ちゃんと表示されました。

やはりkotlin.jsの何かが問題なようです。(37000行あるので全部見れてません…)

うーむ、いいところまで行った気がしたんですがね。

まとめ

現状だと普通に動かすのは無理のようです。

kotlin.jsはKotlinの機能をjsに置き換える処理が書かれているので無くすことは出来ないので、修正されるかReactNative用の何かが出るまで待つしかないですかね。

まぁReactNativeでKotlinJS使いたいって要望無さそうですが…

と思ったらいるっぽい。

Use Kotlin with npm, webpack and react | Kotlin Blog

f:id:anect:20171207172121p:plain

KotlinJSを動かす

エンジニアの原です。

先日Kotlin1.2がリリースされました。

blog.jetbrains.com

バックエンド、Webフロントエンド、Androidでコードの共通化ができるようになったとこのことです。

今回はその機能は触らないんですが、前にリリースされたKotlinJSとKotlinNativeには興味がありました。

で最近はずっとReactNative, ReactSPAばっかりやってて、ふと思いました。

React NativeのAndroidのコードはKotlin化は余裕

KotlinJSでKotlinでJSが吐き出せる

KotlinNativeでiOSが動かせる

「これはReactNativeはKotlinだけで動かせるのでは?」

ということで、色々試してみます。

KotlinJSを動かす

まずここからです。先は長い。

とりあえずハロワ

プロジェクト作成

f:id:anect:20171205163310p:plain f:id:anect:20171205163331p:plain

エントリポイントを作成

fun main(args: Array<String>) {
    console.log("Hello, World")
}

⌘9でビルド

f:id:anect:20171206102359p:plain

outディレクトリにコンパイル結果が入ってます。エラーは無かったようです。

HelloWorld.js

HelloWorld.js見てみましょう

if (typeof kotlin === 'undefined') {
  throw new Error("Error loading module 'HelloWorld'. Its dependency 'kotlin' was not found. Please, check whether 'kotlin' is loaded prior to 'HelloWorld'.");
}
var HelloWorld = function (_, Kotlin) {
  'use strict';
  function main(args) {
    console.log('Hello, World');
  }
  _.main_kand9s$ = main;
  main([]);
  Kotlin.defineModule('HelloWorld', _);
  return _;
}(typeof HelloWorld === 'undefined' ? {} : HelloWorld, kotlin);

Kotlinというグローバルオブジェクトが無いと駄目のようです。

kotlin.js

f:id:anect:20171206102606p:plain

outディレクトリにlibというディレクトリもできており、そのなかにkotlin.jsというのがあります。

これをグローバルオブジェクトとして定義するとHelloWorld.jsが動きそうです。

実行

> const kotlin = require('./out/production/HelloWorld/lib/kotlin');
undefined
> require('./out/production/HelloWorld/HelloWorld');
Hello, World
{}

動きました。

他に何ができるかドキュメントを読んでみます。

JSのコードをKotlin上で書く

Calling JavaScript from Kotlin - Kotlin Programming Language

js()を使うとJSのコードが動くみたいです。ちょっと試してみましょう。

fun main(args: Array<String>) {
    val time: String = js(" new Date().toString()")
    console.log(time)
}

コンパイル結果

if (typeof kotlin === 'undefined') {
  throw new Error("Error loading module 'HelloWorld'. Its dependency 'kotlin' was not found. Please, check whether 'kotlin' is loaded prior to 'HelloWorld'.");
}
var HelloWorld = function (_, Kotlin) {
  'use strict';
  function main(args) {
    var time = (new Date()).toString(); // 新しいコード
    console.log(time);
  }
  _.main_kand9s$ = main;
  main([]);
  Kotlin.defineModule('HelloWorld', _);
  return _;
}(typeof HelloWorld === 'undefined' ? {} : HelloWorld, kotlin);
> const kotlin = require('./out/production/HelloWorld/lib/kotlin');
undefined
> require('./out/production/HelloWorld/HelloWorld');
Wed Dec 06 2017 10:27:43 GMT+0900 (JST)
{}

動きました。

nodeで動かす前提でkotlin.jsをrequireするコードも追加してみましょう。

fun main(args: Array<String>) {
    js("require('./out/production/HelloWorld/lib/kotlin')")
    val time: String = js(" new Date().toString()")
    console.log(time)
}

コンパイル結果

if (typeof kotlin === 'undefined') {
  throw new Error("Error loading module 'HelloWorld'. Its dependency 'kotlin' was not found. Please, check whether 'kotlin' is loaded prior to 'HelloWorld'.");
}
var HelloWorld = function (_, Kotlin) {
  'use strict';
  function main(args) {
    require('./out/production/HelloWorld/lib/kotlin');
    var time = (new Date()).toString();
    console.log(time);
  }
  _.main_kand9s$ = main;
  main([]);
  Kotlin.defineModule('HelloWorld', _);
  return _;a
}(typeof HelloWorld === 'undefined' ? {} : HelloWorld, kotlin);
> const kotlin = require('./out/production/HelloWorld/lib/kotlin');
undefined
> require('./out/production/HelloWorld/HelloWorld');
Wed Dec 06 2017 10:37:43 GMT+0900 (JST)
{}

動いたけど…違う。自動でrequireして欲しい…

調べてみるとKotlin Compilerの設定でCommonJSの形にもできるようです。

f:id:anect:20171206105011p:plain

早速CommonJSにしてコンパイルしてみます。

(function (_, Kotlin) {
  'use strict';
  function main(args) {
    console.log('Hello, World');
  }
  _.main_kand9s$ = main;
  main([]);
  Kotlin.defineModule('HelloWorld', _);
  return _;
}(module.exports, require('kotlin')));

おっ、requireしてくれてる。 しかしデフォルトではlibkotlin.jsが吐かれるのでこれだとエラーが出ます。

そこで先程のKotlin Compilerの設定でDestination directoryという設定がありましたが、これをnode_modulesに変更します。

f:id:anect:20171206105650p:plain

コンパイルすると

(function (root, factory) {
  if (typeof define === 'function' && define.amd)
    define(['exports', 'kotlin'], factory);
  else if (typeof exports === 'object')
    factory(module.exports, require('kotlin'));
  else {
    if (typeof kotlin === 'undefined') {
      throw new Error("Error loading module 'HelloWorld'. Its dependency 'kotlin' was not found. Please, check whether 'kotlin' is loaded prior to 'HelloWorld'.");
    }
    root.HelloWorld = factory(typeof HelloWorld === 'undefined' ? {} : HelloWorld, kotlin);
  }
}(this, function (_, Kotlin) {
  'use strict';
  function main(args) {
    console.log('Hello, World');
  }
  _.main_kand9s$ = main;
  main([]);
  Kotlin.defineModule('HelloWorld', _);
  return _;
}));

大分変わりましたね。

ちゃんとnode_modulesにコンパイル結果が入ってます。

f:id:anect:20171206110107p:plain

実行してみます。

% node out/production/HelloWorld/HelloWorld.js
Hello, World

やったぜ。

KotlinでNodeのサーバー立てる

JSのライブラリをKotlin側から使うのはどうするのでしょうか。

とりあえず雰囲気で書いてみる

interface Http {
    fun createServer(onRequest: (req: Any, res: Res) -> Unit): Proxy

    interface Proxy {
        fun listen(port: Int, callback: () -> Unit)
    }

    interface Res {
        fun writeHead(statusCode: Int)
        fun end(message: String)
    }
}

fun main(args: Array<String>) {
    val http: Http = js("require('http')")
    http.createServer({ _, res ->
        res.writeHead(200)
        res.end("Hello World")
    }).listen(8080, { console.log(it)})
}

コンパイルは通る。

実行してみる。

% node out/production/HelloWorld/HelloWorld.js
/Users/Ryohlan/dev/kotlin/HelloWorld/out/production/HelloWorld/HelloWorld.js:37
    http.createServer_jnnk04$(main$lambda).listen_n53o35$(8080, main$lambda_0);
         ^

TypeError: http.createServer_jnnk04$ is not a function
    at main (/Users/Ryohlan/dev/kotlin/HelloWorld/out/production/HelloWorld/HelloWorld.js:37:10)
    at /Users/Ryohlan/dev/kotlin/HelloWorld/out/production/HelloWorld/HelloWorld.js:43:3
    at Object.<anonymous> (/Users/Ryohlan/dev/kotlin/HelloWorld/out/production/HelloWorld/HelloWorld.js:46:2)
    at Module._compile (module.js:635:30)
    at Object.Module._extensions..js (module.js:646:10)
    at Module.load (module.js:554:32)
    at tryModuleLoad (module.js:497:12)
    at Function.Module._load (module.js:489:3)
    at Function.Module.runMain (module.js:676:10)
    at startup (bootstrap_node.js:187:16)

ですよねー。

そもそもcreateServer_jnnk04$ってなってしまってるのでなんとかしないと

external

これはKotlinコンパイラにそれが生のJSインターフェースだと伝える手段です。

これをHttpにつけてコンパイルしてみます。

external interface Http {
    fun createServer(onRequest: (req: Any, res: Res) -> Unit): Proxy

    interface Proxy {
        fun listen(port: Int, callback: () -> Unit)
    }

    interface Res {
        fun writeHead(statusCode: Int)
        fun end(message: String)
    }
}

fun main(args: Array<String>) {
    val http: Http = js("require('http')")
    http.createServer({ _, res ->
        res.writeHead(200)
        res.end("Hello World")
    }).listen(8080, { console.log("localhost:8080")})
}
% node out/production/HelloWorld/HelloWorld.js
localhost:8080

f:id:anect:20171206112907p:plain

やったぜ。

ついでにrequireもexternalにします。

しかし問題が。

requireは読み込むモジュールごとに返り値が違うので通常だとコンパイルが通りません。

Dynamic Type

これはKotlinJSのための型定義です。

dynamic型を使うとその型はKotlinのタイプチェックから無視されるのでコンパイルが通ります。

今回だと

external fun require(path:String):dynamic

fun main(args: Array<String>) {
    val http = require("http")
    http.createServer({ _, res ->
        res.writeHead(200)
        ...

このようにして使うとcreateServerは補完には出ませんがタイプチェックを無視されるのでコンパイルは通ります。

補完を効かせるためにhttpの型をつけて最終的には以下のようになります。

external interface Http {
    fun createServer(onRequest: (req: Any, res: Res) -> Unit): Proxy

    interface Proxy {
        fun listen(port: Int, callback: (message: String) -> Unit)
    }

    interface Res {
        fun writeHead(statusCode: Int)
        fun end(message: String)
    }
}

external fun require(path:String):dynamic

fun main(args: Array<String>) {
    val http: Http = require("http")
    http.createServer({ _, res ->
        res.writeHead(200)
        res.end("Hello World")
    }).listen(8080, { console.log("localhost:8080")})
}

追記

@JsModuleでrequireいらず

ドキュメント見てたら@JsModuleを使うとrequire要らずということが分かりました。

なので更にスマートに

@JsModule("http")
external object Http { // interfaceだと実装がないのでコンパイル通らない。
    fun createServer(onRequest: (req: Any, res: Res) -> Unit): Proxy

    interface Proxy {
        fun listen(port: Int, callback: (message: String) -> Unit)
    }

    interface Res {
        fun writeHead(statusCode: Int)
        fun end(message: String)
    }
}

fun main(args: Array<String>) =
        Http.createServer{ _, res ->
            res.writeHead(200)
            res.end("Hello World")
        }.listen(8080, { console.log("localhost:8080")})

コンパイル

(function (_, Kotlin, $module$http) {
  'use strict';
  var Unit = Kotlin.kotlin.Unit;
  function main$lambda(f, res) {
    res.writeHead(200);
    res.end('Hello World');
    return Unit;
  }
  function main$lambda_0(it) {
    console.log('localhost:8080');
    return Unit;
  }
  function main(args) {
    $module$http.createServer(main$lambda).listen(8080, main$lambda_0);
  }
  _.main_kand9s$ = main;
  main([]);
  Kotlin.defineModule('HelloWorld', _);
  return _;
}(module.exports, require('kotlin'), require('http')));

まとめ

思ってたより全然簡単でした。

JSのライブラリをKotlin側から呼ぶにはすべてのインターフェースを定義する必要があるので依存ライブラリが増えると大変そう…

次回はReactNativeのJSコードをKotlin書いてみたいと思います。

ReactXPでHello World

エンジニアの原です。

今日はReactXPで遊んでみます。

ReactXPとは?

ReactXPとはMicrosoftが開発しているリポジトリで、ReactNativeをベースにWebとWindowsPlatform(Windows 10, 10mobile, Xbox)でのアプリ開発ができるようにしたマルチプラットフォーム対応のフレームワークです。

XP...?

XP means X-Platform Share most of your code between the web, iOS, Android, and Windows.

間違ってもあっちのXPじゃありません。

ReactNativeではJavaScriptを使って開発をしますが、ReactXPはTypeScriptです。

サンプルを動かしてみる

github.com

公式のリポジトリsamplesというフォルダがあります。この中のhello-worldをコピーして使いましょう。

package.jsonを覗いてみる

{
  "name": "rxp-hello-world",
  "version": "1.0.0",
  "private": true,
  "main": "index.js",
  "scripts": {
    "web-watch": "webpack --progress --colors --watch",
    "rn-watch": "tsc --watch",
    "start": "node node_modules/react-native/local-cli/cli.js start",
    "android": "node node_modules/react-native/local-cli/cli.js run-android",
    "ios": "node node_modules/react-native/local-cli/cli.js run-ios"
  },
  "devDependencies": {
    "@types/node": "^7.0.12",
    "@types/webpack": "^2.2.14",
    "awesome-typescript-loader": "3.1.2",
    "source-map-loader": "^0.1.6",
    "ts-node": "^3.2.1",
    "typescript": "2.4.1",
    "webpack": "2.2.1"
  },
  "dependencies": {
    "react": "16.0.0-rc.3",
    "react-dom": "16.0.0-rc.3",
    "react-native": "^0.46.0",
    "react-native-windows": "^0.33.0",
    "reactxp": "^0.46.2",
    "reactxp-imagesvg": "^0.2.7",
    "reactxp-navigation": "^1.0.14",
    "reactxp-video": "^0.2.2"
  }
}

Hello worldサンプルなので少ないですね。

とりあえずこれから分かることは、

  • WebはReact
  • iOS/AndroidはReact Native
  • Widnowsはreact-native-windows
  • Web版のビルドはweb-watch
  • TSのコンパイルrn-watch
  • react-nativeのcli.jsを直接叩いてるだけ

ということが分かります(あたりまえか とりあえず動かしてみましょう。

ネイティブアプリを動かす

TSをコンパイルします

npm run rn-watch

react-nativeのお作法

npm run start // サーバーを起動
npm run ios // iOSアプリを起動

https://gyazo.com/89f374ea96f34703f5f75c75c2d0a855

動きました。

アニメーションがぬるっとしてる。Skypeっぽい。

https://gyazo.com/dd0c6d5c6845628756af4304dd59c647

デバッグモードも問題なく使えますね。

Webを動かしてみる

TSをコンパイルします

npm run rn-watch

Web用のbundle.jsを作成

npm run web-watch

webのエントリポイントはrootのindex.htmlです。 とりあえずBrowserにファイルパスを指定してみます。

https://gyazo.com/d47e5982b094340065ebb531dd50640f

動きましたね。

画面遷移もちゃんとアニメーションしてるのは意外でした。

ただ、サンプルの画面遷移はBrowserのヒストリには残らないようです。

Windows機無いのでWindowsPlatformsの確認は出来ません。

です。

react-nativeの部分を見てみる

react-nativeはプロジェクトrootにios, androidというフォルダが作られ、その中身は純粋なiOS, Androidのプロジェクトになっています。 ReactXPもios, androidというフォルダがあるので中身がどう違うか見てみましたが、特に大きな変更点は見られませんでした。

MainActivity.java

package com.rxphelloworld;

import com.facebook.react.ReactActivity;

public class MainActivity extends ReactActivity {

    /**
     * Returns the name of the main component registered from JavaScript.
     * This is used to schedule rendering of the component.
     */
    @Override
    protected String getMainComponentName() {
        return "RXApp";
    }
}

AppDeletege.m

/**
 * Copyright (c) 2015-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 */

#import "AppDelegate.h"

#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  NSURL *jsCodeLocation;

  jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index.ios" fallbackResource:nil];

  RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
                                                      moduleName:@"RXApp"
                                               initialProperties:nil
                                                   launchOptions:launchOptions];
  rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1];

  self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
  UIViewController *rootViewController = [UIViewController new];
  rootViewController.view = rootView;
  self.window.rootViewController = rootViewController;
  [self.window makeKeyAndVisible];
  return YES;
}

@end

Webの部分を見てみる

Webのエントリポイントはrootのindex.htmlです。

<!doctype html>
<html>
<head>
  <title></title>
  <style>
    html, body, .app-container {
      width: 100%;
      height: 100%;
      padding: 0;
      border: none;
      margin: 0;
      font-family: proxima-nova, "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif
    }
    *:focus {
        outline: 0;
    }
  </style>
</head>
<body>
  <div class="app-container"></div>
  <script src="dist/bundle.js"></script>
</body>
</html>

簡素。

まとめ

ReactXPでHelloWorldしてみました。 まだガッツリ使う予定は無いのでまた暇があったら掘り下げます。

Kotlinのlet, apply, run, also

エンジニアの原です。

twitterのタイムラインに

「runとalsoの使い所分からん」

というツイートが流れてきたので。

Kotlinには便利な拡張関数があるのですが、それぞれが微妙に違うので用途を結構迷います。

そこでrun, alsoに加えよく使うであろうletとapplyの4つの特徴から用途を考えていこうと思います

定義の確認

run

public inline fun <T, R> T.run(block: T.() -> R): R = block()
  • レシーバの拡張関数
  • 任意の型を返す
  • thisはレシーバ

let

public inline fun <T, R> T.let(block: (T) -> R): R = block(this)
  • レシーバの拡張関数
  • 任意の型を返す
  • スコープ内外でthisが同じ

apply

public inline fun <T> T.apply(block: T.() -> Unit): T { block(); return this }
  • レシーバの拡張関数
  • 返り値はレシーバ
  • thisはレシーバ

also

public inline fun <T> T.also(block: (T) -> Unit): T { block(this); return this }
  • レシーバの拡張関数
  • 返り値はthis
  • スコープ内外でthisが同じ

違い

返り値がレシーバ(apply, also)

apply, alsoは返り値がレシーバに固定されます。 なのでレシーバの内部状態を変えるような処理(初期化など)が想定されます。

返り値が任意(run, let)

run, letは返り値がスコープ内の最後の処理の型になります。 つまり、レシーバを加工したい場合に使う事ができます。 加工はapply, alsoでは出来ない処理です。

スコープ内thisがレシーバ(run, apply)

レシーバ内のthisがレシーバに固定されます。 レシーバのメソッドを呼んだり、レシーバに対する処理を行うときは使いやすい。 反面、スコープ外のthisに参照したいときはthis@~~と各必要がある。

スコープ内外でthisが同じ(let, also)

tスコープ内のthisがスコープ外と同じです。thisが変わらないのでthis@~~と書く必要が無いので楽。 反面、レシーバはitなどで受け取らないといけない。 スコープ内でthisを多用するときは良いかも。

用途

上記を踏まえて私が思った使いどころ

let

ある値の加工だったり変換に使う

val age = 10
val ageStr = age.let{ "age : $it" }

//Nullable時とかよく見るよね
age?.let { " age: $it" }

letはitでレシーバにthisでスコープ外にアクセスできて任意の型を返せるので、

レシーバの加工や変換には一番適していると思います。

apply

初期化処理、メソッドの複数呼び出し

val human = Human().apply { 
   name = "sabure"
   age = "30"
   context = this@HogeActivity
}

applyはthisがレシーバでレシーバを返すので内部状態の変更・初期化処理などに向いているかと思います。

あとはthis@~が複数在るような場合はalsoだと混乱しそうなのでapplyが良いかと思います。

run

エルビス演算時のnullの時の処理?

val name = n ?: run {
   ...
   ...
}

KotlinのNullチェックはエルビス演算子を使うと思うのですが、

nullの時に幾つか処理をして返り値が必要な時などに使えるのではないでしょうか。

also

applyで処理するにはthisの参照が多そうなときかな…

val human = Human().also { 
   setOnClickA { this.startActivity(...) }
   setOnClickB { this.startActivity(...) }
   setOnClickC { this.startActivity(...) }
}

うーん、alsoはあんまり良い使い方思いつきませんね…

返り値がレシーバ固定なのでレシーバに関係ない事をするあまり良くないですし、

そう考えるとthisもレシーバのapplyの方がalsoに比べると使う機会が多そうな気がします。

まとめ

Kotlinの拡張関数は基本的に使ってて気持ちいいくらいなんですが、

それぞれの特徴を踏まえて用途を考えて使わないと、メンバー内などで書き方の統一が難しくなるので気をつけないといけませんね。

全部letでもいけるっちゃいけますからね。

新しい技術導入に関する勉強会で発表してきました

エンジニアの原です。

昨日、「新しい技術導入に関する勉強会」にて

「ReactNativeで始めるアプリ開発」というタイトルで発表してきました。

connpass.com

スライドはSlideShareに上げています。

www.slideshare.net

今回は勉強会自体が特定の分野の発表ではないので、聞き手の知識がバラバラで

どこまでを前提知識として発表内容を考えるかが非常に難しかったです。

最終的に割とふんわりした内容になってしまって、マサカリ投げどころ満載だったのでビクビクしながら発表しました(笑)

とりあえずReact Nativeがどういうものか、導入にかんして何を考えれば良いのか、など少しでも伝わっているといいなーと思います。

今後も勉強会では積極的に発表していきたいですね。

react-reduxのmapStateToPropsとmapDispatchToPropsは必要なのか

アプリエンジニアの原です。

ReactNativeでアプリを作る時はReduxを使っていて、ReduxのStoreとReactのComponentをBindingしてくれるreact-reduxが便利なので使っています。

github.com

react-reduxが提供するconnectというStoreとComponentをBindingする関数が在るのですが、

connect(
  mapStateToProps,
  mapDispatchToProps,
  mergeProps,
)(Component)

という感じで、3つの関数を引き数にしています。

mapStateToProps

これはReduxのStoreを第一引き数に取る関数で、ComponentにPropsとして渡すものをフィルタリングしたい時に使います。例えばStoreのUserからnameをPropsとして渡したい場合は

//第一引き数はstore
const mapStateToProps = store => ({ name: store.user.name });

connect(mapStateToProps)(Component);

//Componentはthis.props.nameでアクセス可能

こんな感じでフィルタリングしてComponentにPropsと渡すことが出来ます。

mapDispatchToProps

これはReduxのDispatchを第一引き数に取る関数で、変更を伝えるアクションを作成する時に使います。例えばボタンが押された時にUserのnameを変更するアクションを作成する場合は

// 第一引き数はReduxのdispatch関数
const mapDispatchToProps = dispatch => ({ updateName: name => dispatch({ type: UPDATE_NAME, name }) });

connect(null, mapDispatchToProps)(Component);

//Componentはthis.props.updateName('name')でアクセス可能

こんな感じで作成したアクションをComponentのPropsとして渡すことが出来ます。

mergeProps

これは第一引き数にmapStateToProps、第二引き数にmapDispatchToPropsをとる関数で新しPropsを作成します。 デフォルトでは

Object.assign({}, ownProps, stateProps, dispatchProps)

という処理を行います。

(ownPropsはComponentが持つプロパティです。mapStateToProps, mapDispatchToProps共に第二引き数, mergePropsは第三引き数で受取ることができます)

疑問

結論からいうとmapStateToPropsとmapDispatchToPropsの使い所がわかりません

例えば, Userの情報をサーバーから取得してStoreを更新する処理を考えます。

ユーザーの情報を取得するAPIにはユーザーのトークンが必要なのでStoreから取得する必要があります。

サーバーから取得したレスポンスでStoreを更新するためにDispatch関数が必要です。

なので、mergePropsを使わない場合は

const mapStateToProps = store => ({ token: store.user.token });

const mapDispatchToProps = dispatch => ({ fetchUser: token => fetch(...) }  });

connect(
  mapStateToProps,
  mapDispatchToProps,
)(Component);

のようになり、ComponentがfetchUserにトークンを渡す責務が発生します。

Componentは表示やUIイベントの通知に関わる処理だけをするべきであり、またtoken自体描画には全く関係無いのでPropsとして渡すのは不適切な気がします。

なので、ここでmergePropsを使ってtokenを渡す処理を内包すると

const mapStateToProps = store => ({ 
  token: store.user.token 
});

const mapDispatchToProps = dispatch => ({ 
  fetchUser: token  => fetch(...),  //tokenを受け取ってfetchを実行するための関数を返す。
});

const mergeProps = (state, action) => ({
     fetchUser: () => action.fetchUser(state.token),
});

connect(
  mapStateToProps,
  mapDispatchToProps,
  mergeProps
)(Component);

これでも可能ですが、

なんなら

const mapStateToProps = store => store;

const mapDispatchToProps = dispatch => ({ dispatch });

const mergeProps = (store, { dispatch })  => ({
    fetchUser: () => {
        const token = store.user.token;
        fetch(...);
    }
});

connect(
  mapStateToProps,
  mapDispatchToProps,
  mergeProps
)(Component);

コレが一番シンプルなのでは??

という結論に今のところ至っており、

mapStateToPropsとmapDispatchToPropsは必要なのか

です。

だれか教えてください。よろしくお願いします!

宣伝

このイベントでReactNativeに関する発表をします。

connpass.com

ReactNativeのView構造をAndroidDeviceMonitorで確認する

こんにちは。アプリエンジニアの原です。

テックブログ始めました。

一発目はReactNativeの記事です。

巷で話題のReactNative、最近弊社のネイティブアプリをReactNative化するプロジェクトがスタートしています。

ReactNativeはロジック部分をJavascriptで、Viewの部分をJavascriptのライブラリのReactで書くことができ、実行時はReactで書いたソースからiOS/AndroidのネイティブコードのViewに変換してくれます。

では実際にReactNativeのコードからどのようなViewに変換されてるでしょうか。

ソースを読むのも1つの手ですが、AndroidSDKにはAndroidDeviceMonitorというツールがありViewツリーをGUIで確認できる機能があります。このツールを使うことでViewの構造を簡単に確認することが出来ますので今回をこれを使って見てみます。

新規プロジェクトを作成

ReactNativeとAndroidSDKの設定はできてい前提で進めていきます。

ReactNativeの環境設定はこちら facebook.github.io

AndroidStudioまたはコマンドラインツールズのダウンロードはこちら developer.android.com

react-native init で新規作成、react-native run-androidでアンドロイドのビルドが走ります。

react-native init AnectSample1
cd AnectSample1
react-native run-android

新規のプロジェクトだと下記のような画面になります。 f:id:anect:20170227233703p:plain

Javascript側のView構造は下記のようになっています。

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 * @flow
 */

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

export default class AnectSample1 extends Component {
  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.welcome}>
          Welcome to React Native!
        </Text>
        <Text style={styles.instructions}>
          To get started, edit index.android.js
        </Text>
        <Text style={styles.instructions}>
          Double tap R on your keyboard to reload,{'\n'}
          Shake or press menu button for dev menu
        </Text>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  welcome: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
  },
  instructions: {
    textAlign: 'center',
    color: '#333333',
    marginBottom: 5,
  },
});

AppRegistry.registerComponent('AnectSample1', () => AnectSample1);

render()メソッドがViewを返します。

Viewの中にTextが3つ入っている構造です。

AndroidDeviceMonitorで確認

このViewをAndroidDeviceMonitorで確認してみます。

monitor

パスが通っていない場合はAndroid/SDK/toolsの中にあります。

AndroidDeviceMonitorが起動したら下図の赤丸の部分をおしてHierarchyViewを選択します。

f:id:anect:20170228005304p:plain

左側のナビゲーションのwindowsから作成したアプリを選択すると下記の図のようにViewの構造が表示されます。

f:id:anect:20170228005619p:plain

変換されたViewを探す

一番左がルートのViewになり階層順に右に並んでいます。 右端の一番上のReactTextViewをクリックしてみると

f:id:anect:20170228011347p:plain

となり、このViewが「Welcome to React Native」を表示していることがわかります。

同様に残り2つのReactTextViewもJavascript側で書いたテキストを表示していることがわかりました。

このことからJavascript側のTextクラスがAndroid側ではReactTextViewというViewとに変換されているということが確認できました。

またこれらを子Viewに持つReactViewGroupがJavascript側のViewクラスが変換されたものだということもわかり、Javascriptで定義したすべてのViewがネイティブのコードに変換されていることが確認できました。

まとめ

RectNativeで作成したアプリをAndroidDeviceMonitorでViewの構造を調べました。 Javascript側のViewがAndroid側のネイティブのViewとしてちゃんと変換されていることがわかりました。 実際の実装で頻繁に使うことは無いと思いますが、ReactNativeのコンポーネントがネイティブでどう変換されているかを知ることは実装やデバッグに役に立つこともあるかと思いますので機会があれば使ってみてください。