アクトインディ開発者ブログ

子供とお出かけ情報「いこーよ」を運営する、アクトインディ株式会社の開発者ブログです

Retrofit + Kotlin Coroutines

Android版いこーよを担当しているhondaです。

Android版いこーよでは通信処理でKotlin Coroutines(以下、コルーチン)を使い始めました。
コルーチンは簡単に言うと「スレッドインスタンスのようなものであり、中断可能な計算インスタンス」と言えます。
また、コルーチンは「中断されている最中にスレッドをブロックしない」という特徴を持っています。
なので、通信処理や時間がかかる処理などを非同期で実行でき、かつシンプルなコードで実装できます。

コルーチンについては前回のエントリーでも簡単にですが紹介しました。
tech.actindi.net

いこーよで通信処理にRetrofitを使っています。
Retrofitはバージョン2.5までコルーチンには未対応でした。
よって、バージョン2.5までのRetrofitでコルーチンを使うためには前回のエントリーで書いたようにこちらのサードパーティーのライブラリを使用したり、自前でDeferredを使って対応する必要があります。

最新バージョン2.6でコルーチンに対応したことによって、サードパーティや独自実装なしでコルーチンを使って非同期通信処理を書けるようになりました。

今回はGitHubのSearchAPIに対してのGETリクエストする処理でコルーチンを使わない場合と使った場合でコードの比較をしたいと思います。

GitHubAPIとのインターフェースとなるメソッドの定義

interface GitHubServiceInterface {
    //コルーチン使わないコード
    @GET("search/repositories")
    fun getSearchedRepositories(
        @Query(value = "q") query: String,
        @Query(value = "sort") sort: String,
        @Query(value = "order") order: String
    ): Call<GitHubRepositoriesData>

    // コルーチンを使うコード
    @GET("search/repositories")
    suspend fun getSearchedRepositoriesWithCoroutines(
        @Query(value = "q") query: String,
        @Query(value = "sort") sort: String,
        @Query(value = "order") order: String
    ): GitHubRepositoriesData
}

ここでの違いはsuspendと戻り値です。
コルーチンを使わないコートではリクエストの結果が返ってくるCallオブジェクトを返します
コルーチンを使ったコードではインターフェースとなるメソッドにsuspend修飾子が付き、Callオブジェクトではなく、GETリクエストで取得できるデータのオブジェクトを返します。
suspend修飾子はそのメソッドがコルーチンを中断するかもしれないことを表します。
今回の場合はRetrofit側がGETリクエストを開始してからレスポンスが返ってくるまでコルーチンを中断するため、suspend修飾子を付ける必要があります。

GitHubServiceInterfaceのインスタンスの生成

val gitHubService = Retrofit.Builder()
    .baseUrl("https://api.github.com")
    .build()
    .create(GitHubServiceInterface::class.java)
}

ここはとくにコードの違いはありません。

GETリクエストメソッド

コルーチンを使わないコード

   fun fetchSearchedRepositories(
        query: String,
        sort: String,
        order: String,
        onResponse: (response: GitHubRepositoriesData?) -> Unit,
        onFailure: (throwable: Throwable) -> Unit
    ) {
        gitHubService
            .getSearchedRepositories(query = query, sort = sort, order = order)
            .enqueue(
                object : Callback<GitHubRepositoriesData> {
                    override fun onResponse(
                        call: Call<GitHubRepositoriesData>,
                        response: Response<GitHubRepositoriesData>
                    ) {
                        onResponse(response.body())
                    }

                    override fun onFailure(call: Call<GitHubRepositoriesData>, t: Throwable) {
                        onFailure(t)
                    }
                }
            )
    }

コルーチンを使ったコード

   suspend fun fetchSearchedRepositoriesByCoroutines(
        query: String,
        sort: String,
        order: String
    ): GitHubRepositoriesData {
        return gitHubService.getSearchedRepositoriesWithCoroutines(query = query, sort = sort, order = order)
    }

ここでの違いはコールバックと再び出てきたsuspend修飾子です。
コルーチンを使わないコードではGETリクエストで得られるデータやGETリクエストが失敗時の結果をコールバックで受け取る必要があります
コルーチンを使うコードではまずGETリクエストを行うメソッドはsuspend修飾子が付いています。
これは前述したGitHubEndpointInterface.getSearchedRepositoriesByCoroutines()にsuspend修飾子が付いているため、それを呼び出すfetchSearchedRepositoriesByCoroutines()にもsuspend修飾子を付ける必要があるからです。
GETリクエストで得られるデータはメソッドの戻り値として取得することができます。
コルーチンを使ったコードのほうが圧倒的にシンプルに書けることがわかります。

取得したデータを画面に表示

コルーチンを使わないコード

    fun searchGitHubRepository(query: String) {
        fetchSearchedRepositories(
            query = query,
            sort = "stars",
            order = "desc",
            onResponse = {
                // 検索したGitHubリポジトリを画面に表示する
            },
            onFailure = {
                // リクエスト失敗時の処理を行う
            }
        )
    }

コルーチンを使ったコード

    val coroutineScope = CoroutineScope(context = Dispatchers.Main)
    fun searchGitHubRepositoryByCoroutines(query: String) {
        coroutineScope.launch {
            try {
                val gitHubRepositoriesData = fetchSearchedRepositoriesByCoroutines(
                    query = query,
                    sort = "stars",
                    order = "desc"
                )
                // 検索したGitHubリポジトリを画面に表示する
            } catch (e: HttpException) {
                // リクエスト失敗時の処理を行う
            }
        }
    }

ここでの違いはcoroutineScope.launchです。
CoroutineScope#launchメソッドでコルーチンが生成、実行されます。*1
コルーチンを使っているコードではその生成、実行されたコルーチンの中でfetchSearchedRepositoriesByCoroutinesを呼び出して結果を戻り値として取得することが出来ます。
つまり、非同期通信処理を通常のメソッド呼び出しのように「同期的な」書き方で実装できるため、見通しが良くなることがおわかりになると思います。

まとめ

Retrofitがコルーチンに対応したことで、Retrofitを使った非同期通信処理がとてもシンプルに書けるようになりました。
また、コルーチンに特性により、意図しないUIスレッドのブロックを防ぐことも出来ます。
そして、シンプルに書けることによって、よりアプリとして本質的な機能実装にフォーカスすることができるようになりました。
今後もコルーチンをうまく使って、Android版いこーよアプリをグロースさせていきます!

現在、コルーチンを使ってAndroid版いこーよを一緒にグロースしてくれる仲間を募集しています!
少しでもご興味ある方!以下のリンクからご応募よろしくおねがいします!

actindi.net www.wantedly.com

*1:ちなみにCoroutineScopeを実際のAndroidアプリで使用する際はAndroid Architecture ComponentsでのViewModelなどで生成し、Androidのライフサイクルにリンクするように使用するのが一番多いかと思います。https://developer.android.com/topic/libraries/architecture/coroutines

2019年現在のAndroid版いこーよの開発現場

ブログを見ている皆様、おはこんばんは。
いこーよAndroidアプリを担当しているhondaです。
現在弊社ではAndroidエンジニアを募集しています! www.wantedly.com

今回は「いこーよアプリの応募が気になるけど、何やってるかわからないな」という方向けに現在の仕事をしている環境や体制を簡単などについて書きたいと思います。

開発体制

いこーよアプリは以下のチーム構成で機能追加や改善を行っています。

  • Androidエンジニア:1名
  • iOS兼WebAPI(RoR):1名
  • ディレクター兼デザイナー:1名

通常は自分のロールをこなしていますがチームメンバーが少ないのでロール外のことであっても少しでも気になることは発言して解決していっています。
ディレクター兼デザイナーのメンバーがいこーよアプリ推進の旗振りをしていますが、追加したい機能や施策など思いついたことを毎週月曜に開催しているMTGで発言し、実際にそれをきっかけに実現した機能などもあります。
チームで一番大事にしているのはアプリを使っているユーザであるパパ、ママそして子供たちが楽しくお出かけができることです。
本当に楽しくおでかけができているのかを数値化するのは難しいですが、メンバー全員がパパであり、いこーよアプリのヘビーユーザなのでアプリを使った時に感じる不便さやこんな機能があればいいのにという思いを大事にしています。

開発言語

Android版いこーよは2016年8月から開発がスタートしましたが、当初からKotlinを使っています。

minSdkVersion

現在、23(Android6.0以上をサポート)です。 minSdkVersionはユーザ率が一定数下回ったら、積極的に上げていっています。

targetSdkVersion

現在、27(Android Oreo)です。 現在リニューアルを進めており、そちらは28で開発しています。

設計

方針としてはGuide to app architectureで推奨されている設計をベースにしています。
Guide to app architectureではViewModelがRepositoryを参照していますが、ViewModelが肥大しそうだったのでViewModelからビジネスロジックをUseCaseクラスとして分離したりしています。 リニューアルの開発ではこの設計で進めています。

使っているライブラリ

主に使っているライブラリをいくつかご紹介します。

Okhttp+Retrofit

いこーよアプリの処理でサーバとの通信処理が大部分を締めています。 通信処理でのセッション処理などはretrofitのInterceptorで行っています。 github.com github.com

Glide

スポットの写真やユーザが投稿した口コミの写真の表示に使用しています。 github.com

Permissions Dispatcher

位置情報取得時などで使っています。 github.com

Realm

データの性質によって、通常の保存方法で永続的に取り扱ったり、In Memoryでキャッシュとして保存したりと使い分けをしています。 RealmRecyclerViewAdapterでリストの更新処理が簡単に実装できます。 github.com

Koin

DI(依存性の注入)で使っています。 ViewModelに対応しているのでViewModelの使用がとても楽になりました。 github.com

Androidx

リニューアル開発からandroidxでの開発を進めています。

Kotlin Coroutines

以前は非同期通信でRxJavaを使っていました。
しかし、ほとんどSingleやCompletableを使っていてRxJavaの使い方としてベターではないなと感じていたこととKotlin Coroutines 1.0がリリースされたため、新規機能実装からKotlin Coroutinesを使っています。
既存のRxJavaのコードも順次Kotlin Coroutinesに切り替えていく予定です。

CI

CIはBitriseをDeployGateと連携させて使っています。
GitHubのPRで修正あるたびにビルドして、DeployGateを経由して検証端末で検証できるようにしています。
Android Lint、ktlintも同時に行っていて、Dangerを使ってlint結果をGitHubのPRに表示させています。 www.bitrise.io deploygate.com danger.systems

アナリティクス

Firebase AnalyticsとGoogle Analyticsを併用してログ収集を行っています。
追いかける数値ごとにspreadsheetで集計したり、DataStudioを使って定点観測を行ったりしています。 firebase.google.com analytics.google.com spreadsheets.google.com datastudio.google.com

クラッシュ検知

Firebase Crashlyticsを使っています。
クラッシュを検知した場合、slackに通知するようにしてクラッシュに気づける体制にしています。

firebase.google.com

まとめ

簡単ではありますが、いこーよアプリの開発現場のご紹介でした。
もし、これを見てご興味を持った方がいらっしゃったら幸いです。

現在Android版いこーよをどのように作っていけば迅速に価値を提供するにはどうしたらいいかを模索しながら進めています。
まだまだ、改善すべきところがたくさんあります。
新しいメンバーの方とは一緒に議論しながら今よりもより良いもの作っていきたいと思っています。
一緒に日本中の親子が楽しくおでかけできるサービスを一緒に作っていきましょう。
お待ちしております。

actindiでの3年間のモバイルアプリ開発を振り返る

これはactindi Advent Calendar 2018、1日目の記事です。
Androidアプリエンジニアのhondaです。
実は今日でactindi勤続3年になります。
3年間、いこーよモバイルアプリを担当してきました。
今回は軽く3年間どのようにモバイルアプリ開発をしてきたのか振り返ってみたいと思います。

actindiにジョインした理由

そもそもactindiにジョインした理由ですが、前職で一緒に仕事したことがあり、先にactindiにジョインしていたディレクター兼デザイナーのsuzukiから「一緒にいこーよのモバイルアプリを作らない?」というお誘いを頂いたことがきっかけでした。

いこーよは世の中のパパ、ママに向けて子供とのお出かけ先情報を提供する情報サイトです。  当時のいこーよはモバイルアプリはなく、いこーよからファンのもとへお出かけ先情報を提供していくためにモバイルアプリを作ろうというフェーズでした。
カジュアル面談や面接を受けて、以下の点でジョインを決めました。

  • actindiは当時から個人の自主性を重んじたり、ルールでがんじがらめにするのではなく、自分たちで「一番アウトプットを出す方法は何か?」を考え、実践することを大事にしていました。そこに強く共感することが出来ました。会社のホームページにも掲載されています。
    actindi.net 実際に入社してからもちゃんと文化として定着していて、リモート勤務や何時から何時まで働くか?ということが自分で判断して決めることができるので子持ちの身としてはとても助かりました。
  • いこーよは当時知らなかったのですが、1歳の娘がいたので、サービスにとても魅力を感じた。
  • 最初はiOSアプリを作るがAndroidアプリも作る予定なので今までのAndroid開発経験を活かせる。
  • お給料が上がった。

開発スタート

モバイルアプリ開発は私を誘ってくれたディレクター兼デザイナーと私の2名体制でスタートしました。
前述した通り、いこーよユーザはAndroidよりiOSが多いため、iOS開発から取り掛かりました。
最初の1ヶ月はモバイルアプリの目的を明確にしてペルソナの設定、盛り込む機能/盛り込まない機能の決定、UIの検討&調査に費やしました。
最初のGitHubリポジトリへのコミットは2015年12月28日でした。

技術選定も一任されていました。
方針としては凝ったことをせず、シンプルに作ることを心がけました。
アーキテクチャはMVCライク、データベースはRealm、ネットワーククライアントはAlamofireというような実績のあるものや使いやすいものを採用しました。
また、開発言語はSwiftを採用しました。Objective-CでiOSアプリを作っていたことがあったのでSwiftの書きやすさに感動したものです。
CIはサーバサイドがJenkinsを使っているのでモバイルアプリでもJenkinsを使おうか?とも考えましたが、環境構築に時間をかけたくなかったので、bitriseを使うことにしました。 qiita.com

iOS版開発にWebエンジニアがジョイン

年明けからWebエンジニアでiOSアプリ開発経験もあったnamikataがアプリチームに参画し、チーム3名体制になりました。 チームメンバー全員でのデザインの共有や議論はProttを使って行いました。
実装は私とnamikataで作業を分担して、お互いにコードレビューしながら進めました。
二人ともSwiftはほぼ初心者の状態でしたが、コードレビューで議論しながら開発を進めて行けたことはとても有意義でした。

Swiftに興味のあるWebエンジニアの方からもコードレビューをしていただける機会があったのでとてもありがたかったです。このようなエンジニアがプラットフォームを超えて、プロダクトに対してバリューを出せるのはactindiの強みだと思います。

また、アプリチーム全員が子持ちのお父さんだったこともあり、「親としてどういうアプリだと嬉しいか?」というようなユーザ目線でのサービス開発が出来ました。
actindiに入社する前はチーム全体でユーザ目線で開発するという経験が少なかったので大変でしたがとても楽しかったです。

iOS版いこーよリリース

そして、2016年7月にiOS版いこーよがリリースされました。
2018年11月現在、iOS版のダウンロード数は11万強に到達しました!

f:id:kou_hon:20181128154657p:plain 上の画像はバージョン1.0.0のUIです。
最初のころのUIは自宅や現在位置、お出かけ先でのスポットを直感的に見つけられるようにマップをメイン画面にしました。
また、行きたいスポットを見つけたら保存しやすくするためボタンを大きめにしたり、行ったスポットの口コミを書きやすくるために入力エリアをわかりやすくしたり、書きやすくするような配慮をUI設計で行いました。

Android版開発スタート

iOSがリリースした後、2016年8月からAndroid版の開発がスタートしました。
基本的にはiOSで実装したUIや機能をAndroid版に移植するような流れでした。
が、Android版ではiOS版をそのまま移植してしまうとAndroidアプリらしさがないものになってしまいます。
Androidユーザの混乱を少しでも軽減するためMaterial Designに則ったUIにするようにしました。 アーキテクチャはiOS版の開発で出てきた課題を鑑みつつ、最初はMVC+レイヤードアーキテクチャーのようなものにしました。
データベースはiOSで使い慣れたRealm、ネットワーククライアントはRetrofitを採用しました。 Android版でもシンプルな設計を目指しました。

言語は私の提案でKotlinにしました。Kotlinはいいぞ。 tech.actindi.net

CIはAndroid版でもbitriseにしました。 現在、iOS版とともにbitriseでプルリクごとにビルドして、deploygateで社内配信する構成になっています。

息子誕生

私事ですが、Android版開発中に息子が誕生しました。
この頃から自宅でのリモート勤務が多くなってきましたが、前述した通りチームメンバーがみんな子持ちなのでメンバーがお互いを考えながらフォローしあえました。
actindiはコアタイムの無いフレックス制を導入しています。子供の風邪などの急な対応がしやすくとてもありがたかったです。
こういう時、「周りに迷惑をかけて申し訳ない・・・」と思いがちですが全社的に「お互い様」の考え方が定着しているので、安心してリモート勤務や勤務時間を変更したりすることができました。
アウトプットも極端に低くなったということはなく、チームに貢献することができたと思います。

Android版いこーよリリース

2017年2月にAndroid版いこーよがリリースされました。
2018年11月現在、Android版もダウンロード数が11万強に到達しました!

f:id:kou_hon:20181130135556p:plain:w320

Android版はiOS版よりシンプルなUIになっていますが、スポットの検索性や口コミのユーザ体験はiOSと同じになるように実装しました。

アプリの成長

Android版リリース後、iOS版、Android版ともにFirebaseAnalytics、GoogleAnalyticsを使ったユーザ行動解析、アプリ内でのユーザアンケート、などなどいろいろな角度からいこーよユーザにとっての使いやすいアプリを目指し改修を進めていきました。
毎週月曜日は定例ミーティングを開き、上記のデータやKPI、KGIをチーム全体で確認しあい、アプリとしてどういった成長が必要なのかを話し合っていきました。
そのミーティングでエンジニア、ディレクターの垣根を超えて、施策の提案なども行い、チームメンバー全員が施策に対してポジティブな意見で肉付けをしていき、具体的にサービスづくりに落とし込んでいきました。
小さいチームならではの動き方ではあると思いますがactindiらしい良いチームの動きだなと感じています。

UIの変化

下の画像は2018年11月現在のAndroid版、iOS版のメイン画面です。リリース当時に比べてだいぶ変わりました。
Android版はマップベースですが、iOS版はさらに普段遣いができるアプリを目指すため、週末に子供とのお出かけ先をすぐに見つけられるように季節、イベント、口コミ、クーポンなど様々な角度からおすすめのスポット情報を提供できるUIにしました。

f:id:kou_hon:20181130135235p:plain:w290 f:id:kou_hon:20181130134850p:plain:w290

アーキテクチャー

アプリのアーキテクチャーも最初のころとは変化していきました。
namikataがブログでiOS版の実装について触れていますのでぜひ御覧ください。 tech.actindi.net tech.actindi.net tech.actindi.net

Android版はMVCからAndroid Architecture Componentsをベースにしたものに設計方針を変え、よりテスタブルで追加実装しやすい設計に変更しています。詳しいことは別のエントリでお伝えできればと思います。

不具合対応

運用初期のころは毎朝、Google Play ConsoleやFirebaseをチェックしていち早く不具合に対応するように心がけていました。
今ではクラッシュ通知をslackに通知するようにして、より早く不具合に対応できる体制にしています。段階的リリースの利用やリリースタイミングなどを臨機応変に変えていきながら、安全にサービスを提供できるように心がけています。

評価

これまでお伝えしたアプリの成長によって、いこーよユーザの方々からAndroid版、iOS版ともに4.4の評価をいただきました。ありがとうございます!

f:id:kou_hon:20181130135858p:plain:w290 f:id:kou_hon:20181130135948p:plain:w290

これからのモバイル版いこーよアプリ

上記の現状のiOS版のUIでいい結果を残せたのでそのUIをベースにした新UIを目下開発中です。
iOS版のUIでの反省点や改善点を踏まえつつ、Androidらしい新UIを目指していきます。
iOS版、Android版ともにこれからもいこーよユーザにとって使いやすいアプリを目指すために成長させていきます。ご期待下さい!

まとめ

actindiでの3年間のモバイルアプリ開発の中でチームメンバー同士助け合い、より良いアプリを作るために議論し、いこーよモバイルアプリを成長させてきました。
そんないこーよアプリチームではモバイルエンジニアを募集しています!

actindi.net

いこーよアプリチームに興味がある方、いこーよアプリに興味がある方、いこーよアプリチームメンバーと話してみたい方などなど、ご連絡お待ちしています!

KOINのバージョンが1.0になりました。

すっかり秋めいて来ましたね。
いこーよのAndroidアプリを担当しているhondaです。
前回、KOINの導入をお話をしました。 tech.actindi.net この時に使ったKOINのバージョンは0.9.3でした。
今回、KOINのバージョンが1.0にアップデートされたので(2018/10/26現在の最新バージョンは1.0.1)前回のサンプルコードでのKOIN0.9.3→1.0への以降をしながらKOIN1.0のお話をしたいと思います。

バージョンを変更

build.gradle(app)のKOINバージョンを0.9.3から1.0.1に変更します。
gradle syncすると以下のエラーが表示されます。

f:id:kou_hon:20181026130023p:plain

org.koin:koin-android-architectureがなくなって、Android Architecture ComponentsのViewModelを使用するためには「org.koin:koin-android-viewmodel」を導入する必要があります。
よって、前回の

def koin_version = '0.9.3'
// Koin for Kotlin
implementation "org.koin:koin-core:$koin_version"
// Koin for Android
implementation "org.koin:koin-android:$koin_version"
// Koin for Android Architecture Components
implementation "org.koin:koin-android-architecture:$koin_version"
// Koin for JUnit tests
testImplementation "org.koin:koin-test:$koin_version"


def koin_version = '1.0.1'
// Koin for Kotlin
implementation "org.koin:koin-core:$koin_version"
// Koin for Android
implementation "org.koin:koin-android:$koin_version"
// Koin Android ViewModel feature
implementation "org.koin:koin-android-viewmodel:$koin_version"
// Koin for JUnit tests
testImplementation "org.koin:koin-test:$koin_version"

となり、gradle syncが成功します。

ビルド

gradle syncが完了したのでビルドしてみます。
すると、ビルドエラーが発生します。

f:id:kou_hon:20181026130804p:plain

これはViewModelのinjectionを行うviewModelのパッケージが変更されたためです。

import org.koin.android.architecture.ext.viewModel

から

import org.koin.android.viewmodel.ext.android.viewModel

に変更します。

Deprecated対応

ビルドは通るようになって動くようにはなりました。
が、いくつかのメソッドがDeprecatedになっています。
Deprecatedの対応をしていきましょう。
カスタムApplicationクラスのコードを見るとこうなっています。

f:id:kou_hon:20181026131158p:plain

applicationContext関数はmodule関数に変更、beanメソッドはsingleメソッドに変更になり、よりわかりやすい命名になりました。

val myModule: Module = applicationContext {
    viewModel { MainViewModel(get()) }
    factory { SampleNumCounter(get()) as SampleNumCounterInterface }
    bean { SampleNumDataSource() as SampleNumDataSourceInterface }
}

から

val myModule: Module = module {
    viewModel { MainViewModel(get()) }
    factory { SampleNumCounter(get()) as SampleNumCounterInterface }
    single { SampleNumDataSource() as SampleNumDataSourceInterface }
}

に変更します。
以上で前回のサンプルコードを使ったKOIN0.9.3→1.0への変更対応になります。

AndroidX対応

サンプルコードではandroid.arch.lifecycleのViewModelを使っています。
androidx.lifecycleのViewModelを使う場合はorg.koin:koin-android-viewmodel
ではなくAndroidXに対応した
org.koin:koin-androidx-viewmodel
をbuild.gradleに書いておきます。
これでAndroidXでも安心です。

追加されたパッケージ

KOIN1.0から
koin-android-scope
というパッケージが追加されました。
これはActivity、Fragment、Serviceにインジェクトするオブジェクトをインジェクト先のライフサイクルに合わせて管理するためのパッケージのようです。
下記のコードは公式サイトより抜粋したものです。

基本的な使い方

公式ではMVPアーキテクチャーでのPresenterを例にあげています。

まず、Presenterクラスをmoduleに追加します。
scopeで"scope_id"というIDで追加追加します。

val androidModule = module {
    scope("scope_id") { Presenter() }
}

次にSampleActivityにPresenterクラスをインジェクトします。
getScopeでPresenterのインスタンスが新規で生成され、bindScopeでMyActivityにbindされます
これでPresenterインスタンスはMyActivityのライフサイクルが終了すると破棄されるようになります。

class MyActivity : AppCompatActivity() {

    val presenter : Presenter by inject()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        bindScope(getScope("scope_id"))
    }

※注意:getScopeだとインスタンスがないと例外を吐いてしまうのでgetOrCreateScopeを使ったほうが良いようです。

インスタンスをシェアする使い方

また、koin-android-scopeの用途としてActivity間などでオブジェクトをシェアする使い方も公式より紹介されています。
例としてユーザのセッション情報を保持しているUserSessionクラスをシェアするパターンがあります。
まずModuleにUserSessionクラスを追加します。

module {
    scope("session") { UserSession() }
}

次にユーザセッションを使用したいタイミングで以下のコードでスコープを開始します。

getKoin().createScope("session")

そして、以下のようにUserSessionを使用したいActivityなどでinjectします。
これでMyActivity1とMyActivity2でUserSessionのインスタンスをシェアすることが出来ます。

class MyActivity1 : AppCompatActivity() {
    val userSession : UserSession by inject()
}
class MyActivity2 : AppCompatActivity() {
    val userSession : UserSession by inject()
}

また、Kotlin DSLに沿ってシェアすることも出来ます。 例えば、前述したPresenterでUserSessionを使いたい場合、以下のようにModule定義します。

module {
    scope("session") { UserSession() }
    factory { Presenter(get())}
}

次に以下のようにPresenterクラスの生成時にuserSessionを渡すように書き直します。
これでシェアされているUserSessionのインスタンスがPresenterにinjectされます。

class Presenter(val userSession : UserSession)

最後にユーザセッションを終了させたいためなどにUserSessionのインスタンスを破棄する場合は以下のようにclose処理を呼び出します。

val session = getKoin().getScope("session")
session.close()

まとめ

いかがでしたでしょうか?
バージョン1.0からよりAndroidフレンドリーな使い方が出来るようになりました。
KOINはいいぞ。

最後に

We're Hiring Android Engineers! actindi.net

VSCodeでFlutter、最初の一歩

Web エンジニアの morishitaです。

Web アプリは Android も iOS を1つのソースで動かせるのに、 それぞれ作らないといけないなんてネイティブアプリ開発は大変だなぁといつも横目でちら見しております1

プッシュ通知やオフラインキャッシュなどアプリ開発の強い動機になっていた機能が Webアプリでも実装可能になってきており、AndroidではPWAアプリをホームスクリーンに追加すると 単なるショートカットではなくネイティブアプリ同様に扱われるようになってきています2

それでも、端末の機能をほぼ制限なく使え、カメラやセンサやNFCなどを利用した3ネットとリアルのよりリアル寄りのアプリケーションを開発できるネイティブアプリの自由さはいいなぁと思うことがあります。

Webアプリのようにネイティブアプリを開発できる、AndroidもiOSもワンコードですと謳う開発環境やフレームワークはこれまでもありましたが、スマホの2大プラットフォーマーの1つGoogleを中心に開発が進められているものが出てきました。それがFlutterです。

真打ち登場かもしれません。ということで、ちょっと触ってみました。

Flutterとは

Google が公開しているスマホ向けネイティブアプリの開発プラットフォームと言うかフレームワークで OSS として開発されています。

flutter.io

次の特徴を謳っています。

  • Fast development
    • ホットリロードでコードの変更がすぐに反映され、試しながら実装できる
    • カスタマイズ可能なUI部品
    • 1つのコードベースでAndroid、iOS のクロスプラットフォーム開発できる
  • Expressive and Flexible UI
    • マテリアルデザインとクパティーノ (iOS-flavor)のUIパーツ
    • 各プラットフォームに配慮したリッチな動きやスムーズなスクロール
  • Native Performance
    • iOSとAndroidの両方で完全なネイティブパフォーマンスを提供

また、実装言語にはDartを採用ししています。 サードパーティのパッケージを Flutter Packagesで探して利用することもできます。

このエントリではVSCodeでFlutterによるアプリ開発するためのセットアップからサンプルアプリの動作確認までやってみます。

セットアップ

以降はMac OS 上でのセットアップの流れです。

Android Studio と Xcode のインストール

あれ?VSCode で Flutter じゃないの? と思われるかもしれないですが、 最終的には Android /iOS 向けにビルドする必要があり、それぞれビルドするためのツールチェインが必要になります。そのためにインストールします。

まずはAndroidから。Android Studio をインストールしましょう。 Android に詳しい人は SDK だけでもいいかもしれないですが、そういう人はすでに Android Studio もインストールしているでしょう。多分。 詳しくない人は Android Studio をインストールしてしまうのが手っ取り早いし簡単です。

Android Studio のインストール  |  Android Developers

つづいて、iOS。 Xcode は App Store からインストールしましょう。

Xcode

Xcode

  • Apple
  • 開発ツール
  • 無料

Flutter SDK のインストール

お待ちかね、Flutterのインストールです。
インストールと言っても次のページから ZIP ファイルをダウンロードして展開するだけです。

flutter.io

展開したら、flutter というディレクトリができます。それを任意の場所に配置します。 私は ~/Libraryの下に置いています。

置き場所を決めたら次の様に flutter/bin にパスを通します。

export PATH=$HOME/Library/flutter/bin:$PATH

Flutter Doctorの実行

これで、flutter コマンドが使えるようになっているはずです。

ターミナルを開いて環境チェックコマンド flutter doctorを実行しましょう。

私の環境では次の様な結果が出力されました。

$ flutter doctor

  ╔════════════════════════════════════════════════════════════════════════════╗
  ║                 Welcome to Flutter! - https://flutter.io                   ║
  ║                                                                            ║
  ║ The Flutter tool anonymously reports feature usage statistics and crash    ║
  ║ reports to Google in order to help Google contribute improvements to       ║
  ║ Flutter over time.                                                         ║
  ║                                                                            ║
  ║ Read about data we send with crash reports:                                ║
  ║ https://github.com/flutter/flutter/wiki/Flutter-CLI-crash-reporting        ║
  ║                                                                            ║
  ║ See Google's privacy policy:                                               ║
  ║ https://www.google.com/intl/en/policies/privacy/                           ║
  ║                                                                            ║
  ║ Use "flutter config --no-analytics" to disable analytics and crash         ║
  ║ reporting.                                                                 ║
  ╚════════════════════════════════════════════════════════════════════════════╝

Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel beta, v0.7.3, on Mac OS X 10.12.6 16G1510, locale ja-JP)
[!] Android toolchain - develop for Android devices (Android SDK 28.0.1)
    ! Some Android licenses not accepted.  To resolve this, run: flutter doctor --android-licenses
[!] iOS toolchain - develop for iOS devices (Xcode 9.2)
    ✗ libimobiledevice and ideviceinstaller are not installed. To install, run:
        brew install --HEAD libimobiledevice
        brew install ideviceinstaller
    ✗ ios-deploy not installed. To install:
        brew install ios-deploy
    ✗ CocoaPods not installed.
        CocoaPods is used to retrieve the iOS platform side's plugin code that responds to your plugin usage on the Dart side.
        Without resolving iOS dependencies with CocoaPods, plugins will not work on iOS.
        For more info, see https://flutter.io/platform-plugins
      To install:
        brew install cocoapods
        pod setup
[✓] Android Studio (version 3.1)
[!] IntelliJ IDEA Ultimate Edition (version 2018.2.1)
    ✗ Flutter plugin not installed; this adds Flutter specific functionality.
    ✗ Dart plugin not installed; this adds Dart specific functionality.
[!] VS Code (version 1.27.2)
[!] Connected devices
    ! No devices available

! Doctor found issues in 5 categories.

PC 内の必要なモジュールがチェックされます。 [!]がついた項目が問題のある項目です。

これらを解消していきます。

Android 環境と iOS 環境の準備

上記の問題箇所に対処して、Android 環境と iOS 環境を整えます。

Android 環境

flutter doctorの結果の次の部分ですが、これは何やらライセンスに同意していないと注意されています。

[!] Android toolchain - develop for Android devices (Android SDK 28.0.1)
    ! Some Android licenses not accepted.  To resolve this, run: flutter doctor --android-licenses

指示に従って、flutter doctor --android-licensesを実行して同意すれば解決です。

次の項目ですが、IntelliJ についてはプラグインが足りないと言っているので、Flutter pluginDart pluginをインストールします。

[✓] Android Studio (version 3.1)
[!] IntelliJ IDEA Ultimate Edition (version 2018.2.1)
    ✗ Flutter plugin not installed; this adds Flutter specific functionality.
    ✗ Dart plugin not installed; this adds Dart specific functionality.

上記では Android Studio には問題ないですが、予めこれらのプラグインをインストールしていたためです。
インストールしていなけれは IntelliJ と同様のメッセージが出るので、それに従います。

iOS 環境

iOS toolchain についても解決方法が示されています。 それぞれ解決するための brewのパッケージインストールコマンドが示されているので、順に実行します。 上記のflutter doctorの結果の場合、次のコマンドを実行します。

$ brew install --HEAD libimobiledevice
$ brew install ideviceinstaller
$ brew install ios-deploy
$ brew install cocoapods
$ pod setup

brew install --HEAD libimobiledeviceが python2 に依存するらしく、python2 が無い環境だと自動的にpython2がインストールされます。 しかし、python2 にはpipが含まれないので、途中でコケます。
その場合、Installation — pip 18.0 documentationを参考にインストールして再実行すれば解決します。

また、cocoapodsのインストールには結構時間がかかるのでご注意を。

Flutter Doctorが解決方法も示してくれるのでそれに従っていけば割とスルッとできると思います。

VSCode と Flutter プラグインのインストールと設定

さて、いよいよ VSCode です。 Flutter Doctorの出力で次のように指摘されています。 特に解決方法が示されていませんが、単に Flutter プラグインがインストールされていないだけです。

[!] VS Code (version 1.27.2)

なので、次のプラグインをインストールしましょう。

marketplace.visualstudio.com

marketplace.visualstudio.com

インストールしたら、コマンドパレットからFlutter: New Projectを実行してみてください。

初めてだと次のようなダイアログが表示されると思います。
もし、プロジェクト名を聞いてきたら、飛ばして次に進んでいいです。

f:id:HeRo:20180920083921p:plain

Flutter SDK の場所がわからないと言っているだけなので、Locate SDKボタンをクリックして設定します4

設定したらコマンドパレットからFlutter: Run Flutter Doctorを実行します。
次のように結果がターミナルに出力されれれば OK です。

$ flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel beta, v0.8.2, on Mac OS X 10.12.6 16G1510, locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK 28.0.1)
[✓] iOS toolchain - develop for iOS devices (Xcode 9.2)
[✓] Android Studio (version 3.1)
[✓] IntelliJ IDEA Ultimate Edition (version 2018.2.1)
[✓] VS Code (version 1.27.2)
[!] Connected devices
    ! No devices available

Connected devicesは気にしなくていいです。
Flutterプラグインから接続できる端末がないと行っているだけです。エミュレータを起動すれば解決します。

Flutter Doctorの結果がきれいになったところでセットアップは終了です。

サンプルプロジェクトで動作確認

さて、セットアップが終わったところで、サンプルを実行してみましょう。

プロジェクトの初期化

再び Flutter: New Project を実行します。 今度はプロジェクト名を聞いてくるので、適当に入力して保存する場所を選択したら、ファイル群が生成されプロジェクトが初期化されます。

この時点で、簡単なアプリのコードが含まれています。

仮想デバイスの準備

VSCodeのステータスバーに No Devices と表示されているのでクリックするとコマンドパレットに選択肢が表示されます。多分、iOS Simulator は最初から選択できるのではと思います。選択するとエミュレータが起動します。

f:id:HeRo:20180920235957p:plain

Android のエミュレータはCreate Newを選択すると自動的に作成されて起動します。

なお、作成するにはエミュレータのシステムイメージが必要です。 SDK マネージャを開いて、Android8.1Google play Intel x86 Atom System Imageをインストールすると、作れるようになります。

f:id:HeRo:20180920084025p:plain

Create Newを実行すると次の仮想デバイスが作成されます。

f:id:HeRo:20180920084038p:plain

実行

エミュレータの準備ができたらサンプルアプリを動かしてみましょう。

VSCode のデバッグを開くと Flutter を選択できるようになっています。 選択して、実行するとプロジェクトがビルドされエミュレータで実行されます。

f:id:HeRo:20180920235031p:plain

サンプルアプリは右下のボタンをクリックすると数字がカウントアップするだけのシンプルなものです。

試しに、lib/main.dartを開いてFlutter Demo Home Pageの文字列を書き換えたり、UIの色を変更ししてみると、すぐにエミュレータで実行されているアプリに反映されます。
これが Flutter の特徴のホットリロードですね。素晴らしい。

f:id:HeRo:20180921000823g:plain

以上でサンプルアプリの動作確認も終了です。

おまけ

セットアップしてからこのエントリを書くまでの間に Flutter のバージョンが0.7.2から0.8.3上がってしまいました。 ちょうどいいのでアップデートも試してみました。

FLutter のバージョンアップは flutter upgradeコマンドで可能です。

flutter upgradeの実行の様子はちょっと長いので折りたたみます。 興味ある方は開いて見てください。

で、アップデート後、flutter --versionでバージョンを確認します。

$ flutter --version
Flutter 0.8.2 • channel beta • https://github.com/flutter/flutter.git
Framework • revision 5ab9e70727 (12 days ago) • 2018-09-07 12:33:05 -0700
Engine • revision 58a1894a1c
Tools • Dart 2.1.0-dev.3.1.flutter-760a9690c2

ちゃんと上がっていますね。 アップデート後、VSCodeからアプリを起動してみましたが問題ありません。 アップデートも簡単です。

まとめ

難しいハマりどころはなく、VSCodeでFlutter環境は作れます。アップデートも難しくありませんでした。

結局 Android Studioをインストールするので(しかも私は Intellijも使っていてAndroid Studioにも抵抗ないので) VSCodeを使うのは微妙な気もしないではないですが、選択肢があるのは良いことではないでしょうか。

Flutter自体がまだRelease Preview 2が出たところなので実戦投入には十分な検証が必要だと思うのですが、プロトタイピングしてみる程度なら十分使えそうです。 特にホットリロードは小気味よく、Web開発感覚でアプリ開発できそうです。

最後に

アクトインディではWebでもアプリでも、インフラでもいろいろやりたいエンジニアを募集しています。


  1. 両プラットフォームへの配慮は必要ですし、時には作り分けたりする必要もありますが。

  2. プッシュ通知欲しさに開発されたほぼWeb版と変わらない(しかも全面的にUIはwebviewだったりするような)ネイティブアプリはAndroidに於いては減ってくるだろうなぁと個人的に思っています。

  3. ブラウザでもだいぶサポートされてきていますが、安定性やパフォーマンス含め広く一般的に使える状況まではもう少し掛かりそうですね。

  4. ~/Libraryに SDK を置いている場合、ファイル選択ダイアログでこのディレクトリが表示されなくて選択できないかもしれません。ターミナルを開いてchflags nohidden ~/Libraryを実行すると表示されるようになります。

koin始めました。

f:id:kou_hon:20180821105751j:plain 暑中お見舞い申し上げます。
ビールが美味しい季節ですね。

いこーよのAndroidアプリを担当しているhondaです。
この夏、koinを始めました。

koinとは?

Kotlinのために作られたDI(依存性の注入)を実現するための軽量フレームワークです。 github.com

DI(依存性の注入)とは?

以下のようなクラスがあったとします。

public SampleNumCounter {
    private val sampleNumDataSource: SampleNumDataSource
[f:id:kou_hon:20180821105751j:plain]
    init {
        this.sampleNumDataSource = SampleNumDataSource()
    }

    fun countSampleNum() {
        this.sampleNumDataSource.sampleNum++
    }
}

SampleNumCounterはSampleNumDataSourceに依存しています。
SampleNumCounterにDI(依存性の注入)を適用すると下記のようになります。

public SampleNumCounter(private val sampleNumDataSource: SampleNumDataSource) {
    fun countSampleNum() {
        this.sampleNumDataSource.sampleNum++
    }
}

SampleNumCounterのコンストラクタで依存するSampleNumDataSourceを注入するようにしました。
これでSampleNumDataSourceをモックすることができ、SampleNumCounterがTestableになりました。

サンプルアプリを用意しました。

ボタンをクリックする事に数字をインクリメントして画面に表示するアプリを作りました。
まず画面となるActivityクラスです。

class MainActivity : AppCompatActivity() {
    private lateinit var mainViewModel: MainViewModel

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

        this.mainViewModel = ViewModelProviders
            .of(
                this,
                MainViewModelFactory(
                    sampleNumCounter = SampleNumCounter(
                        sampleNumDataSource = SampleNumDataSource()
                    )
                )
            )
            .get(MainViewModel::class.java)

        this.mainViewModel.sampleNumStringLiveData.observe(this, Observer {
            this.activityMainSampleTextView.text = it
        })

        this.activityMainSampleButton.setOnClickListener {
            this.mainViewModel.countSampleNum()
        }
    }
}

MainViewModelにパラメータを渡すために必要になるFactoryクラスです。

class MainViewModelFactory(
    private val sampleNumCounter: SampleNumCounterInterface
) : ViewModelProvider.Factory {

    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        return MainViewModel(sampleNumCounter = this.sampleNumCounter) as T
    }
}

ActivityクラスとSampleNumCounterクラスとの仲立ちをするViewModelクラスです。

class MainViewModel(val sampleNumCounter: SampleNumCounterInterface) : ViewModel() {
    val sampleNumStringLiveData = MutableLiveData<String>()

    fun countSampleNum() {
        this.sampleNumCounter.countSampleNum()
        this.sampleNumStringLiveData.value = this.sampleNumCounter.getSampleNumString()
    }
}

数字のインクリメントと出力を担当するSampleNumCounterクラスとそのinterfaceです。

interface SampleNumCounterInterface {
    fun countSampleNum()
    fun getSampleNumString(): String
}
class SampleNumCounter(
    private val sampleNumDataSource: SampleNumDataSourceInterface
) : SampleNumCounterInterface {
    override fun countSampleNum() {
        this.sampleNumDataSource.sampleNum++
    }

    override fun getSampleNumString(): String {
        return "SampleNum is ${this.sampleNumDataSource.sampleNum}"
    }
}

数字を保持するDataStoreクラスとそのinterfaceです。

interface SampleNumDataSourceInterface {
    var sampleNum: Int
}
class SampleNumDataSource: SampleNumDataSourceInterface {
    override var sampleNum: Int = 0
}

koinを始めてみましょう

以下の設定でkoinを使えるようになります。 build.gradle(project)にjcenter()が記述されているかを確認します。

buildscript {
    repositories {
        jcenter()
    }
}

build.gradle(module)のdependenciesに以下を追加します
※下記に記述されているkoin_versionは2018/08/06付現在の最新バージョンです。

def koin_version = '0.9.3'
// Koin for Kotlin
implementation "org.koin:koin-core:$koin_version"
// Koin for Android
implementation "org.koin:koin-android:$koin_version"
// Koin for Android Architecture Components
implementation "org.koin:koin-android-architecture:$koin_version"
// Koin for JUnit tests
testImplementation "org.koin:koin-test:$koin_version"

koinを使ってみましょう

まずkoinは依存定義をApplicationクラスを継承したクラス内で行います。

class KoinSamleApplication : Application() {
    override fun onCreate() {
        super.onCreate()

        // startKoin()でkoinを経由したDIが使用できるようになります。
        startKoin(this, listOf(myModule))
    }
}
// applicationContext関数を使って依存関係を定義します。
// factoryで依存定義されたSampleNumCounterは呼び出される度に新しいインスタントが生成されます。
// get()でSampleNumCounterが依存するクラスのインスタンスが挿入されます。
// beanで依存定義されたSampleNumDataSourceはシングルトンとして生成されます。
val myModule: Module = applicationContext {
    factory { SampleNumCounter(get()) }
    bean { SampleNumDataSource() as SampleNumDataSourceInterface }
}

SampleNumCounterクラスに依存しているMainViewModelFactoryクラスを書き直します。

class MainViewModelFactory : ViewModelProvider.Factory, KoinComponent {

    // 下記でkoinが変数sampleNumCounterにSampleNumCounterのインスタンスをインジェクトしてくれます
    // Activity以外でinjectを使う場合はKoinComponentを適用すると使用できます。
    private val sampleNumCounter by inject<SampleNumCounter>()

    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        return MainViewModel(sampleNumCounter = this.sampleNumCounter) as T
    }
}

koinはViewModelにも対応しています。 まずKoinSamleApplicationクラスの依存定義を書き直します。

class KoinSamleApplication : Application() {
    override fun onCreate() {
        super.onCreate()

        startKoin(this, listOf(myModule))
    }
}

val myModule: Module = applicationContext {
    // viewModelでMainViewModelの依存定義を追加します。
    viewModel { MainViewModel(get()) }
    factory { SampleNumCounter(get()) as SampleNumCounterInterface }
    bean { SampleNumDataSource() as SampleNumDataSourceInterface }
}

次にMainActivityを書き直します。

class MainActivity : AppCompatActivity() {
    // 下記で変数mainViewModelにMainViewModelのインスタンスがインジェクトされます。
    // これによりonCreateで記述してたViewModelProvidersの処理とMainViewModelFactoryクラスが必要なくなります。
    private val mainViewModel by viewModel<MainViewModel>()

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

        this.mainViewModel.sampleNumStringLiveData.observe(this, Observer {
            this.activityMainSampleTextView.text = it
        })

        this.activityMainSampleButton.setOnClickListener {
            this.mainViewModel.countSampleNum()
        }
    }
}

まとめ

いかがでしたでしょうか?
koinを使うことによって、DI定義がすっきりしました。
Android Architecture ComponentのViewModelの取り扱いも楽になりました。
koinはKotlinで書かれているのでKotlinとの相性も抜群です。

今回使用したAndroid Architecture ComponentのViewModelはandroid.arch.lifecycleのものです。
androidxは現時点では対応していないようです。
ですが、バージョン1.0.0のブランチを見るとAndroidXに対応する記述があるので一安心です。
github.com

最後に

We're Hiring Android Engineers! actindi.net

Kotlin Android Extensionsを使ってみよーよ

この記事はactindi Advent Calendar 2017の15日目の記事です。

Androidエンジニアのhondaです。去年のアドベントカレンダーではKotlinのことを書きましたが今年もKotlinについて書きます。よろしくお願いします。

現在、Android版いこーよではKotlinを使っています。100%Kotlinです。 弊社でKotlinアプリを作ってみたい方はこちらとかこちらなどで応募お願いします。お願いします!

前置きはこのくらいにして、Android版いこーよではKotlin Android Extensionsを使っています。 なかなか、プロダクトでのKotlin Android Extensionsの採用を聞かないので、今回はKotlin Android Extensionsの素晴らしさをお伝えしたいと思います。

Kotlin Android Extensionsとはなにか?

Kotlin Android ExtensionsはKotlinの開発元であるjetbrains社が開発したAndroid開発を支援するためのプラグインです。 このプラグインを使うことによって、Androidアプリエンジニアの方ならおなじみの「findViewById」を使わずにコードからUIの要素を参照することが出来るようになります。

Kotlin Android Extensionsを導入してみよう!

Android Studio 3.0以上で新規プロジェクトを作成するとデフォルトで Kotlin Android Extensions使えるようになっています。素晴らしいですね。 既存プロジェクトの場合はbuild.gradle (Module: app)に以下を追加するだけです。

apply plugin: 'kotlin-android-extensions'

簡単ですね。

Kotlin Android Extensionsでコードを書いてみよう!

・Activity

簡単なアプリを作ってみましょう。 TextViewとButtonが配置された画面でTextViewには”Hello World!”と表示されています。 ButtonをタップするとTextViewの”Hello World!”の表示が”ハローワールド!”に切り替わります。 コードは以下の通りです。

Activityのレイアウトファイル
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="sample.samplekotlinandroidextensions.MainActivity">

    <TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content"
              android:text="Hello World!"
              app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent"
              app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" />
    <Button android:id="@+id/button" android:layout_width="wrap_content"
            android:layout_height="wrap_content" android:layout_marginTop="8dp"
            android:text="ハローワールド!" app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/textView" />

</android.support.constraint.ConstraintLayout>
Activityのソース
class MainActivity : AppCompatActivity() {

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

        this.button.setOnClickListener {
            this.textView.text = "ハローワールド!"
        }
    }
}

これでfindViewByIdを使わずにtextViewに対して文言を設定することが出来ます。 レイアウトファイルでIDに設定した名前でコード上から参照出来ます。

・Fragment

次にFragmentで試してみましょう。 先程のActivityのSample同様にButtonをタップするとTextViewの内容が変わるものを作成します。

Fragmentのレイアウトファイル
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
                                             xmlns:app="http://schemas.android.com/apk/res-auto"
                                             xmlns:tools="http://schemas.android.com/tools"
                                             android:id="@+id/frameLayout"
                                             android:layout_width="match_parent" android:layout_height="match_parent"
                                             tools:context="sample.samplekotlinandroidextensions.SampleFragment">

    <TextView android:id="@+id/helloFragmentTextView" android:layout_width="wrap_content"
              android:layout_height="wrap_content" android:text="@string/hello_fragment"
              app:layout_constraintBottom_toBottomOf="parent"
              app:layout_constraintEnd_toEndOf="parent"
              app:layout_constraintStart_toStartOf="parent"
              app:layout_constraintTop_toTopOf="parent" />
    <Button android:id="@+id/helloFragmentButton" android:layout_width="wrap_content"
            android:layout_height="wrap_content" android:layout_marginEnd="8dp"
            android:layout_marginStart="8dp" android:layout_marginTop="8dp"
            android:text="@string/hello_fragment" app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/helloFragmentTextView" />

</android.support.constraint.ConstraintLayout>
Fragmentのソース
class SampleFragment : Fragment() {

    override fun onCreateView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?): View? {
        val view = inflater.inflate(R.layout.fragment_sample, container, false)
        view.helloFragmentButton.setOnClickListener {
            view.helloFragmentTextView.text = "ハロー フラグメント"
        }
        return view
    }
}

ここで注意しなければならないのがonCreateViewでのUIの要素の参照方法です。 上記のサンプルの用にonCreateViewではinflateしたviewに対してUIの参照を行わないといけません。

override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?): View? {
    val view = inflater.inflate(R.layout.fragment_sample, container, false)
    this.helloFragmentButton.setOnClickListener {
        this.helloFragmentTextView.text = "ハロー フラグメント"
    }
    return view
}

上記の様にUIに参照しようとするとNullPointerExceptionが発生します。 onCreateView以外では下記の様にUIに参照出来ます。

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    this.helloFragmentButton.setOnClickListener {
        this.helloFragmentTextView.text = "ハロー フラグメント"
    }
}

・RecyclerView

次にRecyclerViewで試してみましょう

RecyclerViewに表示するItemのレイアウトファイル
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView android:id="@+id/sampleRecyclerItemTextView" android:layout_width="wrap_content"
              android:layout_height="wrap_content" android:layout_marginBottom="8dp"
              android:layout_marginEnd="8dp" android:layout_marginStart="8dp"
              android:layout_marginTop="8dp"
              tools:text="TextView"
              app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent"
              app:layout_constraintHorizontal_bias="0.0" app:layout_constraintStart_toStartOf="parent"
              app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
RecycleViewのAdapterコード
class SampleRecyclerViewAdapter(private val itemDataList: List<String>) :
        RecyclerView.Adapter<SampleRecyclerViewAdapter.ViewHolder>() {
    override fun getItemCount(): Int = itemDataList.count()

    override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ViewHolder? {
        parent ?: return null
        return ViewHolder(LayoutInflater.from(parent.context).inflate(
                R.layout.view_sample_recycler_item, parent, false))
    }

    override fun onBindViewHolder(holder: ViewHolder?, position: Int) {
        holder?.itemView?.sampleRecyclerItemTextView?.text = itemDataList[position]
    }

    inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        init {
            itemView.setOnClickListener {
                Toast.makeText(
                        itemView.context,
                        itemView.sampleRecyclerItemTextView.text,
                        Toast.LENGTH_SHORT).show()
            }
        }
    }
}

RecycleViewのAdapterがListに入った文言をItemのレイアウトファイルを使ってリスト表示しています。 レイアウトのsampleRecyclerItemTextViewへはViewHolder内でitemView経由で参照します。

Kotlin Android Extensionsの挙動を理解しよう!

さて、とても便利なKotlin Android Extensionsですが、内部でどんなことをしているのでしょうか? Activityのサンプルコードを逆コンパイルしてJavaコードに変換して、どんなことをしているのか理解していきましょう。

public final class MainActivity extends AppCompatActivity
{

    public MainActivity()
    {
    }

    public void _$_clearFindViewByIdCache()
    {
        if(_$_findViewCache != null)
            _$_findViewCache.clear();
    }

    public View _$_findCachedViewById(int i)
    {
        if(_$_findViewCache == null)
            _$_findViewCache = new HashMap();
        View view1 = (View)_$_findViewCache.get(Integer.valueOf(i));
        View view = view1;
        if(view1 == null)
        {
            view = findViewById(i);
            _$_findViewCache.put(Integer.valueOf(i), view);
        }
        return view;
    }

    protected void onCreate(Bundle bundle)
    {
        super.onCreate(bundle);
        setContentView(0x7f09001b);
        ((Button)_$_findCachedViewById(R.id.button)).setOnClickListener((android.view.View.OnClickListener)new android.view.View.OnClickListener(this) {

            public final void onClick(View view)
            {
                view = (TextView)_$_findCachedViewById(R.id.textView);
                Intrinsics.checkExpressionValueIsNotNull(view, "this.textView");
                view.setText((CharSequence)"\u30CF\u30ED\u30FC\u30EF\u30FC\u30EB\u30C9\uFF01");
            }

            final MainActivity this$0;


            {
                this$0 = mainactivity;
                super();
            }
        }
);
    }

    private HashMap _$_findViewCache;
}

_$_findViewCache というハッシュマップのプロパティを用意して、Viewを管理しています。 参照時に_$_findViewCacheにViewインスタンスが無ければ、findeViewByIdをしてViewインスタンスを格納して参照し、すでに_$_findViewCacheにViewインスタンスがあれば、それを参照するという処理だということがわかりました。

注意点

Kotlin Android Extensions、とても便利ですが気をつけなければならない部分があります。

・誤って他の画面のUIへの参照するコードが書けてしまう。ビルドも通る。

Kotlin Android ExtensionsはData Bindingのようにコード生成しているのではなく、UIの要素をハッシュマップで管理しています。 誤って他の画面の要素を参照するコードを書いてしまっても、ビルドは正常に通ってしまうので気づけません。 実行時に_$_findCachedViewByIdでNullが返却されるため、NullPointerExceptionでクラッシュして、気づくことになります。 レイアウトファイルの要素のIDは他の画面の要素のIDが被ったり、どこの画面の要素なのかわからなくなることを防ぐために命名規則を設けておくと良いかもしれません。 例えば、「sampleActivityTextView」というような感じです。

・設計と相談

すでにデータバインディングを使っているような既存のプロジェクトではKotlin Android Extensionsを使っていくとなると設計を変えざるを得ないと思います。 使用する場合は設計と相談の上、ご使用ください。

まとめ

  • Kotlin Android Extensionsはjetbrains社製のAndroid開発支援プラグイン
  • Kotlin Android Extensionsは導入が簡単!
  • Kotlin Android Extensionsを使うとfindViewByIdを使わずに簡潔にUIを参照するコードが書けるようになる!
  • FragmentやRecyclerViewのAdapterなどでも使える!けど、Activityでのやり方とは違うので注意。
  • Kotlin Android ExtensionsはUIの要素をViewインスタンスとしてハッシュマップで管理している。
  • 間違って他の画面のUIを参照するとクラッシュするので命名規則などで回避。
  • 使う時は設計と相談。

DroidKaigi2017に行ってきました。(2日目)

こんにちは、hondaです。

DroidKaigi2017に行ってきた(1日目)

こちらではDroidKaigi2日目で聴講したセッションを簡単にまとめたいと思います。

2日目に見てきたセッション

ウェルカムトーク

DroidKaigi参加者には事前にアンケートを取っていてその結果がスライドで紹介されていました。 気になった項目を抜粋してみました。

・参加者の年齢

年齢 割合
20代  46%
30代  45%
40代  6%

私は30代の部類です。今後40代も増えてくるのでしょうか。

・事業分類

事業分類 割合
自社サービス  68.2%
受託開発  23.5%

受託開発ではなかな参加出来ないのでしょうか。弊社は自社サービスですが、参加させてもらえた事に感謝ですね。

・開発規模

開発規模 割合
3〜5名  36%
2名  25.8%
1名  24.7%
6〜9名  7.9%

1名〜2名での開発規模が5割行ってますね。職場でアプリエンジニアが少ないとこういったカンファレンスが本当にありがたいです。アプリ開発で6〜9名って逆に開発を回すのが大変そうなイメージなのですがどうやって回していっているのでしょう。

minSdkVersion

minSdkVersion 割合
4.1〜4.3  36%
4.0.x  33.7%
4.4.x  15.2%
5.0.x  12.4%

6,7はさすがにないですねー

DroidKaigi公式アプリ

  • 296PRs
  • 126issues
  • 68contributors

@konifarさん、ありがとうございました!アプリ助かりました。

Android ORMの選び方

Ormaが個人的に気になっていたので聴講しました。 なぜデータをローカルに保存するのか?どうやって保存するのか?そして、複数のORM(ActivitAndroid,greenDAO,Requery,SQLBrite & SQLDelight,Realm,Orma)を比較して、良し悪しを浮き彫りにするような内容でした。 プロダクトでRealmを使っている分、若干色眼鏡気味ではありますがはやり今のところはRealmなのかなと。 ただ、マイグレーションの煩雑さやequalToでの型安全の指摘は納得できるものでした。 Ormaは型安全に関しては解決されていて、Realmに比べて実装の軽さを感じました。 サービスや作るアプリの性質次第では採用してみたいですね。

未熟なチーム開発

弊社でもアプリチームは1年ちょっとでチームらしくなってきました。 さらに成熟させるヒントなどありそうだと思ったので聴講しました。 秩序づくりのところは大いに活かせそうでした。秩序とそのドキュメント化は面倒ですが大事ですね。 あとはよりテストブルな実装は大事。 実業務を進めながら新人教育をするのはなかなか難しい。。。

Kotlin + RxJava + Dagger2 + Orma + Retrofit で作るAndroidアプリ

より実践的な内容でいこーよアプリに活かせそうだったので聴講しました。 早口ではありましたが、DroidKaigiのアプリのコントリビュータをgithubから取得して表示するサンプルアプリを使っての説明でした。 とりあえず、RxJavaあたりから導入してみて、楽していきたい気持ちになりました。 Dagger2はテストとかも絡みそうなのでもうちょっと後から入れていこうかと。

4年続くアプリにおけるチーム開発

未熟なチーム開発と同じ理由で聴講しました。 UIファースト(UI駆動?)な開発スタイルでのバージョンごとでのアーキテクチャーの移り変わりと規約やドキュメントの整備やユーザインタビューでのアプリの質を高める話でした。 いこーよでもアーキテクチャーの見直しや規約、ドキュメントの整備は今まさに行っていることでとても参考になりました。 あと、目的別での部署を超えたチーム編成はとても興味深いものでした。

コマンドなしでぼくはAndroid開発できない話

コマンドで楽したい!という動機で聴講しました。

特に気に入ったコマンドを紹介します。

端末のワイヤレス接続

adb shell ip addr show wlan0 | grep 'inet ' | cut -d' ' -f6|cut -d/ -f1  ←端末のローカルIPを取り出す
adb tcpip 5555
adb connect "取り出したローカルIP":5555

条件としては端末とPCは同じWifi環境下にいる必要があります。 端末をUSBケーブルでPCとつなげて、上記コマンドを実行するとUSBケーブルが外れた状態でもコマンドを送ることが出来ます。

キーイベント送信

adb shell input keyevent KEYCODE_POWER ←電源キーを押す
adb shell input keyevent KEYCODE_SLEEP ←スクリーンをOFF

端末再起動

adb shell reboot

Activity情報取得

adb shell dumpsys activity top

今表示しているActivitiyのクラスがわかる 今表示しているActivityのレイアウト構造がわかる!このアプリどういうレイアウトしてるんだろう?って思った時はレイアウト構造を丸裸に出来ます。

/data/data/packageName配管のファイルを見たい

adb shell run-as packageName

アプリの設定画面が見たい

shell am start -a android.settings.APPLICATION_DETAILS_SETTINGS -d package:"アプリのパッケージ名"

UIをカスタムしてあって、設定アプリが探しにくい端末だと使えます。

上記以外の使えるコマンドがここにまとめれてるそうです。

サンプルアプリをサクッと試したい

dryrun adbのコマンドではないですが、Githubに公開しているアプリをサクッと試すときにすごく重宝するツールです。

dryrun "GitHubリポジトリURL"

エンジニアが武器にするMaterial Design

まだMaterialDesignをものに出来ていないところがあるのでものにしたいために聴講しました。 ナノハさんではスピードも機能の1つと捉えてアニメーションもユーザに対するコンテンツの提供スピードを損ねないように実践されていて、とても興味深かったです。 その中でもアニメーションの改善で離脱率が半分に削減した話は特に興味深く登壇後やアフターパーティーでお話しを聞いてslackで弊社内のアプリチームに展開したところとても反響が大きかったです。 いこーよでも上手にMaterialDesignを活用して、ユーザに心地よくアプリを使っていただけるように改善していきたい。しよう。

テスト0から目指すクラッシュフリー率99%

テスト以外でも品質担保や不具合改善などの知見が得られそうだと思い聴講しました。 すごく当たり前のことですが、テストうんぬんの前のテスタブルにコードを書くというのが一番大切なんだなーと改めて感じました。 Activityからモデルを分離して、Interfaceを定義し、コンストラクタで依存するインスタンスを渡すようにするだけどすごくテスタブルでコードの見通しがよくなります。 あと、Fragment実装で一度は目にしたことがあるIllegalStateExceptionで悩んでる方はvultureを使うと幸せになれるようです。 最後にこの言葉は忘れないようにしよう。

まとめ

2日間ずっとお話しを聞き続けるっていうのは結構疲れました。 が、知見だらけの2日間はすごくエクサイティングで楽しかったです。 ずっと言い続けてますが、参加させてもらえたことに本当に感謝です。 この思いと知見をチームメンバーに伝えつつ、より良いプロダクトを作りたいですねー。 来年もぜひともDroidKaigiに参加したい! できれば、チームメンバー全員で参加して、お互い見れなかったセッションを現地で共有しあいたい!

以上、DroidKaigi2017に参加した感想でした。

DroidKaigi2017に行ってきました。(1日目)

こんにちは、hondaです。

2017/03/09〜2017/03/10に開催されたDroidKaigi2017に参加してきました。 まずはチケット代8000円を負担してくれた会社と通常業務がある中、参加することにLGTMを出してくれた会社のメンバー、そして、帰宅が遅くなっても文句も言わず子供の面倒を見てくれた妻に感謝です。本当にありがとうございました! あとDroidKaigiを運営している有志の方々にも感謝しかありません。

今回ぼっち参加でしたが、登壇者の方に質問してもちゃんと話くれましたし、DroidKaigiの協賛企業のブースは面白かったし、おやつは充実してたし、ごはんは美味しかったし、とても楽しめました。

今回は見てきたSessionなどの感想をざっくり書いていきたいと思います。

アプリについて

DroidKaigi2017アプリ DroidKaigiアプリリポジトリ 毎朝、リポジトリをチェックしてチャンスがあればPRを出してみようかと思ってたのですが、コントリビュータの方々のPRのスピードに圧倒されるばかり参加できなかったのが少し残念でした。 内部実装を眺めては「ここを参考にしたらいこーよアプリの改善で使えるかも」と妄想していました。 DroidKaigi当日はこのアプリのおかげで見たかったセッションはすべて見ることが出来ました。 アプリ開発に当たられた@konifarさん、そしてコントリビュータの方々には感謝しかありません。

1日目に見てきたセッション

ウェルカムトーク

今回は800人近くのエンジニアの方が来場したそうです。 すごーい!

How to apply DDD to Android Application Development

特定のアーキテクチャーではなく、アーキテクチャーを採用するまでの考え方を知りたくて聴講しました。 わかる! ドメイン駆動設計~もちこちゃんの大冒険~は読んだことあるのですが、とても分かりやすい説明でした。 ドメインとモデルを明確に定義し、それを意識した考え方とそれにあった設計を実装していくことが大事!

リリース自動化と効率のよいリリースフローを求めて

弊社アプリチームはまだ人数も少なく、少しでも効率よく開発を進めることが重要なので、そのヒントを掴みたいので聴講しました。 現状、CIを入れて内部の動作確認などで自動化は進めていて、製品版リリースでは自動化はまだなのでその辺のTIPSが聞けたり、裏付けが出来てとても良かったです。 ただ、サービスの規模などの状況によっては自動化の「やりすぎ」も起こり得るのでそのへんは注意が必要だなと感じました。

Androidリアルタイム通信アプリ作成Tips

弊社いこーよアプリでもリアルタイムの技術はのちのち必要な場面出てきそうだなと感じているので聴講しました。 リアルタイム通信を実現するライブラリの紹介でした。 200枚のスライドだっため、説明がとても早口で聞いてるが少し大変でした。 結果としてはrealm mobile platformはすごいなの一言でした。 弊社アプリでもRealmを使っているので状況によってはrealm mobile platformをうまく使ってサービスを大きくしていきたいですね。

Data Bindingで開発を気持ちよくしよう

弊社アプリでは100%Kotlinで実装されているのですが、KotlinとData Bindingは相性が悪いという情報を聞いていたので採用を控えていました。 本セッションでその辺が触れられなかったので残念でしたが、たしかにActivityの見通しはよくなったのでタイミングを見て採用したいですね。

実践アニメーション

現状のいこーよアプリでは積極的にアニメーションを採用していません。 今後はユーザの使い心地のためにMaterialDesignの積極的に採用すると思い聴講しました。 実践的な説明もあり、聴講してよかったです。

オフラインファーストなアプリケーション開発

いこーよアプリではAndroidiOS両方でRealmを採用しています。同じ時間帯で他のセッションも気になったのですが開発メンバーの強い要望もあり、こちらを聴講しました。 Androidリアルタイム通信アプリ作成Tipsでも出ていましたが、realm mobile platformはやはり素晴らしいテクノロジーですね。 他の聴講者も興味が強かったようで、聞いてきたセッションの中で一番質問数が多かった気がします。

How to remodel current testing environment

テストコードはアプリの動作保証をする上で重要ですが、アプリやサービスの規模、リソースの関係でなかなかガッツリと取り組めていないのが現状です。 なのでその辺をうまくバランスとりながら、保証を担保していく方法がないかと思い聴講しました。 テストをやる理由からBDDやTDDでのテスト方針的なことがとてもロジカルに語られていました。 当たり前なことではあると思いますが、全くテストがない状態から部分的にテストコードを拡充させていく際もテスト方針や設計は明確にして進めるべきだなと感じました。

Android Bikeを作ろう

個人的に今年はIOTやってみたいなーと思い聴講しました。 実際の実装した話を元にしたスライドだったので具体的な話が聞けて良かったです。 まずはうちにあるものでIOTを実感してみようと思わせてくれるセッションでした。

以上、DroidKaigi1日目の感想でした。