Studyplus Engineering Blog

スタディプラスの開発者が発信するブログ

iOSDC2018に参加しました

こんにちは。スタディプラスに9月からiOSエンジニアとして入社した弘田です。

Kotlin festの記事でも紹介しましたが、 弊社には 勉強会・カンファレンス参加補助があるので、iOSDC2018に参加させていただきました。ありがとうございます。 弊社のiOSエンジニアも登壇したので弊社からは2名参加しました。 内一名は「iOSでグラフを描くために必要な知識について」というタイトルで登壇しています。

ちなみにスタディプラスは今回シルバースポンサーとして協賛して、ノベルティ(付箋とボールペン)を提供しております。

iOSDC 2018とは

iOSDC JapanはiOS関連技術をコアのテーマとした技術者のためのカンファレンスです。 去年の開催を大盛況のもと終え、みなさまのBlogやSNSでのシェアのおかげで無事、帰って参りました! そして、今年は3日+前夜祭の3.5日開催です!祭りです!予定を空けておいてください!

iOSDC公式ページから引用

参加したセッション

セッションの数が多いのでいくつか抜粋してご紹介させていただきます。 ※登壇者の敬称略

「標準アプリから学ぶ、HIGが教えてくれないiOSデザインのこと」

Ryo Usami : https://speakerdeck.com/usa619/biao-zhun-apurikaraxue-bu-higgajiao-etekurenaiiosdezainfalsekoto

最近よく見るSemi-ModalViewの解説があり、ふわっとした認識が固まったので聞けて良かった。


MicroViewControllerで無限にスケールするiOS開発

tarunon : https://www.icloud.com/keynote/0vgTYDXyHQTd0l1FKTiF1jT7g#MicroViewController-en

ベストトーク賞1位のセッション まだちゃんと理解しきれてないので、スライドの読み直しやビデオを見たいと思います。


5000行のUITableViewを差分更新する

ばんじゅん🍓 : https://speakerdeck.com/banjun/difference-update-uitableview-with-5000-rows

最近流行っている差分更新 5000行もあれば遅くなるので、差分更新ライブラリなどを比較して早くするのが非常に面白かった。


UIViewとUITextInputで作る縦書きのTextView

六々 : https://speakerdeck.com/cc4966/vertical-textview-based-on-uiview-with-uitextinput

標準では用意されていない縦書きのTextViewを自作するセッション 文字の描画エンジンも自作だそうで開発に数年かかっている大作らしい。 基本的に縦書きで表現したいことは少ないが需要はあると感じた。


iOSでグラフを描くために必要な知識について

須藤将史 : https://speakerdeck.com/masashi_sutou/iostekurahuwomiao-kutamenibi-yao-nazhi-shi iOSDC2018で「iOSでグラフを描くために必要な知識について」というタイトルで発表しました

弊社iOSエンジニアのセッションです。 Studyplusではグラフが多く使用されており、その全てをライブラリを使用せず自前で書いています。 グラフを書く際に必要な知識などがセッション内で解説されています。


その他のセッション

Qiitaにまとめてくださっている方がいました。 iOSDC 2018 セッション資料まとめ

雑感

  • 今回が3回目の開催 回を重ねるごとに参加者が増えている 参加者が増えているのにスタッフの対応の質などが毎年上がっている 会場のネットワークチームが快適なWi-Fiを提供してくれていた!

  • ランチは毎日違う物が提供されていた

  • スポンサーブースの力の入れ方がすごかった

まとめ

聞きたいセッションが被っていることが多かったので、「まだiOSDCは終わっていない!」というのが個人的な感想。 いつになるのかは不明だが、全セッションの動画がYouTubeに上がるはずなので早く見たいです。

懇親会にも参加し美味しい料理やお酒を楽しみながら、交流をできたのも良かったです。

来年も是非開催してくれることを願っています。

スタディプラスでは現在、Swift化を進めています。是非、興味ある方はご連絡お待ちしています

GraphQLを導入しようとしている話

こんにちは。Studyplusでサーバーサイドを担当している金澤です。 弊社ではいまapiの一部にGraphQLを導入するべく取り組んでいます。

GraphQLってなんだという話や導入手順などはweb上にすでに沢山あると思います。 なのでそのへんはあっさりめで、検証にあたってどのような実装をしているかという話をします。

で、GraphQLってなんだ

公式ページから言葉を借りれば、

A query language for your API

です。

apiに対する問い合わせをクライアントで組み立てて柔軟にできます。

上記ページのデモがとても分かりやすいのでピンと来ない方は是非ご覧ください。

GraphQLでできる3つのこと

query

  • データの問い合わせ
  • 今回はこの話だけします

mutation

  • データの変更

subscription

  • いわゆるpub/sub

なぜGraphQLなのか

動機としては

  • パフォーマンスの改善のため、各種apiを一つにまとめようという話が以前からあった
    • GraphQLによってパフォーマンスがよくなるという話ではなく、apiが細分化しすぎていてインデックスも複雑(あるいは効いてない)というケースを整理しましょうという流れ
  • 画面をトライアンドエラーするとき、いちいち微調整にコミュニケーションコストがかかるのは勿体無い
    • ある程度好きにできるapiを用意するので、開発フェーズによってはそれを柔軟に使って完結してほしい
    • クライアントとサーバを同じ人が作るような体制の場合はこういう凝った仕組み必要ないかもしれない
  • ナウい
    • たまには新しいものを取り入れないと澱む

といったところです。

導入方法

まっさらなrailsプロジェクトにGraphQLを導入する手順です。

railsプロジェクトを作る

$ rails new graphql_test

graphql-rubyのインストール

https://github.com/rmosolgo/graphql-ruby

gem 'graphql'
$ bundle install
$ rails generate graphql:install
  • 上記コマンドでGemfileにgem 'graphiql-rails', group: :development というのが追加されるので、使う場合はもう一度bundle install

    • graphiqlは開発用のインタラクティブな画面です
    • 多分graphicとgraphqlの言葉遊びなのかな?
  • app/graphql以下にいろいろ関連ファイルができます

    • application.rbconfig.eager_load_paths << "#{config.root}/app/graphql" を足すと便利

graphiql

rails s して http://localhost:3000/graphiql から見れます

Hello, World!

インストールが終わったら、graphiql画面に{ testField }と入力して実行しましょう。 いつものが返ってきます。

Queryで頻出しそうなパターンをどう実装したかの話

ここからが本題です。 データの問い合わせapiを作るにあたって、よく登場するパターンをどう解決したかという例を三つほどご紹介します。

ユーザー権限をチェックしたいパターン

正しくインストールされていると、app/controllers/graphql_controller.rb というコントローラができてます。 下記のような感じです。

class GraphqlController < ApplicationController
  def execute
    variables = ensure_hash(params[:variables])
    query = params[:query]
    operation_name = params[:operationName]

    context = {
      # Query context goes here, for example:
      # current_user: current_user,
    }
    result = GraphqlTestSchema.execute(
      query, variables: variables, context: context, operation_name: operation_name
    )
    render json: result
  rescue => e
    raise e unless Rails.env.development?
    handle_error_in_development e
  end
  # 以下略

コメントにあるように、コンテキストを意識したい場合はcontext に追加し、後から取り出せます。 権限チェックのためにログインユーザーが欲しい、というような場合は例えば下記のように実現できます(実際のコードではないです)。

GraphiQL::Rails.config.headers["Authorization"] = -> (context) { "Bearer hogehoge" }
    # 前略
    auth_header = request.headers["Authorization"]
    session = find_session_by_header(auth_header)
    context = {
        session: session
    }
    # 以下略

こうすると、query_typeやobject_type内でresolveする際にctxからセッションを取得できるようになります。

field :test, types.String do
    resolve ->(obj, args, ctx) {
        ctx[:session] # コントローラでセットしたオブジェクト
    }
}

ページングするパターン

データの問い合わせにページを指定したり、検索文字列を指定したりなどよくあると思います。

以下のようにクエリにはargumentを設定することができます。

Types::QueryType = GraphQL::ObjectType.define do
  name "Query"

  field :page, !types[types.Int] do
    description "ページングテスト"
    argument :per_page, types.Int, default_value: 5, prepare: ->(limit, _ctx) { [limit, 5].min }
    argument :page, types.Int, default_value: 1
    resolve ->(_obj, args, ctx) {
      page = args[:page]
      limit = args[:per_page]

      arr = %w(1 2 3 4 5 6 7 8 9 10)
      offset = [(page - 1), 0].max * limit
      last = offset + limit
      arr[offset...last]
    }
  end
end

上記query_type.rbを上記の内容にし、下記クエリをqraphiql画面から入力することで、ページングが確認できます。

{ page(per_page: 3, page: 2) }
{
  "data": {
    "page": [
      4,
      5,
      6
    ]
  }
}

lazyに呼ぶパターン

GraphQLはapi呼び出し側でクエリを自由に書けるので、呼び出される側が無駄な計算をしないように気をつける必要がありました。

例えば、下記のようなTypeがあったとします。

Types::Test::LazyType = GraphQL::ObjectType.define do
  name 'Lazy'

  field :karui, !types.String
  field :omoi, !types.String
  field :sinu, !types.String
end

このtypeを使った返事を以下のように実装した場合、

Types::QueryType = GraphQL::ObjectType.define do
  name "Query"

  field :lazy, !Types::Test::LazyType do
    resolve ->(_obj, _args, _ctx) {
      karui = "軽い!"
      omoi = -> () {
        sleep(3)
        "重い..."
      }.call # ここが重い
      sinu = ->() {
        raise "死ぬ"
      }.call # ここで死ぬ

     OpenStruct.new(karui: karui, omoi: omoi, sinu: sinu)
    }
  end
end
  • クエリで必要なものだけ取得できる意味がない
  • というかこの場合は必ず死ぬ
  • しかも順番に実行されていくので重い作業を待った末に死ぬ

という本末転倒なことになります。

そこで下記のように、必要な分だけ計算するようにしました。

Types::QueryType = GraphQL::ObjectType.define do
  name "Query"

  field :lazy, !Types::Test::LazyType do
    resolve ->(_obj, _args, _ctx) {
      res = OpenStruct.new
      def res.karui
        "軽い!"
      end

      def res.omoi # ここでは重くない
        sleep(3)
        "重い..."
      end

      def res.sinu # ここでは死なない
        raise "死ぬ"
      end

      res
    }
  end
end

こうすることで、

  • { lazy { karui } } 実行時は軽い
  • { lazy { karui omoi } } 実行時は重いものを取得したいのでしょうがない
  • { lazy { sinu karui omoi } } とすることで一瞬で死ぬため計算資源を無駄にしない
    • ただし、{ lazy { karui omoi sinu} } とすると順番に実行されてやはりomoi分が無駄になる

当たり前の話ですが、個別に実行できるようにし、必要なものを必要なだけ取るようにできると無駄がありませんでした。

終わりに

まだ手探り状態なので、実運用に乗せたら何が起きるかなどはわかっていませんし、テスト運用して採用を取りやめる可能性もゼロではないと思っています。

起きそうな問題としてパッと思いつくのは、

  • cartesian product問題やN+1問題に代表されるようなデータアクセス上の典型的諸問題が、レイヤーを一段またいで脳みそが二つになることによってより厄介になったりしないだろうか
    • たくさん取得してクライアントで選別しよう、みたいなコードを書こうと思えば書けてしまう
    • もちろんapiの作りっぷりで制限することは可能で、そういう意味では従来の作りとあまり変わっていないかもしれない
  • endpointが一つになるので、NewRelicなどでパフォーマンス計測する際よくわからないことにならないか?
    • クエリ別に集計する必要がありそうだが対応しているのか?
      • { hoge fuga piyo}fuga piyo hoge は同じものとして集計されてほしいのか違うのかなど場合によって違いそう
    • 関係ないけどNewRelicの無料プランがだいぶ制限されるようなのでどなたか良い代替プロダクトを教えてください

といったところでしょうか。

とはいえ、うまく使えば開発効率が上がりそうな手応えも感じています。 色々な導入例や運用例から、ベストプラクティスやアンチパターンが出来上がっていくと思いますし、追随していきたいと思います。

Kotlin festに行ってきました

弊社には 勉強会・カンファレンス参加補助 があります。 その制度を使って会社のお金でKotlin Festに参加させていただきました。補助といいつつ基本全額サポートしてくれました。 ありがとうございます。

image.png (42.8 kB)

Kotlin Festとは

「Kotlinを愛でる」をビジョンに、Kotlinに関する知見の共有と、Kotlinファンの交流の場を提供する技術カンファレンスです。(connpassから引用)

愛でる と言われる言語なんて今まであっただろうか。 Kotlinかわいいとも言われている。Kotlinかわいい。

そんな素敵な言語のカンファレンスです。

ざっくりとKotlinをご紹介

色々な方がKotlinについて説明してくれているので、詳しいことはそちらにお願いするとして。 オープニンセッションでお聞きしたKotlinの哲学をそのまま流用してます。

  • 実用主義: Javaの考えがそのまま使える、学習が容易!
  • 簡潔: 読みやすさ重視でボイラープレートを減らす
  • 安全: 静的型付け、Null安全
  • 相互運用性: Javaと相互に行き来、Javaライブラリの豊富な資産が使える!

より良いJavaなイメージです!

セッション

※登壇者の敬称略

image.png (11.4 MB)

オープニングセッション

長澤太郎 藤原聖

ここで発表いただいた Kotlinの哲学 は上記で引用させていただきました

参加したセッション

「Kotlin で改善する Android アプリの品質」

あんざいゆき https://speakerdeck.com/yanzm/kotlinfest

「How to Test Server-side Kotlin」

鈴木 健太 ・ 前原 秀徳 http://htn.to/kwU92M

「start from Convert to Kotlin」

望月美帆 https://speakerdeck.com/mochico/start-from-convert-to-kotlin

「Kotlin コルーチンを理解しよう」

八木俊広 https://speakerdeck.com/sys1yagi/kotlin-korutinwo-li-jie-siyou

その他のセッションなど

こちらにまとめられています。Kotlin Fest 2018 - 資料一覧 - connpass

雑感

  • 記念すべき一回目
    今回の盛り上がりを見ると第二回も開催しそう!

  • 参加者 367名

  • チケット完売!
  • 人が多すぎてwifiつながらないときもアリ
    ほんとに人がたくさんいました。会場の椅子が足りなくて床に座ってセッション聞いてたこともありました。登壇者の発表に拍手が頻繁に起きたりいい雰囲気でした。

  • ランチは会場のレストランで特別に500円ランチが。カツカレー
    ちなみに会場はコチラ

  • mixiさんのブースでやってたKotlin Quiz 5問正解でお菓子もらえた。

  • Androidエンジニア以外にもサーバーサイドでkotlin使ってますという人が意外と多かった。
  • トートバッグもらった
    友人たちに好評。 ことりかわいい→KotlinかわいいでひとつKotlinの普及

IMG_4865.JPG (81.7 kB)

まとめ

セッションの内容や途中少しお話させていただいた方、企業ブースなどを見てKotlinの盛り上がりを改めて認識しました。Kotlin正式リリース前に初めて触ってから考えると、日本語の記事も導入事例も爆発的に増えてきました。 今年GoogleがAndroid公式言語としてKotlinが選ばれたためKotlin == Android と思われがちですが、サーバーサイドでの導入事例も増えてきています。実際私が参加したセッションではサーバーサイドで使ったことある方が4割ほどいました。

AndroidのみならずサーバーサイドやJs / Kotlin nativeなどもあり、これから益々Kotlinを活用するプロジェクトが増えてくると思います。むしろKotlin好きとしては増えてくれることを願っています。

私は参加してないのですが、Kotlin Festの懇親会はとても美味しい料理が出たという噂が。 心残りはそれだけです。第二回は必ず懇親会に参加したい。

スタディプラスでは現在、Kotlin化を進めています。是非、興味ある方はご連絡お待ちしています

FirebaseでSlack Slash Commandsを楽に運用する

ForSchool事業部開発グループの松田です。

弊社ではSlackを導入しており、いわゆるChatOpsに活用しています。
Slackにはさまざまな便利機能がついていますが、その中の一つにSlash Commandsというものがあります。
今回はこのSlash Commandsを自作して、そのバックエンドにFirebaseを利用した例をご紹介します。

Slack Slash Commandsとは

Slack内で /command のようにスラッシュから始まるコマンドを発言することで、特定のエンドポイントにリクエストを送信する機能です。
実装次第では /command sugoi argument などのように引数をとることもできるので汎用性が高いです。
標準機能として /remind/invite など複数のコマンドが定義されています。
Built-in slash commands – Slack Help Center

Firebaseとは

ここのところ流行っている感の強いモバイルプラットフォームです。
Googleの展開しているプラットフォームで、最近では国内の導入事例も見かけるようになりました。
認証周りをやってくれるAuthenticationや、ファイルホスティングのCloud Storageなどいくつかのサービスがありますが、
今回はJSの関数をさまざまなトリガーで実行できるCloud Functionsと、DBを提供するFirestoreを利用して簡単なコマンドを実装してみました。

実際に作ってみる

今回は弊社のボードゲーム部で使用する /boardgame コマンドを実装してみます。
仕様は /boardgame search players 4 の入力からDBを検索しプレイ人数に合致するボードゲームを返すコマンドとします。
Slash Commandsはコマンドが発行されると設定したURLにリクエストを投げるので、その先をCloud Functionsで受ける感じでやっていきます。

Slack AppとFirebaseプロジェクトを新規登録

Slack AppとFirebaseのプロジェクトを新規に登録します。
名前などいい感じにしましょう。

Cloud Functionsを仮デプロイ

firebase/firebase-toolsを使って、ローカルにFirebaseのプロジェクトと連携するフォルダを作ります。
初回設定時に使用するサービスを選択しますが、前述の通りFunctionsとFirestoreを選択します。
f:id:matsudap:20180817154806p:plain Functionsを選択するとJSとTSのどちらで書くかを聞かれますが、なんとなくTSにしました。
この時点で ./functions/src/index.tshelloWorld というHTTPリクエストがトリガーの関数が定義されているので、 boardgame に改名して、以下のコマンドを実行します。

$ firebase deploy --only functions

すると、デプロイ結果としてトリガーになるURLが表示されるので、ブラウザ等でリクエストをして Hello, World! と表示されればここまでは完了です。

自作Appをワークスペースに追加する

先程作ったSlack Appの設定メニュー「Slash Commands」からコマンドを登録します。
Request URLには先ほどデプロイしたCloud Functionsに割り当てられたURLを設定しておきます。
あとはよしなに設定します。
f:id:matsudap:20180817154828p:plain

Slash Commandから呼ばれる関数を実装する

とりあえず手元にボードゲームのリストを用意しました。

{
  "title": {
    "ja": "アグリコラ",
    "default": "Agricola"
  },
  "minPlayers": 1,
  "maxPlayers": 5,
  "minPlayingTime": 30,
  "maxPlayingTime": 150,
  "properAge": 12
},
{
  "title": {
    "ja": "デッドオブウィンター",
    "default": "Dead of Winter: A Crossroads Game"
  },
  "minPlayers": 2,
  "maxPlayers": 5,
  "minPlayingTime": 60,
  "maxPlayingTime": 120,
  "properAge": 13
},
...

これを適当なスクリプトを書いてFirestoreに突っ込みました。
最近使えるようになった配列の array-contains オペレータを使ってみたかったので、
Firestoreのトリガーで、minPlayersとmaxPlayersを下限と上限にした数値の配列をplayersフィールドに追加するようにしています。

そして実装ですが、公式のチュートリアルを参考にざっくりと以下のようになりました。1

import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin'

admin.initializeApp()
const db = admin.firestore()

class RequestError extends Error {
  code: number;
}

const commands = ['search'];

const searchFunction = (args) => {
  return new Promise((resolve, reject) => {
  })
}

export const boardgame = functions.https.onRequest((req, res) => {
  return Promise.resolve()
    .then(() => {
      if (req.method !== 'POST') {
        const error = new RequestError('Only POST requests are accepted');
        error.code = 405;
        throw error;
      }
      if (!req.body || req.body.token !== functions.config().slack.token) {
        console.log(req.body, functions.config().slack.token);
        const error = new RequestError('Invalid credentials');
        error.code = 401;
        throw error;
      }
      const [command, ...args] = req.body.text.split(' ');
      if (command == 'search') {
        let query:FirebaseFirestore.Query | FirebaseFirestore.CollectionReference = db.collection('games')
        if (args.includes('players')) {
          const playersIndex = args.indexOf('players') + 1;
          const players = args[playersIndex];
          query = query.where('players', 'array-contains', parseInt(players));
        }
        query.limit(3).get().then((querySnapshot) => {
          resolve({
            text: 'Boardgame Search Result',
            attachments: querySnapshot.docs.map((doc) => {
              const game = doc.data();
              return {
                title: game.title.ja,
                text: game.title.default,
                fields: [
                  {
                    title: 'Players',
                    value: `${game.minPlayers} ~ ${game.maxPlayers}`,
                    short: true,
                  },
                  {
                    title: 'Time',
                    value: `${game.minPlayingTime} ~ ${game.maxPlayingTime}`,
                    short: true,
                  },
                  {
                    title: 'Age',
                    value: game.properAge,
                    short: true,
                  }
                ]
              }
            }),
          });
        })
      } else {
        return new Promise((resolve, reject) => {
          resolve({
            text: 'Unknown command!',
            attachments: [],
          })
        })
      }
    })
});

functions.config().slack.token にはSlack App Basic Information内のVerification Tokenを予め設定しておきます。

Cloud Functionsにデプロイする

再度を以下のコマンドを実行してCloud Functionsにデプロイします。

$ firebase deploy --only functions:boardgame

Slach Commandを実行する

Slack Appを追加したワークスペースのチャンネル上で以下を発言します。

/boardgame search players 1

f:id:matsudap:20180817154858p:plain

便利ですね。

まとめ

Firebaseは運用コストが低いので、こういったピンポイントなAppのバックエンドとしては最適だと思いました。
FirebaseのいろいろなサービスもCloud Functionsからスッと使えるので対応できる幅も広そうです。
一方で、今回使用したCloud FunctionsとFirestoreは(2018/08/20現在)ベータでの提供なので留意してください!

また、弊社では就業後にボードゲーム部が不定期活動中です!
ボードゲームがしたい、Firebaseで業務改善したいエンジニアをどしどし募集しております〜。
まずはお気軽にオフィスに(ボードゲームを)遊びに来てください〜。
採用情報 | スタディプラス株式会社

www.wantedly.com


  1. このチュートリアルはFirebaseではなくGoogle Cloudのものなので、設定値の取得方法など微妙に差異があるので注意してください。

スタディプラス のAndroid事情

https://s3-ap-northeast-1.amazonaws.com/esa.s3.prod.lithium.studylog.jp/uploads/production/attachments/8682/2018/07/30/34447/a298aca0-1cc6-4021-aa28-f6f3bb44beeb.png

スタディプラス のAndroid事情

今回はAndroidアプリ担当エンジニアのNabesukeです。
組み込みエンジニア→Android→放浪→iOS→バックエンド→フロントエンド→Androidというよくわからない経歴を持っています。
今回はStudyplus Android版の裏側をご紹介します。TOP画像はこの前のスタディプラス LT大会2018(夏) - Studyplus Engineering Blogで使ったもの。

Android版の現状

Kotlin化を進めているが未だJavaが8割以上

image.png (8.9 kB)

目下Kotlin化( + ゆるいDatabinding)を進めていますが記事執筆時点でJavaは8割以上残っています。
(入社時は9割5分くらいがJavaでした。あんまり覚えてない)

Studyplusは画面が多いのでその分クラス数も多いです(1000ファイル以上あります)。
したがってヒマを見つけてはKotlin化に励んでいますがまだまだ終わりは見えていません。

MVCで作られています

比較的ファイル分割もされており、見やすく作られているので、学習コストも少ない分見やすいです。

使っているライブラリ

  • RxJava2
  • Retrofit2
  • Okhttp3
  • Glide
  • Orma(Roomに置き換えます)
  • fabric

その他広告SDKやらプッシュ通知など

クラスのpackage分けについて

これは開発者の好みとか諸事情がたくさん絡んでくるのでなんとも言えませんが、現状は「ふるまい」毎に分けられています。adapter/dialog/view/などなど。app直下にactivity達が大集合しています。
数えたらapp直下に160オーバーのActivityがありました。これは絶賛整理中なので、最初はもっとたくさんのactivity達がひしめいていました。

いま、どういうことをしているか

いまのところ私一人で開発しているので、ひとりでがんばってるなー程度に見ていただけたら幸いです。

packageの整理

私は機能単位でファイルを分けたい方なのでActivityやFragmentなどのPresentation層部分は機能別で振り分けています。(messageパッケージの中にメッセージ画面と関連するactivityやfragment、adapterなどをまとめる)
こうすると比較的関連activity/fragmentなどのクラスを探しやすくなるメリットがあります。modelやextentionなど、機能横断で使うものは現行そのままにしています。

Kotlin化

image.png (5.1 kB)

常に最優先で進めています。 いまのところ 「ぬくもりあふれる手作業」 で置き換えています。

ファイル数でいうと

  • java 719 file
  • kotlin 178 file(頑張った!)

その他xml多数

となっております。

なんでKotlin化するか?

ざっくり言ってしまうとKotlinが好きだから。これが一番大事だと思います。Kotlinかわいい。 過去、Androidアプリを開発した方で心がくじけた方もKotlinの書きやすさを見たら戻ってきたくなります(Android StudioやAndroid自体の良さもありますが)。自分もKotlinと会わなかったらAndroidに戻ってこなかったです。

大体以下の理由になります。主観が多分に混じっているので見識者はツッコミお願いします。

  • Googleが公式言語として認めている。
  • 割とコードを短縮できる(体感4/5くらいにはなる気がする)
  • スコープ関数や拡張関数、配列に対しての処理が便利。
  • Null safe。javaにもあるが、まだイマイチ使いにくい印象。
  • コルーチン(のasync/await)が使いたい。

MVCからゆるいMVVMへ

MVCで見やすいとは言いましたが、これのデメリットはよく言われるようにFatActivityになりがちです。
今後の機能追加で肥大化する可能性が大いにあります。
よって、MVVMに移行していますが機能追加との兼ね合いで進捗は芳しくありません。

AACを使ってActivityやFragmentから処理をひっぺがす

今は通信処理や、状態の更新処理などがActivityに書かれているので一旦それをViewModelに切り出します(いずれはViewModelからも引き剥がす)。 AACのViewModelはActivityのライフサイクルに合わせてデータをよしなに管理してくれるのでちょっと気持ちが楽になります。楽になりたい。
尚、Layout.xml側にViewModelをbindさせるかどうかは絶賛考え中です。ものによりますが、CustomAdapterとか定義すると途端にコードが追いにくくなる印象。

テスト

リリースの度に胃が痛くなる原因なんですが、いまのところテストがありません。
ぬくもりあふれる手作業でテストしてます。テスト導入したい。入社してから思ってるけど未だ着手の気配すらない。他にやること多すぎるという言い訳をさせてください。

CIツール

Bitriseを使いはじめました。いまのところBranch毎の各Build Variant向けビルドとfabricへのアップロード、play storeへのアルファ版リリースを自動化してる程度です。

おわりに

ぬくもりあふれる手作業が多い開発ですが、 これを書いている間にKotlinが2割を超えました。

image.png (16.0 kB)

私が入ってから5ヶ月。Convert to Kotlin してコードを修正し、ButterKnifeを取り除きDatabindingに移行するという作業を延々と手作業で繰り返しました。この調子で頑張りたいと思います。

「自分だったらもっとうまくやれるぜ!」や「ここをもっとこうしたら良くなるのにな」と思った方、弊社でお待ちしています

AWS Glueを用いたデータ分析基盤を構築した✨

こんにちは。業務委託の@morix1500と申します。

この度、スタディプラス様からデータ分析基盤の構築の業務委託を受け、AWSのマネージドサービスを用いて構築を行いました。
その際に得られた知見を共有したいと思います。

データ分析基盤について

今回スタディプラス様から受けたデータ分析基盤の要件は以下のようなものでした。

  • S3にあるログをAWS Athenaから閲覧できるようにしてほしい
  • S3にあるJSON形式のログを列指向型のフォーマット(Parquet)に変換してほしい
  • ログは順次取り込み(毎朝、昨日分のログが見れるようにする)

すでにログはS3にあったのでログ収集は終わっています。

データ分析基盤の構成

今回作成したデータ分析基盤はAWSのマネージドサービスで完結してます。
今回構築したのはGlueの部分です。

f:id:mori_morix:20180730165858p:plain

Glueの構成や初期構築の手順は以下のドキュメント通りです。 https://aws.amazon.com/jp/blogs/news/build-a-data-lake-foundation-with-aws-glue-and-amazon-s3/

構築時のTIPS

S3のパーティション分け

Amazon Athena のパフォーマンスチューニング にも記載されていますが、
データをパーティション分けするとパフォーマンスが上がります。

今回取り込み対象のS3のログのパスは以下のようにうまいことパーティション分けされていました。

s3://{BucketName}/2018/07/30/12/hoge.log.gz

この場合は

でパーティション分けされています。

Glueでこの形式のログをロードすると「partition_0」という名前でパーティションをAthenaなどで参照できるようになります。

Glueジョブの対象データの絞り込み

今回行いたいJSONからParquetに変換作業は、Glueの「ETLジョブ」を使用します。
コンソールでポチポチやるといい感じの変換用Pythonスクリプトを出力してくれますが、
デフォルトだと対象のS3パスのデータを毎回全件取り込んでしまいます。

「ログは順次取り込み」という要件があるので、なんとかジョブ対象のデータを絞り込みたいです。

その方法は、上記で作成したパーティションを利用します。
作成したパーティションはGlueのETLジョブでも参照できますので、ジョブ実行時パーティションで絞り込みを行います。
この機能のことを「Pre-Filtering」というそうです。
https://docs.aws.amazon.com/glue/latest/dg/aws-glue-programming-etl-partitions.html

# ソースを一部抜粋

# 1時間前の時間を指定
one_hour_ago = datetime.today() - timedelta(hours = 1)
year  = one_hour_ago.year
month = '{0:02d}'.format(one_hour_ago.month)
day   = '{0:02d}'.format(one_hour_ago.day)
hour  = '{0:02d}'.format(one_hour_ago.hour)
pushDownPredicateString = "(partition_0='{0}' and partition_1='{1}' and partition_2='{2}' and partition_3='{3}')"
pushDownPredicateString = pushDownPredicateString.format(year, month, day, hour)

datasource0 = glueContext.create_dynamic_frame.from_catalog(
    database = "Glue DB名",
    table_name = "テーブル名",
    push_down_predicate = pushDownPredicateString,
    transformation_ctx = "datasource0")

変換後のデータをS3に出力する際パーティション作成

GlueのETLジョブでは、変換後のデータをS3に保存できます。
その際にS3のパーティションを分けないと、Athenaでの参照時にパフォーマンスが上がりません。

こちらの設定もPythonで設定できます。
"year"や"month"と指定していますが、その前のマッピングで
partition_0 と year を関連付けないと使えませんので注意してください。

# ソースを一部抜粋

datasink4 = glueContext.write_dynamic_frame.from_options(
    frame = dropnullfields3, connection_type = "s3",
    connection_options = {
        "path": "s3://{Bucket Name}/",
        "partitionKeys": ['year','month','day','hour']
    },
    format = "parquet",
    transformation_ctx = "datasink4"
)

この設定を行うと以下のようなログが出力されます。

s3://{BucketName}/year=2018/month=07/day=30/hour=12/xxxxxxxxxxxxx.parquet

ログのカラムの増減時の対応

Glueはよく出来ていて、カラムが増えても動作に影響がありません。
ログのカラムが変更されると、CrawlersのTables Updatedが「1」になります。
f:id:mori_morix:20180730165904p:plain

カラムが増えたら、ETLジョブのマッピングのところに追加されたカラムの情報を追加。
カラムが減ったら特になにもする必要はないですが、ETLジョブのマッピングにはその情報は不要なので削除してもよいでしょう。

最後に

このようにログをデータ分析で使える形式にするためにやらなきゃいけないことが
AWSのマネージドサービスですべて済んでしまいました。

そして導入は非常に簡単です。
同じ課題をお持ちの方はこの記事を参考にデータ分析基盤を構築してみてはいかがでしょうか!

また宣伝になりますが、私@morix1500はこのような基盤構築やその他クラウド系のインフラの仕事を副業としてやらせていただいています。
なにかございましたらTwitterのDMなどでぜひ!

スタディプラス LT大会2018(夏)

こんにちは、CTOの島田です。
今回は7/13(金)に行われた社内LT大会についてです。(平成最後の13日の金曜日!?)

LT大会とは?

  • 趣旨
    • 日ごろの勉強・活動・興味関心を共有し、相互理解を促進する
  • 内容
    • 業務内外問わず、自身が技術を用いて取り組んだものを発表
  • ルール
    • 1人5分の持ち時間
    • CTO、外部審査員による審査により、賞を決定し、受賞者に賞品を贈呈
      • 外部審査員にリブセンス創業メンバーの桂大介氏を招待

発表内容一覧

タイトル 概要
AlexaとサーバーレスでつくるVUI勉強記録サービスのプロトタイプ Echo Dot, Lambda, DynamoDB, API Gateway, React を利用して、VUI勉強記録サービスのプロトタイプを作成、その発表をしました。
Graal Graal VM
暗号通貨の裁定取引ボットを作った Node.jsで暗号通貨の裁定取引ボットを作り、お金を稼ぎました
テックブログ考察 各社のテックブログをクロールして分析
iOSDC2018草稿 iOSDC2018で発表する内容を少し話しました
Webアプリの脆弱性スキャン OWASP ZAP を使ってStudyplus for Schoolの脆弱性検査をしました。
弊社の組織課題に関するご提案 ボードゲームサークル向けwebサービスをNuxtとFirebaseで作り始めました
入門 DAapps & Web 3.0 ブロックチェーンを基盤にしたアプリケーションDAppsとWeb 3.0について発表しました。
スタプラAndroid Studyplus for Androidで驚いたことと現状。

発表会の風景

結果

  • 桂賞: 「弊社の組織課題に関するご提案」
    • 寸評:スライドの構成と持ち時間ピッタリの発表内容。実装して動作する環境を用意した点。
  • CTO賞: 「AlexaとサーバーレスでつくるVUI勉強記録サービスのプロトタイプ」
    • 寸評:スマートスピーカとStudyplusのプロダクトを絡めた着眼点と、プロトタイプを実装した点。

受賞作品の紹介

speakerdeck.com

賞品

受賞者には目録が与えられ、それぞれの希望の賞品が贈呈されました。

  • 桂賞:ボードゲーム3万円分

  • CTO賞:Kindle Oasis

最後に

  • こういったLT大会のような場を定期的に持つ事で、メンバーにとって日頃の過ごし方の中にアウトプットの意識が根付くと感じました。
  • 発表内容もそれぞれユニークで、日々の取り組みでは触れる事のない内容もあり、良いインプットの場になったと思います。
  • ボードゲームに興味のある方、スタディプラス で一緒にボードゲームをしませんか?
  • 次回は半年後の1月に実施を予定。その際にはさらにメンバーが増えているかも!?