とにかく次の10年を生き残りたい

ついに2020年になってしまった。

この10年はとにかくネイティブアプリ開発にすべてを投資した。
京都GTUGのイベント情報によれば、第1回Androidハッカソンは2009年9月5日に開催されている。このハッカソンのためにドコモ HT-03Aを契約しに行ったのがAndroidとの出会いだった。当時APIレベル3、コードネームCupcake、バージョン1.5である。加速度がホントにJavaのコードから取れてびっくりしたのを覚えている。

実に丸10年の月日が過ぎた。スマートフォンの普及率は20-30代で90%を超え、今後も低くなるということはないだろう。当然、それらスマートフォンで動作するネイティブアプリ開発の需要もしばらくは続くに相違ない。ただし、開発の前線から見える情景は少しずつ変わってきているようにも感じられる。
3-4年前だとネイティブファースト(特にiOS)が当然であり、ウェブが存在しないサービスも少なくなかったが、ウェブへの回帰も見られるようになってきた。また、FlutterによるiOS/Androidアプリ同時開発も流行しているように思う。この辺りは個人の感覚だし、東京とそれ以外、あるいは米国、欧州でもかなり違っているのだろうと想像するが、本ポエムの主眼ではない。

次の10年は何に投資すればよいのだろうか。本当に知りたい。僕は深刻にこの辺りの嗅覚が鈍い。
iPhoneがリリースされた2007年当時、僕はすでにプログラマだった。当時所属していた会社はSkypeのようなP2P通信アプリを開発しており、LinuxザウルスやSymbianOSなんかにも対応していた記憶がある。その渦中にあって、iPhoneがその後天下を取ることを想像できなかった。BlackBerryNokia端末との違いが分からなかった。当事者とは案外そんなものだ。何回目かのxR元年と、いつ来るかわからないシンギュラリティ。また同じように、10年後振り返って「ああ、なんであのときアレに投資していなかったのか…」と嘆息するのだろう。

次の10年は僕の40代を捧げる10年となる。実際に35歳を過ぎてみて、新しいことに興味を持って勉強をし続ければまだまだ全然戦えるという感覚はある。理論や研究を補うために大学院にも進学した。競プロも始めた。酒もやめた。ただまあ、抗いきれない事実として体力は落ち続けているし頭の回転も少しずつ鈍っている。圧倒的な瞬発力の戦いから、総合格闘技で最後は寝技で絞めるみたいな感じになってきている。次の投資先は慎重に決めねばならない。

一旦、2020年Q1は再びウェブフロントエンドへの投資を少し増やしてみようと思っている。2013年まではフロントエンド開発(なんと、jQueryとbackbone.js)もしていたのだが、7年も経ってしまって自分の知識はバクテリアに分解されて石油となっている。その間ウェブ技術は目覚ましい発展を遂げ、現在のJSは当時とは完全に別物だと感じる。TypeScriptで型の恩恵も受けられる。ありがたいことだ。この状況を作ってくれた人々に感謝したい。

という訳で何卒諸々ご指導ください。本年も宜しくお願いいたします。

アルゴリズムと数学的思考力

厳しい。年始早々厳しさを感じている。自分のプログラミング力にだ。伸び悩んでいる。

端的に言って、数学力のなさが自分のプログラミング能力に制限をかけている。例えばこの問題。

560. Subarray Sum Equals K
入力として与えられる配列 nums のうち、合計が k となる部分配列の個数を数え上げよ。どうも有名な問題らしいが…

まず大前提として、部分配列なので i, j の2重ループで始点・終点を定めて sum(nums[i, j]) = k になるものを数え上げれば必ず答えが得られる。最悪計算量は O(N^3) ただし i < nums.length < 20000 という制約があるので N^3 では遅すぎるから何か考えてくださいというのがスタート地点。

ここで、結果の変わらない累積和を何度も求めているので nums[i, j] = k を求めたい場合、 nums[0, j] - nums[0, i - 1] としてこれまでの計算結果を利用できることに気付けると計算量を大幅に減らせる。ただし、nums には負数が含まれているので同じ累積和が何度も出現する可能性があるので Map で出現頻度をカウントする

public int subarraySum(int[] nums, int k) {
    int j = 0;
    Map<Integer, Integer> seen = new HashMap<>();
    seen.put(0, 1);
    int counter = 0;
    for (int num : nums) {
        j += num;
        int i = j - k;
        if (seen.containsKey(i)) {
            counter += seen.get(i);
        }
        seen.put(j, seen.getOrDefault(j, 0) + 1);
    }
    return counter;
}

これで時間計算量 O(N) 空間計算量 O(N) で通った。 seen.put(0, 1) の部分だけトリッキーで、このアルゴリズムがそもそも前の計算結果を利用するものなので、初めて sum(nums[0, j]) == k になるケースをカバーする。

これってもはや算数の問題で、区間 nums[i, j] = knums[0, j] - nums[0, i - 1] で求まるっていうのは答え見て初めてマツコデラックスが感心する時の声で「ハ〜〜〜〜〜〜!」って唸ることしかできなかった。もう1題。

373. Find K Pairs with Smallest Sums
ソート済み配列 nums1, nums2 と整数 k が与えられる。このとき nums1, nums2 から1つずつ取り出してつくるペア (u,v) を、和が小さい順に k 個取り出せ。

Input: nums1 = [1,7,11], nums2 = [2,4,6], k = 3
Output: [[1,2],[1,4],[1,6]] 

これも O(N^2) で全ペアが列挙でき、それを整列するコストが O(N log N) なので最悪計算量 O(N^2) で必ず解ける問題。これには特に nums.length, k の制約が示されていないが、AtCoderなどのコンテストでは間違いなく O(N^2) では制限時間に収まらない(=不正解となる)データが与えられるだろう。したがってもっと高速なアルゴリズムを考えなさいというのが趣旨となる。

ここで nums1, nums2 はソート済みなので、nums1[0], nums2[0] は常に最小のペアであることが保証される。このような最小のインデックスをそれぞれ i, j とした場合、nums1[i], nums2[j] が解答のひとつならば nums1[i + 1], nums2[j]nums1[i], nums2[j + 1] が次の答えの候補になる。言われてみればそうである(孫権顔)

後は候補が複数あった場合に低コストで最小値を得られるデータ構造を考える。そう、二分ヒープ(最小)である。min heap の追加・削除の最悪コストは O(log N) なので、今回要素数 k の候補から常に最小を取り出しつつ k 個集めるまで繰り返すので O(K log K) が模範解答のひとつとなる。本件は競プロ界で著名な 宇宙ツイッタラーX さんに解説いただいて初めて理解できた。スレッドを参照されたい。

実装例も、氏のそれより綺麗には到底書けないので 氏のgist を見ていただきたい。

で、ここからが主題なのだが、これらの問題はプログラミング能力というか算数の問題なのだ。最初の問題はデータ構造と言っても HashMap ぐらいしか使ってないし、2題目も min heap こそ確かにコンピュータ・サイエンス的なデータ構造ではあるものの、これは最小(最大)値を O(log N) で出し入れできる(最小(最大)値の参照だけなら O(1))データ構造とだけ覚えていれば使える類のものだし、そこがこの問題を解く最大のキーというわけではない。現に僕は min heap を使うと模範解答になるというヒントを貰った後も「 nums1[i], nums2[j] が答えならば nums1[i + 1], nums2[j]nums1[i], nums2[j + 1] がその候補になる」という核心部分が出てこなかったし、むしろここがこの問題の最大のキーポイントであるわけで。

ここからは自分でもうまく整理できていないのだが、これは「数学力」なんだろうか。僕が数学が苦手なのは自他ともに認めるところだが、いま大学院の授業についていくためにマセマの微分積分とかを解いてるけど、なんというかこう、公式あてはめて微分するとかとは違うわけじゃないですか。使われているのは中学生レベルの四則演算で、むしろ「数学的思考力」を問われているというか。

社会人になってプログラマとして10年ぐらいやっていて忘れかけていたけど、そういえば僕は勉強まったくできなかったな、受験敗者だったわ、忘れてた。僕には数学的思考力が致命的にない。こういうのをアルゴリズムクイズを解いていると嫌というほど思い出させられる。

今回の2題は非常に悔しい思いをしたので覚えたが、これを繰り返していけば僕のように頭がよくない人間も類題を解けるようになるのか、それともちょっとひねられるだけでまたまったくお手上げになってしまって悔し涙に枕を濡らすのか、いまは分からない。願わくは2年後に読み返したときに「あの時はあんなしょうもない問題で悩んでいたんだなぁ😅」と思えるようになっていたいが、どうやることやら。正月に書くような内容ではないな…

Amplify for Androidでより直感的になったGraphQLを試した

TL;DR

本エントリはAWS Amplify Advent Calendar 2019の24日目です!

今回は新しくリリースされたAmplify for Androidを使って、前回と同じく下のようなチャットアプリを作るので、興味のある方は前口上を飛ばして後半をお読みください🎄

f:id:fushiroyama:20191201023816g:plain

Amplifyとは

本題に入る前に、Amplifyを最近よく耳にするようになったけど何かよく分かっていないという人向けの説明をします。

Amplifyはモバイルバックエンドを爆速で作るためのサービスです。 https://aws.amazon.com/jp/amplify/

他のいわゆるモバイルバックエンドとの際立った違いのうち、個人的に強調したいのは以下2点です。

  • バックエンドはAWSのサービスであり、真の意味でスケールする
  • GraphQLのマネージドサービスを利用できる本日時点で唯一のプラットフォームである

Amplifyの良い点として、フロントエンドから利用する時はそれと意識せずに簡単に構築できるけれども、バックエンドはAWSが個別に提供するサービスで構成されているという点です。これは、バックエンドをキャパシティのよく分からないブラックボックスとして扱うことなく、必要であればAWSを使い慣れたインフラ部隊と協力して本当の意味でスケールするようにチューニング可能だということです。

さらに、AmplifyはGraphQLのマネージドサービスを提供する今日時点で唯一のプラットフォームです。GraphQLの良さは他の記事に譲りますが、これを自前で構築するのは中々骨が折れます。AmplifyでAPIを追加するときにGraphQLを選択すると、裏ではAWS AppSyncが使われますが、これはGraphQLのマネージドサービスであり、サーバのプロビジョニングやチューニングを気にすることなくGraphQLのメリットを享受できます。

Amplifyの構成要素

Amplifyは主に次の要素で構成されています

  • バックエンドプロビジョニングのためのCLI
  • ウェブ、iOS/Androidに組み込むためのフレームワーク
  • CI/CDや管理のためのコンソール

f:id:fushiroyama:20191229142819p:plain
AWS re:Invent 2019のセッション動画より引用

後述しますがAmplify for iOS/AndroidのリリースでXcode/Android Studioとの統合がすすみ、CLIを単独で使用することは今後かなり少なくなります。したがって、基本的には普段開発しているときのようにCocoaPodsやGradleでライブラリを導入するように使い始めることができます。具体的な使い方はこのエントリの後半で解説します。

Amplifyのカテゴリ

Amplifyはカテゴリという概念があり、

  • APIREST APIやGraphQL
  • Auth…認証と認可
  • Storage…ストレージ
  • Analytics…分析とユーザエンゲージメント
  • Predictions…AI/MLの組み込み
  • XR…AR/VR

のような機能のうち、使いたいものを好きなだけ選んで使うことができます。

これまでもモバイルからAWSのサービスを使うことは当然できました。これはAWS Mobile SDKによって簡単に実現できます。 ただ、これはどちらかと言えば「AWSのこのサービスを組み込みたい」というマインドセットで使います。対してAmplify for iOS/Androidでは「APIを組み込みたい」というように、ユースケースの側面から使うことができます。

f:id:fushiroyama:20191229144655p:plain
AWS re:Invent 2019のセッション動画より引用

前置きが長くなりました。ここまでがAmplifyの概要です。次からはAmplify for iOS/Androidについて解説します。

Amplify for iOS/Androidとは

先日のAWS re:Invent 2019で新しく発表された、Amplifyをモバイルからより簡単に使うためのアップデートです(公式ブログ)。

fushiroyama.hatenablog.com

先日、上記エントリで AWSAppSyncClient を使ってGraphQLにつなぐ方法を紹介しましたが、これが新しいAPIでどんな感じになるのか見てみましょう。

注意点

公式サイトに明示されていますが、Amplify for iOS/Androidはプレビューリリースで、本番環境での利用はまだ想定されていません。他の多くのOSSがそうであるように、開発者が想定していないと思われることをやろうとするとまだ粗削りな部分が見て取れますが、コントリビューションのチャンスでもあります!本エントリで興味を持ってくださった方は是非お試しください!

Androidクイックスタート

それではAmplify for Androidを使ってGraphQLの読み書きをしてみたいと思います。序盤は公式サイトの手順に準じます。

まずはプロジェクトの build.gradleプラグインをインストールします。

buildscript {
    ext.kotlin_version = '1.3.61'
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.5.3'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        // Amplify
        classpath 'com.amplifyframework:amplify-tools-gradle-plugin:0.1.0'
    }
}

// Apply Plugin
apply plugin: 'com.amplifyframework.amplifytools'

allprojects {
    repositories {
        google()
        jcenter()

    }
}

次にアプリケーションレベルの build.gradle にJava8の設定と依存関係のインストールをします。

android {
  compileOptions {
        sourceCompatibility 1.8
        targetCompatibility 1.8
    }
}

dependencies {
  implementation 'com.amplifyframework:core:0.9.0'
  implementation 'com.amplifyframework:aws-api:0.9.0'
}

次に、Android StudioをProjectビューにして amplify/backend/api/amplifyDatasource/schema.graphql を編集します。これはGraphQLのスキーマ定義で、これを元にAndroid用のモデルが自動生成されます。せっかくなので、以前のエントリと同じくリアルタイムチャットアプリを開発してみます。まずは、前回と同じ次の定義で作ってみましょう。

type Message @model {
  id: ID!
  username: String!
  content: String!
}

次に、Amplify CLIをインストールします。npmを使うのでNode.jsが入っていない人はインストールしてください。

$ npm install -g @aws-amplify/cli
$ amplify -v
4.7.0

初めてAmplifyを利用する場合は、CLIAWSのクレデンシャルを設定する必要があるので amplify configure コマンドを実行します。AWSのマネジメントコンソールにリダイレクトされるので、そこでユーザを作成してキーを発行してください。ここは次のチュートリアルに非常に詳細な解説があります。

この状態でAndroid Studioに戻ってくると、Build > Make Project でプロジェクトをビルドします。

f:id:fushiroyama:20191229153500p:plain

すると、Graldeメニューに modelgenamplifyPush のタスクが追加されます。

f:id:fushiroyama:20191229153610p:plain

まず、modelgen を実行すると先程のGraphQLのスキーマから com.amplifyframework.datastore.generated.model にメッセージを表すモデルの Message.java が生成されます。 次に amplifyPush を実行すると、Amplify CLIが必要なバックエンドをプロビジョニングしてくれます。この作業は数分かかることがあります。 完了したら、これらを使ってチャットのメッセージを読み書きしてみましょう。

最初にAmplifyを初期化するための設定をします。カスタムApplicationクラスを作成し、次の内容を追記します。

class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        try {
            Amplify.addPlugin(AWSApiPlugin())
            Amplify.configure(applicationContext)
        } catch (e: AmplifyException) {
            Log.e(TAG, e.message)
            throw RuntimeException(e)
        }
    }
}

MyApplicationAndroidManifest.xml に追加するのを忘れないでください。

    <application
        android:name=".MyApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">

あとは読み書きをするだけです!まずはミューテーションから。

modelgen によって作られたモデルは Message.builder() のようなビルダーがついてきます。ユーザの入力に合わせて簡単にモデルを組み立てることができます。 あとは Amplify.API.mutate メソッドを呼ぶだけです。結果を受け取りたい場合は ResultListener も一緒に渡します。

private fun mutation(
    username: String,
    content: String
) {
    val message: Message =
        Message.builder()
            .username(username)
            .content(content)
            .build()

    Amplify.API.mutate<Message>(
        message,
        MutationType.CREATE,
        object :
            ResultListener<GraphQLResponse<Message>> {
            override fun onResult(response: GraphQLResponse<Message>) {
                response.data?.let {
                  // do things with message
                }
            }

            override fun onError(e: Throwable) {
            }
        })
}

次はクエリです。こちらも Amplify.API.query メソッドを呼ぶだけです。 結果は同じように ResultListener で受け取れるので、これを RecyclerView で描画するといった使い方が一般的でしょう。

private fun query() {
    Amplify.API.query(
        Message::class.java,
        object :
            ResultListener<GraphQLResponse<Iterable<Message>>> {
            override fun onResult(response: GraphQLResponse<Iterable<Message>>) {
                val messages: MutableList<Message> = mutableListOf()
                for (message in response.data) {
                    messages.add(message)
                }

                // update RecyclerView
                updateMessages(messages)
            }

            override fun onError(e: Throwable) {
            }
        })
}

最後にサブスクリプションです。なんとこちらも Amplify.API.subscribe メソッドを呼ぶだけです。 これだけでメッセージの作成をリアルタイムに通知してもらえるので、チャットアプリのようなものがいとも簡単に作成できます。

private fun subscribe() {
    Amplify.API.subscribe(
        Message::class.java,
        SubscriptionType.ON_CREATE,
        object :
            StreamListener<GraphQLResponse<Message>> {
            override fun onNext(response: GraphQLResponse<Message>) {
                response?.data?.let {
                    // update RecyclerView
                    addMessage(it)
                }
            }

            override fun onComplete() {
            }

            override fun onError(e: Throwable) {
            }
        }
    )
}

めちゃくちゃ簡単ですね!ここまでが公式のチュートリアルに相当する内容です。

時系列でメッセージを取得

ここからは少し発展的な内容を扱います。

AmplifyでGraphQLを使う場合、デフォルトではAppSyncとDynamoDBが使われます。DynamoDBは扱うデータサイズや規模によらず、うまく設計すればミリ秒単位のパフォーマンスを維持できるKVSおよびドキュメントデータベースですが、一般的なRDBMSを1台だけで扱うときとは違う点を考慮する必要がある場合があります。たとえば「メッセージを投稿順で取得し、ページングさせたい」という場合には、DynamoDBではいくつか実現方法がありますが、

として設計するのがよくあるパターンです。ここではDynamoDBの詳細なデザインパターンには触れませんが、チャットサービスといえば普通はチャンネル(部屋)を複数持てるのが普通なので、各部屋を表現する roomId + メッセージの日付 createdAt を使ってメッセージを時系列に取得することにします。

まずは前出のGraphQLのスキーマを次のように変更します。

type Message
  @model
  @key(name: "SortByCreatedAt", fields:["roomId", "createdAt"], queryField: "listMessagesSortedByCreatedAt" )
{
  id: ID!
  username: String!
  content: String!
  roomId: String!
  createdAt: AWSDateTime!
  updatedAt: AWSDateTime
}

name: "SortByCreatedAt", fields:["roomId", "createdAt"] の部分で時系列に取得するためのキーを追加しています。 また queryField: "listMessagesSortedByCreatedAt" の部分で、このキーを使ったクエリをする際の名前を指定しています。

できたら、以前同様 modelgenamplifyPush を実行してモデルファイルを再生成し、バックエンドにプロビジョニングします。 これで、GraphQL的には次のようなクエリで時系列で取得できるはずです。

query GetPost(
  $roomId: String!
  $limit: Int!
) {
  listMessagesSortedByCreatedAt(
    roomId: $roomId
    sortDirection: ASC
    limit: $limit
  ) {
    items {
      id
      content
      username
      roomId
      createdAt
    }
  }
}

ただし、現状のAmplify for Androidではこのような任意のクエリを簡単に送る方法がないので、GraphQLRequest を利用します。 GraphQLRequest(query, variables, Message::class.java, GsonVariablesSerializer()) のように使います。

注意点として、スキーマcreatedAt: AWSDateTime! のように定義した場合、自動生成された Message クラスでは日付は java.util.Date 型として生成されますが、このモデルを透過的にGraphQLのクエリに変更してくれるライブラリ組み込みの GsonVariablesSerializerjava.util.Date を雑に yyyy-MM-dd に変換します。 これでは精度的に問題なので、ISO 8601 にするために独自の GsonVariablesSerializer を作成します。

class GsonVariablesSerializer : VariablesSerializer {
    override fun serialize(variables: Map<String, Any>): String {
        return GsonBuilder()
            .registerTypeAdapter(
                Date::class.java,
                DateSerializer()
            )
            .create()
            .toJson(variables)
    }

    internal inner class DateSerializer : JsonSerializer<Date?> {
        override fun serialize(
            date: Date?,
            typeOfSrc: Type,
            context: JsonSerializationContext
        ): JsonElement {
            val df: DateFormat = SimpleDateFormat(
                "yyyy-MM-dd'T'HH:mm:ssXXX",
                Locale.getDefault()
            )
            return JsonPrimitive(df.format(Date()))
        }
    }
}

あとは次のようにクエリを組み立てて、GraphQLRequest を作成して Amplify.API.query します。 残念ながらKotlinのraw stringsでは$自体の文字として扱えないので${'$'}のようにしています。

val query = """
    query GetPost(
      ${'$'}roomId: String!
      ${'$'}limit: Int!
    ) {
      listMessagesSortedByCreatedAt(
        roomId: ${'$'}roomId
        sortDirection: ASC
        limit: ${'$'}limit
      ) {
        items {
          id
          content
          username
          roomId
          createdAt
        }
      }
    }
    """.trimIndent()
val variables: Map<String, Any> = mutableMapOf(
    "roomId" to DEFAULT_ROOM,
    "limit" to DEFAULT_LIMIT
)
val request =
    GraphQLRequest(query, variables, Message::class.java, GsonVariablesSerializer())

val listener = object :
    ResultListener<GraphQLResponse<Iterable<Message>>> {
    override fun onResult(response: GraphQLResponse<Iterable<Message>>) {
        val messages: List<Message> = response?.data?.map { it }.orEmpty()
        // do things with messages
    }

    override fun onError(e: Throwable) {
    }
}
Amplify.API.query(request, listener)

ミューテーションもまったく同様です。

val query = """
    mutation CreateMessage(
      ${'$'}id: ID!
      ${'$'}content: String!
      ${'$'}roomId: String!
      ${'$'}username: String!
      ${'$'}createdAt: AWSDateTime!
    ) {
      createMessage(input: {
        id: ${'$'}id
        content: ${'$'}content
        roomId: ${'$'}roomId
        username: ${'$'}username
        createdAt: ${'$'}createdAt
      }) {
        id
        content
        roomId
        username
        createdAt
      }
    }
    """.trimIndent()
val variables: Map<String, Any> = mutableMapOf(
    "id" to id,
    "content" to content,
    "roomId" to roomId,
    "username" to username,
    "createdAt" to createdAt
)
val request =
    GraphQLRequest(query, variables, Message::class.java, GsonVariablesSerializer())

Amplify.API.mutate(
    request,
    object :
        ResultListener<GraphQLResponse<Message>> {
        override fun onResult(response: GraphQLResponse<Message>) {
        }

        override fun onError(e: Throwable) {
        }
    }
)

これで、時系列にメッセージを読み書きするための対応ができました。

改善を望む内容

最後に今回気付いたいくつかの今後改善されるであろう内容を取り上げます。

まず、Amplify.API.query, Amplify.API. mutate, Amplify.API.subscribe はいずれも現在のところブロッキングAPIとなっています。これは、LoaderObservable 等でラップして扱うことを意図しているのか、それとも単に設計上のミスなのか判然としません。とにかく、このままではUIをブロックしてしまうので、RxJavaの Single, Completable でラップして使いました。

// query
val source: SingleOnSubscribe<List<Message>> = SingleOnSubscribe {
    val listener = object :
        ResultListener<GraphQLResponse<Iterable<Message>>> {
        override fun onResult(response: GraphQLResponse<Iterable<Message>>) {
            val messages: List<Message> = response?.data?.map { it }.orEmpty()
            it.onSuccess(messages)
        }

        override fun onError(e: Throwable) {
        }
    }
    Amplify.API.query(request, listener)
}
return Single.create(source)

// mutation
return Completable.create { emitter ->
    Amplify.API.mutate(
        request,
        object :
            ResultListener<GraphQLResponse<Message>> {
            override fun onResult(response: GraphQLResponse<Message>) {
                response.data?.let {
                    emitter.onComplete()
                }
            }

            override fun onError(e: Throwable) {
                emitter.onError(e)
            }
        })
}

// subscription
return Single.create { emitter ->
    Amplify.API.subscribe(
        Message::class.java,
        SubscriptionType.ON_CREATE,
        object :
            StreamListener<GraphQLResponse<Message>> {
            override fun onNext(response: GraphQLResponse<Message>) {
                response?.data?.let {
                    emitter.onSuccess(it)
                }
            }

            override fun onComplete() {
            }

            override fun onError(e: Throwable) {
                emitter.onError(e)
            }
        }
    )
}

利用する側は次の通り subscribeOn(Schedulers.io()) とすればワーカスレッドで処理が実行されます。

compositeDisposable.add(
    mutation(
        name,
        editTextContent.text.toString(),
        DEFAULT_ROOM
    )
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribeBy(
            onComplete = {
                editTextContent.setText("")
            },
            onError = {
                Log.e(TAG, it.message, it)
            }
        )
)

APIとしてListenerを受け取る以上、設計としては非同期なコールバックとして意図されている気がします。この辺りは折角のオープンソースなので何らかのフィードバックを行いたいと思います。

それから、前回のエントリ同様に、AWSMobileClient を使うことで認証機能を簡単に追加することができますが、認証にAWSのCognito User Poolsを使った場合Subscriptionがうまく動かないようです。SubscriptionAuthorizationHeader.from の実装を見る限り、まだAPI Keyにしか対応していないのかも知れません。この辺り、やはりまだまだプロダクション・レディとは言えません。

まとめ

ということで、駆け足になりましたが、今月プレビューリリースされたばかりのAmplify for Androidをざっと触ってみた感じをご紹介しました。 ライブラリとしてはまだまだこれから成熟していくのに期待しますが、AmplifyはたとえばAppSyncのコンソールで非常に簡単にクエリを実行して確認できるなど、デベロッパー体験が非常によいです。Amplifyは正直もっと流行っていいと思っています。これからも情報発信してゆくのでどうかお楽しみに。

それではよいお年を。

社会人大学院で得たもの、失ったもの

こんにちは。本エントリは 社会人学生 Advent Calendar 2019 の第7日目です!

このエントリでは、社会人大学院で得たもの、失ったものについて思うところを思いつくままに書こうと思います。 特に、失ったものについては正直に書いておく必要があるでしょう。

自己紹介

僕のブログでは社会人大学院のことをたびたび書いており今更感もあるのですが、このアドベントカレンダー経由で本エントリを読んでくださる方も当然いらっしゃると考えるので、コンテキストの共有のために改めて自己紹介をさせてください。

白山と申します。36歳会社員です。妻と2歳6歳の女児を育てながらフルタイム会社員をしています。
現在は北陸先端科学技術大学院大学(通称JAIST)の修士課程で情報科学を専攻しています。元々いわゆる文系出身ですが、かれこれ10年以上もIT産業の片隅で禄を食みつづけておりました。去年〜今年の春にかけて1年と少し米国にいた経験から、大学院に進む決断をしました。その辺りの詳細は次の記事に寄稿しましたので良かったら読んでいただけたら幸いです。

engineer-lab.findy-code.io

また、特にJAISTに関してはこれらのエントリにまとめてあります。JAISTに興味のお有りの方はご参照ください。

fushiroyama.hatenablog.com

fushiroyama.hatenablog.com

社会人大学院で得たもの

それでは早速、良い面から書いていきましょう!

学位

すみません、まだまったく修了していないし目処も立っていないですが、これは書かないわけにはいかないですよね。学位。学位記。修士(博士)号。欲しい。

学位って何なんでしょうか。
前出のエントリには偉そうに「今ここに無い価値を創造する力を養うため(キリッ」みたいなことを書きましたし「米国の就労ビザが求める要件として関連学位が必要」とも書きました。しかしですね、力を養いたいなら別に大学院に行かなくたって夜中にシコシコ勉強すればいいんですよね。就労ビザだって、実のところ僕はIT業界で実務経験が12年ぐらいあるので、仮にBachelor of Artsでも弁護士の腕次第でH-1Bに申請できる可能性が高いです。

じゃあ何かって、ソシャゲのメダルみたいなものではないでしょうか。究極的には自己満足です。2〜3年間痛みに耐えてよく頑張った!感動した!それでいいじゃありませんか。その上で、就職にもうな重の山椒ほどの効果が上乗せできるなら上出来です。

よく言われるらしいですが、学部は参加賞、修士は努力賞だそうです。では博士は何賞なのか。 僕の指導教官が言っておりましたが、

「博士はね、いまも新規性は大事ではあるけど、最近では『自分で考えて』『計画を立てて』『表に出した』ということを『やれる子ですよ!』という認定に近いと思いますよ😌」

だそうです。なるほど認定証。欲しい!

モチベーションの高い同志

これは社会人大学院固有かもしれません。社会人大学院は非常にモチベーションが高い同級生がたくさんいます。 働きながら苦労してまで大学院に行こうという人たちなので、これは当然のことです。

20年前に修士を修了したけど、子供が独立したし役職定年で時間が出来たので博士課程に来ているお父さん。 教育学修士をすでに持っているけど、自分の幅を広げるためにさらに別の専攻に進んだ小学校の先生。 そして僕のように専攻も違うし学部しか出ていないけどキャリアアップのために修士にきてる人。 実に多種多様です。

企業の研究員も看護師さんもお医者さんも大学の先生まで同級生にいました。みんな色んな思いで大学に来ています。この情熱を分けてもらえるだけで通っている甲斐があります。

学力や能力

じゃあ大学院はやりがいや自己満足だけかというと、そんな訳ないですね。学力が普通に身につきます。 というか学力がないと単位が取れません。修了できません…

他のエントリにも書きましたが、JAIST情報科学は甘くないです。これだけ長いことIT産業にいて、しかも僕は競技プログラミングや数学、UNIXオペレーティングシステムなどの自習をかれこれ結構長いことやってきたのでまあ何とかなるかな?と高をくくっていたのですが、あんまり何とかなっていません。

勉強している内容は難しく、しかもその前提となる学部レベルの線形代数微積分学の勉強もまったく足りておらず、普通に座っているだけでは全然勉強についていけません。毎晩必死に勉強しています。自分の脳が、20年ぶりに呼吸をしているのを感じます。これが成長…!

他にも得たものはたくさんあると思うんですけど、あまり長くなっても読んでもらえないのでこの辺で。

社会人大学院で失ったもの

ここからは重く苦しい現実をお伝えしたいと思います。

子供との時間

JAIST(社会人コース)の情報科学は、金曜の夜と土日に開講しています。僕の場合は必修と修士論文を除けば、だいたい20単位取らないといけません。 JAISTは4学期制なので、1学期あたり2.5単位とれば2年で(単位としては)足りる計算です。3ヶ月ごとに1〜2講義です。

これがそれほど甘くありません。当初、この計算をしてみて「土日の1日ずつ差し出せば行ける!」と喜んだのですが、実際に蓋を開けてみると大学の授業はそんなに都合良くは開講しないので、授業によっては2コマずつ土日両方みたいな感じになります。 土日両方なので家族で旅行にもいけませんし、そもそも連日開講される授業の予習復習で土日も朝3時に起きて3時間資料に目を通すような生活がずっと続いています。

妻にも「大学院に行くのはいいよ・・・でも、『今』なの?」とため息まじりに言われました。何も言えない。

余暇

余暇などというものは何もなくなりました。1学期にたった1講義でも、予習復習で他のことを考えている余裕がありません。

そもそも僕は理系学部を出ていません。そうすると彼らが当然知っている線形代数や大学の微積分学をちゃんと身に付けていません。 ちょうど2年前ぐらいに機械学習が流行り始めて、僕も高校微積をひとりでコツコツやってたんですが、割と冗談抜きで大学の微積はレベルが違って、授業の板書を見返したときに式がなにをやってるか分からず半泣きで大学積分のテキストを開くというようなことを毎夜しています。

これはJAIST固有なのか理系固有なのかわかりませんが、出席点などというものはないので、基本的にテストができないと落第します。留年します。このプレッシャーは僕は未経験でした。酒を飲んでる余裕すらないんです。酒を飲むと朝起きて勉強ができない。勉強ができないと単位が取れない。 仕事終わりや休日に講義を受けて、みんなで楽しく議論してレポートを書いて、2〜3年で帽子🎓かぶって卒業なんて、とんでもない。そんな甘いものではなかった。

修士論文が始まると、おそらく行き帰りの電車の中ですらそれをずっと考える日々が始まると思います。大丈夫なのか不安です。

まとめ

本当に「今」大学院なのかというは未だに自問自答します。家族の信頼を揺るがしてまでやることなのかと。 僕は結構海外出張があるのですが、そこから逆算して合間を縫って取れる講義をとっているので、休日は大学に行ってていないし講義が少し途切れるタイミングで待っていたかのように一週間仕事で海外に行くような感じになるので、家族は「パパは休日家にいない人」みたいな感じになりつつあります。

結婚して、子供ができて、僕は一貫して定時で帰って家族と一緒に過ごすという日々を過ごしてきたので、これには忸怩たる思いもあります。 ただね、桜木花道じゃないですが、僕は「今」なんですよ…30代でエンジニアとしても脂の乗った「今」さらなる力をつけて飛翔したい…40歳までのあと4年の投資がその先の20年を左右すると思っているのです。それまで家族とのバランスを取りながらも前に進むしかない。ボーッと生きてたら人生終わってしまう!

また来年のアドベントカレンダーで進捗を共有したいと思います。また会うその日まで、じゃあの!

AWS Amplifyで30分でGraphQL環境を用意してAndroidとつなぐぞっ!つかまれッ!

こんにちは!本エントリは「AWS Amplify Advent Calendar 2019」の記念すべき第1日目です!

このエントリではAWS Amplifyを使ってユーザ認証機能付きのリアルタイムチャットアプリを30分で作ってみたいと思います。

AWS Amplifyとは

AWS Amplify(以下Amplify)を初めて耳にする方、ご安心ください!
Amplifyはスケールするモバイルアプリおよびウェブアプリを最速で構築するためのサービス/ツール群です。

aws.amazon.com

Amplifyは

  • API機能の簡単な追加
  • 認証機能の簡単な追加
  • 画像/文字認識などのAI機能の簡単な追加
  • オフラインのデータ同期
  • これらを利用するためのライブラリやUIコンポーネント

などを含んでおり、モバイルアプリやウェブアプリを開発して実際にサービスを提供するのに必要なフロントエンド/バックエンドを非常に簡単に用意することができます。実際にその様子をお見せしましょう!

今回作るアプリ

冒頭で述べたように、ユーザ認証機能付きのリアルタイムチャットアプリを作ってみたいと思います。 このアプリは次のような機能を備えています。

  • アプリから簡単にユーザ登録してログインできる
  • ログインしたらこれまでのチャット履歴が表示される
  • 好きな文章を入力して送信できる
  • 任意の人数がチャットに参加でき、参加者のコメントは能動的にリロードする必要なくリアルタイムに表示される
  • ログアウトしてチャットを終了できる

f:id:fushiroyama:20191201023816g:plain
Amplifyで作ったリアルタイムチャットアプリ

APIにGraphQLを使ってみる

AmplifyでAPIを追加するとき、主に2つの選択肢があります。

RESTful APIについては言わずもがな。これは裏ではAmazon API GatewayAWS Lambdaによって実現されますが、こちらは想像がつきやすいので今回は利用しません。

今回はRESTにかわる規格として注目を集めているGraphQLを利用してみたいと思います。GraphQLに関しては「GraphQL」徹底入門 ─ RESTとの比較、API・フロント双方の実装から学ぶが素晴らしい記事ですので別途ご覧ください。

AWSではAWS AppSyncというGraphQLのマネージドサービスが提供されています。マネージドサービスということは、自分たちでサーバを用意しなくても簡単にGraphQLを利用できるということです!

AmplifyでGraphQLをAPIに選択すると裏ではこのAppSyncが使われますが、今回のアプリを作るにあたってはバックエンドで何が使われているかといった内容はほとんど意識することなく簡単に開発することができます。

Androidプロジェクトを作成

今回はAndroidアプリでGraphQLを使ってみたいと思います。本エントリのiOS対応版も近い内に公開予定なので楽しみにしてください!☺️

AndroidアプリはAndroid Studioのウィザードから普通に作成して、アプリが起動するのを確認できればOKです。ここを便宜上 ${PROJECT_ROOT} とします。

Amplify CLIのインストール

Amplify CLIというコマンドラインツールをインストールして、これで対話的に機能を追加したり編集するのがAmplifyを利用する基本的な流れとなります。

ターミナルから次のコマンドでAmplify CLIをインストールします。環境によっては sudo が必要です。

$ npm install -g @aws-amplify/cli
$ amplify configure

amplify configureAWSにIAMユーザを作成し、そのユーザ権限でCLIを実行できるようになります。 この辺りに少し不慣れな方はAWS Amplify ハンズオン 基本ステップに画像つきで詳細に解説されているので参考にしてください!

amplify init

次に ${PROJECT_ROOT}amplify init コマンドを実行し、AndroidプロジェクトにAmplifyをセットアップします。

  • Enter a name for the project AmplifyAndroid
  • Enter a name for the environment dev
  • Where is your Res directory: app/src/main/res

あたりを選択/入力すれば、あとはデフォルトで問題ありません。

$ amplify init

Note: It is recommended to run this command from the root of your app directory
? Enter a name for the project AmplifyAndroid
? Enter a name for the environment dev
? Choose your default editor: Vim (via Terminal, Mac OS only)
? Choose the type of app that you're building android
Please tell us about your project
? Where is your Res directory:  app/src/main/res
Using default provider  awscloudformation

For more information on AWS Profiles, see:
https://docs.aws.amazon.com/cli/latest/userguide/cli-multiple-profiles.html

? Do you want to use an AWS profile? Yes
? Please choose the profile you want to use default
⠋ Initializing project in the cloud...
✔ Successfully created initial AWS cloud resources for deployments.
✔ Initialized provider successfully.
Initialized your environment successfully.

Your project has been successfully initialized and connected to the cloud!

Some next steps:
"amplify status" will show you what you've added already and if it's locally configured or deployed
"amplify <category> add" will allow you to add features like user login or a backend API
"amplify push" will build all your local backend resources and provision it in the cloud
"amplify publish" will build all your local backend and frontend resources (if you have hosting category added) and provision it in the cloud

Pro tip:
Try "amplify add api" to create a backend API and then "amplify publish" to deploy everything

ここで何が行われているかは、AWS Amplify ハンズオン 基本ステップの「このとき何が起きているか」を参照してください。

無事に完了すると amplify status でこのプロジェクトのAmplifyの設定を見ることができます。

$ amplify status
| Category | Resource name | Operation | Provider plugin |
| -------- | ------------- | --------- | --------------- |

いまは何もしていないのでこれで問題ありません。

GraphQL(AppSync)セットアップ

このプロジェクトにGraphQLをセットアップします!
amplify add api で「GraphQL」を選択します。 本アプリでは認証機能を利用するので、認証は API Key ではなく API Amazon Cognito User Pool を選択します。

$ amplify add api

? Please select from one of the below mentioned services GraphQL
? Provide API name: amplifyandroid
? Choose an authorization type for the API Amazon Cognito User Pool
Using service: Cognito, provided by: awscloudformation

 The current configured provider is Amazon Cognito.

 Do you want to use the default authentication and security configuration? Default configuration
 Warning: you will not be able to edit these selections.
 How do you want users to be able to sign in when using your Cognito User Pool? Email
 Warning: you will not be able to edit these selections.
 What attributes are required for signing up? (Press <space> to select, <a> to toggle all, <i> to invert selection)Email
Successfully added auth resource

次にGraphQLのスキーマを変更します。今回はチャットのメッセージとして

  • 一意なID
  • メッセージの送信者
  • メッセージ本文

を持ったMessageというモデルを作ることにします。

次の例のように選択して進んでいき、 ${PROJECT_ROOT}/amplify/backend/api/amplifyandroid/schema.graphql を編集します。

? Do you have an annotated GraphQL schema? No
? Do you want a guided schema creation? Yes
? What best describes your project: Single object with fields (e.g., “Todo” with ID, name, description)
? Do you want to edit the schema now? Yes
Please edit the file in your editor: /Users/shiroyaf/git/amplify/AmplifyAndroid/amplify/backend/api/amplifyandroid/schema.graphql
? Press enter to continue

モデルは次のようにします。

type Message @model {
  id: ID!
  username: String!
  content: String!
}

編集したらウィザードを進めます。

GraphQL schema compiled successfully.
Edit your schema at /Users/shiroyaf/git/amplify/AmplifyAndroid/amplify/backend/api/amplifyandroid/schema.graphql or place .graphql files in a directory at /Users/shiroyaf/git/amplify/AmplifyAndroid/amplify/backend/api/amplifyandroid/schema
Successfully added resource amplifyandroid locally

Some next steps:
"amplify push" will build all your local backend resources and provision it in the cloud
"amplify publish" will build all your local backend and frontend resources (if you have hosting category added) and provision it in the cloud

ここまで来たらセットアップ完了です! amplify status するとローカルに「Auth機能」と「Api機能」が追加されています。

| Category | Resource name   | Operation | Provider plugin   |
| -------- | --------------- | --------- | ----------------- |
| Auth     | xxxxxxxxxxxxxxx | Create    | awscloudformation |
| Api      | amplifyandroid  | Create    | awscloudformation |

amplify push することで、このバックエンド構成がそのままAWS上にプロビジョニングされます。
とっつきにくいのはここまでです!ここからは楽しい楽しいアプリ開発です!

Androidアプリへの組み込み

このあと慎重な人であれば、AWSマネジメントコンソールから認証機能の実態である Cognito User Pool にユーザを作成してログインを試みたり、GraphQLのコンソールからクエリを発行したりしたいところですが、今回は敢えてそのあたりに可能な限り触れません。その辺りを意識しなくても開発できるのがAmplifyの良さだと思うからです。

なので、いきなりアプリを書き始めます。 まずGetting Startedを参考に必要なライブラリや設定を追加します。 どれも、Androidエンジニアにはお馴染みの設定なので特に詰まることもないでしょう。

// project's build.gradle
classpath 'com.amazonaws:aws-android-sdk-appsync-gradle-plugin:2.9.+'
// app's build.gradle
apply plugin: 'com.amazonaws.appsync'

dependencies {
    //AWS Base SDK
    implementation 'com.amazonaws:aws-android-sdk-core:2.15.+'

    //AppSync SDK
    implementation 'com.amazonaws:aws-android-sdk-appsync:2.8.+'
    implementation 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.0'
    implementation 'org.eclipse.paho:org.eclipse.paho.android.service:1.1.1'

    // Needed for AppSync Subscription https://github.com/eclipse/paho.mqtt.android/issues/321
    implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'

    // Cognito
    implementation 'com.amazonaws:aws-android-sdk-cognitoidentityprovider:2.9.+'
    //For AWSMobileClient only:
    implementation 'com.amazonaws:aws-android-sdk-mobile-client:2.15.+'

    //For the drop-in UI also:
    implementation 'com.amazonaws:aws-android-sdk-auth-userpools:2.15.+'
    implementation 'com.amazonaws:aws-android-sdk-auth-ui:2.15.+''
}
// AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>


    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">

        <service android:name="org.eclipse.paho.android.service.MqttService" />

    </application>

認証機能の実前

前出の手順で amplify init を正しくセットアップできていると、 ./app/src/main/res/raw/awsconfiguration.json というファイルが作成されているはずです。これはAndroidアプリからAmplifyを利用するための設定を中央集権的にするための設定ファイルです。

念の為中を確認し、CognitoUserPool など設定がきちんとされているか確認してください。PoolIdAppClientId 等はAWSマネジメントコンソールの Cognito User Pool の設定画面で見つけることができます。

$ cat ./app/src/main/res/raw/awsconfiguration.json

{
    "UserAgent": "aws-amplify-cli/0.1.0",
    "Version": "1.0",
    "IdentityManager": {
        "Default": {}
    },
    "CognitoUserPool": {
        "Default": {
            "PoolId": "ap-northeast-1_xxxxxxxxx",
            "AppClientId": "xxxxxxxxxxxxxxxxxxxxxxxxxx",
            "AppClientSecret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
            "Region": "ap-northeast-1"
        }
    },
    "AppSync": {
        "Default": {
            "ApiUrl": "https://xxxxxxxxxxxxxxxxxxxxxxxxxx.appsync-api.ap-northeast-1.amazonaws.com/graphql",
            "Region": "ap-northeast-1",
            "AuthMode": "AMAZON_COGNITO_USER_POOLS",
            "ClientDatabasePrefix": "amplifyandroid-dev_AMAZON_COGNITO_USER_POOLS"
        }
    }
}

いよいよAndroidのコードに取り掛かります。 まずはGraphQLのクエリ(取得)やミューテーション(追加/変更)などの操作のすべての起点となる AWSAppSyncClient を作ります。
次の例を参考に、ビルダーには cognitoUserPoolsAuthProvider を指定するのを忘れないようにします。

lateinit var awsAppSyncClient: AWSAppSyncClient

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    val awsConfig = AWSConfiguration(applicationContext)
    val cognitoUserPool = CognitoUserPool(applicationContext, awsConfig)
    val basicCognitoUserPoolsAuthProvider = BasicCognitoUserPoolsAuthProvider(cognitoUserPool)

    awsAppSyncClient = AWSAppSyncClient.builder()
        .context(applicationContext)
        .awsConfiguration(awsConfig)
        .cognitoUserPoolsAuthProvider(basicCognitoUserPoolsAuthProvider)
        .build()
}

次に、AWSのモバイルSDKでJWTトークンやAWSクレデンシャルを扱うための AWSMobileClient を初期化します。

AWSMobileClient.getInstance()
    .initialize(applicationContext, object : Callback<UserStateDetails> {
        override fun onResult(userStateDetails: UserStateDetails) {
            Log.i(TAG, "onResult: " + userStateDetails.userState)
        }

        override fun onError(e: Exception) {
            Log.e(TAG, "INIT: Initialization error.", e)
        }
    })

次に、認証画面を表示する部分を作ります。

先にインストールしたdependenciesのおかげで、認証用の画面は自分で作らなくても利用することができます。呼び出しは AWSMobileClient.getInstance().showSignIn(context) だけです。

問題は呼び出す場所ですが、AWSMobileClient には認証状態に合わせてコールバックしてもらえるリスナが用意されているので次のように登録します。 あとは、ユーザがサインインしていない状況で showSignIn メソッドを呼んでやるだけです。

リスナは、ログインの前後でActivityを行ったり来たりしても多重登録されないように onStart/onStop 辺りで登録解除してやるとよいでしょう。

private val userStateListener = UserStateListener { details ->
    Log.i(TAG, "onUserStateChanged: " + details.userState)
    when (details.userState) {
        UserState.SIGNED_IN -> {
            Log.i(TAG, "userState: SIGNED_IN")
        }
        else -> {
            Log.i(TAG, "userState: else: " + details.userState)
            AWSMobileClient.getInstance().showSignIn(this@MainActivity)
        }
    }
}

override fun onStart() {
    super.onStart()
    AWSMobileClient.getInstance().addUserStateListener(userStateListener)
    query()
}

override fun onStop() {
    AWSMobileClient.getInstance().removeUserStateListener(userStateListener)
    super.onStop()
}

これでログイン機能は実装できました!

サインイン画面にはユーザの新規作成、確認コードの送信なども最初から組み込まれており完全に動作します。ここで早速ユーザを作成してログインを試してみてください。

f:id:fushiroyama:20191201052622j:plain
サインイン画面
f:id:fushiroyama:20191201052751j:plain
ここからサインアップもできる
f:id:fushiroyama:20191201052828j:plain
確認コードも提供される

これでログイン状態でGraphQLのAPIを自由にアクセスすることができるようになりました。

ミューテーションを実装する

GraphQLではデータの作成や変更、削除などをミューテーションと呼びます。

先にウィザードでMessageモデルを作ったことで、Amplifyが自動的にそのモデルを使って行うミューテーションのためのデータ型などを自動生成してくれます。なのでアプリ作者は

  1. CreateMessageMutation オブジェクトを作る
  2. AwsAppSyncClient#mutate に対してエンキューする

だけでミューテーションを簡単に行うことができます。

次のようなメソッドを作って、ボタンクリックなどをトリガとして実行することで簡単にデータをGraphQL越しに永続化することができます。

private fun mutation(
    id: String = System.currentTimeMillis().toString(),
    username: String,
    content: String
) {
    val callback: GraphQLCall.Callback<CreateMessageMutation.Data> =
        object : GraphQLCall.Callback<CreateMessageMutation.Data>() {
            override fun onFailure(e: ApolloException) {
                e.message?.let {
                    toast(it)
                    Log.e(TAG, it, e)
                }
            }

            override fun onResponse(response: Response<CreateMessageMutation.Data>) {
                response.data()?.createMessage()?.let {
                    Log.i(TAG, it.toString())
                    editTextContent.setText("")
                }
            }
        }

    val input = CreateMessageInput
        .builder()
        .id(id)
        .username(username)
        .content(content)
        .build()

    awsAppSyncClient
        .mutate(CreateMessageMutation.builder().input(input).build())
        .enqueue(callback)
}

Amplifyのウィザードでセットアップした場合、デフォルトでAmazon DynamoDBをデータソースとして扱います。したがってミューテーション後はマネジメントコンソールからデータが格納された様子を確認することができます。

クエリを実装する

同様に、GraphQLではデータ取得操作をクエリと呼びます。
クエリもまったく同じように

  1. 型安全にリクエストやコールバックを作成し、
  2. AwsAppSyncClient#query

するだけでデータを取得することができます。

private fun query() {
    val callback: GraphQLCall.Callback<ListMessagesQuery.Data> =
        object : GraphQLCall.Callback<ListMessagesQuery.Data>() {
            override fun onFailure(e: ApolloException) {
                e.message?.let {
                    toast(it)
                    Log.e(TAG, it, e)
                }
            }

            override fun onResponse(response: Response<ListMessagesQuery.Data>) {
                val messages: MutableList<ListMessagesQuery.Item> =
                    response.data()?.listMessages()?.items() ?: mutableListOf()

                updateMessages(messages)

                val text = messages.joinToString()
                Log.i(TAG, text)
            }
        }
    awsAppSyncClient.query(
        ListMessagesQuery
            .builder()
            .limit(DEFAULT_LIMIT)
            .build()
    )
        .responseFetcher(AppSyncResponseFetchers.CACHE_AND_NETWORK)
        .enqueue(callback)
}

取得したデータは RecyclerView などに渡して画面に反映するのが典型的な使い方でしょう。

サブスクリプションを実装する

最後に、リアルタイムにミューテーションを通知してもらうためのサブスクリプションを実装します。 もうお気づきだと思いますが、これもまったく作法は同じです。

private fun subscription() {
    val subscription: OnCreateMessageSubscription =
        OnCreateMessageSubscription.builder().build()
    val callback = object : AppSyncSubscriptionCall.Callback<OnCreateMessageSubscription.Data> {
        override fun onFailure(e: ApolloException) {
            e.message?.let {
                toast(it)
                Log.e(TAG, it, e)
            }
        }

        override fun onResponse(response: Response<OnCreateMessageSubscription.Data>) {
            response.data()?.onCreateMessage()?.let {
                val item = ListMessagesQuery.Item(
                    it.__typename(),
                    it.id(),
                    it.username(),
                    it.content()
                )
                addMessage(item)
                Log.i(TAG, "subscription onResponse: ${it.toString()}")
            }
        }

        override fun onCompleted() {
            Log.i(TAG, "subscription onCompleted")
        }
    }
    val subscriptionWatcher: AppSyncSubscriptionCall<OnCreateMessageSubscription.Data> =
        awsAppSyncClient.subscribe(subscription)
    subscriptionWatcher.execute(callback)
}

新しいメッセージが追加されるごとに onResponse がコールバックされるので、それを RecyclerView が持つリストに追加して表示を更新するなどといった使い方が一般的でしょう。

f:id:fushiroyama:20191201055140j:plain
完成図

サインアウトを実装

最後にサインアウトを実装します。 サインアウト処理自体は AWSMobileClient#signOut メソッドを呼ぶだけです。

今回はオプションメニューに追加してみました。

override fun onOptionsItemSelected(item: MenuItem): Boolean {
    when (item.itemId) {
        R.id.signOut -> {
            AWSMobileClient.getInstance().signOut(
                SignOutOptions.builder().invalidateTokens(true).build(),
                object : Callback<Void> {
                    override fun onResult(result: Void?) {
                        Log.i(TAG, "signOut(): onResult ok")
                    }

                    override fun onError(e: java.lang.Exception?) {
                        Log.e(TAG, "signOut(): onResult error")
                    }
                })
            return true
        }
        else -> {
            return super.onOptionsItemSelected(item)
        }
    }
}

f:id:fushiroyama:20191201055459j:plain
サインアウト

サインアウトすると自動的に認証状態の変更を検知するリスナが発火されてサインイン画面が再び表示されるはずです。

まとめ

ちょうど30分ぐらいでしょうか!GraphQLがこれほど簡単に使えることに驚かれたのではないでしょうか。

AmplifyはバックエンドはすべてAWSのサービスであり、設定や組み合わせは柔軟でスケールもします。この使い勝手の良さとカスタマイズ性のバランスがAmplifyの大きな魅力だと個人的には考えています。

今日の例はごくごく初歩的な内容に留めましたが、今後いくつかのエントリでAmplifyを使ったネイティブアプリ開発の実践的な例をご紹介できたらなと考えています。楽しみにお待ち下さい!

JAISTに入学してひと月が経ちました

JAISTに入学してようやくひと月が経ちました1
ひと言でいうとJAISTは最高です。働きながら大学院生になった感想を残しておこうと思います。

JAISTは最高

JAISTは最高です。僕は東京サテライトの学生なので以下特に「石川本校」と断りのない限り東京社会人コースのことだと思ってください。

学生のレベルとモチベーションが高い

社会人コースはその名の通り社会人しかいません。働きながら勉強しようという連中なので当然非常に高いモチベーションです。
グループワークをするとみんなつばを撒き散らしながら白熱の議論をしますし、発表するとなるとマイクを奪い合って登壇します。

また、どういうわけかすでに高い教育を受けて世界を股にかけて活躍している第一線のビジネスパーソンがずらりと揃っています。JAISTは入試の際に「自分の出身大学、指導教官、勤め先などを一切明かしてはならない」というルールがあります。これは面接官にバイアスを与えないためですが、それにも関わらず彼らがきっちり勝ち残って入学しているのはとても不思議に感じます。
とにかく、そのおかげで授業は引き締まって高レベルに展開され、議論は実経験を反映した実りの多いものになり、みな自分の社会人生活を通じて得た知識や学びをシェアするのに惜しみがありません。これは大変な刺激です。

JAISTは甘くない

JAIST浅野哲夫学長の入学の挨拶の言葉を引用すると「偏差値一辺倒の大学教育から一線を画した多様な人材を受け入れる」大学です。このため、確か数年前から「線形代数」や「英語」の筆記試験を廃止し、いまは小論文と面接一本に絞られました2。これによってこれまでは入学できなかった数学/英語が苦手な生徒も多く入学したことと思います。ただし当然のことですが、入学したからといって修了できるとは限らないのです。

JAISTの授業はレベルが高く、単位の認定は厳しいです。これは複数の教授と元卒業生の同僚の証言から確かなことです。「門戸は開いてやるから実力で這い出てこいよ」ということです。例えば僕が履修している「情報解析学特論」という講義は、初回の授業で「今日はいきなりで心の準備もできてないだろうから、高校・大学の復習のつもりで聞いてくださいね」と始まった内容すら、僕にはついていくのが必死でした。

あらためて、高校まででやるような数学はごくごく基本的なパズルのピースであって、大学や大学院の授業ではそれらを使って応用的な内容を学ぶのです。院試でわざわざ問わないから君たちが当然にそれを用意してくるのですよ、さもなくば単位は与えませんよということです。とにかく必死に食らいついていくしかありません。

それから内容以上にキツイのがその過密スケジュールです。JAIST東京では情報科学系の授業は金曜夜と土日にだけ開講されるのですが、これで平日フルに勉強している石川本校と同じ到達点を目指すので、土日100分 * 2コマずつで2ヶ月で期末試験まで完了というような厳しい日程になります。土日連続開講なので予習復習の期間もほとんどなく、試験勉強も充分にできません。履修がたとえ4半期1教科でも平日夜は予習復習で必死です。授業はほとんどその確認ぐらいの余裕がないと到底ついていけません。

正直すでに働きながら2年での修了は現実的ではないので3年計画を立てています。JAISTは長期履修制度を使って2年分の学費で3年通うことができるのでこれもありがたい点ではあります。

コンピュータサイエンスって何なんスかね

僕はアルゴリズムとデータ構造、グラフ理論、計算複雑性理論などに関心が高く、ちょいちょい自習していたので正直に言うと大学院でコンピュータサイエンスを学ぶと言ってもこれまでの復習かそれを補強するものになるだろうと高をくくっていましたが、これは大変に甘い目論見であることを知りました。コンピュータサイエンスの裾野は自分が知っているよりも遥かに広く、そのごく一分野を掘るだけでもやっとだとよくわかりました。

シラバスを眺めるだけでも

と楽しみな授業ばかりです。

いまはBluetoothメッシュネットワーク、非同期分散データベース、信号解析(とくに音声データ)などに関心が高まっているので、この3年で研究につながる授業を幅広く取れたらなぁと思っています。JAISTに在学中の先輩方、これから入学を考えているみなさんとぜひ交流したいです。

JAISTは大学を出てなくても入れるよ

そういえば意外にご存じない方が多かったのでここで触れておくと、JAISTは必ずしも大学の学部を卒業していなくても出願できます。
詳しくは「学生募集要項」を見て欲しいですが、社会人経験や取得資格を「学部卒業相当」と見なし、受験資格を与えるための事前審査があり、概ね5年以上の勤務経験があれば受験できることが多いようです。なんでもチャレンジしてみるものです!応援しています!

おわりに

そういえば「34歳からの数学博士」のさのたけとさんのVoicyに呼んでいただけたので、さのさんと堤修一さんと一緒に大学教育やらについて話しました。
さのさんも堤さんも理系修士で、堤さんは情報系なのですが、修了された立場から必ずしもコンピュータサイエンスが必要かどうか分からないといった生の意見が収録されています。もし良かったらぜひ聴いてみてください!ほいじゃあの!

voicy.jp


  1. 指導教官とは入学前からかれこれ半年も研究テーマについて議論したり面談をしてもらったりしていたのでまだひと月か!という感想が大きいです。

  2. その代わり面接は論文発表会さながらの緊迫感で、自分の研究計画を他の論文やデータを交えながら発表して自分が入学を希望する学科の教授4人に囲まれてボコボコにされるという厳しい場です。

ゴキブリが出た

乾かして取り込んだばかりの子供のお昼寝タオルを見ていると、もうすぐ小学校に行ってしまう娘のことを考えて胸が詰まった。

毎週毎週洗われて少しずつ色あせ、厚みもだんだん減っていったタオルケット。
保育園のベッドにフィットするようにゴムが縫い付けてあり、長女の卒園と共に退役してももはや普通のタオルには戻れない、まさに長女の幼少期に命を捧げたタオルケット。

乳児クラスのときにだけ使っていたお手拭きが引退し、幼児クラスのお昼寝タオルも現役を退く。それに連れて子供は大きく強く成長し、少しずつだがそれでも確実に親からの自立の道を歩んでいる。子供の親への依存と執着はちょっとずつタオルに吸い込まれ、気付いたら子供らは親のことは忘れて自分の足で歩んでゆくのだろう。

まるでトイストーリーのおもちゃのようではないか。タオルがではない、我々親がだ。 いつしか子供はおもちゃから巣立ち、親もまたその現実を受け入れて子供から巣立たねばならないのだ。

子供との出会いはあまりにも破壊的で不可逆に僕の人生を変えてしまった。もうそれ以前の暮らしがどうだったかすらよく覚えていない程だ。 それほどまでに愛情を注いだ対象がだんだん我々の存在を必要としなくなるという事実を受け入れるのは難しいことだ。しかしそれは正しいことだし、確実に起こることなのだ。かつての自分がそうだったのだから。

とか何とか言ってたらゴキブリが出た。終わった、我が心の安寧は子供の自立よりも早く失われた。 このマンスョンを買って5年。初めてゴキカブリにお目にかかった。なに?住んでるの?実は住んでたの?娘と共に成長していたの?

恐怖で背筋が凍るとはこのことだ。私は机の上に飛び乗ってそのまま動けない。本当の恐怖に触れると、人間は声すら出ないのだ。 奴はどこだ。一旦部屋の対角に行った。するとメラメラと闘争心が湧き上がってきた。子供たちを守らなくてはならない(論理の飛躍)

まず奴の退路を断つために逃げられる扉をすべて閉じた。それから然るべき決戦の地、五丈原(風呂場)に誘導するために岸田メルのポーズで新聞紙を掻き鳴らす。 しめた!うまく五丈原に陽動した。そのまま一旦風呂場の扉を閉める。

すぐに我が家の論理ストレージであるところのセブンイレブンで高エネルギーポジトロンライフルキンチョール型を調達して戻る。 敵は渭水を背にして陣取っているが、背を向けた一瞬を捉えて一気に噴射した!ここで引くわけにはいかないのだ。やるかやられるかなのだ…許してくれ張郃

いずこよりいまし荒ぶる神とは存ぜぬも、かしこみかしこみ申す。この地に塚を築きあなたの御魂をお祭りします。怨みを忘れ静まりたまえ🙏