DroidKaigi 2018 スポンサー担当と撮影担当の舞台裏と心境

2018/02/08-09 に開催された DroidKaigi 2018 に、スタッフとして参加していた。

役割は、スポンサー担当と撮影担当。その舞台裏を記していこうと思う。

droidkaigi.jp

スポンサー担当

時はさかのぼり 2017/05 。

DroidKaigi 2018 の第 1 回目 MTG が開催される。前回 DroidKaigi 2017 開催日から 2 ヶ月後のことだった。

前回と同様、今回の会場はベルサール新宿グランド・ 5F コンファレンスセンターを貸し切って開催することになった。

そして前回と違うのは、 1F にあるホールの利用も計画に入っていること。ここでパーティやらオープニングを催す予定だ。

f:id:sho5nn:20180211133156j:plain

1F ホールの利用に加えて、予定参加人数も前回より更に増した 1000 人として計画を進めることになり、それ即ち、全体予算も比例して増すことを表している。

DroidKaigi の準備に必要なお金の収入源は、一般参加者に購入していただくチケットの代金と、企業からいただく協賛金、この2つ。

様々な企業からの協賛を募るためにはまず、協賛金総額とスポンサープランの内容を練るところから始める必要がある。

画して、代表の mhidaka 氏 を中心に、今回登壇もしていた kmats 氏 と僕の 3 人が走り始めるのだった。

企業訪問

スポンサープランが fix した後、公式サイトや twitter 公式アカウントで、スポンサー募集の開始を告知する。 2017/07 のことだった。

その日から、企業からのお問い合わせに対応していくことになる。

どのようにスポンサープランが fix していったのか?については、内部情報のため明かすことはできない。

f:id:sho5nn:20180211143748j:plain

基本的にスタッフは、ボランティアとして活動している。

スタッフの多くはソフトウェアエンジニアであり、普段はプログラムのコードを書いてお賃金を貰っている生き物だ。僕も例に漏れない。

営業や広報の業務などと違い、頻繁にメールの文章を書くことも外回りをすることもない。痔に気をつけつつイスに座りながらコードと対峙する時間が、圧倒的に多い。

それとは正反対に、企業からのお問い合わせにはメールでやりとりし、初めて DroidKaigi に協賛していただく企業にはなるべく、ご挨拶に伺った。表はソフトウェアエンジニアの顔、裏は営業の顔、どこぞの特命係長かと思わす形振りと化した。

それらは、募集を開始した 2017/07 から 2018/01 頃まで継続的に続いた。

進捗管理

複数用意していたスポンサープランは順調に埋まっていき、DroidKaigi 2018 のスポンサー企業は最終的に 40 社ほどになった。

裏を返せば、長いライフサイクルの中で、非同期でメッセージングが行われる 40 もの singleton object を管理していたということになる。

いかように管理していたか?それは、まごころを込めた進捗管理表( Google スプレッドシート)によるものだった。

管理たいへんだよね問題は、前回・前々回から上がってはいたけど、最善最適な解決方法は未だ見つかってはいない。正直、無いと思っている。

少しでも楽にできないかと kintone のような業務アプリを触ってみたが、オーバーキル感が否めなかったのと、 Google スプレッドシートはスタッフ全員がある程度は使い慣れてることもあり、本格的に運用はしなかった。が、この選択は間違っていなかったと思う。新しいものを導入するには何かとリスクが付き物だし。

続ける理由

僕は、 DroidKaigi 2016, 2017, 2018 の 3 回、スタッフとして参加している。全てスポンサー担当。2016 のときに hotchemi 氏 からお誘いを受け、空いてる担当に成り行きで割り振られたのがきっかけだった。

僕個人として在り続けたいエンジニア像が恥ずかしながらあり、エンジニアのためのカンファレンスのスタッフ、という "縁の下の力持ち" 的な存在は、それと近しいものを感じている。

道中、スタッフを続けることの厳しさを感じることもあったが、当日を迎えると、やっててよかったと噛みしめることが多い。企業ブースで一般参加者と企業関係者が会話しているシーンを見ると、保証はできないけど何かのキッカケにでもなったら嬉しい、とか感じていた。

その感じた気持ちは、 DroidKaigi の開催趣旨からの影響もある。

DroidKaigi はエンジニアが主役の Android カンファレンスです。

Android 技術情報の共有とコミュニケーションを目的に 2018年2月8日(木)、9日(金) の 2 日間開催します。

セッションを聴講する、オフィスアワーで登壇者と直接会話をする、懇親会で今まで話したことがなかった人と会話して仲良くなる、どれもこれも、開催趣旨の現れとなったシーンだ。

参加者全員が少しでも何かを得られていれば、やっててよかったと自分もまた噛み締められる。無理しない範囲で今後も、エンジニアのためのカンファレンスのスタッフは続けていきたいと、スタッフ打ち上げの帰りに再確認した次第だった。

そして、開催趣旨の現れとなったシーンは写真におさめたくなる傾向が誰にでもある。僕も例に漏れず、スポンサー担当の傍ら、撮影担当としてカメラを片手に会場を回っていた。

ここで、撮影担当の舞台裏に話を移そう。

撮影担当

スポンサー担当の項目が少し硬い文章だったので、ここからはゆるい感じで書きなぐろうと思う。

DroidKaigi の撮影担当は、セッション動画の撮影担当と、カメラパシャパシャする担当の2つに分かれる。僕はカメラパシャパシャする担当。

舞台裏と言ってもそんなに難しいことはしてなかった。

  1. 撮影したい人を募る
  2. 撮っておきたいものリストを作る
  3. シフトを組む
  4. 当日がんばって撮影する

撮影したい人を募る

スタッフも楽しくやるぞってノリなので、撮りたい人が撮ろう!という感じ。

f:id:sho5nn:20180211193157p:plain

撮っておきたいものリストを作る

2016, 2017 で「あれも撮っておきたかった」などなど KPT して反省点は見えていたので、今回 2018 では、あらかじめ「これは最低限撮っておきたいよね」リストをまず作成した。

f:id:sho5nn:20180211193635p:plain

シフトを組む

各々がよしなに撮影すると、撮っておきたいものリストから漏れたものが出てくると思ったので、タイムテーブルに合わせて担当を振り分けた。

f:id:sho5nn:20180211194042p:plain

当日がんばって撮影する

振り返り

  • 撮っておきたいものリストを作ったのは良かった。先に QA テストケースを書いて仕様漏れないかチェックする感じ。前回までの KPT が生きた気がする。
  • シフトは組んだものの、当日はわりと自由に撮っていた気がした。が、強いレンズ所持者が ATI を発揮してくれたりして有り難かった。
    • シフト組むぐらいまでしなくとも、認識合わせするぐらいでも十分だったと思う。
  • 全員のカメラの日付設定をちゃんと合わせよう、と事前に声掛けできてたのが最高だった。
    • これは、撮影写真を一般公開するために Google Photo アルバムでまとめるとき、EXIF の撮影日時ソートでいい感じの時系列で並べられるようにするため。
    • 2017 はこれがうまくできてなくて少し手間取った
  • オフィスアワーの風景をもう少しうまく撮りたかった
    • 登壇者と参加者の会話シーンを撮るのが難しかった。お互い対面で話しているので、 2 人の顔をいい感じにフレーム内におさめるのが難しい。どちらかが後ろを向いているか、 2 人とも横顔になってるか、の2パターンの構図がほとんど。

次は、もっと強いレンズで挑みたい。


拡張関数の定義先ファイルのパッケージ

例えば android.content.res.Resources に対して次のような拡張関数を生やすとして、

fun Resources.isLandscape() = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE

拡張関数の定義先を com.sample.app.extensions パッケージ配下に作成した ResourcesEx.kt とすると、

package com.sample.app.extensions

import android.content.res.Configuration
import android.content.res.Resources

fun Resources.isLandscape() = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE

使う側では次のようなコードになる

package com.sample.app.layer.presentation.view

import com.sample.app.extensions.isLandscape

class FooActivity : Activity {

  override fun onCreate(savedInstanceState: Bundle?) {
    if (resources.isLandscape()) {
      ...
    }
  }
}

こうするより...

ResourcesEx.ktcom.sample.app.extensions.android.content.res.Resources パッケージ配下にしとくと、

package com.sample.app.extensions.android.content.res.Resources

import android.content.res.Configuration
import android.content.res.Resources

fun Resources.isLandscape() = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE

使う側では次のようなコードになり、 import 文がより明確になった。

package com.sample.app.layer.presentation.view

import com.sample.app.extensions.android.content.res.Resources.isLandscape

class FooActivity : Activity {

  override fun onCreate(savedInstanceState: Bundle?) {
    if (resources.isLandscape()) {
      ...
    }
  }
}

パッケージ名に「何に対する拡張関数か?」を明示的に含めとくと良いのではないかな。

パッケージ名に大文字が入ることに抵抗ある人には厳しそうだけど、個人的には感覚より明示的さを優先したい。

com.google.gms:oss-licenses を使ってオープンソースライセンスを表示する

ライブラリのライセンス管理・表示を簡単に行えるツールが Google Play サービス 11.2.0 から含まれるようになり、とりあえず表示するだけなら非常に簡単だったので、導入方法と注意点を残します。

https://developers.google.com/android/guides/opensource

導入

Android Studio 3.0 で新規作成したプロジェクトに対して導入してみます。

導入は非常に簡単です。

依存しているライブラリの pom からライセンス情報を取得するための Gradle プラグイン com.google.gms.oss.licenses.plugin と、ライセンス表記を行う Activity を提供するライブラリ play-services-oss-licenses を dependencies に追加します。

build.gradle

buildscript {
    ext.kotlin_version = '1.1.51'
    repositories {
+       google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.0.0'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+       classpath 'com.google.gms:oss-licenses:0.9.1'
    }
}

app/build.gradle

  apply plugin: 'com.android.application'
  apply plugin: 'kotlin-android'
  apply plugin: 'kotlin-android-extensions'
+ apply plugin: 'com.google.gms.oss.licenses.plugin'

  android {
      compileSdkVersion 26
      defaultConfig {
          applicationId "com.github.sho5nn.sample"
          minSdkVersion 23
          targetSdkVersion 26
          versionCode 1
          versionName "1.0"
          testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
      }
      buildTypes {
          release {
              minifyEnabled false
              proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
          }
      }
  }

  dependencies {
      implementation fileTree(dir: 'libs', include: ['*.jar'])
      implementation"org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
      implementation 'com.android.support:appcompat-v7:26.1.0'
      implementation 'com.android.support.constraint:constraint-layout:1.0.2'
      testImplementation 'junit:junit:4.12'
      androidTestImplementation 'com.android.support.test:runner:1.0.1'
      androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
+     implementation 'com.google.android.gms:play-services-oss-licenses:11.6.0'
  }

この状態でビルドすると app/src/main/res/raw ディレクトリに、プロジェクトが依存しているライブラリのライセンス情報をまとめたファイルが 2 つ生成されます。

// third_party_license_metadata

0:46 appcompat-v7
47:47 play-services-oss-licenses
95:46 animated-vector-drawable
142:46 support-vector-drawable
189:47 play-services-basement
237:46 support-v4
284:46 support-fragment
331:46 support-core-ui
378:46 support-media-compat
425:46 support-core-utils
472:46 support-compat
519:46 support-annotations
566:731 UTF
1298:2500 zlib
3799:19442 ICU4C
23242:11358 CCTZ
34601:680 STL
35282:1602 JSR 305
36885:1732 Protobuf Nano
38618:1481 darts_clone
40100:243 tz database
40344:1558 RE2
41903:3182 PCRE
45086:11358 safeparcel
56445:46 annotations
56492:46 runtime
56586:46 common
// third_party_licenses

http://www.apache.org/licenses/LICENSE-2.0.txt
https://developer.android.com/studio/terms.html
http://www.apache.org/licenses/LICENSE-2.0.txt
http://www.apache.org/licenses/LICENSE-2.0.txt
https://developer.android.com/studio/terms.html
http://www.apache.org/licenses/LICENSE-2.0.txt
http://www.apache.org/licenses/LICENSE-2.0.txt
http://www.apache.org/licenses/LICENSE-2.0.txt
http://www.apache.org/licenses/LICENSE-2.0.txt

...

次に、ライセンス表記を行ってくれる Activity を呼び出すコードを書きます。

import com.google.android.gms.oss.licenses.OssLicensesMenuActivity

val intent = Intent(this, OssLicensesMenuActivity::class.java)
intent.putExtra("title", "おーぷんそーすらいせんす")
startActivity(intent)

これだけで、ライセンス一覧画面を表示することができます。ライセンス一覧画面からはライセンス詳細画面に遷移することができ、そこでライセンスを確認することができます。 ライセンス一覧画面の ActionBar のタイトルは Intent#putExtratitle のキー名で変更可能です。

f:id:sho5nn:20171110001734g:plain

注意点

既にアプリで定義している Theme が Theme.AppCompat.Light.NoActionBar など ActionBar がないものだった場合、ライセンス一覧画面から遷移する詳細画面を表示するときにクラッシュしてしまいます。これは play-services-oss-licensesActionBar#setTitle でライブラリ名を設定しようとしているためです。

Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'void android.support.v7.app.ActionBar.setTitle(java.lang.CharSequence)' on a null object reference
   at com.google.android.gms.oss.licenses.OssLicensesActivity.onCreate(Unknown Source:30)
   at android.app.Activity.performCreate(Activity.java:6975)
   at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1213)
   at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2770)
   at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2892) 
   at android.app.ActivityThread.-wrap11(Unknown Source:0) 
   at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1593) 
   at android.os.Handler.dispatchMessage(Handler.java:105) 
   at android.os.Looper.loop(Looper.java:164) 
   at android.app.ActivityThread.main(ActivityThread.java:6541) 
   at java.lang.reflect.Method.invoke(Native Method) 
   at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240) 
   at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:767) 

回避方法としては、ライセンス詳細画面である OssLicensesActivity の Theme を、 ActionBar ありの Theme で上書きしてしまえばよいです。ライセンス一覧画面の OssLicensesMenuActivity も上書きできるので、ちょっとしたカスタマイズはできそう。

<manifest>
  <application>
    <activity android:name=".MainActivity">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>

        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>
+     <activity
+       android:theme="@style/Theme.AppCompat.Light.DarkActionBar"
+       android:name="com.google.android.gms.oss.licenses.OssLicensesActivity"
+       />
+     <activity
+       android:theme="@style/Theme.AppCompat.Light.DarkActionBar"
+       android:name="com.google.android.gms.oss.licenses.OssLicensesMenuActivity"
+       />
  </application>
</manifest>

以上です。

MVP + Clean Architecture における DI 設計の一案

MVP + Clean Architecture っぽいプロジェクトで各レイヤーの依存関係を、 DI コンテナである dagger2 で表現する一案の備忘録。

結論

  • クラス数が多くなる。あまり現実的じゃない。
  • 結局 ViewLayer から見れば、注入オブジェクトたちを ApplicationComponent で管理してるのと変わらないのでは。

サンプルコード

sho5nn/android-tasting-mvp-dagger2

Activity で依存オブジェクトを確認できる。

UserView FriendListView FriendProfileView
f:id:sho5nn:20170910163030p:plain f:id:sho5nn:20170910163043p:plain f:id:sho5nn:20170910163054p:plain

依存図

各レイヤー間の依存方向を青い矢印で、依存性の注入方向を赤い矢印で表した図。

f:id:sho5nn:20170910162920p:plain

言わないこと

  • Clean Architecture + MVP の在り方
    • コードで表現したときの在り方やネーミング等は、適宜補完してもらえると。前述したサンプルコードにおいては、例えば Repository が抽象的ではない( Interface を定義していない)など。
    • 本来は DomainLayer に Repository Interface を定義し、 DataLayer で実装するのが望ましい気がする。

スタートアップ企業に移って 5 ヶ月目の備忘録

  • 以前の現場
    • 大企業のモバイル開発グループの技術基盤 Android チーム
    • 在籍期間は約 2 年半 
  • 現在の現場
    • スタートアップ企業の Android アプリ開発チーム
    • アプリは正式リリースからまだ 1 年も経過していない
    • 在籍期間 5 ヶ月目に入った

スタートアップ企業に移って 5 ヶ月目の気持ちの備忘録。

開発スピードの向上と維持

f:id:sho5nn:20170902195542j:plain

実は、スタートアップ企業での開発経験は、今の現場が 2 回目ではあるけど、 1 回目のときは、この業界に入って 2 年目ぐらいの頃で、とにかくがむしゃらに開発、具体的に仮説立てて施策立てて検証して次に回して、とかそういう感じではなかった。

今の現場は、エンジニアやマーケチーム、事業推進の方々など見渡せど頭のキレる方ばかり。最小の入力で最大ダメージコンボをいかに叩き込めるか、を常に念頭に置いてプロダクトを成長させようとしている感じ。とにかく刺激があるし、そんな 0F コンボを如何程に安定して出せるんですか、という。言わずもがな、以前の現場とは全く違う。

当然ながら競合他社は存在していて、最近、その競合他社がリリースしているアプリをチーム内で使ってみようということになり、結果として「我々のよりも優れているな…」という評価が出ているなど、やっていきのこころが高まっている。

いかに自社プロダクトをユーザーたちに浸透させ、いかに競合他社とのアドバンテージを取るか、を考えると、障害となる問題とその課題を見つけ検証して改善する、当たり前だけど当たり前に維持するのが難しい PDCA をどれだけ早く回せるか、につきる。

どれだけ早く回せるかを、開発者視点のアプローチとしても取り組み続けるべきで、日々の作業にかかる時間の短縮化、本来やりたいことから道を逸れてしまうような問題に遭遇しないよう防御策を仕込んでおくなど、小さいことでもいいのでチリを積んでおけば早いサイクルの山を築けるのでは、と感じている。本当にやりたい PDCA は叶えたいのだ。

例えば直近では、テンプレート機能でクラスファイルを自動生成する仕組みを導入(きりみん氏ありがとう)、ビルド速度の短縮化、 Primary Issue の合間にリファクタリングを行うなど、大小関係なく取り組み続けている。全く書かれていなかったテストコードも今に至っては主要機能はカバーできているようになり、デグレで無駄足を踏む確率は確実に低くなっている。

目に見えて劇的な効果は感じられなくとも、長期的スパンで見れば効果は出るので、このマインドを持ってして 0F コンボを叩き込みたい。そんな 5 ヶ月目。

Dagger2 メモ

n 個の @Component から 1 個のインスタンスに inject

不可

public class Main {

  @Inject
  Red red;

  @Inject
  Blue blue;

  public Main() {
    DaggerBlueComponent.builder().blueModule(new BlueComponent.BlueModule()).build().inject(this);
    DaggerRedComponent.builder().redModule(new RedComponent.RedModule()).build().inject(this);
  }
}
public class Blue {
}

@BlueScope
@Component(modules = BlueComponent.BlueModule.class)
public interface BlueComponent {

  void inject(Main main);

  @Module
  class BlueModule {

    @BlueScope
    @Provides
    Blue provideBlue() {
      return new Blue();
    }
  }
}

@Scope
@Retention(RUNTIME)
public @interface BlueScope {
}
public class Red {
}

@RedScope
@Component(modules = RedComponent.RedModule.class)
public interface RedComponent {

  void inject(Main main);

  @Module
  class RedModule {

    @RedScope
    @Provides
    Red provideRed() {
      return new Red();
    }
  }
}

@Scope
@Retention(RUNTIME)
public @interface RedScope {
}
:sample:compileDebugJavaWithJavac
/Users/shogo/dev/code/android/myrepos/android-tasting-dagger2/sample/src/main/java/com/sho5nn/sample/di/BlueComponent.java:13: エラー: com.sho5nn.sample.di.Red cannot be provided without an @Inject consttor or from an @Provides- or @Produces-annotated method.
  void inject(Main main);
       ^
      com.sho5nn.sample.di.Red is injected at
          com.sho5nn.sample.Main.red
      com.sho5nn.sample.Main is injected at
          com.sho5nn.sample.di.BlueComponent.inject(main)
/Users/shogo/dev/code/android/myrepos/android-tasting-dagger2/sample/src/main/java/com/sho5nn/sample/di/RedComponent.java:13: エラー: com.sho5nn.sample.di.Blue cannot be provided without an @Inject consttor or from an @Provides- or @Produces-annotated method.
  void inject(Main main);
       ^
      com.sho5nn.sample.di.Blue is injected at
          com.sho5nn.sample.Main.blue
      com.sho5nn.sample.Main is injected at
          com.sho5nn.sample.di.RedComponent.inject(main)
エラー2個

@Scope ( @RedScope, @BlueScope ) を持っている/持っていないは関係ない

n 個の @Component に依存した 1 個の @Component から 1 個のインスタンスに inject

可能

public class Main {

  @Inject
  Red red;

  @Inject
  Blue blue;

  public Main() {
    BlueComponent blueComponent = DaggerBlueComponent.builder()
      .blueModule(new BlueComponent.BlueModule()).build();
    RedComponent redComponent = DaggerRedComponent.builder()
      .redModule(new RedComponent.RedModule()).build();

    DaggerColorComponent.builder()
      .blueComponent(blueComponent)
      .redComponent(redComponent)
      .build()
      .inject(this);
  }
}
@ColorScope
@Component(dependencies = {BlueComponent.class, RedComponent.class})
public interface ColorComponent {

  void inject(Main main);
}

@Scope
@Retention(RUNTIME)
public @interface ColorScope {
}
//@BlueScope
@Component(modules = BlueComponent.BlueModule.class)
public interface BlueComponent {

  Blue blue();

  @Module
  class BlueModule {

//    @BlueScope
    @Provides
    Blue provideBlue() {
      return new Blue();
    }
  }
}

//@RedScope
@Component(modules = RedComponent.RedModule.class)
public interface RedComponent {

  Red red();

  @Module
  class RedModule {

//    @RedScope
    @Provides
    Red provideRed() {
      return new Red();
    }
  }
}

依存先 @Component 全てが @Scope ( @RedScope, @BlueScope ) を持っていると不可

:sample:compileDebugJavaWithJavac
/Users/shogo/dev/code/android/myrepos/android-tasting-dagger2/sample/src/main/java/com/sho5nn/sample/Main.java:5: エラー: シンボルを見つけられません
import com.sho5nn.sample.data.DaggerBlueComponent;
                             ^
  シンボル:   クラス DaggerBlueComponent
  場所: パッケージ com.sho5nn.sample.data
/Users/shogo/dev/code/android/myrepos/android-tasting-dagger2/sample/src/main/java/com/sho5nn/sample/Main.java:6: エラー: シンボルを見つけられません
import com.sho5nn.sample.data.DaggerRedComponent;
                             ^
  シンボル:   クラス DaggerRedComponent
  場所: パッケージ com.sho5nn.sample.data
/Users/shogo/dev/code/android/myrepos/android-tasting-dagger2/sample/src/main/java/com/sho5nn/sample/Main.java:9: エラー: シンボルを見つけられません
import com.sho5nn.sample.domain.DaggerColorComponent;
                               ^
  シンボル:   クラス DaggerColorComponent
  場所: パッケージ com.sho5nn.sample.domain
/Users/shogo/dev/code/android/myrepos/android-tasting-dagger2/sample/src/main/java/com/sho5nn/sample/domain/ColorComponent.java:10: エラー: @com.sho5nn.sample.domain.ColorScope com.sho5nn.sample.domain.ColorComponent depends on more than one scoped component:
@Component(dependencies = {BlueComponent.class, RedComponent.class})
^
      @com.sho5nn.sample.data.BlueScope com.sho5nn.sample.data.BlueComponent
      @com.sho5nn.sample.data.RedScope com.sho5nn.sample.data.RedComponent
エラー4個

depends on more than one scoped component


Android で複数非同期処理のシーケンス制御をライブラリに依存せず Pure Java で実装しなければならない場合の一案

もし、 Android 開発において、ライブラリに依存せず Pure Java を前提とし、複数の非同期処理をシーケンス制御する必要が出てきた場合に、どう解決すればよいか、の問題に対する一案を残した記録文章です。

背景

直近の仕事で、 Android 向け社内ライブラリの開発に携わっていたのですが、いくつかの社内事情があり、そのライブラリが依存するライブラリは appcompat-v7 のみという制約がありました。

f:id:sho5nn:20170506202536j:plain

あまり詳しくは書けないですが、そのライブラリは SSO を実現する機能を備えていて、いくつかのログインシーケンス制御があり、そのシーケンス中にあるそれぞれのステップが非同期処理で実行され、前の非同期処理の結果を元に次の非同期処理を行うような処理を実装する必要がありました。

最近のトレンドに習えば、 RxJava ライブラリなどを使用して精神的安定を維持しつつ開発・保守したいところですが、先述した制約のため Pure Java で実装するしかない状態でした。

その状態のまま開発した場合は言わずもがな、先行き怪しく最悪死人も待ったなしなのでどうにかしないとその先は地獄だぞという気持ちがあり、他のメンバーと議論したところ、 JavaScript の Promise からヒントを得てみてはどうだろうかということになり、 Promise like なインターフェースをまず実装し、そのインターフェースを用いてシーケンス制御の実装を進めました。

以下、そのときに実装した Promise like なインターフェースのコード(実際は、そのときに書いたコードを元に更に修正した)と、それを使用したサンプルコードを雑に残します。

参考

コード

基本的には Rx と似ていて、何らかのデータがどのようにして伝達していってその結果をどのように扱うか、のデータフローを先にコードで表現し、実行時に何らかのデータがそのデータフローに則って実行結果を取得します。

// sample
ExecutorService executor = Executors.newCachedThreadPool();

Promise
  .when(executor, Promise.single(new FooTask()))
  .then(new FulfillCallbackThenAll<String>() {
    @Override
    public PromiseTask.All onFulfilled(String value) {
      return Promise.all(new BarTask(), new BazTask());
    }
  })
  .then(new FulfillCallbackThenSingle<Object[], Void>() {
    @Override
    public PromiseTask.Single<Void> onFulfilled(Object[] value) {
      return Promise.single(new QuxTask());
    }
  }, new RejectCallbackDone<Throwable[]>() {
    @Override
    public void onRejected(Throwable[] value) {
      Toast.makeText(SimpleActivity.this, "failure", Toast.LENGTH_SHORT).show();
      progress.setVisibility(View.GONE);
    }
  }).atMain()
  .done(new FulfillCallbackDone<Void>() {
    @Override
    public void onFulfilled(Void value) {
      progress.setVisibility(View.GONE);
      updateViewWhenSuccess();
    }
  }, new RejectCallbackDone<Throwable>() {
    @Override
    public void onRejected(Throwable value) {
      progress.setVisibility(View.GONE);
      updateViewWhenFailure();
    }
  }).atMain();


// with lambda
Promise
  .when(executor, Promise.single(new FooTask()))
  .then((FulfillCallbackThenAll<String>) value -> Promise.all(new BarTask(), new BazTask()))
  .then(
    (FulfillCallbackThenSingle<Object[], Void>) value -> {
      return Promise.single(new QuxTask());
    },
    (RejectCallbackDone<Throwable[]>) value -> {
      Toast.makeText(SimpleActivity.this, "failure", Toast.LENGTH_SHORT).show();
      progress.setVisibility(View.GONE);
    }).atMain()
  .done(
    value -> {
      progress.setVisibility(View.GONE);
      updateViewWhenSuccess();
    },
    value -> {
      progress.setVisibility(View.GONE);
      updateViewWhenFailure();
    }).atMain();

非同期処理は Callable もしくは Runnable インターフェースで実装し、 Promise オブジェクトに渡します。

Promise.when() を呼び出して Promise オブジェクトを取得し、 Promise.then() でチェーンしつつ次の Promise オブジェクトを取得し、最後に Promise.done() を呼び出して最初の Promise オブジェクトの非同期処理を実行します。

チェーンする場合、 Promise.then() に渡すコールバックインターフェースの返り値として、次に実行したい非同期処理を表す Callable もしくは Runnable インターフェースオブジェクトを渡すことで、その次の Promise.then() に渡したコールバックインターフェースが呼ばれます。

Promise.single()/all()/race()

Callable もしくは Runnable を Promise オブジェクトに渡す際に、実行方法を指定します。

  • Promise.single(Callable/Runnable)
    • 1 つの非同期処理を実行する
  • Promise.all(Callable[]/Runnable[])
    • 全て成功した場合のみ成功時コールバックインターフェースを呼ぶ
    • どれか 1 つでも失敗した場合には失敗時コールバックインターフェースを呼ぶ
  • Promise.race(Callable[]/Runnable[])
    • どれか 1 つの非同期処理が成功/失敗に関係なく終了した時点で、成功時/失敗時コールバックインターフェースを呼ぶ

Promise.atMain()/at(Handler)

成功時/失敗時コールバックインターフェースの呼び出されるスレッドを指定します。 Promise オブジェクトごとに指定するので、 View 更新等、ステップごとに細かく制御が可能。

雑感

余程のことがないかぎりは、世に存在する便利なライブラリを使うべきなのは言うまでもなく、しかし、個人的にはまあまあ楽しめたとしとこうというのが本音だった。


Photo by Jamison McAndie | Unsplash