2017年12月8日金曜日

Google Assistant, Actions on Google の Google I/O 2017 セッションまとめ

こんにちは、やんざむです。このエントリは Actions on Google Advent Calendar 2017 の8日目です。

Google I/O 2017 の Actions on Google のセッション を見て Bagel のサンプルを試してみたりしていたのですが、しばらく遊ばないでいたら API.AI の名前が変わって DialogFlow になっていました。Google Home が日本でも発売され、いまこそ絶好の Actions on Google 始めどきですね。

最初、11月に新しく増えた機能のことを書こうかと思っていたのですが、Google I/O のセッションのまとめが下書きのまま放置されていたので、この機会に書き上げました!どうぞ!


* 注意: セッション中の API.AI は DialogFlow に置き換えてあります。
* 注意: Google I/O 2017 時点の情報なので、もしかしたら現在変わっている部分があるかもしれません。

Building apps for the Google Assistant



Google Assistant でも ecosystem を作る
ecosystem のキモは Actions on Google という platform
  • 1. Actions Console でプロジェクトを作る
  • 2. DialogFlow を使って自然言語を処理する
  • 3. webhook につなぐ
  • 4. web simulator かデバイスでテストする
  • 5. mobile phone 向けに拡張する
  • 6. Transactions API と integrate する

ユーザーが Google Home に「Ok Googke, talk to Google IO 17」と話す
  ↓
  • Assistant で text に transcribe され
  • NLP(自然言語処理)され
  • Knowledge Graph を使った Ranking を適用し
  • ユーザーのコンテキストを使ってユーザーのクエリを解釈し
  • I/O アプリが適切なサービスだと判別する
  • Assistant は I/Oアプリの DialogFlow agent を起動し、DialogFlow agent は対応する intent (定義済み)がどれか理解する
  • DialogFlow はビジネスロジックを実行するために web hook を呼び出し結果を受け取る
  • 会話のレスポンスを公式化
    • DialogFlow から Assistant に送り返され、ユーザーに出力される
Actions on Google Console
  • Google I/O 2017 で公開
  • metadata と directory listing, ブランディング情報の設定
  • Analytics
  • Simulator
DialogFlow
  • natural language tool
1. Define Intents and Entities
  • intents と entities を定義する
  • intents
    • user query のモデル
    • 構造化された方法でユーザーが何をしたいかを理解できる
  • entities
    • 文字列の構造化されたパケット
    • ユーザーのリクエストから意味を取り出す
2. Writing responses

応答を書く

3. Use Training to improve

基本的なモデルを設定(ユーザーの会話例を入力)したら、DialogFlow で training を使って会話モデルを改善する

「What Android sessions are there?」

list session intent にマッピング

webhook
ListSessions(session_tags.TRACK_ANDROID) const App = require('actions-on-google').ApiAiApp; function listTopicsIntent(app) { GoogleIOAPI.getCategories().then( categories => app.ask("The topics covered are: ${topics}. What do you want to learn?") ) } exports.myApp = function(request, response) { var app = new App({ request, response }); app.handleRequest(listTopicsIntent); }; npm install actions-on-google 4. Web Simulator でテスト

5. mobile 向けに拡張する

API.AI ではビルドインの作成機能があり、 cards, chips, carousels, lists を簡単に定義できる

webhook に request を投げて動的な response を生成することも可能 // screen があるかチェック if(app.hasSuraceCapability(app.SurfaceCapabilities.SCREEN_OUTPUT)) {} app.ask(app.getIncomingRichResponse() .addBasicCard(app.buildBasicCard(sessionData.desc) .setTitle(sessionData.title) .addButton('Google IO Schedule', sessionData.url)) ); } 6 Transaction API
  • Google I/O 2017 で developer preview としてリリース
  • 購入とオーダーが可能
    • seamless, build-in experience を提供
    • payments と identity information の共有
    • receipts の発行による reengagement
1. Basket または shopping card を作成

配送料を決めたり、サービス可能かどうかを判定するために delivery address などが必要になることがあり、これらをリスエストする API が用意されている

2. オーダーの設定が終わったらユーザーに propose して authorization を取得する必要がある
  • propose order API を使う
  • ユーザーに購入を聞いて approve してもらう
3. approve されたらオーダーを confirm する必要がある
  • order が active かどうか知らせ、レシートを送るため
  • confirm order API を使う
4. user's identity
  • OAuth 2 Web server と統合する機能を提供
  • seamless log in flow をユーザーに提供
let order = app.buildOrder() .setCart(app.buildCart() .addLineItems(app.buildLineItem(session.title)) .setNotes(session.description)) .setTotalPrice(app.Transactions.PriceType.ACTUAL, 'USD', 0) app.askForTransactionDescription(order); let orderUpdate = app.buildOrderUpdate(orderId) .setOrderState(app.Transactions.OrderState.CONFIRMED, "Booked!") .setReceipt("io-seat-order-" + orderId) app.tell(app.buildRichResponse() .addSimpleResponse("Thanks! Here's your receipt.") .addOrderUpdate(orderUpdate)); 自分の Assistant App へ繋げるには
  • ソシャルメディアを通してシェアする
  • 自分のサイトやアプリを通してプロモートする
  • Assistant app へのトラフィックが増えるようプレスを奨励する
ユーザーがどのように App を見つけるか
  • 会話中に
    • Explicit triggering(暗黙的なトリガー)
    • Implicit triggering(明示的なトリガー)
  • Assistant Directory
  • シェアされたリンクから




Bringing the Google Assistant to any device



Google Assistant SDK
  • I/Oの3週間前にリリース
  • gRPC API
  • Docs and samples
  • Tools
AIY Voice Kit Google Assistant SDK
  • Hotword support ("Ok, Google ...")
  • Timers and alarms
  • Enhanced device control (Coming soon)
Hotword Library を試すための構成例
  • Respberry Pi 3B
  • One or optimally Two microphone
  • Speacker
  • Internet Connection
Library Calls start() set_microphone_mute(boole is_muted) start_interaction() # To initiate with button instead of hotword Library Events
  • ON_START_FINISHED
  • ON_CONVERSATION_TURN_STARTED
  • ON_CONVERSATION_TURN_TIMEOUT
  • ON_END_OF_UTTERANCE
  • ON_RECOGNIZING_SPEECH_FINISHED
  • ON_RESPONDING_STARTED
  • ON_RESPONDING_FINISHED
  • ON_NO_RESPONSE
  • ON_INTERACTION_FINISHED
  • ON_ALERT_STARTED
  • ON_ALERT_FINISHED
  • ON_ASSISTANT_ERROR
  • ON_MUTED_CHANGE
gRPC API
  • Client/server API - Remote Procedure Call
  • Minimal Processing
  • Minimal Power reqirements
  • Runs on most platforms, OS's Programming Languages
  • Open-Source Sample Code
gRPC API - Bindings for nearly any platform

Runs on nearly any platform with a TCP/IP socket.
  • Bi-directional streaming:
    • streams audio up and streams audio down - fo minimal latency
  • Robust transport over HTTPS/2
  • Open protobuf description on Github
  • gRPC is free and open-source
    • Platforms: Linux, MacOS, Windows, Android, iOS
    • Languages: C/C++/C#, Go, Python, Node.js, Ruby, and more
    • Open toolchain to generate bindings in any language
phone に 「tell my oven to set the temperature to 425」(オーブンを220℃にセットして)と言うより、直接オーブンまで行ってボタンを押して「set the temperature to 425」(220℃にセット)と言うほうがいい

Connect channel_factory = ChannelFactory(api_host, credentials) service = EmbeddedAssistantStub(channel_factory.make_channel()) response_stream = service.Converse(request_stream(), DEADLINE_SECS) return handle_response_stream(response_stream) Configure def _create_config_request(self): audio_in_config = embedded_assistant_pb2.AudioInConfig( encoding='LINEAR16', sample_rate_hertz=16000, ) audio_out_config = embedded_assistant_pb2.AudioOutConfig( encoding='OPUS_IN_OGG', sample_rate_hertz=22050, ) converse_config = embedded_assistant_pb2.ConverseConfig( audio_in_config=audio_in_config, audio_out_config=audio_out_config, ) return embedded_assistant_pb2.ConverseRequest(config=converse_config) Send Config and Streaming Audio def request_stream(self): yield self._create_config_request() while True: data = self._audio_queue.get() if not data: return yield self._create_audio_request(data) Receive and Process Streaming Results def _handle_response_stream(self, response_stream): for resp in response_stream: self._response_audio += resp.audio_out.audio_data if resp.event_type =- ConverseResponse.END_OF_UTTERANCE: self._end_audio_request() if resp.result.mucrophone_mode: _dialog_follow_on = (resp.result.microphone_mode == ConverseResult.DIALOG_FOLLOW_ON) Neural Beamforming
  • One, or optimally Two microphones
  • Server-side Neural Beamforming
    • Farfield
    • Noise robust
    • Machine Learning
  • Minimal client-side processing and cost
Add the Assistant to your Device



Building Rich Cross-Platform Conversational UX with API.AI (Google I/O '17)



DialogFlow(Google I/O 2017 時点)
  • 150K+ developers
  • 14 languages
  • 32 platform SDKs and integrations
2 main concepts
  • Intents : サポートしたい action の種類
  • Entities : Intents で使われる objects
pre-build Intents
  • Default Fallback Intent : システムが理解できない場合 Fallback Intent がトリガーされる
  • Default Welcome Intent : agent が開始されるときにトリガーされる
chatbase.com
  • analytics service
  • request early access
What's next



Defining Multimodal Interactions: One Size Does Not Fit All (Google I/O '17)

このセッションは音声レスポンスの Good, NG 例が紹介されていてよかったです。TV と Auto の話もあります。



マルチモダリティ (multimodality) に影響する要因
  • 動き (Motion)
  • 環境 (Environment)
  • 近さ (Proximity)
  • 音声機能 (Audio capability)
  • 視覚的機能 (Visual capability)
Actions on Google Home Guidelines
  • 読まない、聞く
  • 情報の過負荷を避ける
  • 質問に答える
Guidelines for Phone
  • あるモードが終わったら、別のがそれを引き継ぐ
  • 一番強いモードに最適化するが、両方を許可する
  • 各モードの強みを活かす
  • モダリティ間に冗長性を持たせる
Take-Aways (TV, Auto)
  • 各モードの強みと弱みを知る
  • 一番強いモードに最適化するが、両方を許可する
  • 読まない、聞く
  • 短く、良いものにする
Future Considerations
    - ユーザーの環境と条件への自動的な適応 - 複数の Surface (Home, Phone ...) 体験の関連 - より複雑なマルチモーダルインタラクション




Finding the Right Voice Interactions for Your App (Google I/O '17)

会話デザインの話です。例がたくさん出てきてとてもよかったです。



なぜ音声なのか?
  • 速度
  • 簡易・簡潔
  • 普遍性 (Ubiquity)
15 * 24 を計算するのに phone でやるなら何回タップする? phone を取り出してアプリを選択して....
音声は究極的に便利

会話デザインの戦略
  • 1. ユーザーと関連する日常の言語を使う
  • 2. 簡単に答えられる質問をする
  • 3. 簡単に再呼び出しできるような情報構造
技術的に可能なこと
  • ユーザーが言ったことを認識する
  • ユーザーが意図していることを理解する
認識エラーはかなり下がっている
Keynoteでは deep learning の適用によって 4.9% になってことが発表された
一方で、ユーザーが意図していることが何かを"理解"することはより難しい

例1) What's the weather in Springfield?

Springfield はメジャーな街の名前、あちこちにある
ユーザーはどこの Springfield のことを言っている?
正しく理解するにはコンテキストを適用しないといけない
例えば、ユーザーはミズーリ州の Springfield に住んでいるとか
でもこれは正しくない可能性がある
もし Springfield に住んでいるならこの聞き方はおかしい
what's the weather here とか what's the weather like today のように聞くだろう
他の Springfield のことを意味している可能性がある

例2) Play Yesterday.

Yesterday は Song? Movie? Playlist? Audiobook? game の可能性もある
同じようにコンテキストとかユーザーについて知っていることを適用して、song を探しているだろうとする

でもここで別の問題が出てくる
Yeasterday は
Original version by The Beatles?
Cover version by Boyz II Men?
Some other cover version ?
他にもいろいろある、どれかなんてわからない


いくつか戦略がある
acknowledge ambiguity あいまいさを認めよう

会話デザインの戦略
  • 1. あいまいさを認め、ユーザーに聞く
  • 2. ユーザーの選択を覚えておいて、次回に活用する
あいまいなときはユーザーに聞こう
余計なステップではと思うかもしれないが、間違いを犯すよりはステップを経るほうがよりよい

可能ならユーザーの選択を覚えておいて次回に生かす

Users in context
  • 手が離せない (Hands-busy)
  • 目が離せない (Eyes-busy)
  • マルチタスク中
  • プライベートな場所にいる
  • 家族と共有している場所にいる
voice は hiro になる
  • 教える必要がない (Instant experts)
  • 高い予想
  • 許容誤差が小さい
ユーザーが無意味なことを言わない限り、エラーは常にシステム側の問題

会話デザインの戦略
  • 1. 例外に対する戦略を開発するのに時間をかける
  • 2. ユーザーが元のフロー (track) にすごく簡単に戻ってこれるようにする
  • 3. 日常の会話で使用するテクニックを活用する
日付を聞いて 4/14 と答えられたとき、もし年も指定してほしいなら、エラーにするのではなく 4/14 を受け付けた上で年を聞く

機会を活用する例

ユーザーの視点
  • 自分の質問に答えてほしい
  • 何かするのを助けてほしい
  • トラック上にキープしてほしい
  • やることを教えてほしい
開発者の視点
  • 素早い回答
  • ひっかかりのないやりとり
  • 関連する提案
例)Financial

"What's my current balance?"
"How much do I owe?"
"What was my last transaction?"
"I need to make a payment"
"Transfer $100 to Joe's account"
"Whan does my policy expire?"

例) Retail

"Is my order on the way?"
"What's your return policy?"
"Do you do same-day delivery?"
"Do you carry milk?"
"Any headphones on sale?"
"I need to re-order cat food"

例)Healthcare
"Is my prescription ready?"
"Can I get a flu shot?"
"Any appointments available?"
"I need to schedule a follow up"
"Leave a note for my doctor"
"Refill my prescription"

例)Fun & Wellness

"What's my horoscope?"
"Got any good dad jokes?"
"Read me a story"
"Let's play a game"
"I need to meditate"

g.co/dev/ActionsDesign



Home Automation with the Google Assistant (Google I/O '17)



Vision : Google Assistant は IoTデバイスとのインテリジェントなやりとりを促進する中心的存在になる

Conversations > Commands
コマンドを覚えるのではなく、人に話すように操作できる

Home Graph
家のなかの device の contextual data を保存し Google Assistant に提供する

Structure
  • Address
  • Managers
  • Rooms
    • Labels
    • Devices
  • Labels
  • Devices
    • Type
    • Traits
    • Attributes
    • Labels
    • State
「dim the lights in the living room a little bit」 と言われたとき、Home Graph ではこうする
  • living room に light はある? - ある
  • light は点いてる ? - 点いてる
  • 現在の明るさは? - 50%
  • little bit ってどのくらい? - デフォルトの設定による、little bit や a lot を support してるかも、いずれにしろ値を決める、例えば 3%
  • Google Assistant に最終的なデータ(47%にする)を渡す
  • Google Assistant が light の明るさを変更する
Smart Home Apps
  • Actions on Google を使って build
  • device を直接 Google Graph に登録
Smart Home partners start with hardware
  • パートナーはクラウドサービスの薄いレイヤーを追加する
  • Google が言語の理解、Home Graph、各種デバイスの詳細なハンドリングを提供する


2つのフローがある

device 登録フロー
  • 1. アプリを登録すると、Smart Home Apps の一覧で出てくるようになる
  • 2. ユーザーがそれをクリックしたら、アプリの OAuth registration を呼び出す
  • 3. 戻ってきたら device に関するリクエストを送るので、ユーザーが持っている全てのデバイスの情報を JSON で返す
  • 4. 状態は Home Graph で持つのでアプリ側で持つ必要はない
実行フロー
  • 1. ユーザーの音声入力を解析し、結果を単純な JSON にして Agent に送る
  • 2. Agent が IoT デバイスを操作し、結果を返す

基本的な device type を用意しており、より多くの device type が今後数週間から数ヶ月で作られる

これらの device type は traits で構成される
traits は実際に行われる機能
例えば最も簡単な trait は on/off





Getting Your Assistant App Discovered (Google I/O '17)

Assistant App をユーザーに見つけてもらうようにするには、という話



g.co./dev/ActionsDesign/

Checklist: Your calls to action
  • ブランド認定を行う : ウェブサイトやアプリ (soon) と関連づける
  • 有益なディレクトリリストを作成する : 実行可能な会話の例を出す
  • フレーズを追加する
  • 人々がまた使いたいと思うアプリを作る!




In Conversation, There Are No Errors (Google I/O '17)

14:15 からのデモがよいです。エラー時に何が起こるかがよくわかります。



エラーを防ぐために質問を工夫する

DialogFlow では agent は intent で構成される
intent はユーザー特定の入力とコンテキストに対応する

Repeat - 「Pardon?」
Help - 「I'm not sure.」
Quit - 「I have to go!」

なんどもエラーのやり取りを繰り返すとユーザーはすぐに離脱してしまう
エラーを防ぐように準備すること

I don't know は通常は Help に割り当てられるべきだが、予想ゲームをしているときは give up と解釈するべき

Conversation Helpers
  • AskForSignIn
  • AskForOption
  • AskForDateTime
  • AskForConfirmation
  • Transactions related system intents (delivery address, payments)
No Input時のレスポンスのパターンは複数用意する
DialogFlow 以外にも sdk で複数指定することもできる

必要に応じて default fallback intent を書き換える
より context に沿った内容に

fallback intent に到達した回数を数えて、prompt の内容を変える
もし回数が閾値(ユーザーペルソナによる)を超えたら会話を終了する

ペルソナを作るべき、すごく役に立つ

コンテキストを維持して流れを強化する
  • ランダムな prompt のリストを作る
  • 連結を駆使して多くの選択肢を用意する
  • 動的な値を活用する
  • 必要に応じてトラッキングとスキップを活用し、繰り返しを避ける
  • エラーの数や種類を覚えておいて prompt を調整する
DialogFlow の Training 機能をチェックする
handle できなかった query を intent に割り当てることができる

number genie のサンプルをチェックする



Transactions with the Google Assistant (Google I/O '17)

Google Assistant (Actions on Google) で Transaction を行う話です。途中でコードサンプルがたくさんでてきます。



優秀な Assistant というのは情報を与えるだけではなく、get things done を助ける
そのために ecosystem のパートナーが必要だった

Assistant での transaction とは何か?
food delivery やコンサートチケットを購入できる
美容院やレストラン、ヨガクラスなど、予定の予約ができる

なぜこれが重要なのか
  • ユーザーにとって良いから
    • 情報と行動のギャップを埋める
  • 開発者にとって良いから
    • 新しいユーザーが見つけられる
    • 既存のユーザーを re-engage できる
    • Actions on Google app でマネタイズできる
購入は敷居が高い
ユーザーが脱落するハードルがたくさんある
2つの大きなポイントが payments と identity

お店に行って買うとする
ほしいものが売り切れていたり、少し前にセールしていたかもしれない

アプリで買うとする
アプリをダウンロードしないといけない
支払い情報、住所な電話番号なども入力しないといけない
最後にカートに入れて支払いをする
悪くないけど、これをアプリごとにやるとしたら大変
デバイスを変更したらその度にダウンロードしてサインインしないといけない

Google はすでにユーザーの payment と identity 情報を持っているので、その情報を渡すことで transaction を容易にすることができる
ハードルが下がるということは、より多くの購入につながる

3つの主な機能
  • 1. payments
  • 2. identity
  • 3. Re-engagement

Payments

1/3のユーザーがクレジットカードの入力中に購入を諦める

Google Play で買い物をしたことがあるユーザーならクレジットカード情報が Google Account に登録されている
Google は Assistant での transaction 中に payment 情報を安全に渡すことができる

処理の流れ(以下でのアプリは自分の Assistant app のこと)
  • 1. 特定のユーザーに特定の額のチャージが必要なことをアプリから Google に伝える
  • 2. Google はアプリの payment prosessor's key を使って生の credit card 情報を暗号化する(payment prosessor's key はアプリ specific)。これにより暗号化された payment credential が生成される
  • 3. Google は暗号化された payment credential をアプリに渡す
  • 4. アプリは通常の debit card やクレジットカードのように credential に対して課金する
Google-facilitated payments
  • Convenient
    • ユーザーは single tap で支払いできる
  • Free
    • Google はこれに関して手数料を取らない
    • アプリは payment processor に対する通常の processing fee だけ払えばよい
  • Lightweight
    • payment credential が必要なのは runtime 時だけ
    • 通常の credit card と同じように課金できる
Payment processors
  • Supported
    • stripe
    • Braintree
    • vantiv
  • Coming soon(in the next several months)
    • ACI UNIVERSAL PAYMENTS
    • adyen
    • First Data
    • worldpay
Google-facilitated payments は完全に optional
もし顧客がすでに支払い方法を持っている場合(例えば会員カードやポイント)、それらを使うこともできる


Identity

54%のユーザーがアカウント登録処理で離脱する
ログイン情報を忘れたユーザーの90%が離脱する


Assistant App でログインまたはアカウントの作成が必要になった場合、Google はユーザーの identity 情報をもっているので、それを使って簡単にログイン・アカウントの作成を行うことができる Google アカウントの選択と確認の2タップで終わる
eye-free, hands-free なシナリオでも可能、Google Home は Link されているユーザーがいればそれを使うので Google アカウントの選択をスキップできる

ユーザーが Google アカウントを選択したら、Assistant App に identity token を送る
token にはユーザー名やメールアドレスなどの情報が含まれる
アプリはこれらの情報を使って対応するアカウントが存在しているかチェックする
アカウントがない場合、ユーザーの Google identity を使って作成することができる
Google Sign in と同じような処理 アカウントがすでにある場合、アカウント作成プロセスを完了するために追加の OAuth 2 script を許可するか、任意でユーザーに聞くことができる


Re-engagement

transaction はユーザーが支払ったときに終わるわけではない
ユーザーはオーダーに対して質問があるかもしれない
今どこにある?現在のステータスは?配送される前に他のものを追加できる?
ユーザーはオーダーに対して何か行動したいかもしれない
アップグレードしたい、ドライバーと話したい、金曜日に変更したい

Google Assistant が全ての transaction 履歴を一箇所で管理する
ユーザーの follow-up アクションへの導線をわかりやすくできる
オーダーの update を提供できる
ユーザーはそのためのアプリをいれたり、メールを探さなくてよくなる

Transaction Helpers
  • buildCart()
  • askForDeliveryAddress()
  • getDeliveryAddress()
  • buildOrder()
  • askForTransactionDecision()
  • getTransactionDecision()
  • buildOrderUpdate()
  • getUser()
  • askForSignIn()
  • getSignInStatus()




2017年11月23日木曜日

KOIN 使ってみた

KOIN は Android 向けのシンプルな Dependency Injection フレームワークです。Kotlin の機能を使って DI を実現しています(proxy/CGLib なし、コード生成なし、introspection(リフレクションとかバイドコードいじりとか)なし)。

使い方

interface Heater interface Pump class ElectricHeater : Heater class Thermosiphon(private val heater: Heater) : Pump class CoffeeMaker(val heater:Heater, val pump:Pump)

1. dependency

implementation 'org.koin:koin-android:0.6.0' testImplementation 'org.koin:koin-test:0.6.0'

2. AndroidModule を継承したクラスを用意し、context() メソッドを実装する

この Context は Android の Context ではなくて org.koin.dsl.context.Context です。 Context の applicationContext() を使って構成します。 class DripCoffeeModule : AndroidModule() { override fun context(): Context { return applicationContext { provide { ElectricHeater() } bind Heater::class provide { Thermosiphon(get()) } bind Pump::class provide { CoffeeMaker(get(), get()) } } } } applicationContext() は name として Scope.ROOT を指定した Context を生成します。
fun applicationContext(init: Context.() -> Unit) = Context(Scope.ROOT, koinContext).apply(init) provide はデフォルトでは singleton になります。singleton にしない場合は isSingleton に false を指定するか、provideFactory を使います。 provide(isSingleton = false) { CoffeeMaker(get(), get()) } provideFactory { CoffeeMaker(get(), get()) } provide で name を指定することもできます。name を変えることで、同じ型を返す provide を複数定義できます。 provide("Coffee") { CoffeeMaker(get(), get()) } provide("Coffee2") { CoffeeMaker(get(), get()) } context() で sub context を作ることができます。Context には Scope 名を指定することができます。 class DripCoffeeModule : AndroidModule() { override fun context(): Context { return applicationContext { context("MainActivity") { provide { ElectricHeater() } bind Heater::class provide { Thermosiphon(get()) } bind Pump::class provide { CoffeeMaker(get(), get()) } } } } } provide するインスタンスを生成するときに Android の Context が必要な場合、androidApplication で Application インスタンスを取得することができます。 provide { ElectricHeater(androidApplication) } bind Heater::class

3. アプリケーションクラスの onCreate() で startKoin() を呼ぶ

class MyApplication : Application() { override fun onCreate() { super.onCreate() startKoin(this, listOf(DripCoffeeModule())) } } startKoin() は android.app.Application の拡張関数として定義されています。

4. inject

class MainActivity : Activity() { val maker by inject<CoffeeMaker>() } inject() inline fun <reified T> ComponentCallbacks.inject(name: String = "") = lazy { (StandAloneContext.koinContext as KoinContext).get<T>(name) } なので以下と同じ、つまり lazy です。 class MainActivity : Activity() { val maker by lazy { (StandAloneContext.koinContext as KoinContext).get<CoffeeMaker>() } } lazy が嫌なら onCreate() で (StandAloneContext.koinContext as KoinContext).get() を自分で呼んで代入すればできますが、全部自動で一括でとなるとやはりアノテーションなどが必要ですね。 class MainActivity : Activity() { private lateinit var maker: CoffeeMaker override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) maker = (StandAloneContext.koinContext as KoinContext).get() } }

provide で name を指定した場合、inject にその name を指定して取得できます。 class MainActivity : Activity() { val maker by inject<CoffeeMaker>("Coffee") } Scope 名を指定してインスタンスを解放することができます。 override fun onDestroy() { super.onDestroy() releaseContext("MainActivity") } ContextAwareActivity を継承して Scope 名を指定すると同じことができます。ContextDropMethod を指定しないときのデフォルトは ContextDropMethod.onPause(onPause() で releaseContext() される)です。 class TestActivity : ContextAwareActivity("MainActivity", ContextDropMethod.OnDestroy) { val maker by inject<CoffeeMaker>() }


おまけ、Daggerの場合

kapt 'com.google.dagger:dagger-compiler:2.11' implementation 'com.google.dagger:dagger:2.11' interface Heater interface Pump class ElectricHeater : Heater class Thermosiphon @Inject constructor(private val heater: Heater) : Pump class CoffeeMaker @Inject constructor(val heater: Heater, val pump: Pump) @Module class DripCoffeeModule { @Provides fun provideHeater(): Heater { return ElectricHeater() } @Provides fun providePump(pump: Thermosiphon): Pump { return pump } } @Component(modules = arrayOf(DripCoffeeModule::class)) interface CoffeeShop { fun maker(): CoffeeMaker } val coffeeShop = DaggerCoffeeShop.builder() .dripCoffeeModule(DripCoffeeModule()) .build() val maker = coffeeShop.maker() assertThat(maker.heater).isNotNull() assertThat(maker.pump).isNotNull()


2017年11月19日日曜日

Android Things をやってみよう - InputDriver 編

User-Space Drivers : input

InputDriver を使うと、タッチイベントやキーイベントをシステムに流すことができます。

これにより、例えば GPIO から入力があったときに Space キーや Enter キーなどの KeyEvent を流すことで、KeyEvent を処理する機能やライブラリをそのまま流用できます。

InputDriver を使うときは com.google.android.things.permission.MANAGE_INPUT_DRIVERS パーミッションが必要です。 <uses-permission android:name="com.google.android.things.permission.MANAGE_INPUT_DRIVERS" />

InputDriver.Builder を使って InputDriver を生成します。

UserDriverManagerregisterInputDriver() で InputDriver を登録し、最後に unregisterInputDriver() で登録を解除します。

InputDriver にキーイベントを流すときは InputDrivder.emit() に KeyEvent を渡します。 class InputDriverActivity : Activity() { private var gpio: Gpio? = null private var inputDriver: InputDriver? = null private val callback: GpioCallback = object : GpioCallback() { override fun onGpioEdge(gpio: Gpio): Boolean { val action = if (gpio.value) KeyEvent.ACTION_DOWN else KeyEvent.ACTION_UP // ドライバーに Enter キーイベントを流す inputDriver?.emit(arrayOf(KeyEvent(action, KeyEvent.KEYCODE_ENTER))) return true } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val pioService = PeripheralManagerService() try { gpio = pioService.openGpio("GPIO_174").apply { setDirection(Gpio.DIRECTION_IN) setEdgeTriggerType(Gpio.EDGE_BOTH) setActiveType(Gpio.ACTIVE_LOW) registerGpioCallback(callback) } } catch (e: IOException) { Log.e(TAG, "Error initializing GPIO", e) } // Enter キーをサポートするドライバーを用意 inputDriver = InputDriver.Builder(InputDevice.SOURCE_CLASS_BUTTON) .setName("Button") .setVersion(1) .setKeys(intArrayOf(KeyEvent.KEYCODE_ENTER)) .build() // ドライバーを登録 UserDriverManager.getManager().registerInputDriver(inputDriver) } override fun onDestroy() { if (inputDriver != null) { // ドライバーの登録を解除 UserDriverManager.getManager().unregisterInputDriver(inputDriver) inputDriver = null } gpio?.unregisterGpioCallback(callback) try { gpio?.close() } catch (e: IOException) { Log.e(TAG, "Error closing GPIO", e) } super.onDestroy() } override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { Log.d(TAG, "onKeyDown : $keyCode") if (keyCode == KeyEvent.KEYCODE_ENTER) { return true } return super.onKeyDown(keyCode, event) } override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean { Log.d(TAG, "onKeyUp : $keyCode") if (keyCode == KeyEvent.KEYCODE_ENTER) { return true } return super.onKeyUp(keyCode, event) } companion object { private const val TAG = "InputDriverActivity" } }

contrib-drivers

https://developer.android.com/things/training/first-device/drivers.html#initialize_the_driver_library

contrib-drivers の button にある ButtonInputDriver を利用すると、GPIO の入力を簡単に KeyEvent に割り当てられます。 implementation 'com.google.android.things.contrib:driver-button:0.3' class ButtonInputDriverActivity : Activity() { private var inputDriver: ButtonInputDriver? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) inputDriver = ButtonInputDriver("GPIO_174", Button.LogicState.PRESSED_WHEN_LOW, KeyEvent.KEYCODE_ENTER) } override fun onDestroy() { inputDriver?.unregister() try { inputDriver?.close() } catch (e: IOException) { Log.e(TAG, "Error closing GPIO", e) } super.onDestroy() } override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { Log.d(TAG, "onKeyDown : $keyCode") if (keyCode == KeyEvent.KEYCODE_ENTER) { return true } return super.onKeyDown(keyCode, event) } override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean { Log.d(TAG, "onKeyUp : $keyCode") if (keyCode == KeyEvent.KEYCODE_ENTER) { return true } return super.onKeyUp(keyCode, event) } companion object { private const val TAG = "ButtonInputDriverActivity" } }

moshi で独自の Adapter は KotlinJsonAdapterFactory より先に add すべし

Retrofit と一緒にmoshi を使っています。
moshi には Kotlin Support 機能があり、別途 moshi-kotlin を追加して、 implementation 'com.squareup.moshi:moshi-kotlin:1.5.0' KotlinJsonAdapterFactory を MoshiBuilder に add します。 val moshi = Moshi.Builder() .add(KotlinJsonAdapterFactory()) .build()

また、moshi には Custom Type Adapters 機能があり、JSON の値とオブジェクトとの変換をカスタマイズすることができます。

この CustomAdapter および、CustomAdapter の Factory は KotlinJsonAdapterFactory より先に add しなければいけません (Kotlin Support のところに書いてあるのに気づかずはまってしまった...)。 Retrofit.Builder() .addConverterFactory(MoshiConverterFactory.create( Moshi.Builder() .add(MyCustomAdapter()) // KotlinJsonAdapterFactory より先なので OK .add(KotlinJsonAdapterFactory()) .build())) ... Adapter を生成するときに、追加された順番で Factory に Adapter が生成できるか問い合わせていくため、KotlinJsonAdapterFactory を先にすると CustomAdapter に来ないので気をつけましょう。


2017年11月12日日曜日

Android Things をやってみよう - Peripheral 操作編

Peripheral の操作は PeripheralManagerService を使って行います。

GPIO の名前の一覧は PeripheralManagerService.getGpioList() で取得できます。 val pioService = PeripheralManagerService() for (name in pioService.gpioList) { Log.d(TAG, "gpio : $name") } 実行結果 gpio : GPIO_10 gpio : GPIO_128 gpio : GPIO_172 gpio : GPIO_173 gpio : GPIO_174 gpio : GPIO_175 gpio : GPIO_32 gpio : GPIO_33 gpio : GPIO_34 gpio : GPIO_35 gpio : GPIO_37 gpio : GPIO_39

PeripheralManagerService.openGpio() で GPIO を開きます。戻り値は Gpio です。引数には getGpioList() で取得した名前を指定します。 val ledPin = pioService.openGpio(name)
GPIO は General-purpose input/output のことで、GPIO pin はソフトウェアからコントロールできる集積回路上の物理的ピンです。GPIO pin は入力として使って電圧値を読んだり、出力として使って電圧値を変えたりできます。

扱えるのは論理値(true / false)のみで、low value(ピンがグラウンドと同じ電圧値)と high value(ピンが IOREF と同じ電圧値)が論理値にマッピングされます。論理値と high/low のマッピング(true が high/low どちらに対応するか)は設定で変えることができます。

GPIO pin を開くということは、システム全体でこのピンに対するオーナシップを取るということです。これにより close() が呼ばれるまで他から GPIO を開いたりアクセスしたりするのを防ぎます。close() を呼ぶのを忘れると、同じプロセス・アプリに関係なくどこからも GPIO が使えなくなるので注意が必要です。


論理値と high/low のマッピングは Gpio.setActiveType() で指定します。
true に high を対応させるときは Gpio.ACTIVE_HIGH を、true に low を対応させるときは Gpio.ACTIVE_LOW を指定します。 ledPin.setActiveType(Gpio.ACTIVE_HIGH)
ピンの方向(入力 or 出力)は Gpio.setDirection() で指定します。
ピンを入力として使うときは Gpio.Gpio.DIRECTION_IN を指定します。出力として使うときは、初期状態を high にするときは Gpio.DIRECTION_OUT_INITIALLY_HIGH を初期状態を low にするときは Gpio.DIRECTION_OUT_INITIALLY_LOW を指定します。 ledPin.setDirection(Gpio.DIRECTION_OUT_INITIALLY_LOW)
値が変わったときのトリガーのタイミングを Gpio.setEdgeTriggerType() で指定することができます。
ピンを出力として使うときはトリガーの必要がないので Gpio.EDGE_NONE を指定します。
ピンを入力として使う場合、立ち上がり時(low から high になったとき)だけトリガーさせるなら Gpio.EDGE_RISING を指定します。立ち下がり時(high から low になったとき)だけトリガーさせるなら Gpio.EDGE_FALLING を指定します。立ち上がり、立ち下がり両方トリガーさせるなら Gpio.EDGE_BOTH を指定します。 ledPin.setEdgeTriggerType(Gpio.EDGE_NONE)
現在の値を変更するときは Gpio.setValue() を使います。このメソッドはピンが出力として設定されているときだけ使うことができます。 ledPin.value = isChecked
現在の値を読むときは Gpio.getValue() を使います。このメソッドはピンが入力として設定されているときだけ使うことができます。 val value = ledPin.value
ピンの値が変わった時にコールバックを受け取るには Gpio.registerGpioCallback()GpioCallback を登録します。登録した GpioCallback は Gpio.unregisterGpioCallback() で登録解除します。 private val callback: GpioCallback = object : GpioCallback() { override fun onGpioEdge(gpio: Gpio?): Boolean { Log.d(TAG, "onGpioEdge: ${gpio?.name}, ${gpio?.value}") return true } override fun onGpioError(gpio: Gpio?, error: Int) { super.onGpioError(gpio, error) /** error : [android.system.OsConstants] */ Log.d(TAG, "onGpioError: ${gpio?.name}, $error") } } override fun onCreate(savedInstanceState: Bundle?) { ... ledPin.registerGpioCallback(callback) } override fun onDestroy() { super.onDestroy() try { ledPin.unregisterGpioCallback(callback) ledPin.close() } catch (e: IOException) { Log.e(TAG, "Error closing GPIO", e) } }

Android Things をやってみよう - プロジェクト作成編



form factors に Android Things を選ぶ









以下のような build.gradle が作成される apply plugin: 'com.android.application' ... android { ... } dependencies { ... compileOnly 'com.google.android.things:androidthings:+' }


2017年11月11日土曜日

Android Things をやってみよう - セットアップ編

https://developer.android.com/things/index.html

DroidCon SF で Android Things のハンズオンに参加し、Developer Kit をもらったのでいろいろやってみています。

事前準備

- Android Studio 3.0 をインストールする


- システム Image を用意する 1. Android Things Console に行ってサインイン(初回時に Terms of Service の同意が必要)する

2. Create product ボタンをクリックし、プロダクト名と詳細を入力し、対応する SOM type (もらった kit だと NXP Pico i.MX7D) を選択して Create をクリックする



3. 作成されたプロダクトを選択する



4. FACTORY IMAGES タブを選択する

5. Bundles で Empty Bundle を選択する



6. Android Things versions で最新のバージョンを選択し、Click Create Build Configuration をクリックする



7. Build が完了するまで待って、Build configuration list に追加されたアイテムをダウンロードし、解凍する




- Flashing the image 1. ボードと USB ケーブルで接続する (NXP i.MX7D なら USB-C cable が必要)

2. Android SDK の platform-tools/ にパスを通す

3. macOS が High Sierra (10.13) の場合、別の fastboot バイナリをダウンロードする

macOS が High Sierra (10.13) の場合、platform-tools/fastboot に既知の Issue があります。
https://issuetracker.google.com/issues/64292422
#comment8 に添付されているバイナリが使えます。

4. 以下のコマンドを実行してボードが Fastboot モードで起動していることを確認する $ fastboot devices Fastboot モードで起動していない場合は以下のコマンドでデバイスを再起動する $ adb reboot bootloader

5. ダウンロード&解凍した Image 内の flash-all.sh または flash-all.bat を実行する

6. 成功したら再起動するので、以下のコマンドで Android が動いていることを確認する $ adb devices

- WiFi に接続する $ adb shell am startservice -n com.google.wifisetup/.WifiSetupService -a WifiSetupService.Connect -e ssid [network_ssid] -e passphrase [network_pass]

サンプルで遊ぶ

https://developer.android.com/things/sdk/samples.html

ハンズオンでは をやりました。他にもサンプルたくさんあります。

ボードと USB で接続して、普通に Android Studio で開いて実行すれば OK です。別のものをインストールする前に今インストールされているものをアンインストールする必要があるので、忘れないように注意です


コードラボで学ぶ

https://codelabs.developers.google.com/io2017?cat=IoT


2017年11月6日月曜日

Kotlin for Android What's New メモ

Update on Kotlin for Android : Android Developers Blog

Support Library 27 から API に nullability annotations をつけはじめた。

Android Kotlin Guides を公開。Kotlin for Android の style と interop のためのガイド。
  • 1. Style guide - Kotlin for Android でコーディングするときの Google が推奨するルールと coding standards の詳細。命名規約、フォーマット、ソースの構成などについて言及している。
  • 2. Interop guide - Java および Kotlin で API を作成するときに、もう一方の言語で慣例的に利用できるようにするためのルールセットを提供している。
Android samples のいくつかを Kotlin に porting。official documentation に Kotlin を追加することも開始。

Android Kotlin page

Kotlin on Android FAQ

Resources to Learn Kotlin

2017年11月4日土曜日

KotlinConf 2017 に参加してきました。

KotlinConf 2017 | Kotlin Programming Conference in SF, USA

KotlinConf は Kotlin のカンファレンスです。今回が第1回目で、1200のチケットは完売したそうです。



1日目

キーノート前。人いっぱいです。



Coroutines



experimental は unstable ということではない。まだ API を変更する自由度を持っておきたいので experimental にしている。coroutines は production ready。Kotlin 1.3 で experimental が外れる予定。





Ktor

読み方はケイター、100% Kotlin の web framework
http://ktor.io/



Kotlin/JS





Android

Play Store で Kotlin を使っているアプリの数が 2.5倍に(いつからかは不明)



Android Studio Project の 17% が Kotlin を使っている(具体的な話は不明)



Android Studio
- Bundled Kotlin Plugin
- Kotlin Lint Support (in-IDE)
- Kotlin Templates for Projects and Activities



Android Support library 27.0.0 Kotlin Annotations



Android Kotlin Guides でました。
android.github.io/kotlin-guides/

Kotlin の Style Guide はこちら。
https://android.github.io/kotlin-guides/style.html

Java 呼ばれることを意識した Kotlin の書き方、Kotlin から呼ばれることを意識した Java の書き方はこちら。
https://android.github.io/kotlin-guides/interop.html



Android Kotlin Docs & Samples
https://developer.android.com/samples/index.html?language=kotlin



iOS

Kotlin で iOS きたー。





Multiplatform Projects

expect と actual 修飾子で共通の部分の Kotlin コードから .class と .js を生成。





Server, iOS, Android, Browser 全部 Kotlin!すごい世界だ。



Summary / Today

- Kotlin 1.2 RC available (keynote recap at blog.jetbrains.com)
- Multiplatform Projects (KotlinConf アプリが参考になるよ https://github.com/jetbrains/kotlinconf-app)
- Common modules
- expect/actual fun/class
- More in the blog post



Summary / Future

- Multiplatform libraries
- I/O, networking, serialization, dates...
- Kotlin/Native : compiler and IDE



Gradle Kotlin DSL

gradle の kotlin DSL よさげだった
GitHub - gradle/kotlin-dsl: Kotlin language support for Gradle build scripts
https://github.com/gradle/kotlin-dsl
↑まだ情報古そう

2日目キーノート

話は面白かった。あんまり Kotlin 関係なかった。





Kotlin/Native Demo

Swift の interop は in progress。現状は Objective-C の interop。CLion と XCode でデモしてた。





Kotlin Types

個人的にこのセッションが一番よかったです。おすすめ。





その他

Coding Conventions : the current coding style for the Kotlin language.

Udacity に Kotlin コースくるらしい



お昼ご飯おいしかった。でも初日のパーティのご飯はいまいちで、結局外に食べに行った。

おやつの Kotlin カップケーキかわいかった。すごく甘くてアメリカを感じた。



会場がちょっと寒いからかすぐコーヒーなくってしまってた。

1日目の最後、Party Keynote。なんかずっと手品してた。



1日目夜。中心部のビルの夜景がきれい。



2日目昼ごはん

2017年9月22日金曜日

Play Billing Library 1.0 がリリースされました

2017年9月19日に Play Billing Library の 1.0 がリリースされました(Android Developers Blog : Google Play Billing Library 1.0 released)。



Play Billing Library 1.0 では自動で com.android.vending.BILLING Permission が追加されるので手動で追加する必要はありません。

ライブラリの設定 compile 'com.android.billingclient:billing:1.0' or implementation 'com.android.billingclient:billing:1.0' BillingClient というクラスを利用します。 val billingClient : BillingClient = BillingClient.newBuilder(context) .setListener(this) // PurchasesUpdatedListener .build() Builder パターンになっていますが、PurchasesUpdatedListener を設定しないと build() を呼んだときに IllegalArgumentException が起こります。
IabHelper にあった enableDebugLogging() に相当するメソッドは BillingClient にはありません。


開始と終了

IabHelper の startSetup() に相当するのが BillingClient.startConnection() です。 startConnection() で開始して endConnection() で終了します。 private lateinit var billingClient: BillingClient private var isBillingClientConnected: Boolean = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) billingClient = BillingClient.newBuilder(this) .setListener(this) .build() startServiceConnection(null) } private fun startServiceConnection(executeOnSuccess: Runnable?) { billingClient.startConnection(object : BillingClientStateListener { override fun onBillingSetupFinished(@BillingClient.BillingResponse response: Int) { when (response) { OK -> { isBillingClientConnected = true executeOnSuccess?.run() } else -> { ... } } } override fun onBillingServiceDisconnected() { isBillingClientConnected = false } }) } override fun onDestroy() { billingClient.endConnection() super.onDestroy() }

購入

購入処理は BillingClient の launchBillingFlow() で開始します。 val params = BillingFlowParams.newBuilder() .setType(BillingClient.SkuType.INAPP) .setSku(SKU_INAPP) .build() val responseCode = billingClient.launchBillingFlow(activity, params) val params = BillingFlowParams.newBuilder() .setType(BillingClient.SkuType.SUBS) .setSku(SKU_SUBS) .addOldSku(SKU_OLD_SUBS) // replace するときは必須 .setReplaceSkusProration(true) // Optional : デフォルトは false .setAccountId(hashedUserAccountId) // Optional : 難読化されたユーザー別の文字列 .setVrPurchaseFlow(false) // Optional : デフォルトは false .build() val responseCode = billingClient.launchBillingFlow(activity, params) BillingFlowParams の各設定値については BillingFlowParams.Builder のドキュメントをよく読みましょう。

購入処理の結果は PurchasesUpdatedListener の onPurchasesUpdated() で通知されます。

launchBillingFlow() の戻り値と PurchasesUpdatedListener.onPurchasesUpdated() の第1引数は @BillingClient.BillingResponse です。 override fun onPurchasesUpdated(@BillingClient.BillingResponse responseCode: Int, purchases: List<Purchase>?) { when (responseCode) { OK -> { if (purchases != null) { purchases.forEach { handlePurchase(it) } } else { ... } } ITEM_ALREADY_OWNED -> { ... } USER_CANCELED -> { // do nothing } ... else -> { ... } } } 第2引数は最新の購入(Purchase)リストです。

IabHelper のときは com.android.vending.billing.PURCHASES_UPDATED を受け取るために IabBroadcastReceiver を用意していましたが、その必要はなくなりました。代わりに、アプリからの購入だけでなく Play Store で開始された購入のときも onPurchasesUpdated() に通知されます。


購入済みアイテムの問い合わせ

BillingClient の queryPurchases() で行います。 このメソッドは Google Play Store アプリが提供するキャッシュから結果(PurchasesResult)を受け取ります。ネットワーク処理は開始されません。 val result : PurchasesResult = billingClient.queryPurchases(BillingClient.SkuType.INAPP)

購入可能なアイテムの問い合わせ

BillingClient の querySkuDetailsAsync() で行います。 問い合わせるアイテムは SkuDetailsParams で設定します。 fun querySkuDetailsAsync() { val executeOnConnectedService = Runnable { val params = SkuDetailsParams.newBuilder() .setType(BillingClient.SkuType.INAPP) .setSkusList(listOf("gas", "premium")) .build() billingClient.querySkuDetailsAsync(params) { responseCode, skuDetailsList -> when (responseCode) { OK -> { skuDetailsList?.forEach { ... } } else -> { ... } } } } if (isBillingClientConnected) { executeOnConnectedService.run() } else { startServiceConnection(executeOnConnectedService) } }

注意

2017年9月22日時点では、https://codelabs.developers.google.com/codelabs/play-billing-codelab は dp-1 の内容なので古いです(Builder インスタンスの生成方法や購入可能なアイテムの問い合わせのAPIが 1.0 で変わっています)。https://github.com/googlecodelabs/play-billing-codelab は新しくなっています。

追記: https://codelabs.developers.google.com/codelabs/play-billing-codelab も更新されました。



2017年9月18日月曜日

Kotlin メモ : Class Delegation を使って Adapter の処理を委譲する

ベースクラスの異なる 2つの Adapter があります

1. ArrayAdapter を継承した FavoriteAdapter
  • FavoriteAdapter は任意の型のデータを取りうる
  • その型のデータに対する date(T), balance(T) の実装が必要
abstract class FavoriteAdapter<T>(context: Context, objects: List<T>) : ArrayAdapter<T>(context, 0, objects) { abstract fun date(data: T): String abstract fun balance(data: T): Int private val inflater = LayoutInflater.from(context) override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { val view: View = convertView ?: ItemViewHolder .create(inflater, parent) .also { it.view.tag = it } .view getItem(position)?.let { (view.tag as ItemViewHolder).bind(date(it), balance(it)) } return view } } 2. CursorAdapter を継承した HistoryAdapter
  • CursorAdapter は任意の型のデータを取りうる
  • その型のデータに対する date(T), balance(T) の実装が必要
  • BaseData からその型のデータに変換するメソッドの実装が必要
abstract class HistoryAdapter<T>(context: Context) : CursorAdapter(context, null, 0) { abstract fun date(data: T): String abstract fun balance(data: T): Int abstract fun create(data: BaseData): T private val inflater: LayoutInflater = LayoutInflater.from(context) override fun newView(context: Context, c: Cursor, parent: ViewGroup) { return ItemViewHolder.create(inflater, parent).also { it.view.tag = it }.view } override fun bindView(view: View, context: Context, c: Cursor) { val baseData = convert(c) val data = create(baseData) (view.tag as ItemViewHolder).bind(date(data), balance(data)) } private fun convert(c: Cursor): BaseData { ... } }

Delegation なし

データとして MyData をとる Adapter を用意してみましょう。 class MyDataAdapter(context: Context, objects: List<MyData>) : FavoriteAdapter<MyData>(context, objects) { override fun date(data: MyData) = DateFormat.format(context.getString(R.string.format_date), data.getDate()).toString() override fun balance(data: MyData) = data.getBalance() } class MyDataAdapter(context: Context) : HistoryAdapter<MyData>(context) { override fun date(data: MyData) = DateFormat.format(context.getString(R.string.format_date), data.getDate()).toString() override fun balance(data: MyData) = data.getBalance() override fun create(data: BaseData): MyData = MyData(data) } val adapter: FavoriteAdapter<*> = MyDataAdapter(context, list) val adapter: HistoryAdapter<*> = MyDataAdapter(context) FavoriteAdapter を継承した MyDataAdapter と HistoryAdapter を継承した MyDataAdapter をそれぞれ用意しました。しかし2つの Adapter の処理はほぼ同じなので、1つのクラスにまとめるのがよいでしょう。

そこで、まずは通常の Delegation パターンで実装してみます。

Delegation パターン

Adapter<T> インターフェースを用意し、FavoriteAdapter と HistoryAdapter に abstract で定義していたメソッドを Adapter のメソッドに置き換えます。 interface Adapter<T> { fun date(data: T): String fun balance(data: T): Int fun create(data: BaseData): T } class FavoriteAdapter<T>(context: Context, objects: List<T>, private val adapter: Adapter<T>) : ArrayAdapter<T>(context, 0, objects) { fun date(data: T): String = adapter.date(data) fun balance(data: T): Int = adapter.balance(data) ... } class HistoryAdapter<T>(val context: Context, private val adapter: Adapter<T>) : CursorAdapter(context, null, 0) { fun date(data: T): String = adapter.date(data) fun balance(data: T): Int = adapter.balance(data) fun create(data: BaseData): T = adapter.create(data) ... } Adapter を継承した MyDataAdapter を用意します。 class MyDataAdapter(private val context: Context) : Adapter<MyData> { override fun date(data: MyData) = DateFormat.format(context.getString(R.string.format_date3), data.getDate()).toString() override fun balance(data: MyData) = data.getBalance() override fun create(data: BaseData): MyData = MyData(data) } val adapter: FavoriteAdapter<*> = FavoriteAdapter(context, list, MyDataAdapter(context)) val adapter: HistoryAdapter<*> = HistoryAdapter(context, MyDataAdapter(context)) FavoriteAdapter と HistoryAdapter を継承しなくてよくなりました。

一方、FavoriteAdapter と HistoryAdapter の date() や balance() メソッドでは、Adapter のメソッドをそのまま呼び出しているだけです。
Class Delegation を使うと、このような明示的な記述をしなくてよくなります。

Class Delegation

FavoriteAdapter と HistoryAdapter も Adapter<T> を実装し、by を使ってコンストラクタでもらった adapter に処理を委譲します。 class FavoriteAdapter<T>(context: Context, objects: List<T>, private val adapter: Adapter<T>) : ArrayAdapter<T>(context, 0, objects), Adapter<T> by adapter { ... } class HistoryAdapter<T>(val context: Context, private val adapter: Adapter<T>) : CursorAdapter(context, null, 0), Adapter<T> by adapter { ... } Class Delegation により、FavoriteAdapter と HistoryAdapter では date() や balance() の明示的な記述をしなくてよくなりました。


2017年9月15日金曜日

自前アプリを Java から Kotlin に書き換えてみた。

Kotlin の練習のために Suica Reader のコードを Kotlin で書き換えてみました。

最終的にコード量は2割ちょっと減りました。



Java コードを全て一括で Kotlin に自動変換することも可能ですが、個別のクラスを順番に Kotlin 化していくことにしました。
どのように変換されるかを確認し、さらにより Kotlin らしい記述に書き換えるには、全部一括でやるのは合わないと思ったからです。


書き換え順

1. enum

ほとんど自動変換で済みました。 まだ他の Java コードが残っているので、companion object の関数に @JvmStatic をつけるのと、Companion が差し込まれた Java 側のコードを元に戻すことを追加でやりました。


2. interface

これもほとんど自動変換で済みました。


3. Parcelable

できるものは data class にしました。
いつも CREATOR に @JvmField をつけ忘れそうになる... Parcelable については「Kotlin メモ : Parcelable」に書きました。

Android Kotlin Extensions の Parcelable サポートの experimental が取れたらそっちに移行したいです。


4. ロジック部分

途中 data class ではまって interface にすることで解決しました。
詳しくは 「Kotlin メモ : data class を継承できないので interface で実現した話」に書いてあります。


5. データ保存部分

SharedPreferences と ContentProvider を使っています。
SharedPreferences についてはドメインの方に interface を定義してあり、get と set 用にメソッドが分かれていたのを、フィールド1つにして実装側はカスタムアクセサを用意するように変えました。詳しくは「SharedPreferences を使ったデータアクセス部分を Kotlin のカスタムアクセサ で実装する」に書いてあります。
ContentProvider については特にないです。Cursor 部分で use を使ったくらいかな。


6. UI部分

ButterKnife を使っていたのですが、Android Kotlin Extensions に切り替えました。
Nullable か NonNull か確認するためにプラットフォームのコードを見にいくのがめんどかったです。
(Java 側には @Nullable, @NonNull アノテーションつけよう!)

自動変換後 ? と !! をちまちま直します。適宜 lateinit とか使います。



よかった点

テストコードをがっつり書いてあるので、安心して移行処理ができました。


反省点

先にテストコードを Kotlin 化してもよかったかもしれない。
でも Java のテストコードいじらないほうが移行後の Kotlin に自信(安心?)持てるかも。


まとめ
  • NonNull, Nullable のコンパイル時チェックの安心感ぱない。
  • 標準ライブラリが充実していて素晴らしい。
  • 一から新しいアプリを Kotlin で書き始めるより既存のアプリを Kotlin に変換するほうが早くかけるようになりそう。

2017年9月14日木曜日

Kotlin メモ : use

https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.io/use.html

Java public int queryCount(Context context, Uri uri) { final Cursor c = context.getContentResolver() .query(uri, null, null, null, null); if (c == null) { return 0; } final int count = c.getCount(); c.close(); return count; } Kotlin : 自動変換直後 fun queryCount(context: Context, uri: Uri): Int { val c = context.contentResolver .query(uri, null, null, null, null) ?: return 0 val count = c.count c.close() return count } Kotlin : use 使用 fun queryCount(context: Context, uri: Uri): Int { val c = context.contentResolver .query(uri, null, null, null, null) ?: return 0 c.use { return it.count } }

Kotlin メモ : CustomView は @JvmOverloads でコンストラクタ部分を短くするのがよさげ

class CustomView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : View(context, attrs, defStyleAttr) { init { ... } }

2017年9月10日日曜日

Android Studio 3.0 で compileSdkVersion を 26 にすると findViewById で型の指定が必要になる

環境
  • Android Studio 3.0 Beta 5 (Android Studio 2系では試してません)
  • Kotlin
compileSdkVersion を 25 から 26 にしたところ、以下のように findViewById で型の指定を求められるようになりました。



以下のようにすれば ok です。 as TextView もいらなくなります。



追記: 変数の型を明示する方法でも ok です。



2017年9月1日金曜日

SharedPreferences を使ったデータアクセス部分を Kotlin のカスタムアクセサ で実装する

ユーザーの血液型を保存したいとします。

SharedPreferences に保存するとして、保存・読み出しでキーを間違えたり、対象の SharedPreferences を間違えたりしないためには、SharedPreferences への保存と読み出しを行うためのクラスを用意するとよいです。

例えば次のような Utils クラスを用意したとしましょう。 public class ProfileSettingUtils { private static final String PREF_KEY_BLOOD_TYPE = "blood_type"; private static SharedPreferences getPref(@NonNull Context context) { return PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()); } @Nullable public static String getBloodType(@NonNull Context context) { return getPref(context).getString(PREF_KEY_BLOOD_TYPE, null); } public static void setBloodType(@NonNull Context context, @Nullable String bloodType) { getPref(context) .edit() .putString(PREF_KEY_BLOOD_TYPE, bloodType) .apply(); } } この Utils クラスで保存・読み出しを行えば、キーを間違えたり対象の SharedPreferences を間違えたりはしません。
しかし、"a" がA型を意味するなど、利用側が返される文字列の意味を知っている必要がありますし、"-" など意図しない文字列も保存できてしまいます。

血液型のような取り得る値が決まっているものは enum で定義して、保存・読み出し部分も enum でやりとりするべきです。 public enum BloodType { A("a"), B("b"), O("o"), AB("ab"); @NonNull public final String value; BloodType(@NonNull String value) { this.value = value; } @Nullable public static BloodType from(@Nullable String value) { if (value != null) { for (BloodType bloodType : values()) { if (bloodType.value.equals(value)) { return bloodType; } } } return null; } } public class ProfileSettingUtils { private static final String PREF_KEY_BLOOD_TYPE = "blood_type"; private static SharedPreferences getPref(@NonNull Context context) { return PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()); } @Nullable public static BloodType getBloodType(@NonNull Context context) { return BloodType.from(getPref(context).getString(PREF_KEY_BLOOD_TYPE, null)); } public static void setBloodType(@NonNull Context context, @Nullable BloodType bloodType) { getPref(context) .edit() .putString(PREF_KEY_BLOOD_TYPE, bloodType != null ? bloodType.value : null) .apply(); } } Robolectric を使えばテスト時に SharedPreferences の動きをモック化できますが、テストのセットアップとして SharedPreferences に値をセットするよりは、BloodType を返す部分をモック化できたほうが柔軟性があります。

では Utils クラスをやめてみましょう。 public class ProfileSetting { private static final String PREF_KEY_BLOOD_TYPE = "blood_type"; @NonNull private final SharedPreferences pref; public ProfileSetting(@NonNull Context context) { pref = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()); } @Nullable public BloodType getBloodType() { return BloodType.from(pref.getString(PREF_KEY_BLOOD_TYPE, null)); } public void setBloodType(@Nullable BloodType bloodType) { pref .edit() .putString(PREF_KEY_BLOOD_TYPE, bloodType != null ? bloodType.value : null) .apply(); } } このクラスを使って、読み出した BloodType を判断・加工するロジック部分があるとします。
このままだとロジック部分がデータアクセス部分に依存しています。
そこで、依存関係逆転の原則(DIP)を適用してロジックが抽象に依存できるように interface を用意します。 public interface Profile { @Nullable BloodType getBloodType(); void setBloodType(@Nullable BloodType bloodType); } public class ProfileSetting implements Profile { private static final String PREF_KEY_BLOOD_TYPE = "blood_type"; @NonNull private final SharedPreferences pref; public ProfileSetting(@NonNull Context context) { pref = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()); } @Override @Nullable public BloodType getBloodType() { return BloodType.from(pref.getString(PREF_KEY_BLOOD_TYPE, null)); } @Override public void setBloodType(@Nullable BloodType bloodType) { pref .edit() .putString(PREF_KEY_BLOOD_TYPE, bloodType != null ? bloodType.value : null) .apply(); } } ロジック部分は ProfileSetting ではなく interface の Profile を外部から渡してもらうようにします。

さて、これを Kotlin 化してみましょう。 enum class BloodType(val value: String) { A("a"), B("b"), O("o"), AB("ab"); companion object { fun from(value: String?): BloodType? = value?.let { values().firstOrNull { it.value == value } } } } interface Profile { var bloodType: BloodType? } class ProfileSetting(context: Context) : Profile { companion object { private const val PREF_KEY_BLOOD_TYPE = "blood_type" } private val pref: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context.applicationContext) override var bloodType: BloodType? get() = BloodType.from(pref.getString(PREF_KEY_BLOOD_TYPE, null)) set(bloodType) = pref.edit().putString(PREF_KEY_BLOOD_TYPE, bloodType?.value).apply() } Kotlin ではプロパティの定義にカスタムアクセサを書けるので、同じキーに対する保存と読み込みを一箇所に書けて対応がわかりやすくなりますね。