error: cannot find symbol import com.reactnativenavigation.NavigationReactPackage;

react-native-navigationをビルドした時に失敗したのでその解決法をメモします。

/Users/asmsuechan/src/my_mobile/android/app/src/main/java/com/my_mobile/MainApplication.java:6: error: cannot find symbol
import com.reactnativenavigation.NavigationReactPackage;
                                ^
  symbol:   class NavigationReactPackage
  location: package com.reactnativenavigation
1 error
:app:compileDebugJavaWithJavac FAILED

import法が変わっているみたいで、import com.reactnativenavigation.NavigationReactPackage;ではなくimport com.reactnativenavigation.bridge.NavigationReactPackage;としたら解決しました。

FAILURE: Build failed with an exception. · Issue #1242 · wix/react-native-navigation · GitHub

react-nativeでPinterestみたいなUIを実現する

Pinterestの2カラムで高さ可変のUIをreact-nativeで作ります。

目標としたUI

f:id:asmsuechan:20180903203039p:plain

このレイアウトはMansoryといいます。

できたUI

f:id:asmsuechan:20180903203900p:plain

ライブラリ

Mansoryレイアウトを実現するJSのライブラリはいくつかあって、PackeryMansory.jsBricks.jsなどが有名です。しかしこれらはそのままreact-nativeでは使うことができないのでreact-native用のコンポーネントを探しました。

すると以下の2つが見つかりました。

github.com

github.com

まずスター数が多いreact-native-masonryの方を試したのですが、これが結構曲者でなかなか期待通りに動いてくれないしバグってるしなんかオーナー修正するつもりなさそうだしで諦めました。

次に試したのがreact-native-masonry-layoutです。今回はこちらを使用しました。

これも正直スター数が少なかったりインタフェースが微妙だったりといろいろ不穏な雰囲気はあるのですが、動くのでよしとしました。

コード

実際に使ったコードはこちらです。

import React, {Component} from 'react';
import {Platform, StyleSheet, Text, View, Image, TouchableHighlight, Dimensions} from 'react-native';
import Masonry from 'react-native-masonry-layout';

type Props = {};
export default class App extends Component<Props> {
  constructor() {
    super();

    this.state = {
    };
  }

  componentDidMount() {
    const { width } = Dimensions.get( "window" );
    const columnWidth = ( width - 10 ) / 2 - 10;
    const data2 = [
      {
        image: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSPL2GTXDuOzwuX5X7Mgwc3Vc9ZIhiMmZUhp3s1wg0oHPzSP7qC',
        text: 'wwwww',
        key: Math.random().toString(36).slice(-8),
        height: columnWidth / 400 * 500
      },
      {
        image: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQGYfU5N8lsJepQyoAigiijX8bcdpahei_XqRWBzZLbxcsuqtiH',
        text: 'wwwww',
        key: Math.random().toString(36).slice(-8),
        height: columnWidth / 400 * 600
      },
      {
        image: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQGYfU5N8lsJepQyoAigiijX8bcdpahei_XqRWBzZLbxcsuqtiH',
        text: 'wwwww',
        key: Math.random().toString(36).slice(-8),
        height: columnWidth / 400 * 700
      },
      {
        image: 'https://img.buzzfeed.com/buzzfeed-static/static/2017-03/30/12/asset/buzzfeed-prod-fastlane-01/sub-buzz-24597-1490890739-1.jpg',
        text: 'wwwww',
        key: Math.random().toString(36).slice(-8),
        height: columnWidth / 400 * 600
      },
      {
        image: 'https://img.buzzfeed.com/buzzfeed-static/static/2017-03/30/12/asset/buzzfeed-prod-fastlane-01/sub-buzz-24597-1490890739-1.jpg',
        text: 'wwwww',
        key: Math.random().toString(36).slice(-8),
        height: columnWidth / 400 * 300
      },
      {
        image: 'https://img.buzzfeed.com/buzzfeed-static/static/2017-03/30/12/asset/buzzfeed-prod-fastlane-01/sub-buzz-24597-1490890739-1.jpg',
        text: 'wwwww',
        key: Math.random().toString(36).slice(-8),
        height: columnWidth / 400 * 400
      },
      {
        image: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSPL2GTXDuOzwuX5X7Mgwc3Vc9ZIhiMmZUhp3s1wg0oHPzSP7qC',
        text: 'wwwww',
        key: Math.random().toString(36).slice(-8),
        height: columnWidth / 400 * 500
      },
      {
        image: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSPL2GTXDuOzwuX5X7Mgwc3Vc9ZIhiMmZUhp3s1wg0oHPzSP7qC',
        text: 'wwwww',
        key: Math.random().toString(36).slice(-8),
        height: columnWidth / 400 * 500
      },
    ]
    this.refs.list.addItems( data2 );
  }

  render() {
    return (
      <View style={{ flex:1 }}>
        <View style={{flex: 1, flexGrow: 10, padding: 5}}>
          <Masonry
            ref="list"
            columns={2}
            renderItem={(item)=><View
                          style={{
                            margin: 5,
                            backgroundColor: "#fff",
                            borderRadius: 5,
                            overflow: "hidden",
                          }}>
              <Image source={{ uri: item.image }} style={{ height: item.height }}/>
              <Text>some text</Text>
            </View>}
          />
        </View>
      </View>
    );
  }
}

const styles = StyleSheet.create({
});

環境

$ react-native --version
react-native-cli: 2.0.1
react-native: 0.56.0

AliEaters meetup #6 に参加した

8/29にあった Alibaba Cloud Developers Meetup #6に参加しました。

togetterにまとめがあります。これを見たら雰囲気はわかると思います。 8/29 AliEaters Meetup#6 - Togetter

会場は霞が関にあるWeWork日比谷パークフロントです。行く途中に国会議事堂があったのですけど、初めて見ました。国会議事堂。

f:id:asmsuechan:20180831112615p:plain 会場には想像以上に人がいたのですが、WeWorkのドロップインメンバーもいたのでそう見えたみたいです。

そしてなんと会場はビール飲み放題でした。最高です。めちゃくちゃ飲みました。ごちそうさまです。

セッション

今回は15分のセッションが5つあって、

  • 中国のIT事情~中国のITリーディングカンパニー~
  • 一番身近なAlibaba『Aliexpress』を使おう!
  • アリババクラウドK8S上のGreen/Blueデプロイメントの実現方法
  • IoT Platformで作るスマートロックシステム
  • The Solution of IoT ー農業IoTー

でした。私はスマートロックの話をしました。あんなに酒飲んで発表したのは初めてです

会場の雰囲気はちょっとザワザワしていて(コワーキングスペースだから)発表するにはちょっと楽です。傾注!って感じで聞かれるとドキドキしてしまいますからね。

それぞれめちゃくちゃ簡単にまとめます。

中国のIT事情~中国のITリーディングカンパニー~

SBクラウドのSissyさんの発表です。

f:id:asmsuechan:20180831121134j:plain

中国のBAT(Baidu, Alibaba, Tencent)に関する情報や独身の日の爆買いなど、中国のインターネット企業に関することを概要的に説明してくれました。

独身の日の購入額が楽天の1年の流通額に近いって話があったんですが、すごいですね。

そして上の画像にあるCreate@TOKYOが10月9日に開催されます。これはビジコンで、優勝賞金はAlibaba Cloudのクーポン300万円分!x.largeのインスタンスを立てまくれます。日本を勝ち抜けば中国に行けるみたいです。

peatix.com

さらにAlibabaはAI/機械学習コンテストもやっていて、優勝賞金は300万円くらいです。すごい。

天池大赛列表页

Sissyさん写真提供ありがとうございます!

一番身近なAlibaba『Aliexpress』を使おう!

スターティアの石橋さんの発表です。

f:id:asmsuechan:20180831121336j:plain

www.slideshare.net

Aliexpressはいわゆる中華スマホやガジェットを安く見つけることができて、シャオミのスマホも楽に(?)手に入るそうです。

ちなみに私はEssential Phoneを使っています(布教)

次デバイス系の何かを探すときはAliexpressを当たってみることにします。

アリババクラウドK8S上のGreen/Blueデプロイメントの実現方法

SBクラウドのWangzzさんの発表です。

f:id:asmsuechan:20180831120950j:plain

今回のミートアップで唯一のガッツリとテクノロジーな発表でした。ブルーグリーンデプロイメントとは何か、の話から始まって実演動画(ターミナルのスクショ動画)に終わるよい発表でした。

たぶんAlibaba Cloud Kubernetesに関する知見は日本語だとどこにもないからとてもありがたいですね。

k8sやっていかねば。。。

IoT Platformで作るスマートロックシステム

不肖私の発表です。

f:id:asmsuechan:20180831121023j:plain

内容はただスマートロックを作ったよ、という話でIoT Platformをゆるふわに布教したといった感じです。

短い発表でしたが終始ゴキゲンな感じに進んだのでなかなか良かったように思っています。

この発表で私が言いたかったことはこれだけです。

f:id:asmsuechan:20180831114207p:plain

The Solution of IoT ー農業IoTー

アイフォーカスの小野さんの発表です。

f:id:asmsuechan:20180831121049j:plain

前々から地味に話題になりつつもパンピの私はなかなかお目にかかることがなかった農業IoTの話です。聞けて良かったです。

こういう大きな事業は動くお金も大きそうです。

IoT+AIによる農業の専門家のための農業クラウドサービス開始 | クレバアグリ株式会社

まとめ

AliEatersはこんな人にオススメです。

  • 中国展開を考えている人
  • Alibabaが気になる人
  • Alibaba Cloudの知見を発表したい人
  • Alibaba Cloud使いと知り合いたい人

来月(9月)あるみたいなのでまた参加したいです!

GraphQLのミューテーションを使う

GraphQLのミューテーションはリソースを変更するものです。

実行順の問題で、argsで渡された引数を元にリソースを変更することは好ましくありません。(詳しくは公式ドキュメントに任せます)

const express = require('express')
const graphqlHTTP = require('express-graphql')
const {
  graphql,
  GraphQLSchema,
  GraphQLObjectType,
  GraphQLList,
  GraphQLString,
  GraphQLInt,
  GraphQLInputObjectType,
  GraphQLNonNull
} = require('graphql');
const atob = require('atob');

const userType = new GraphQLObjectType({
    name: 'user',
    fields: () => ({
      id: { type: GraphQLInt },
      sub: { type: GraphQLString },
      created_at: { type: GraphQLString },
      updated_at: { type: GraphQLString }
    })
  })

function createUser(query) {
  console.log(query)
  return {
    id: 1,
    sub: 'auth0|aaaaaaaaaaaaaaa'
  }
}

const createUserType = new GraphQLObjectType({
  name: 'createUserType',
  fields: {
    sub: { type: new GraphQLNonNull(GraphQLString) }
  }
})

const mutation = new GraphQLObjectType({
  name: 'Mutation',
  fields: {
    createUser: {
      type: createUserType,
      args: { sub: { type: GraphQLString } },
      resolve(obj, { sub }) {
        return createUser(sub);
      }
    }
  }
})

const schema = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'Query',
    fields: () => ({
      user: {
        type: userType,
        args: {
          sub: { type: GraphQLString }
        },
        resolve: (value, { sub }) => {
          return createUser(sub)
        }
      }
    })
  }),
  mutation: mutation
});

var app = express();
app.use('/graphql', graphqlHTTP({
  schema: schema,
  graphiql: true,
}));
app.listen(4000, () => console.log('Now browse to localhost:4000/graphql'));

以下のリクエストを投げたところ、正しく結果が返ってきたことが確認できました。ここでMとfirstは任意の名前です。

mutation M {
  first: createUser(sub: "aaaaa") {
    sub
  }
}

f:id:asmsuechan:20180902173017p:plain

GraphQLでパラメータを投げる

最近GraphQLを使っているのですがリクエストにパラメータを乗せるのに四苦八苦したのでメモしておきます。

const express = require('express')
const graphqlHTTP = require('express-graphql')
const {
  graphql,
  GraphQLSchema,
  GraphQLObjectType,
  GraphQLList,
  GraphQLString,
  GraphQLInt,
  GraphQLInputObjectType,
  GraphQLNonNull
} = require('graphql');
const atob = require('atob');

const postType = new GraphQLList(
    new GraphQLObjectType({
      name: 'post',
      fields: {
        id: { type: GraphQLInt },
        imageUrl: { type: GraphQLString },
        created_at: { type: GraphQLString },
        updated_at: { type: GraphQLString }
      }
    })
  )

function getPosts(limit) {
  return [
    { imageUrl: 'aaaaa' },
    { imageUrl: 'bbbbb' }
  ]
}

const schema = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'Query',
    fields: () => ({
      post: {
        type: postType,
        args: {
          limit: { type: GraphQLInt }
        },
        resolve: (value, { limit }) => {
          return getPosts(limit)
        }
      }
    })
  })
});

var app = express();
app.use('/graphql', graphqlHTTP({
  schema: schema,
  graphiql: true,
}));
app.listen(4000, () => console.log('Now browse to localhost:4000/graphql'));

以下のリクエストを投げて結果を確認します。getPostsは任意の名前、postはフィールド名、limitは引数名、imageUrlはpostで返ってくるpostTypeの属性名です。

query getPosts {
  post(limit: 10) {
    imageUrl
  }
}

$ node index.jsをするとgraphiqlが見えてレスポンスを確認できます。

f:id:asmsuechan:20180902155059p:plain

コードを書くときはgraphql-jsのテストが参考になります。

https://github.com/graphql/graphql-js/blob/master/src/tests/starWarsQuery-test.js

https://github.com/graphql/graphql-js/blob/master/src/tests/starWarsSchema.js

にしてもGraphQLめちゃくちゃ取っつきにくいですね。RESTは思考停止でも書けるってことを強く感じました。

ApsaraDB for RDSへのマイグレーション

sequelizeを使ってApsaraDB for RDSにマイグレーションを流します。

ApsaraDB for RDSはちょっとお金がかかって、日本リージョンは1日120円くらいです。1ヶ月だと4000円ですね。個人利用だと地味に効いてくる値段です。

準備

準備として以下を行います。多分特に迷うところはなく画面をポチポチすれば大丈夫です。

ホスト名は「データベースの接続」でインターネット接続を有効化すると表示されます。 f:id:asmsuechan:20180902100906p:plain

ホワイトリストも忘れないように設定しましょう。

マイグレーションする

DBを作成したので今度はテーブルを作成します。sequelizeマイグレーションできるようにしましょう。

$ yarn add global sequelize-cli
$ yarn add sequelize
$ yarn add mysql2
$ sequelize init

Sequelize CLI [Node: 10.6.0, CLI: 4.1.1, ORM: 4.38.0]

Created "config/config.json"
Successfully created models folder at "/Users/ryouta/src/kumonotami-migration/models".
Successfully created migrations folder at "/Users/ryouta/src/kumonotami-migration/migrations".
Successfully created seeders folder at "/Users/ryouta/src/kumonotami-migration/seeders".

config/config.jsonを編集して先ほど調べたホスト名、ユーザー名、パスワードを入力します。

{
  "development": {
    "username": "myapp_test",
    "password": "xxxxxxxxxxxxxx",
    "database": "myapp_db_test",
    "host": "xxxxxxxxxxxxx.mysql.japan.rds.aliyuncs.com",
    "dialect": "mysql"
  }
}

次に新しくテーブルを作ります。

$ sequelize model:create --underscored --name user --attributes "sub:string,deleted_at:date"

Sequelize CLI [Node: 10.6.0, CLI: 4.1.1, ORM: 4.38.0]

New model was created at /Users/ryouta/src/kumonotami-migration/models/user.js .
New migration was created at /Users/ryouta/src/kumonotami-migration/migrations/20180902014424-user.js .

これでマイグレーションファイルとモデルファイルが新規作成されます。これでよければマイグレーションを実行します。

$ sequelize db:migrate --env development

Sequelize CLI [Node: 10.6.0, CLI: 4.1.1, ORM: 4.38.0]

Loaded configuration file "config/config.json".
Using environment "development".
sequelize deprecated String based operators are now deprecated. Please use Symbol based operators for better security, read more at http://docs.sequelizejs.com/manual/tutorial/querying.html#operators node_modules/sequelize/lib/sequelize.js:242:13
== 20180902014423-create-user: migrating =======
== 20180902014423-create-user: migrated (0.087s)

MySQL Workbenchで確認してみたところ、ちゃんと作成されていることがわかりました。 f:id:asmsuechan:20180902110414p:plain

Function Compute + API GatewayでGraphQLのAPIを作成する

アリババクラウドのFunction Compute + API Gatewayの構成のAPIでGraphQLを使います。

GraphQLとは

エンドポイントごとに機能を持たせるのではなく、一つのエンドポイントに命令をPOSTしてその結果を得るようにしたものです。

具体例だとこんな感じになります。

$ curl -H "Content-Type: application/json" -X POST -d '
{
  "query": "{ messages { name body } }"
}
' http://xxxxxxxxxxxxxxxxxx-cn-shanghai.alicloudapi.com/

{"data":{"messages":[{"body":"Hello","name":"asmsuechan"},{"body":"World","name":"suechan"}]}}

私はここを参考にしました。

www.m3tech.blog

適当におググりくだされば分かりやすい記事がたくさん出てくると思うのでGraphQLそのものの説明はそこに任せます。

最小のコード

まず、一番小さなgraphqlのコードを書いてfunction compute上で動かします。

helloというクエリを投げると{ data: { hello: "world" } }という結果が得られます。

投げるクエリ: "{ hello }"
返ってくる結果: { data: { hello: "world" } }

このサンプルは公式GitHubのものです。

// index.js
const { hook } = require('fc-helper');
const {
  graphql,
  GraphQLSchema,
  GraphQLObjectType,
  GraphQLString
} = require('graphql');

const schema = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'RootQueryType',
    fields: {
      hello: {
        type: GraphQLString,
        resolve() {
          return 'world';
        }
      }
    }
  })
});

const query = '{ hello }';

module.exports.handler = (event, context, callback) => {
  graphql(schema, query).then((result) => {
    callback(null, { statusCode: 200, body: result });
  });
});

graphql()の実行をhandler関数の中で行うだけです。簡単です。

なお、Function Compute + API GatewayでPOSTを実装するのは少しクセがあるのでこちらを参考にしてください。

ちょっと高度なAPIにする

では少し複雑にしてみます。以下のようなクエリと結果の組み合わせを想定してコードを書きます。

投げるクエリ: "{ messages { name body } }"
返ってくる結果: {"data":{"messages":[{"body":"Hello","name":"asmsuechan"},{"body":"World","name":"suechan"}]}}

messageという型を定義して、複数のmessageが返ってくるようにします。

// index.js
const { hook } = require('fc-helper');
const {
  graphql,
  GraphQLSchema,
  GraphQLObjectType,
  GraphQLList,
  GraphQLString
} = require('graphql');
const atob = require('atob');

const messages = [
  {
    name: 'asmsuechan',
    body: 'Hello'
  },
  {
    name: 'suechan',
    body: 'World'
  }
];

const schema = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'RootQueryType',
    fields: {
      messages: {
        type: GraphQLList(
          new GraphQLObjectType({
            name: 'message',
            fields: {
              name: { type: GraphQLString },
              body: { type: GraphQLString },
            },
          }),
        ),
        resolve() {
          return messages;
        }
      }
    }
  })
});

module.exports.handler = (event, context, callback) => {
  const request = JSON.parse(event.toString('utf8'))
  const query = JSON.parse(atob(request["body"]))["query"]
  graphql(schema, query).then((result) => {
    callback(null, { statusCode: 200, body: result });
  });
};

実際に使う場合はresolve()でデータベースから全てのメッセージを拾ってくるgetMessages()のような関数を実行します。

curlで確認すると期待する結果が得られていることが分かりますね。

$ curl -H "Content-Type: application/json" -X POST -d '
{ "query": "{ messages { name body } }"}
'http://xxxxxxxxxxxxxxxxxx-cn-shanghai.alicloudapi.com/

{"data":{"messages":[{"body":"Hello","name":"asmsuechan"},{"body":"World","name":"suechan"}]}}

まとめ

Function Compute(Faas全般で言えることです)でGraphQLを使うと、RESTと違って複数のAPI Gatewayを立てることが必要ないので簡単に複雑なAPIを作ることができて幸せになれます。

しかし正直まだ実戦投入には尚早かなと思います。

API GatewayとFunction Compute、めちゃくちゃデバッグしにくい・・・

今回のソースコードはこちらに公開しています。

github.com