みんからきりまで

きりみんです。

Kotlinのリスト操作関数まとめ

長らくご参照頂いたこのエントリですが、最新版をQiitaに書いたので今後はそちらをご利用ください

qiita.com

Kotlinのリスト操作関数、便利なんだけど関数型言語の知見が無い為いつも欲しい機能を探すのに時間を奪われる。
なので適当に調べて備忘メモ。
Kotlin独自ってものはあんまりない気がするので他の言語でもだいたい同じっぽい。

変換系

map

・リストの中身を1つずつ処理して別のリストに変換する

arrayOf(1, 2, 3).map { num -> "num:" + num }

1, 2, 3

"num:1", "num:2", "num:3"

flatMap

・2次元リストをフラット(1次元)リストに変換する ※mapのリストを分解する版

arrayListOf(arrayListOf(1, 2, 3), arrayListOf(4, 5, 6)).flatMap { num -> num }

[[1, 2, 3], [4, 5, 6]]

[1, 2, 3, 4, 5, 6]

抽出系

filter

・trueを返した要素だけ抽出する

arrayOf(1, 2, 3).filter { num -> num != 2 }

1, 2, 3

1, 3

filterNot

・falseを返した要素だけ抽出する

take

・指定した数だけ要素を抽出する

arrayOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10).take(4)

1, 2, 3, 4, 5, 6, 7, 8, 9, 10

1, 2, 3, 4

takeLast

・後ろから数えて指定した数だけ要素を抽出する

arrayOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10).takeLast(4)

1, 2, 3, 4, 5, 6, 7, 8, 9, 10

6, 7, 8, 9, 10

drop

・先頭から指定した数だけ要素を捨てる

arrayOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10).drop(4)

1, 2, 3, 4, 5, 6, 7, 8, 9, 10

5, 6, 7, 8, 9, 10

single

・trueを返した要素が1つならそのまま返し、空もしくは複数ならExceptionを投げる

arrayOf(1, 2, 3, 1).single { num -. num == 1 }

1, 2, 3

1

1, 1, 2

IllegalArgumentException: Collection contains more than one matching element.

singleOrNull

・trueを返した要素が1つならそのまま返し、空もしくは複数ならnullを返す

arrayOf(1, 2, 3).singleOrNull { num -. num == 1 }

1, 2, 3

1

1, 1, 2

null

max

・最大値を返す

arrayOf(1, 2, 3).max()

1, 2, 3

3

min

・最小値を返す

arrayOf(1, 2, 3).max()

1, 2, 3

1

maxBy, minBy

・返した要素を使用して最大値, 最小値を返す

arrayOf("a", "aaa", "aa").maxBy { s -> s.length() }

"a", "aaa", "aa"

"aaa"

distinct

・重複を削除する

arrayOf(1, 2, 1, 2).distinct()

1, 2, 1, 2

1, 2

distinctBy

・返した要素で重複を削除する

arrayOf("abc", "de", "fgh", "i", "jk").distinctBy { s -> s.length() }

"abc", "de", "fgh", "i", "jk"

"abc", "de", "i"

first

・最初の要素を返す

last

・最後の要素を返す

ソート系

sort

・並び替える

arrayOf(2, 1, 3).sort()

2, 1, 3

1, 2, 3

sortBy

・返した要素を使用して並び替える

arrayOf("a", "aaa", "aa").sortBy { s -> s.length() }

"a", "aaa", "aa"

"a", "aa", "aaa"

sortDescendingBy

・返した要素を使用して降順に並び替える

reverse

・要素を逆順にする

・並び替える

arrayOf(1, 2, 3).reverse()

1, 2, 3

3, 2, 1

判定系

all

・全ての要素が条件に一致すればtrue

arrayOf(1, 2, 3, 4).all { num -> num < 10 }

true

any

・どれかの要素が条件に一致すればtrue

arrayOf(1, 20, 300, 4000).any { num -> num < 10 }

true

isEmpty, isNotEmpty

・リストが空ならtrue、Notは逆

none

・条件に一致する要素が無ければtrue

arrayOf(1, 2, 3, 4).none { num -> num > 10 }

true

計算系

sum

・合計値を返す

average

・平均値を返す

reduce, reduceRight

・要素に対して再帰的に関数を適用し、一つの値にまとめる ・reduceは前から、reduceは後ろから要素を取り出す

arrayOf("a", "b", "c", "d", "e", "f", "g").reduce { s1, s2 -> s1 + s2 }

"a", "b", "c", "d", "e", "f", "g"

"abcdefg"

※この例では
"a" + "b" →"ab" + "c" →"abc" + "d" のように順番に要素を足している

fold, foldRight

・初期値を与えられるreduce

arrayOf("a", "b", "c", "d", "e", "f", "g").fold("first:") { s1, s2 -> s1 + s2 }

"a", "b", "c", "d", "e", "f", "g"

"first:abcdefg"

合成系

plus

・2つのリストを結合する

arrayOf(1, 2, 3).plus(arrayOf(4, 5))

1, 2, 3
4, 5

1, 2, 3, 4, 5

zip

・2つのリストの要素をペアにして返す。あまりは捨てられる

arrayOf(1, 2, 3, 4).zip(arrayOf("a", "b", "c"))

1, 2, 3, 4
"a", "b", "c"

[[1, "a"], [2, "b"], [3, "c"]]

marge

・2つのリストの要素に関数を適用して合成する。あまりは捨てられる

arrayOf("a", "b", "c", "d").merge(arrayOf("1", "2", "3")) { s1, s2 -> s1 + ":" + s2 }

"a", "b", "c", "d"
"1", "2", "3"

"a:1", "b:2", "c:3"

処理系

forEach

・要素に対して順番に関数を実行する

forEachIndexed

・要素に対して順番に関数を実行する。要素と一緒にindexも受け取れる

最近

日記です。

最近読んだ本

  • 人月の神話
  • ピープルウェア
  • TeamGeek(再読)

なんか最近チームコミュニケーションとかマネジメントとかそういう事をよく考えていて、チーム開発におけるコミュニケーションの重要性を日々感じるし、自分がそういうものをもっと学んで周りに良い影響を与えられる人間になりたいという気持ちがある。
だけど一方で自分は人間関係で人一倍精神力を消耗するし、もっと毎日静かな家や喫茶店で集中してコードを書き続ける事で金と承認を得て生きていきたいという気持ちもある。
社交的に生きてゆく事に対して頑張れる気持ちの時もあればもう頑張れないという気持ちの時もあって安定しない。むずかしい。

iOS

Android開発者のためのSwift入門という本を買ったのでiOSの勉強をしてる。
書いてるのはSwiftなんだけど情報収集のためにはやっぱりObjective-Cを理解する必要があってObjective-Cと格闘してた。
Objective-Cの構文はどうしても僕の脳が理解を拒否しようとするんだけど、ラベルと引数名が別にある事と1つ目の引数名にはラベルがない事を理解して少しずつ冷静に受け止められるようになってきた。

iOSをやっていると上手く動かない部分があっても、それがUIKitの問題なのかXCodeの問題なのかSwiftの問題なのかすらなかなか掴めなくて、初めてEclipseJavaを勉強した頃の事を思い出した。

Minecraftで猛省

以前からよくニコニコでMinecraftの実況動画を視聴しているんだけど、僕は自分で生産自動化施設とかトラップタワーとかそういうものを作ったりしていなくて、そういうのをガンガンやってる動画を観ていたら自分はエンジニアなのにそういう効率化のための手間を惜しまないみたいな気持ちが欠けているから駄目なんだみたいな気持ちになってすごく反省した。
仕事でも僕はアプリ開発しかやったことがないのであんまり小さいスクリプトとかを作るスキルが無くて、どちらかというとファイルの編集とか手動で頑張っちゃう方なので、本当によくないと思った。
そういうのちゃんと練習してパターンをインプットして突発にサッと出来るようにならねばならぬと強く感じた。

カフェイン依存

珈琲を飲まないととにかく一日中眠くて仕事にならないんだけど、最近胃腸と肝臓がとても弱っていて厳しい。
常に胃もたれや腹痛があって酷い時は精神までも蝕まれて抑うつ状態になってしまう。
元々カフェイン取る習慣がなかった10代の頃は常に眠くて本当にぼーっとした人間だったんだけど、カフェイン取り始めてからようやくうたた寝せずに仕事が出来るようになったので、カフェイン止めると仕事失いそうなんだけど、元々そういう体質なのであれば無理やり身体動かしてるという事なので命削っている感ある。
健康を金で買いたい…。

MVPっぽい設計でAndroidのUIロジックにテストを書くのを試してみた

最近Android界隈ではMVPという言葉をよく聞く気がします。

konifar.hatenablog.com

tech.recruit-mp.co.jp

個人的にも気になっていて、特に「テストが書きやすくなる」という部分がとても気になります。
ところがどうもテストが書かれたよさげなサンプルコードがなかなか見つからない…。

そこで、全然テストが書けていないAndroid開発者の一人として、本当にUIロジックのテストが可能なものなのか、実際にMVPっぽいものを書いて試してみました。
ちなみに僕はユニットテストの知見が一切ありません。

MVP

Model View Presenterの事です。
詳細は僕もまだそこまでちゃんと理解出来ていないので、上記の記事などを参照してください。
ざっくり言うと、Controllerの代わりにPresenterというもの作り、PresenterがUIロジックを担う事でViewなのかControllerなのか扱いが曖昧だったActivityやFragmentを完全に値を受け渡すだけのViewとして扱い取り替え可能なものにするような感じのようです。
上記の記事ではそこにDDDでの設計パターンを取り入れ、UseCaseなど定義しているようです。

書いてみた

以前Kotlinのサンプルで作ったGithubのユーザー情報を表示するだけのアプリを書き換えてみました。
github.com

あ、もちろんKotlinです。

クラス概要

Presenterを作りテストを書くというのを目的として書きました。

  • UserInfoActivity:ViewにあたるActivity。
  • IUserInfoActivity:UserInfoActivityのInterface。
  • UserInfoPresenter:ActivityとUseCaseを持ちUIロジックを担う。
  • UserInfoUseCase:データを取得するビジネスロジックを担う。
  • IUserInfoUseCase:UserInfoUseCaseのInterface。

Activity

Activityには表出判定などのUIロジックは書かず、Viewに値を渡し、Presenterにイベントを渡すだけの存在です。

public class UserInfoActivity : AppCompatActivity(), IUserInfoActivity {

    companion object {
        fun buildBundle(id: String): Bundle {
            val bundle = Bundle()
            bundle.putString("id", id)
            return bundle
        }
    }

    var presenter: UserInfoPresenter? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super<AppCompatActivity>.onCreate(savedInstanceState)
        setContentView(R.layout.activity_user_info)

        presenter = UserInfoPresenter(this, UserInfoUseCase())
        presenter!!.onCreate(RequestQueueSingleton.get(getApplicationContext()))
    }

    override fun onDestroy() {
        presenter!!.onDestroy()
        super<AppCompatActivity>.onDestroy()
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        if (android.R.id.home == item.getItemId()) {
            finish()
        }
        return super<AppCompatActivity>.onOptionsItemSelected(item)
    }

    override public fun initActionBar(id: String) {
        val actionBar = getSupportActionBar()
        actionBar.setDisplayHomeAsUpEnabled(true)
        actionBar.setHomeButtonEnabled(true)
        actionBar.setTitle(id)
    }

    override public fun setParentLayoutVisibility(visibility: Int){
        parentLayout.setVisibility(visibility)
    }

    override public fun getIdFromBundle(): String {
        return getIntent().getStringExtra("id")
    }

    override public fun setName(name: String) {
        nameText.setText(name)
    }

    override public fun setId(id: String) {
        idText.setText(id)
    }

    override public fun setLocation(location: String) {
        locationText.setText(location)
    }

    override public fun setLocationVisibility(visibility: Int) {
        locationText.setVisibility(visibility)
    }

    override public fun setCompany(company: String) {
        companyText.setText(company)
    }

    override public fun setCompanyVisibility(visibility: Int) {
        companyText.setVisibility(visibility)
    }

    override public fun setLink(link: String) {
        linkText.setText(link)
    }

    override public fun setLinkVisibility(visibility: Int) {
        linkText.setVisibility(visibility)
    }

    override public fun setMail(mail: String) {
        mailText.setText(mail)
    }

    override public fun setMailVisibility(visibility: Int) {
        mailText.setVisibility(visibility)
    }

    override public fun setIcon(url: String) {
        Picasso.with(this).load(url).fit().into(iconImage)
    }

    override public fun addLanguage(languageName: String, languageCount: String, languageStartCount: String) {
        val inflater = LayoutInflater.from(this);
        val languageView = inflater.inflate(R.layout.activity_user_info_language, null) as LinearLayout
        val languageNameText = languageView.findViewById(R.id.userInfoLanguageName) as TextView
        val languageCountText = languageView.findViewById(R.id.userInfoLanguageCount) as TextView
        val languageStartCountText = languageView.findViewById(R.id.userInfoLanguageStarCount) as TextView
        languageNameText.setText(languageName)
        languageCountText.setText(languageCount)
        languageStartCountText.setText(languageStartCount)
        languageLayout.addView(languageView)
    }

    override public fun addRepository(iconImageResource: Int, repositoryName: String, starCount: String, language: String, description: String, htmlUrl: String) {
        val inflater = LayoutInflater.from(this);
        val repositoryView = inflater.inflate(R.layout.activity_user_info_repositories, null) as LinearLayout
        val repositoryIconImage = repositoryView.findViewById(R.id.userInfoRepositoryIcon) as ImageView
        val repositoryNameText = repositoryView.findViewById(R.id.userInfoRepositoryName) as TextView
        val repositoryDescriptionText = repositoryView.findViewById(R.id.userInfoRepositoryDescription) as TextView
        val repositoryStarCountText = repositoryView.findViewById(R.id.userInfoRepositoryStarCount) as TextView
        val repositoryLanguageText = repositoryView.findViewById(R.id.userInfoRepositoryLanguage) as TextView
        repositoryIconImage.setImageResource(iconImageResource)
        repositoryNameText.setText(repositoryName)
        repositoryStarCountText.setText(starCount)
        repositoryLanguageText.setText(language)
        repositoryDescriptionText.setText(description)
        repositoryView.findViewById(R.id.userInfoRepositoryBrowser).setOnClickListener {
            startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(htmlUrl)))
        }
        repositoryLayout.addView(repositoryView)
    }

    override public fun networkErrorHandling() {
        Toast.makeText(getApplicationContext(), "Error. Username does not exist?", Toast.LENGTH_SHORT).show()
        finish()
    }
}

そして、メソッドは全てInterfaceに定義します。

interface IUserInfoActivity {

    public fun initActionBar(id: String)

    public fun getIdFromBundle(): String

    public fun setParentLayoutVisibility(visibility: Int)

    public fun setName(name: String)

    public fun setId(id: String)

    public fun setLocation(location: String)

    public fun setLocationVisibility(visibility: Int)

    public fun setCompany(company: String)

    public fun setCompanyVisibility(visibility: Int)

    public fun setLink(link: String)

    public fun setLinkVisibility(visibility: Int)

    public fun setMail(mail: String)

    public fun setMailVisibility(visibility: Int)

    public fun setIcon(url: String)

    public fun addLanguage(languageName: String, languageCount: String, languageStartCount: String)

    public fun addRepository(iconImageResource: Int, repositoryName: String, starCount: String, language: String, description: String, htmlUrl: String)

    public fun networkErrorHandling()
}

Presenter

PresenterはActivityとUseCaseのInterfaceを持ち、Activityのメソッドを呼び出す事でUIロジックを記述します。

public class UserInfoPresenter(val view: IUserInfoActivity, val useCase: IUserInfoUseCase) {

    private val subscriptions = CompositeSubscription();

    public fun onCreate(queue: RequestQueue) {
        val id = view.getIdFromBundle()
        view.initActionBar(id)
        useCase.requestUserInfo ({ pair ->
            val user = pair.first
            val repositories = pair.second
            view.setParentLayoutVisibility(View.VISIBLE)
            view.setName(user.name)
            view.setId(user.login)
            if (isDataEmpty(user.location)) {
                view.setLocationVisibility(View.GONE)
            } else {
                view.setLocation(user.location)
                view.setLocationVisibility(View.VISIBLE)
            }
            if (isDataEmpty(user.company)) {
                view.setCompanyVisibility(View.GONE)
            } else {
                view.setCompany(user.company)
                view.setCompanyVisibility(View.VISIBLE)
            }
            if (isDataEmpty(user.blog)) {
                view.setLinkVisibility(View.GONE)
            } else {
                view.setLink(user.blog)
                view.setLinkVisibility(View.VISIBLE)
            }
            if (isDataEmpty(user.email)) {
                view.setMailVisibility(View.GONE)
            } else {
                view.setMail(user.email)
                view.setMailVisibility(View.VISIBLE)
            }
            if (!isDataEmpty(user.avatarUrl)) {
                view.setIcon(user.avatarUrl)
            }

            useCase.getLanguages(repositories).forEach { language ->
                view.addLanguage(languageName = language.first,
                        languageCount = language.second.count().toString(),
                        languageStartCount = language.second.map { repo -> repo.stargazersCount }.sum().toString())
            }
            useCase.sortRepositories(repositories).forEach { repository ->
                val repoIcon = if (repository.fork) R.drawable.ic_call_split_grey600_36dp else android.R.drawable.ic_menu_sort_by_size
                view.addRepository(iconImageResource = repoIcon,
                        repositoryName = repository.name,
                        starCount = repository.stargazersCount.toString(),
                        language = if (isDataEmpty(repository.language)) "" else repository.language,
                        description = repository.description,
                        htmlUrl = repository.htmlUrl)
            }
        }, { e -> view.networkErrorHandling() }, id, queue, subscriptions)
    }

    public fun onDestroy() {
        subscriptions.unsubscribe()
    }

    private fun isDataEmpty(data: String): Boolean {
        return data.equals("null") || data.isEmpty()
    }
}

UseCase

UseCaseはPresenterから呼び出されるビジネスロジックです。
この場合はGithubのユーザー情報の取得操作ですが、実際の処理は更に具体的なロジッククラスのUserApiに移譲しています。
MVPに書き換える前はActivityが直接UserApiのメソッドを呼ぶ形でした。

public class UserInfoUseCase : IUserInfoUseCase {

    override fun requestUserInfo(onSuccess: (Pair<User, List<Repository>>) -> Unit, onError: (Throwable) -> Unit,
                                 id: String, queue: RequestQueue, subscriptions: CompositeSubscription) {
        val userRequest = UsersApi.request(id = id, requestQueue = queue)
        val repositoryRequest = RepositoryApi.request(id = id, page = 1, requestQueue = queue).toList()
        subscriptions.add(Observable
                .zip(userRequest, repositoryRequest, { user, repositories -> Pair(user, repositories) })
                .subscribeOn(Schedulers.newThread())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe({ response ->
                    onSuccess(response)
                }, { e ->
                    onError(e)
                }))
    }

    override fun getLanguages(repositories: List<Repository>): List<Pair<String, List<Repository>>> {
        return repositories
                .filter { repo -> !repo.language.equals("null") }
                .groupBy { repo -> repo.language }
                .toList().sortDescendingBy { language -> language.second.count() }
    }

    override fun sortRepositories(repositories: List<Repository>): List<Repository> {
        return repositories
                .sortDescendingBy { repo -> repo.stargazersCount }
                .sortBy { repo -> repo.fork }
    }
}

UIロジックにテストを書く

さて、ここからがようやく本題です。
PresenterのUIロジックにテストを書いてみます。

@RunWith(AndroidJUnit4::class)
public class UserInfoPresenterTest {

    Test
    public fun setNameTest() {
        var isSuccess = false
        val presenter = UserInfoPresenter(
                object : UserInfoActivityMock() {
                    override fun setName(name: String) {
                        assertThat(name, `is`("name"))
                        isSuccess = true
                    }
                }, object : UserInfoUseCaseMock(UserMockBuilder.defaultCase()) {})

        presenter.onCreate(RequestQueueSingleton.get(InstrumentationRegistry.getContext()))
        assertTrue(isSuccess)
    }

    Test
    public fun setIdTest() {
        var isSuccess = false
        val presenter = UserInfoPresenter(
                object : UserInfoActivityMock() {
                    override fun setId(id: String) {
                        assertThat(id, `is`("login"))
                        isSuccess = true
                    }

                }, object : UserInfoUseCaseMock(UserMockBuilder.defaultCase()) {})

        presenter.onCreate(RequestQueueSingleton.get(InstrumentationRegistry.getContext()))
        assertTrue(isSuccess)
    }

    Test
    public fun setLocationTest() {
        var isSuccess = false
        val presenter = UserInfoPresenter(
                object : UserInfoActivityMock() {
                    override fun setLocation(location: String) {
                        assertThat(location, `is`("location"))
                        isSuccess = true
                    }

                    override fun setLocationVisibility(visibility: Int) {
                        assertThat(visibility, `is`(View.VISIBLE))
                    }
                }, object : UserInfoUseCaseMock(UserMockBuilder.defaultCase()) {})

        presenter.onCreate(RequestQueueSingleton.get(InstrumentationRegistry.getContext()))
        assertTrue(isSuccess)
    }

    Test
    public fun locationNullCaseTest() {
        var isSuccess = false
        val presenter = UserInfoPresenter(
                object : UserInfoActivityMock() {
                    override fun setLocationVisibility(visibility: Int) {
                        assertThat(visibility, `is`(View.GONE))
                        isSuccess = true
                    }
                }, object : UserInfoUseCaseMock(UserMockBuilder.nullCase()) {})

        presenter.onCreate(RequestQueueSingleton.get(InstrumentationRegistry.getContext()))
        assertTrue(isSuccess)
    }

    Test
    public fun setCompanyTest() {
        var isSuccess = false
        val presenter = UserInfoPresenter(
                object : UserInfoActivityMock() {
                    override fun setCompany(company: String) {
                        assertThat(company, `is`("company"))
                        isSuccess = true
                    }

                    override fun setCompanyVisibility(visibility: Int) {
                        assertThat(visibility, `is`(View.VISIBLE))
                    }
                }, object : UserInfoUseCaseMock(UserMockBuilder.defaultCase()) {})

        presenter.onCreate(RequestQueueSingleton.get(InstrumentationRegistry.getContext()))
        assertTrue(isSuccess)
    }

    Test
    public fun companyNullCaseTest() {
        var isSuccess = false
        val presenter = UserInfoPresenter(
                object : UserInfoActivityMock() {
                    override fun setCompanyVisibility(visibility: Int) {
                        assertThat(visibility, `is`(View.GONE))
                        isSuccess = true
                    }
                }, object : UserInfoUseCaseMock(UserMockBuilder.nullCase()) {})

        presenter.onCreate(RequestQueueSingleton.get(InstrumentationRegistry.getContext()))
        assertTrue(isSuccess)
    }

    Test
    public fun setLinkTest() {
        var isSuccess = false
        val presenter = UserInfoPresenter(
                object : UserInfoActivityMock() {
                    override fun setLink(link: String) {
                        assertThat(link, `is`("blog"))
                        isSuccess = true
                    }

                    override fun setLinkVisibility(visibility: Int) {
                        assertThat(visibility, `is`(View.VISIBLE))
                    }
                }, object : UserInfoUseCaseMock(UserMockBuilder.defaultCase()) {})

        presenter.onCreate(RequestQueueSingleton.get(InstrumentationRegistry.getContext()))
        assertTrue(isSuccess)
    }

    Test
    public fun linkNullCaseTest() {
        var isSuccess = false
        val presenter = UserInfoPresenter(
                object : UserInfoActivityMock() {
                    override fun setLinkVisibility(visibility: Int) {
                        assertThat(visibility, `is`(View.GONE))
                        isSuccess = true
                    }
                }, object : UserInfoUseCaseMock(UserMockBuilder.nullCase()) {})

        presenter.onCreate(RequestQueueSingleton.get(InstrumentationRegistry.getContext()))
        assertTrue(isSuccess)
    }

    Test
    public fun setMailTest() {
        var isSuccess = false
        val presenter = UserInfoPresenter(
                object : UserInfoActivityMock() {
                    override fun setMail(mail: String) {
                        assertThat(mail, `is`("email"))
                        isSuccess = true
                    }

                    override fun setMailVisibility(visibility: Int) {
                        assertThat(visibility, `is`(View.VISIBLE))
                    }
                }, object : UserInfoUseCaseMock(UserMockBuilder.defaultCase()) {})

        presenter.onCreate(RequestQueueSingleton.get(InstrumentationRegistry.getContext()))
        assertTrue(isSuccess)
    }

    Test
    public fun mailNullCaseTest() {
        var isSuccess = false
        val presenter = UserInfoPresenter(
                object : UserInfoActivityMock() {
                    override fun setMailVisibility(visibility: Int) {
                        assertThat(visibility, `is`(View.GONE))
                        isSuccess = true
                    }
                }, object : UserInfoUseCaseMock(UserMockBuilder.nullCase()) {})

        presenter.onCreate(RequestQueueSingleton.get(InstrumentationRegistry.getContext()))
        assertTrue(isSuccess)
    }

    Test
    public fun setIconTest(){
        var isSuccess = false
        val presenter = UserInfoPresenter(
                object : UserInfoActivityMock() {
                    override fun setIcon(url: String) {
                        assertThat(url, `is`("avatar_url"))
                        isSuccess = true
                    }
                }, object : UserInfoUseCaseMock(UserMockBuilder.defaultCase()) {})

        presenter.onCreate(RequestQueueSingleton.get(InstrumentationRegistry.getContext()))
        assertTrue(isSuccess)
    }

    Test
    public fun iconNullCaseTest(){
        val presenter = UserInfoPresenter(
                object : UserInfoActivityMock() {
                    override fun setIcon(url: String) {
                        fail("")
                    }
                }, object : UserInfoUseCaseMock(UserMockBuilder.nullCase()) {})

        presenter.onCreate(RequestQueueSingleton.get(InstrumentationRegistry.getContext()))
    }
}

分かりにくいですが、PresenterにActivityとUseCaseのモック実装を渡し、ActivityのView操作メソッドが正しく呼び出されているかを検知する事でUIロジックをテストしています。
Activityのメソッドを細かく分け、Interfaceに定義したのはこのためです。

所感

勢いで一気に書き換えたので設計にはかなり再考の余地があると思いますが、初めてAndroidのUIロジックに対して意味のあるレベルのテストを書くイメージが見えた気がします。

f:id:kirimin:20150711213902p:plain

この粒度でテストが書けていれば、条件による値の渡し間違いやメソッドの呼び忘れといったデグレは検知する事が出来そうです。

ただし、この粒度でActivityにメソッドを作ってInterfaceを定義してテストを書いて…とやっていくのは正直かなり冗長というか精神的にも工数的にも辛そう。
ここまで細かくしなくてもいい気もするけど、Activityのメソッド粒度を荒くしてしまうとActivityにUIロジックが入り込んでしまい、Presenterのテストがあまり意味がなくなってしまうし…。
今回は取得したデータを単純に一覧表示するだけのサンプルだったから余計に不毛っぽさが出てしまったのかもしれない。

この辺りはテストライブラリや設計の工夫などでもう少しどうにか出来るのだろうか。
もうちょっと色々試して工夫してみたい。

仕事にもイカにも疲れてユーロ トラック シミュレーターやってる

最近仕事が会議ばっかりだったり要件定義だけして実装出来なかったりして疲弊してる。
6月の初めにWiiUとスプラトゥーンを買ってめちゃくちゃやりまくってたんだけど、根が負けず嫌いなので精神が弱ってくるとゲームでまでチームでアレコレみたいなのもだんだん疲れてきて、Steamのセールで買ったEuro Truck Simulator 2をやり始めた。

store.steampowered.com

どういうゲームかというと黙々と交通ルールを守りながらトラックを運転してヨーロッパで貨物輸送をする。
レースでもないし、BGMもなく(ストリームラジオを再生する機能がある)エンジンなどの環境音だけが静かに響く。

f:id:kirimin:20150628223816p:plain

これがたいへん癒される。
バックミラーで白線と追い越し車両に注意しながら平坦な高速道路をひたすら走る。
日本の郊外にもありそうな特に面白みのないのどかな景色やそこそこ青い空などを眺めつつ。
ぼーっとプレイしていると時々ラウンドアバウトがあったり海峡トンネルを渡ったりしてワクワクする。

f:id:kirimin:20150628223850p:plain

夜になると普通に真っ暗なので部屋の電気を消してプレイすると無心になれる。

f:id:kirimin:20150628224457p:plain Steamのセールで時々500円くらいになるのでセールで見かけたら買って損はないと思う。
これからはエンジニアとトラック運転手の二足のわらじでやっていこう。

Kotlin勉強会でAndroidアプリをKotlinに書き換えた話をしてきた

Kotlin勉強会で発表してきた。

kotlin.doorkeeper.jp

僕の発表スライドはこれです。

発表内容について

個人で開発してるはてぶクライアントアプリのMitsumineをJavaからKotlinに全置換した話をした。
去年の第2回Kotlin勉強会では僕も含めKotlinをよく知らないという人が多く、発表も入門的な話が中心だった気がしたので、わりとライトな感じの資料にしてしまったけど、会場で聞いたら7,8割くらいがAndroidアプリ開発経験者でKotlinプラグインも既に試しているという方が結構多かったようなので、もう少し内容深堀りして詰め込めばよかったなーと少し後悔。

しかし改めてAndroid界隈でKotlinの注目度が上がっているのだなーというのを感じた。
僕のブログエントリを読んでKotlinやろうと思いましたと言ってくれた方もいて嬉しかった。
もっと勉強して積極的に情報発信していきたい。

勉強会について

職場内以外でLTをするのは2回目だけど結構緊張した。
水曜日に職場の飲み会があって完全に飲み過ぎ、木曜日は二日酔いで仕事早退し、当日の昼くらいまで体調が最悪だったので不穏だったけどなんとか大失敗せずに発表できてよかった。

懇親会でもいろいろな方とお話出来てとても楽しかった。
いつもお世話になってる@ngsw_taroさんにもご挨拶できて本当によかった。
また次のKotlin勉強会にも参加したいし、他の勉強会でもどんどんLTしていきたい。というかどんどんLT出来るくらいもっとめちゃくちゃコード書いて精進したい。

ちなみに書き換えたmitsumineはこれです。
kirimin/mitsumine · GitHub
Mitsumine はてなブックマーククライアント - Google Play の Android アプリ

まとめ