読者です 読者をやめる 読者になる 読者になる

もやもやエンジニア

IT系のネタで思ったことや技術系のネタを備忘録的に綴っていきます。フロント率高め。

AndroidでEspresso + Spoonでキャプチャを撮りつつUIテストを走らせる

Android Test Kotlin

Android実機上でのテストにはEspressoというテスティングフレームワークを使いますが、Spoonというライブラリと組み合わせることでUIテスト実行時のキャプチャを残すことができます。導入してみたのでメモになります。

導入

  • Espressoは導入済の状態からSpoonを入れます。この記事を書いている時点でのバージョンは以下の通りです。

    • Espresso : 2.2.2
    • Spoon : 1.6.4
  • プロジェクトのbuild.gradleにプラグインを追加。

buildscript {
    dependencies {
        ...
        classpath 'com.stanfy.spoon:spoon-gradle-plugin:1.2.2'
    }
}
  • アプリケーションのbuild.gradleにライブラリを追加。プラグインも有効にしておく。
apply plugin: 'spoon'

spoon {
    // for debug output
    debug = true

    // To execute the tests device by device */
    sequential = true

    // To grant permissions to Android M >= devices */
    grantAllPermissions = true
}

dependencies {
    ...
    androidTestCompile group: 'com.squareup.spoon', name: 'spoon-client', version: '1.6.4'
}
  • これで準備はできました。

キャプチャを撮る

  • テストメソッド内でSpoon.screenshotを呼ぶことで実行時のキャプチャを残せます
    @Test
    fun testInitializedView() {

        // これでキャプチャを残せる
        Spoon.screenshot(activityRule.activity, "initial_state")

        onView(withId(R.id.fragment_initialize_edit_hatena_id))
                .perform(closeSoftKeyboard(), scrollTo())
                .check(matches(isDisplayed()))

        onView(withId(R.id.fragment_initialize_button_set_hatena_id))
                .perform(scrollTo())
                .check(matches(isDisplayed()))
                .check(matches(not(isEnabled())))

        onView(withId(R.id.fab))
                .check(matches(not(isDisplayed())))
    }
  • 実行するときはGradleのタスクとして実行します。他のテストタスクと同じようにspoonDebugのようにフレーバーをつけることも可能です。
./gradlew spoon 

実行結果

  • 実行結果は通常のEspressoの実行結果とは異なり app/build/spoon の下に格納されます。実行されると以下のように表示されます。
  • 画像にはメソッド呼び出し時のタグが振られているのでどの時点のキャプチャかわかるようになっています。

f:id:Rei19:20160910212252p:plain:w300

コード

  • こちらが個人のアプリに導入した時のプルリクになります。参考までに。

add Spoon plugin by rei-m · Pull Request #219 · rei-m/HBFav_material · GitHub

最近の個人的なAndroidの設計とかテスト周りとかまとめ

Android Kotlin
  • 最近、Androidの設計やらテストの書き方やらを試行錯誤していて、ちょっと情報が散らばってきたので個人的なまとめです。これが絶対的にイケてる!とかじゃなくて単にいろんな人のスライド読んだり、自分で試したりして今こんな感じになったというレベルのものです。
  • コードは↓で作ったアプリをベースにいじってます。。Kotlin製のアプリなのでJavaに適時読み替えてください。

rei19.hatenablog.com

設計の話

  • 設計の目標はモジュールの責務を分割して将来の変更に強くするという感じです。Androidの場合は特に適当に作るとActivityとFragmentが膨れ上がってメンテつらい作りになりがちです。で、去年に書いた記事(Androidのデザインパターンを考えてみたの続き。Kotlin対応版。 - もやもやエンジニア)とかを経て、いまのところはMVPな構成にしてModel層をDDDライクに作るようにしてます。先日のAndroidオールスターズ2でもid:kgmyshin 氏が同様の話をしてましたが、自分も近いものを目標に作ってます。

  • 雑な絵だとこんな感じです。Activity/FragmentはViewの操作に集中して、ドメインロジックはModel層に、Presenterが両者を仲介するというふいんきですね。

f:id:Rei19:20160822002625p:plain

実際に作った例

  • 起動時にIDを設定する画面(InitializeFragment)を例にとってみます。この画面は入力されたIDが有効か問い合わせて有効なら端末に保存して次の画面に行くという仕様です。

f:id:Rei19:20160821214641p:plain:w300

  • まずはPresenterとViewをつなぐInterfaceです。ViewはFragmentに実装してActionsはPresenterに実装させます。
interface InitializeContact {

    interface View {
        fun showNetworkErrorMessage()
        fun showProgress()
        fun hideProgress()
        fun displayInvalidUserIdMessage()
        fun navigateToMain()
    }

    interface Actions {
        fun onCreate(view: InitializeContact.View)
        fun onResume()
        fun onPause()
        fun onClickButtonSetId(userId: String)
    }
}
  • 次にFragmentです。ライフサイクルやユーザーの操作で発生したイベントをPresenterに伝えるのと、上で定義したInterfaceを実装してViewの操作をするメソッドを生やしてます。
  • Presenterはテスト時に差し替えできるようにDagger経由で注入できるようにしておきます。Model層の操作はここでは一切出てきません。
class InitializeFragment() : BaseFragment(),
        InitializeContact.View,
        ProgressDialogController {

    companion object {
        fun newInstance(): InitializeFragment = InitializeFragment()
    }

    @Inject
    lateinit var navigator: ActivityNavigator

    @Inject
    lateinit var presenter: InitializeContact.Actions

    override var progressDialog: ProgressDialog? = null

    private var subscription: CompositeSubscription? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        component.inject(this)
        presenter.onCreate(this)
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {

        subscription = CompositeSubscription()

        val view = inflater.inflate(R.layout.fragment_initialize, container, false)

        val editId = view.findViewById(R.id.fragment_initialize_edit_hatena_id) as AppCompatEditText

        val buttonSetId = view.findViewById(R.id.fragment_initialize_button_set_hatena_id) as AppCompatButton
        buttonSetId.setOnClickListener {
            presenter.onClickButtonSetId(editId.editableText.toString())
        }

        subscription?.add(RxTextView.textChanges(editId)
                .map { v -> 0 < v.length }
                .subscribe { isEnabled -> buttonSetId.isEnabled = isEnabled })

        return view
    }

    override fun onResume() {
        super.onResume()
        presenter.onResume()
    }

    override fun onPause() {
        super.onPause()
        presenter.onPause()
    }

    override fun onDestroyView() {
        super.onDestroyView()
        subscription?.unsubscribe()
        subscription = null
    }

    override fun showNetworkErrorMessage() {
        with(activity as AppCompatActivity) {
            val rootView = findViewById(R.id.fragment_initialize_layout_root)
            hideKeyBoard(rootView)
            showSnackbarNetworkError(rootView)
        }
    }

    override fun showProgress() {
        showProgressDialog(activity)
    }

    override fun hideProgress() {
        closeProgressDialog()
    }

    override fun displayInvalidUserIdMessage() {
        view?.findViewById(R.id.fragment_initialize_layout_hatena_id)?.let {
            it as TextInputLayout
            it.error = getString(R.string.message_error_input_user_id)
        }
    }

    override fun navigateToMain() {
        navigator.navigateToMain(activity)
        activity.finish()
    }
}
  • 最後にView層から受け取ったイベントとModel層の操作を仲介するPresenterです。Presenterが依存するModel層のモジュールはコンストラクタで受け取るようにしています。テスト時にはモックにするなりして差し替えます。
class InitializePresenter(private val userRepository: UserRepository,
                          private val userService: UserService) : InitializeContact.Actions {

    private lateinit var view: InitializeContact.View

    private var subscription: CompositeSubscription? = null

    private var isLoading = false

    override fun onCreate(view: InitializeContact.View) {

        this.view = view

        val userEntity = userRepository.resolve()
        if (userEntity.isCompleteSetting) {
            view.navigateToMain()
        }
    }

    override fun onResume() {
        subscription = CompositeSubscription()
    }

    override fun onPause() {
        subscription?.unsubscribe()
        subscription = null
    }

    override fun onClickButtonSetId(userId: String) {

        if (isLoading) return

        isLoading = true
        view.showProgress()

        subscription?.add(userService.confirmExistingUserId(userId)
                .doOnUnsubscribe {
                    isLoading = false
                    view.hideProgress()
                }
                .onBackpressureBuffer()
                .subscribeOn(Schedulers.newThread())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe({
                    onConfirmExistingUserIdSuccess(it, userId)
                }, {
                    onConfirmExistingUserIdFailure(it)
                }))
    }

    private fun onConfirmExistingUserIdSuccess(isValid: Boolean, userId: String) {
        if (isValid) {
            userRepository.store(UserEntity(userId))
            view.navigateToMain()
        } else {
            view.displayInvalidUserIdMessage()
        }
    }

    private fun onConfirmExistingUserIdFailure(e: Throwable) {
        if (e is HttpException) {
            if (e.code() == HttpURLConnection.HTTP_NOT_FOUND) {
                view.displayInvalidUserIdMessage()
                return
            }
        }
        view.showNetworkErrorMessage()
    }
}
  • これでFragmentはViewの操作のにのみ集中出来るようになって見通しがよくなりました。ViewはDatabindingを使うとよりスッキリするかと思います。

テストの話

  • ここでは主にUnitTestレベルの話をします。Androidにおけるテストは単純なJUnitのテストとエミュレーターや実機を使ったテストで分かれますが、前者を指します。

Viewのテスト

  • Viewは本来はEspressoを使ってエミュレーターなり実機なりの上でテストを動かす必要がありますが、Robolectric を使うと端末を使わずにテストができます。
  • 事前にテスト用のアプリケーションクラスを作成してViewが依存するPresenterを差し替えておきます。全体のコードはGitHubを見た方が早いと思うので差し替えの部分だけ紹介。Test用のComponetを作ってファクトリメソッドをオーバーライドして差し替えます。
open class TestApp : App() {
    override fun createApplicationComponent(): ApplicationComponent {
        return DaggerTestApplicationComponent.builder()
                .applicationModule(ApplicationModule(this))
                .infraLayerModule(InfraLayerModule())
                .build()
    }
}
  • こちらが実際のRobolectricを使ったテストケースです。Configで上で作ったテスト用のアプリケーションを指定することで、テスト対象のFragmentが依存するPresenterを差し替えて、Model層の操作を行わないようにして、実装したViewのメソッドが正しく実装されているかのみを検証します。
  • 余談ですがKotlinはMockitoのwhenやらhamcrestのisやらが予約語になってるのでバッククォートで囲む必要があってちょっとめんどくさい。。。
@RunWith(RobolectricTestRunner::class)
@Config(constants = BuildConfig::class,
        application = TestApp::class,
        sdk = intArrayOf(Build.VERSION_CODES.LOLLIPOP))
class InitializeFragmentTest {

    lateinit var fragment: InitializeFragment

    private val view: View by lazy {
        fragment.view ?: throw IllegalStateException("fragment's view is Null")
    }

    private val editHatenaId: EditText
        get() = view.findViewById(R.id.fragment_initialize_edit_hatena_id) as EditText

    private val buttonSetHatenaId: Button
        get() = view.findViewById(R.id.fragment_initialize_button_set_hatena_id) as Button

    private val textInputLayoutHatenaId: TextInputLayout
        get() = view.findViewById(R.id.fragment_initialize_layout_hatena_id) as TextInputLayout

    private val snackbarTextView: TextView
        get() = fragment.activity.findViewById(android.support.design.R.id.snackbar_text) as TextView

    private fun getString(resId: Int): String {
        return fragment.getString(resId)
    }

    @Before
    fun setUp() {
        fragment = InitializeFragment.newInstance()
        SupportFragmentTestUtil.startFragment(fragment, SplashActivity::class.java)
    }

    @Test
    fun initialize() {
        assertThat(editHatenaId.visibility, `is`(View.VISIBLE))
        assertThat(buttonSetHatenaId.visibility, `is`(View.VISIBLE))
        assertThat(buttonSetHatenaId.isEnabled, `is`(false))
    }

    @Test
    fun testButtonSetHatenaIdStatus_input_id() {
        editHatenaId.setText("a")
        assertThat(buttonSetHatenaId.isEnabled, `is`(true))
    }

    @Test
    fun testButtonSetHatenaIdStatus_not_input_id() {
        editHatenaId.setText("")
        assertThat(buttonSetHatenaId.isEnabled, `is`(false))
    }

    @Test
    fun testButtonSetHatenaIdClick() {
        val presenter = mock(InitializeContact.Actions::class.java)
        doAnswer { Unit }.`when`(presenter).onClickButtonSetId("valid")
        fragment.presenter = presenter
        editHatenaId.setText("valid")
        buttonSetHatenaId.performClick()
        verify(presenter).onClickButtonSetId("valid")
    }

    @Test
    fun testShowNetworkErrorMessage() {
        fragment.showNetworkErrorMessage()
        assertThat(snackbarTextView.visibility, `is`(View.VISIBLE))
        assertThat(snackbarTextView.text.toString(), `is`(getString(R.string.message_error_network)))
    }

    @Test
    fun testDisplayInvalidUserIdMessage() {
        fragment.displayInvalidUserIdMessage()
        assertThat(textInputLayoutHatenaId.error.toString(), `is`(getString(R.string.message_error_input_user_id)))
    }

    @Test
    fun testShowHideProgress() {
        fragment.showProgress()
        assertThat(fragment.progressDialog?.isShowing, `is`(true))
        fragment.hideProgress()
        assertNull(fragment.progressDialog)
    }

    @Test
    fun testNavigateToMain() {
        val navigator = spy(fragment.navigator)
        doAnswer { Unit }.`when`(navigator).navigateToMain(fragment.activity)
        fragment.navigator = navigator
        fragment.navigateToMain()
        verify(navigator).navigateToMain(fragment.activity)
    }
}

Presenterのテスト

  • PresenterはAndroidの実装に依存しない形になっているのでピュアなJUnitのテストになります。Contextが必要な場合などはMockitoを使ってContextのMockを作ってあげればいいかなと思います。
  • setUpでモックのViewのメソッドを空実装するのと、RxAndroidのスケジューラーを使った処理がすぐ返るように設定しています。テストの検証はMockのViewのメソッドが呼ばれたかどうかで判定するようにして、その先で何が起きるかはPresenterは関心を持ちません。
  • モデル層はMockitoを使ってテスト時に必要な処理を実装しています。
@RunWith(MockitoJUnitRunner::class)
class InitializePresenterTest {

    @Mock
    lateinit var userRepository: UserRepository

    @Mock
    lateinit var userService: UserService

    @Mock
    lateinit var view: InitializeContact.View

    @Before
    fun setUp() {

        doAnswer { Unit }.`when`(view).navigateToMain()
        doAnswer { Unit }.`when`(view).showProgress()
        doAnswer { Unit }.`when`(view).hideProgress()
        doAnswer { Unit }.`when`(view).displayInvalidUserIdMessage()
        doAnswer { Unit }.`when`(view).showNetworkErrorMessage()

        RxAndroidPlugins.getInstance().reset()
        RxAndroidPlugins.getInstance().registerSchedulersHook(object : RxAndroidSchedulersHook() {
            override fun getMainThreadScheduler(): Scheduler? {
                return Schedulers.immediate()
            }
        })
    }

    @After
    fun tearDown() {
        RxAndroidPlugins.getInstance().reset()
    }

    @Test
    fun testOnCreate_initialize_not_complete_register_user() {

        `when`(userRepository.resolve()).thenReturn(UserEntity(""))

        val presenter = InitializePresenter(userRepository, userService)

        presenter.onCreate(view)
        verify(view, never()).navigateToMain()
    }

    @Test
    fun testOnCreate_initialize_complete_register_user() {

        `when`(userRepository.resolve()).thenReturn(UserEntity("test"))

        val presenter = InitializePresenter(userRepository, userService)

        presenter.onCreate(view)
        verify(view).navigateToMain()
    }

    @Test
    fun testOnClickButtonSetId_success_check_id() {

        `when`(userRepository.resolve()).thenReturn(UserEntity(""))

        doAnswer { Unit }.`when`(userRepository).store(UserEntity("success"))

        `when`(userService.confirmExistingUserId("success")).thenReturn(Observable.just(true))

        val presenter = InitializePresenter(userRepository, userService)
        presenter.onCreate(view)
        presenter.onResume()
        presenter.onClickButtonSetId("success")

        verify(userRepository, timeout(TimeUnit.SECONDS.toMillis(1))).store(UserEntity("success"))
        verify(view, timeout(TimeUnit.SECONDS.toMillis(1))).showProgress()
        verify(view, timeout(TimeUnit.SECONDS.toMillis(1))).hideProgress()
        verify(view, timeout(TimeUnit.SECONDS.toMillis(1))).navigateToMain()
    }

    @Test
    fun testOnClickButtonSetId_fail_check_id() {

        `when`(userRepository.resolve()).thenReturn(UserEntity(""))

        `when`(userService.confirmExistingUserId("fail")).thenReturn(Observable.just(false))

        val presenter = InitializePresenter(userRepository, userService)
        presenter.onCreate(view)
        presenter.onResume()
        presenter.onClickButtonSetId("fail")

        verify(view, timeout(TimeUnit.SECONDS.toMillis(1))).showProgress()
        verify(view, timeout(TimeUnit.SECONDS.toMillis(1))).hideProgress()
        verify(view, timeout(TimeUnit.SECONDS.toMillis(1))).displayInvalidUserIdMessage()
    }

    @Test
    fun testOnClickButtonSetId_fail_check_id_404() {

        `when`(userRepository.resolve()).thenReturn(UserEntity(""))

        `when`(userService.confirmExistingUserId("fail"))
                .thenReturn(Observable.error(HttpException(Response.error<HttpException>(HttpURLConnection.HTTP_NOT_FOUND, ResponseBody.create(MediaType.parse("text/html"), "")))))

        val presenter = InitializePresenter(userRepository, userService)
        presenter.onCreate(view)
        presenter.onResume()
        presenter.onClickButtonSetId("fail")

        verify(view, timeout(TimeUnit.SECONDS.toMillis(1))).showProgress()
        verify(view, timeout(TimeUnit.SECONDS.toMillis(1))).hideProgress()
        verify(view, timeout(TimeUnit.SECONDS.toMillis(1))).displayInvalidUserIdMessage()
    }

    @Test
    fun testOnClickButtonSetId_fail_check_id_network_error() {

        `when`(userRepository.resolve()).thenReturn(UserEntity(""))

        `when`(userService.confirmExistingUserId("fail"))
                .thenReturn(Observable.error(HttpException(Response.error<HttpException>(HttpURLConnection.HTTP_INTERNAL_ERROR, ResponseBody.create(MediaType.parse("text/html"), "")))))

        val presenter = InitializePresenter(userRepository, userService)
        presenter.onCreate(view)
        presenter.onResume()
        presenter.onClickButtonSetId("fail")

        verify(view, timeout(TimeUnit.SECONDS.toMillis(1))).showProgress()
        verify(view, timeout(TimeUnit.SECONDS.toMillis(1))).hideProgress()
        verify(view, timeout(TimeUnit.SECONDS.toMillis(1))).showNetworkErrorMessage()
    }
}
  • これでgradleのtestタスクでUnitTestが回るようになるのでCircleCIなどの上で動かすようにしてあげれば継続的にテストを回せる仕組みができます。

おわり

  • Androidのテストまわりがよくわかってなかったので、一旦立ち止まって調べたり試したりしたことを整理するためにメモを残してみました。
  • サンプルコードをだらだらと貼ってしまいましたが、見返してみるとレイヤーを区切ったことでテストが書きやすくなったなという印象です。まだAndroid歴浅いので、間違ってるところがあるかもしれませんが、もっといいコードを書いてスピード感ある改修ができる作りを目指したいですね。

京都の一軒家借りて開発合宿に行ってきた

開発合宿 Android
  • 2泊3日で京都の一軒家借りて一休.comのエンジニア勢と開発合宿してきました。この開発合宿は東京から離れて泊まりでもくもくして最終日にアニメの聖地巡礼をして帰るという趣のイベントです。今回はちはやふるがテーマということで開発した後にかるたの聖地の近江神宮をぶらりとしてきました。
  • 今年やったことは、2年前くらいからAndroidも開発する人になったものの、テストをちゃんと書けてなかったので去年の合宿で作ったアプリの手直しとEspressoによるUIテストのやり方とか調べながらもくもくしてました。

  • 成果発表したスライドはこれ

github.com

雑感

  • 仲間内の開発合宿は日光、飛騨ときて今回は京都で開催されたのですが、前2回に比べて京都は適当に歩いていても美味そうな飯屋がごろごろあるので誘惑が多いですwとはいえ、東京から離れて非日常に身をおいて集中して開発するという目的は達成できたのでよかったです。
  • 個人の目標はCircleCIの上でだけconnectedAndroidTestがこけるという状態が解消できなかったので微妙に未達でくやしい。。。こいつを解消して合宿で作ったPRをマージしたら僕の合宿は完了という感じです。
  • 飯の写真とか泊まったところとかは他の参加メンバーの記事を見るとよいですね。お好み焼き最高に美味かったです。

rfp.hatenablog.com

blog.shibayan.jp

ちなみに前回

rei19.hatenablog.com

もくもく会でFluxを学んだ with Babel + webpack

JavaScript Babel webpack Flux

仲間内でGWもくもく会やるか!という話になったので、僕のテーマはFluxの考え方を学んで実装できるようにしようということで久しぶりにJS書きました。

やったこと

資料は Flux でぐぐると一番上に出てくる @azu_re さんのこちらのスライドを理解しながら写経 + @ するという感じ。

10分で実装するFlux

Fluxの雑な理解

  • データの流れを一方通行にして表現するアーキテクチャ
  • 登場する要素はComponent、Action、Store、EventEmitter
    • ComponentはActionをCallする + Storeを監視していてStoreの状態が変わったイベントを受け取って自分 = Viewに反映させる
    • ActionはComponentからCallされたActionをEventとして発行する
    • StoreはStateを持つ。Actionを監視していてActionのEventを受け取って自分のStateを更新して状態が変わったEventを発行する
    • EventEmitterはBus的な役割を持っていてObserverパターンの中核を担い、データの流れを繋げる。
  • よく聞くReduxはこの辺をライブラリ化したもの。

書いてみる

書いてみて思ったこと

  • データの流れに制約を持たせることで煩雑になりがちな状態の管理をしやすくできるのかなという印象を持ちました。
  • 仕組み的には新しい物はなく考え方が大事。

次の展開

  • アーキテクチャはざっくり理解したので本業のアプリ開発に展開できるようなAndroidのサンプルを書いてみる予定。クライアントサイドの開発は状態の管理をいかに制御するかがキモですね。

SwiftでiOSアプリ開発の勉強始めた 1

Swift アプリ開発 iOS

今の会社の仕事はAndroidアプリ書いてるけどiOSアプリも書けるようになりたいということでSwiftの勉強を始めました。ブログにはやったこととか覚えたこととかを適当にメモを残していこうかなーと思います。夏くらいまでにはしょぼくてもいいのでアプリを1本ストアに載せるのが目標。iOSアプリ自体はTitaniumで書いたやつは昔リリースしたけど純粋なネイティブアプリは初めてですね。

教科書

  • 特にこれだ!という理由はないけどSwiftは開発スピード早いイメージなので新し目の参考書を選択。2月の中くらいから読み始める。

やったこと

  • PlayGround上でSwiftの文法の確認。評判通り割とモダンな感じで、書いていけば慣れるかなという印象。
  • あとは写経。コードは一応上げている。

GitHub - rei-m/HelloSwift: Swift素振り用

今週までやったこと / 覚えたこと

  • StoryBoard ・・・ これにコンポーネントを配置していく。StoryBoardのコンポーネントをViewControllerのコード上にドラッグするとそのコンポーネントの参照が取れたり、イベントを設定できたりする。AndroidfindViewByIdのようなことをGUIでやっているぽい。
  • Outlet / Action ・・・ コンポーネントの参照はOutlet、イベントはActionと定義されているみたい。作成した後にOutletの名前を変えたりするとビルドエラーになるのでその前に接続を解除する必要がある。コードからStroyBoardへの接続もできる。
  • UIViewController ・・・ 名前の通りViewController。これが画面単位にあるのかな。viewDidLoad は画面の起動時に必ず呼ばれるみたいなので、Viewのイベントや値の設定などはここで行う。Androidと同じでVCに色々仕事をつめ込まないことが作る時のコツになりそう。
  • UIButton、UILabel etc etc ・・・ UIResponder - UIView が親クラスになっていて、UIViewが表示に関する機能を提供している。
  • AutoLayout ・・・ 多様なデバイスサイズと画面の向きにレイアウトをいい感じに表示させるための仕組み。コンポーネントに対して表示位置の制約(Constraints)をつける。コツをつかむまでちょっと時間がかかりそう。
  • segue ・・・ VC間の移動の仕方が定義される。StoryBoard上でVCとVCを繋げることで作成できる。移動のアニメーションの仕方はSegueで定義する。
  • UINavigationController・・・ 複数のシーンをドリルダウンするように移動する。既存のViewと組み合わせて使う。基本的にこれを使うのかなーという印象。
  • ここまでで 「Section 13-4 ナビゲーションコントローラで遷移する」まで。

DroidKaigi 2016に行ってきた 2日目

Android DroidKaigi DroidKaigi2016 レポート

一日目

rei19.hatenablog.com

ということで2日目も行ってきました。2日目も知見にあふれていて最高でしたね!各セッションの資料はこちらのブログであらかたまとめられていたので、今回は参加したセッションと感想を少しだけ残しておこうかなっと思います。

unsolublesugar.com

unsolublesugar.com

参加セッション

第1部「Support Libraryノススメ」 + 第2部「Support Library Internal」 @yuichi_araki さん

  • Googleのエンジニア @yuichi_araki さんによるSupport Libraryの解説でした。Androidに触り始めのころに思った疑問がかなりすっきり。基本的にSupportLibraryを使えば良いということがわかったのは収穫でした。

生まれ変わったUI Automatorを使いこなす @sumio_tym さん

  • UIまわりのテストの知見が少なかったのでこちらを選びました。UIのテストは基本はEspressoを使ってそれでカバーできないところはUI Automatorを使うとよいとのこと。自分のアプリでもテスト書かないとなあ。

Advanced Kotlin for Android @ngsw_taro さん

  • 最近、個人的にホットなKotlinのセッション。Kotlinでアプリを一本書いたけど、後半のExtensionを使ったハックはそんな書き方あるんだ!という感じで参考になりました。Kotlinは書いてて気持ちいいんですよね。

  • ぽろっとTwitterで聞きたかったこと流したら速攻で反応あってびっくりしたw

Android実プロダクトへのKotlinの導入事例 @boohbah さん

  • 連続でKotlinのセッション。こちらは実際にJavaからKotlinに書きなおした事例ということでした。移行の中でハマったことなどが紹介されていて、前のセッションとは違った知見が得られましたね。個人的にはすでにJavaで最適化されているアプリはあえて書きなおさなくてもいいかなーと思っている派ですけど、でもKotlinで書きたい。。。

Androidエンジニアになって2年の学び @ryugoo_ さん

  • 僕もAndroidさわって1年ちょいで割と同じような環境だったので、わかるわ~と思いながら聞いてました。一人プルリクからのセルフレビュー、マージをえんえんと繰り返す感じですね。わかります。規約類は充実させる前にそのプロジェクトは抜けてしまったけど、きっと同じようなことをしてたんだろうな。

Fireside chat @公式アプリのリリースにかかわったみなさん

  • 今回のDroidKaigiの公式アプリに関わった方々でディスカッション。とりあえずIssueたてたらPRが速攻来たみたいなエピソードを聞いてただただOSSすごいって感じました。公式アプリ自体、腕の立つエンジニアたちの知見が集まっていて超勉強になりました。来年は自分もPR送りたいなあ。あとKotlinで書きなおしてみようとも思ったりしました。

まとめ

  • 控えめにいって最高でした。去年は開発が佳境だったこともあり参加できなかったのですが、今年は参加できて本当に良かったと思いました。セッションを聞いて、へー、なるほどすごい!で終わらせるのではなく、実際の開発に活かしたり自分の中の知識として使えるようになるまでがDroidKaigiかなという認識なので、インプットからのアウトプットを心がけるようにします。

  • とりあえず帰ってから書いたもの

qiita.com

  • こっちはKotlinのセッションで聞いた内容をちょっとだけ反映

github.com

DroidKaigi 2016に行ってきた 1日目

Android DroidKaigi DroidKaigi2016 レポート

Androidの開発者向けのお祭り DroidKaigi に参加してきたレポ。去年は行けなかったけど、今年は参加できた!写真撮ろうと思ったけどスマホの充電器を忘れてしまってご臨終な感じだったのでほとんどテキスト。。。

参加セッション

基調講演 : OSSの動向を捉えた実装方針 @wasabeef さん

  • CyberAgentさんで作っているAndroidアプリで使っているライブラリの話を中心に、昨今よく使われるライブラリを紹介。Rxでリスト操作してるところはStreamに置き換えた方がいいかな。Androidはこの辺のライブラリを知っているか知らないかで作りが変わるので聞けてよかったです。

Master of Canvas @amyu_san さん

  • Canvasを使った面白いアニメーションの作り方の話。Androidはアニメーション苦手なイメージあったけど、いろいろ出来るんだなということがわかってよかったです。ちょうど仕事で必要そうな感じでタイムリー。

Android Lintで正しさを学ぼう @Nkzn さん

  • 割と初学者向けの内容で、あー同じことやったわwという内容でした。./gradle lint でlintかけられるの知らなかったので自分のGradle力低いな。。。と反省。仕事で作ってるやつにとりあえず流してみようかなと思います。ちょっとコワイ。

Android Dev Tools Knowledge @operandoOS さん

スライド上がってなかったので雑なメモ

# 便利なコマンド
- 簡単にadbの環境を用意する
    - brew install android-sdk (caskかも?)
- adb-peco
    - 複数のAndroid端末を同時に繋いでるときに便利
- input text
    - adb shell input text droidkaigi
    - adb shellでどのエミュに繋ぐか選べるので↑のコマンドで選択した端末にテキストを送りこめる
- dumpsys
    - system serviceをdumpできる
    - adb shell dumpsys | grep "DUMP OF SERVICEIS"
    - adb shell dumpsys activity top でViewの構造やら見える
    - adb shell dumpsys activity activities | grep apk
- settings
    - 設定にどんな値が入っているか確認できる
- screenrecord
    - 画面録画機能
- systrace
    - パフォーマンスを見るためのツール
    - HTMLでレポートを出力できる
- atrace

- Android-Command-Note
    - いろいろのっけてる
- AndroidSHell
    - もうちょっと細かく載っている
- dexcount-gradle-plugin
- gradle-version-plugin
    - 使っているライブラリの更新をチェックしてくれる
    - gradle dependencyUpdates
- build-time-tracker-plugin
- gradle-android-command-plugin

# Android Studio Plugin
- Android wifi adb
- ADB Idea
    - studio内からadbコマンド打てる
- adb commander for android
- eventbus-intellij-plugin
- android-postfix-plugin
- Google Developers color scheme

# その他のツール
- androidtool-mac
- tonkotsu
- vysor
    - 端末のミラーリングしてくれる
- Android SDK Search
- DPI Calculator
- Android Resource Navigator
- Material Terminal
    - アプリの中でshell使える
- materialdoc.com
    - 公式ページのサンプルをアプリにした
- DesignOverlay
    - 神ツールっぽい
- View Debug

# どこでこの情報を集めるか
- コード読む
- Google+を覗く
- Twitter
- GitHub
  • adb系のコマンドやgradleのコマンドをひたすら紹介する内容でしたがこの辺が勉強不足な僕的にはすごいよかったです。少しずつ使いこなしていきたいですね。

Dagger2とRealmを利用したモダンな開発 @experopero さん

  • RealmとDagger2で実際の運用での使い方を聞けたのはよかったです。Dagger2はいまいち正解がわからない。。。とりあえずSubComponent使ってみようかと思いました。

Androidの省電力について考える 中西 良明さん

スライド上がってなかったので雑なメモ

# 電力使用量削減の歴史
- Android 6.0で大変革が起こった
    - 5系までは開発者側に省電力の仕組みの実装が委ねられたが6系から標準な仕様として実装された
- 以前は通信キャリアごとで対策が取られていた
    - 端末メーカーレベルでもあった
- Job Scheduler(5.0〜)
- Fused Location Provider

# 6.0からの話
- 強制的に電力消費を抑える仕組み
    - Doze
        - 一定の条件を満たすとDozeモードに入る。たまに起きる
        - setAlermClockはDozeを突破できる。が、推奨されなさそう
        - GCMはNWが閉じるので受け取れないが受け取れるようにする指定もある
    - App Standby
        - アプリが一定期間使われていないとアプリはスタンバイ状態に置かれる
        - スタンバイになったアプリは1日1回しかネットーワークに繋がらなくなる
- 電力的に行儀が悪いとStoreから落とされる可能性がある
- 5.0以上はJobSchedulerを以下はAlarmManagerを使うとよいのでは。めんどくさかったらAlarmManagerだけでも。
- adbを使うことで強制的にDozeモードにすることができる
  • この辺、あまり消費電力を意識したことないので聞けてよかったです。とりあえずAlarmManagerのAPIから調べてみよう

実践!Android Studioプラグイン開発 @konifar さん

スライド上がってなかったので雑なメモ

# プラグインの話
- IntelijでJava6開発
- pluginのプロジェクトを作成
- Eventに応じたActionクラスを作成
- 基本的な導入はQiitaに投稿したぞい
    - 簡単なことはすぐできる

# 実践的な話
## 覚えておくべきクラスと役割
- AnActionEvent
    - プラグインのActionに関連するさまざまな情報を扱うクラス
- Project
    - プロジェクトの情報がとれる。例えばSDKのバージョンによって生成するコードを変える。。。ということなど
- Editor
    - Editorの情報を取ることができる。選択中のテキストを取得するなど
- Document
    - EditorからDocumentを取得する。リスナーなど仕込めて、入力に応じてなんかさせる、などできる
- PsFile

### COmponent
- Dialog
    - DialogWrapperを継承して使う
- Editor Hint
    - HintManager
- Notification
    - Notification.Busを使ってNotifyする。これは更新確認で使ったりするのでデファクトで入れる

## 悩んだ時の調べ方
- 公開されているコードを読むのが早い
- indellij-sdk-docs
    - 基本的にはこれを見ると大丈夫そう
- android-parcelable-intellij-plugin
    - Parcelableを自動生成するぷらぎん
- android-drawable-importer-intellij-plugin
    - 実践的な内容は全て詰まっている
- adb-idea
    - adbコマンドをAndroid Studio上から簡単に実行できる
- Android Studio Code
    - 最終兵器。GitHubには上がっていないが別の場所にある

# まとめ
- 基本的なクラスは覚えておく
- わからないときは似たコードを読んでみる
  • アプリ作るとき、Koniferさんのプラグインめっちゃお世話になってるので、自分でも何か作れるようになりたいなーと思って聞いたけど、事前知識を仕入れることができてありがたかったです。おそらく知らないで突撃したらすごい時間かかりそうな感じ。。。

  • 余談

2日目に続く。。。

  • スタッフの方々お疲れ様でした。明日もよろしくお願いいたします。