みんからきりまで

きりみんです。

PresenterとMockitoで今度こそAndroidのUIロジックにテストが書きたい!!

いろいろあって最近また設計について考えています。
AndroidでMVPを使用した設計は色々な人が紹介していますが、記事によって定義がそれぞれ異なっていたり、具体的にどうやってテストコードを書けばいいのかイメージ出来なかったりしてモヤモヤしていました。
以前自分で試したものもUIロジックへの厳密なテストを書くのを目的にしていましたが、実装が面倒過ぎて全然現実的ではありませんでした。

しかし、仕事で実際にMVPベースの汎用的な設計を考える必要があったため、今更ながらMVPをベースにどう実装すれば負担が大きくならずちゃんとテストが書ける設計になるのかを改めて色々試行錯誤しています。
その結果、それなりに実用的な設計になってきたんじゃないかなと思うので一旦進捗を共有します。

クラス構成

基本的には android10/Android-CleanArchitecture · GitHub を参考にしています。

クラス構成のイメージは以下の図のような形になります。
オレンジの部分が画面と一体一で作られるクラスで、緑の部分がその他のクラスです。

f:id:kirimin:20151116214304p:plain

特徴としてはViewをInterfaceとして定義する事と、Viewと一対一になるクラスを各レイヤーごとに定義しハブとする事でモック化しやすくしているところです。
Presenterが直接Activityなどを保持するとPresenterとViewの責務の切り分けが曖昧になり、Viewのモックも作りにくくなってしまうためViewは抽象化します。
一方でPresenterやUseCaseは抽象化しなくてもモック化が容易で抽象化するメリットがあまり無いためInterfaceは作成しません。

実装サンプル

MVPの概念はまあ色々なサイトで説明されているので、実際に個人で開発しているmitsumineというはてぶリーダーアプリに適用した実装例を貼ります。
mitsumineが元々Kotlinで書かれているためサンプルもKotlinです。
(はてなブログにKotlinのシンタックスハイライトが無くて辛い)

View(Interface)

ViewにはActivity・Fragmentのメソッドを全て定義し、Presenterから呼び出せるようにします。

interface FeedView {
    fun initViews()
    fun showRefreshing()
    fun dismissRefreshing()
    fun setFeed(feedList: List<Feed>)
    fun removeItem(feed: Feed)
    fun clearAllItem()
    fun startEntryInfoView(url: String)
    fun sendUrlIntent(url: String)
    fun sendShareUrlIntent(title: String, url: String)
    fun sendShareUrlWithTitleIntent(title: String, url: String)
    fun initListCell(holder: FeedAdapter.ViewHolder, feed: Feed)
    fun loadThumbnailImage(holder: FeedAdapter.ViewHolder, url: String)
    fun loadFaviconUrl(holder: FeedAdapter.ViewHolder, url: String)
}

Fragment(Viewの実体)

Viewを実装ActivityやFragmentです。
Presenterを保持しています。
ActivityやFragmentが行う役割は、ライフサイクルなどのイベントをPresenterに伝える事と、Presenterからの指示で画面を更新する事です。
基本的にはレイアウトやAndroidAPIへのアクセス処理しか書かず、if文やfor文などのロジックが無くなるのが理想です。

public class FeedFragment : Fragment(), FeedView, SwipeRefreshLayout.OnRefreshListener, View.OnClickListener, View.OnLongClickListener {

    companion object {
        public fun newFragment(category: Category, type: Type): FeedFragment {
            val fragment = FeedFragment()
            val bundle = Bundle()
            bundle.putSerializable(Category::class.java.canonicalName, category as Serializable)
            bundle.putSerializable(Type::class.java.canonicalName, type as Serializable)
            fragment.arguments = bundle
            return fragment
        }
    }

    private var adapter: FeedAdapter? = null
    private val presenter: FeedPresenter = FeedPresenter()

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        return inflater.inflate(R.layout.fragment_feed, container, false)
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        val category = arguments.getSerializable(Category::class.java.canonicalName) as Category
        val type = arguments.getSerializable(Type::class.java.canonicalName) as Type
        presenter.onCreate(this, FeedUseCase(FeedRepository(context, category, type)));
    }

    override fun onDestroyView() {
        presenter.onDestroy()
        super.onDestroyView()
    }

    override fun onRefresh() {
        presenter.onRefresh()
    }

    override fun onClick(v: View) {
        presenter.onClick(v.id, v.tag as Feed)
    }

    override fun onLongClick(v: View): Boolean {
        return presenter.onLongClick(v.id, v.tag as Feed)
    }

    override fun initViews() {
        view.swipeLayout.setColorSchemeResources(R.color.blue, R.color.orange)
        view.swipeLayout.setOnRefreshListener(this)
        view.swipeLayout.setProgressViewOffset(false, 0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 24f, resources.displayMetrics).toInt())
        adapter = FeedAdapter(activity.applicationContext, presenter)
        view.feedListView.adapter = adapter
    }

    override fun setFeed(feedList: List<Feed>) {
        adapter!!.addAll(feedList)
    }

    override fun showRefreshing() {
        view.swipeLayout.isRefreshing = true
    }

    override fun dismissRefreshing() {
        view.swipeLayout.isRefreshing = false
    }

    override fun clearAllItem() {
        adapter!!.clear()
    }

    override fun removeItem(feed: Feed) {
        adapter!!.remove(feed)
    }

    override fun sendUrlIntent(url: String) {
        startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
    }

    override fun startEntryInfoView(url: String) {
        val intent = Intent(activity, EntryInfoActivity::class.java)
        intent.putExtras(EntryInfoActivity.buildBundle(url))
        startActivity(intent)
    }

    override fun sendShareUrlIntent(title: String, url: String) {
        val share = Intent(Intent.ACTION_SEND)
        share.setType("text/plain")
        share.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET)
        share.putExtra(Intent.EXTRA_SUBJECT, title)
        share.putExtra(Intent.EXTRA_TEXT, url)
        startActivity(share)
    }

    override fun sendShareUrlWithTitleIntent(title: String, url: String) {
        val share = Intent(Intent.ACTION_SEND)
        share.setType("text/plain")
        share.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET)
        share.putExtra(Intent.EXTRA_TEXT, title + " " + url)
        startActivity(share)
    }

    override fun initListCell(holder: FeedAdapter.ViewHolder, feed: Feed) {
        holder.card.tag = feed
        holder.card.setOnClickListener(this)
        holder.card.setOnLongClickListener(this)
        holder.share.tag = feed
        holder.share.setOnClickListener(this)
        holder.share.setOnLongClickListener(this)
        holder.title.text = feed.title
        holder.content.text = feed.content
        holder.domain.text = feed.linkUrl
    }

    override fun loadThumbnailImage(holder: FeedAdapter.ViewHolder, url: String) {
        Picasso.with(context).load(url).into(holder.thumbnail)
    }

    override fun loadFaviconUrl(holder: FeedAdapter.ViewHolder, url: String) {
        Picasso.with(context).load(url).into(holder.favicon)
    }
}

Adapter

ListViewのAdapterにもViewのPresenterを持たせ、getViewなどのイベントをPresenterに渡します。
その際にViewHolderとItemもPresenterに渡し、ViewHolderの更新はView(Activity・Fragment)内のメソッドで行います。
このようにする事でUIロジックPresenter、画面の更新をViewに集約されAdapterの肥大化を防ぐことが出来ます。

public class FeedAdapter(context: Context, val presenter: FeedPresenter) : ArrayAdapter<Feed>(context, 0) {

    override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
        val view: View
        if (convertView == null) {
            view = LayoutInflater.from(context).inflate(R.layout.row_feed, null)
            val holder = ViewHolder(cell)
            view.tag = holder
        } else {
            view = convertView
        }
        presenter.onGetView(view.tag as ViewHolder, getItem(position));
        return cell
    }

    class ViewHolder(feedView: View) {
        val card: View = feedView.findViewById(R.id.FeedFragmentCardView)
        val thumbnail: ImageView = feedView.findViewById(R.id.FeedFragmentImageViewThumbnail) as ImageView
        val favicon: ImageView = feedView.findViewById(R.id.FeedFragmentImageViewFavicon) as ImageView
        val share: ImageView = feedView.findViewById(R.id.FeedFragmentImageViewShare) as ImageView
        val title: TextView = feedView.findViewById(R.id.FeedFragmentTextViewTitle) as TextView
        val content: TextView = feedView.findViewById(R.id.FeedFragmentTextViewContent) as TextView
        val domain: TextView = feedView.findViewById(R.id.FeedFragmentTextViewDomain) as TextView
    }
}

Presenter

PresenterではViewとUseCaseを持ちます。
Presenterの役割はViewからのイベントを受け取り、表出判定などのUIロジックを行いViewへ画面更新指示を返す事です。
UIに絡まない業務ロジックやデータの取得などはUseCaseクラスに任せます。

class FeedPresenter : Subscriber<List<Feed>>() {

    var view: FeedView? = null
    var useCase: FeedUseCase? = null

    fun onCreate(feedView: FeedView, feedUseCase: FeedUseCase) {
        this.view = feedView
        this.useCase = feedUseCase

        view?.initViews()
        view?.showRefreshing()
        useCase?.requestFeed(this)
    }

    fun onDestroy() {
        view = null
        useCase?.unSubscribe()
    }

    fun onRefresh() {
        view?.clearAllItem()
        view?.showRefreshing()
        useCase?.requestFeed(this)
    }

    override fun onNext(feedList: List<Feed>) {
        view?.setFeed(feedList)
    }

    override fun onError(e: Throwable?) {
        view?.dismissRefreshing()
    }

    override fun onCompleted() {
        view?.dismissRefreshing()
    }

    fun onClick(viewId: Int, feed: Feed) {
        when (viewId) {
            R.id.FeedFragmentCardView -> {
                view?.sendUrlIntent(feed.linkUrl)
            }
            R.id.FeedFragmentImageViewShare -> {
                if (useCase!!.isShareWithTitleSettingEnable()) {
                    view?.sendShareUrlWithTitleIntent(feed.title, feed.linkUrl)
                } else {
                    view?.sendShareUrlIntent(feed.title, feed.linkUrl)
                }
            }
        }
    }

    fun onLongClick(viewId: Int, feed: Feed): Boolean {
        when (viewId) {
            R.id.FeedFragmentCardView -> {
                if (useCase!!.isUseBrowserSettingEnable()) {
                    view?.sendUrlIntent(feed.entryLinkUrl)
                } else {
                    view?.startEntryInfoView(feed.linkUrl)
                }
                return true
            }
            R.id.FeedFragmentImageViewShare -> {
                if (useCase!!.isShareWithTitleSettingEnable()) {
                    view?.sendShareUrlIntent(feed.title, feed.linkUrl)
                } else {
                    view?.sendShareUrlWithTitleIntent(feed.title, feed.linkUrl)
                }
                return true
            }
        }
        return false
    }

    fun onGetView(holder: FeedAdapter.ViewHolder, feed: Feed) {
        view?.initListCell(holder, feed);
        if (!feed.thumbnailUrl.isEmpty()) {
            view?.loadThumbnailImage(holder, feed.thumbnailUrl)
        }
        if (!feed.faviconUrl.isEmpty()) {
            view?.loadFaviconUrl(holder, feed.faviconUrl)
        }
    }
}

UseCase

UseCaseではRepositoryクラスを持ち、画面固有の業務ロジックを担当します。
シンプルな要件のアプリであればUseCaseの処理はほとんどRepositoryに移譲されるかもしれません。
アプリの仕様が複雑だったりAPI側で柔軟な対応が出来ないアプリであれば、UseCaseで画面固有のリスト操作や文字列操作などを行います。

open class FeedUseCase(val repository: FeedRepository) {

    val subscriptions = CompositeSubscription()

    open fun isUseBrowserSettingEnable() = repository.isUseBrowserSettingEnable

    open fun isShareWithTitleSettingEnable() = repository.isShareWithTitleSettingEnable

    open fun requestFeed(subscriber: Observer<List<Feed>>) {
        subscriptions.add(repository.requestFeed()
                .subscribeOn(Schedulers.newThread())
                .observeOn(AndroidSchedulers.mainThread())
                .filter { feed -> !FeedUtil.contains(feed, repository.readFeedList) && !FeedUtil.containsWord(feed, repository.ngWordList) }
                .toList()
                .subscribe(subscriber))
    }

    fun unSubscribe() {
        subscriptions.unsubscribe()
    }
}

Repository

RepositoryではAPIやデータベースなどへのデータアクセスを担当します。
RepositoryクラスをUseCaseクラスとは別に定義する事でUseCaseのテストをしやすくします。

class FeedRepository(val context: Context, val category: Category, val type: Type) {

    fun requestFeed(): Observable<Feed> = FeedApi.requestCategory(context, category, type)

    val readFeedList: List<Feed>
        get() = FeedDAO.findAll()

    val ngWordList: List<String>
        get() = NGWordDAO.findAll()

    val isUseBrowserSettingEnable: Boolean
        get() = PreferenceManager.getDefaultSharedPreferences(context).getBoolean(context.getString(R.string.key_use_browser_to_comment_list), false)

    val isShareWithTitleSettingEnable: Boolean
        get() = PreferenceManager.getDefaultSharedPreferences(context).getBoolean(context.getString(R.string.key_is_share_with_title), false)
}

Presenterのテスト

テストにはMockitoというモックライブラリを使用します。
http://mockito.org/

Mockitoはかなり便利なライブラリで、自動でクラスやインターフェイスからモックインスタンスを生成出来て、そのモックインスタンスメソッドが何回呼ばれたかなどを判定するテストを書く事が出来ます。

MockitoでViewやUseCaseのモックを作りメソッドが呼びだされたかを確認する事でPresenterへのテストを非常に簡単に書く事が出来ます。
UseCaseへのテストも同じように書けるはずです。

import static org.mockito.Mockito.*;

@RunWith(AndroidJUnit4.class)
public class FeedPresenterTest {

    FeedView viewMock;
    FeedUseCase useCaseMock;

    @Before
    public void setup() {
        // モックインスタンスを生成
        viewMock = mock(FeedView.class);
        useCaseMock = mock(FeedUseCase.class);
    }

    @Test
    public void onCreateTest() {
        FeedPresenter presenter = new FeedPresenter();
        presenter.onCreate(viewMock, useCaseMock);

        // verifyメソッドでモックオブジェクトのそれぞれのメソッドが呼ばれたかをチェックしている
        verify(viewMock, times(1)).initViews();
        verify(viewMock, times(1)).showRefreshing();
        verify(useCaseMock, times(1)).requestFeed(presenter);
    }

    @Test
    public void onRefreshTest() {
        FeedPresenter presenter = new FeedPresenter();
        presenter.onCreate(viewMock, useCaseMock);
        presenter.onRefresh();
        verify(viewMock, times(1)).clearAllItem();
        verify(viewMock, times(2)).showRefreshing();
        verify(useCaseMock, times(2)).requestFeed(presenter);
    }

    @Test
    public void onNextTest() {
        FeedPresenter presenter = new FeedPresenter();
        presenter.onCreate(viewMock, useCaseMock);
        List<Feed> list = new ArrayList<>();
        presenter.onNext(list);
        verify(viewMock, times(1)).setFeed(list);
    }

    @Test
    public void onErrorTest() {
        FeedPresenter presenter = new FeedPresenter();
        presenter.onCreate(viewMock, useCaseMock);
        presenter.onError(new Throwable());
        verify(viewMock, never()).setFeed(any(List.class));
        verify(viewMock, times(1)).dismissRefreshing();
    }

    @Test
    public void onCompleteTest() {
        FeedPresenter presenter = new FeedPresenter();
        presenter.onCreate(viewMock, useCaseMock);
        presenter.onCompleted();
        verify(viewMock, never()).setFeed(any(List.class));
        verify(viewMock, times(1)).dismissRefreshing();
    }

    @Test
    public void onClickFeedTest() {
        // whenメソッドで指定したメソッドの戻り値を指定している
        when(useCaseMock.isShareWithTitleSettingEnable()).thenReturn(true);
        Feed feed = new Feed();
        feed.setLinkUrl("http://test");

        FeedPresenter presenter = new FeedPresenter();
        presenter.onCreate(viewMock, useCaseMock);
        presenter.onClick(R.id.FeedFragmentCardView, feed);
        verify(viewMock, times(1)).sendUrlIntent("http://test");
    }

    @Test
    public void onFeedLongClickTest() {
        Feed feed = new Feed();
        feed.setLinkUrl("http://test");
        feed.setEntryLinkUrl("http://entry");

        when(useCaseMock.isUseBrowserSettingEnable()).thenReturn(true);
        FeedPresenter presenter = new FeedPresenter();
        presenter.onCreate(viewMock, useCaseMock);
        presenter.onLongClick(R.id.FeedFragmentCardView, feed);
        verify(viewMock, times(1)).sendUrlIntent("http://entry");
        verify(viewMock, never()).startEntryInfoView("http://test");

        when(useCaseMock.isUseBrowserSettingEnable()).thenReturn(false);
        presenter.onLongClick(R.id.FeedFragmentCardView, feed);
        verify(viewMock, times(1)).sendUrlIntent("http://entry");
        verify(viewMock, times(1)).startEntryInfoView(("http://test"));
    }

    @Test
    public void onFeedShareClickTest() {
        Feed feed = new Feed();
        feed.setTitle("title");
        feed.setLinkUrl("http://test");
        feed.setEntryLinkUrl("http://entry");

        when(useCaseMock.isShareWithTitleSettingEnable()).thenReturn(true);
        FeedPresenter presenter = new FeedPresenter();
        presenter.onCreate(viewMock, useCaseMock);
        presenter.onClick(R.id.FeedFragmentImageViewShare, feed);
        verify(viewMock, times(1)).sendShareUrlWithTitleIntent("title", "http://test");
        verify(viewMock, never()).sendShareUrlIntent("title", "http://test");

        when(useCaseMock.isShareWithTitleSettingEnable()).thenReturn(false);
        presenter.onClick(R.id.FeedFragmentImageViewShare, feed);
        verify(viewMock, times(1)).sendShareUrlWithTitleIntent("title", "http://test");
        verify(viewMock, times(1)).sendShareUrlIntent("title", "http://test");
    }

    @Test
    public void onFeedShareLongClickTest() {
        Feed feed = new Feed();
        feed.setTitle("title");
        feed.setLinkUrl("http://test");
        feed.setEntryLinkUrl("http://entry");

        when(useCaseMock.isShareWithTitleSettingEnable()).thenReturn(true);
        FeedPresenter presenter = new FeedPresenter();
        presenter.onCreate(viewMock, useCaseMock);
        presenter.onLongClick(R.id.FeedFragmentImageViewShare, feed);
        verify(viewMock, never()).sendShareUrlWithTitleIntent("title", "http://test");
        verify(viewMock, times(1)).sendShareUrlIntent("title", "http://test");

        when(useCaseMock.isShareWithTitleSettingEnable()).thenReturn(false);
        presenter.onLongClick(R.id.FeedFragmentImageViewShare, feed);
        verify(viewMock, times(1)).sendShareUrlWithTitleIntent("title", "http://test");
        verify(viewMock, times(1)).sendShareUrlIntent("title", "http://test");
    }
}

所感

f:id:kirimin:20151115235813p:plain

すでに実装されているコードをこの設計に書き換えるのはわりと大変ですが、最初からMVPを前提に実装すればそれほど手間は掛からないはずで、むしろ業務の開発規模であれば設計が明確になりテストが書きやすくなるメリットの方が大きくなるんじゃないかなぁと思っています。

Viewのメソッドをどのくらい細かく分けてどの程度の粒度でテストを書くかなどは試行錯誤が必要そうですが、とりあえずテストが書ける環境が整えば試行錯誤もしやすいのではないでしょうか。
サンプルではViewでUseCaseやRepositoryをnewしていますが、Viewのテストも書きたければDaggerなどを使った方がいいかもしれません。

L字デスク便利

IKEAで買ったそれなりに横幅のあるデスクを3年くらい使っていたんだけど、せっかく横に広くてもデュアルディスプレイだとディスプレイをくの字に配置するので結局半分くらいがデッドスペースになって毎回キーボードなどを片付けないと他の作業が出来ないという問題があった。

そこで思い切ってL字デスクを買ってみたら期待通りとても便利だったという共有(自慢)です。

f:id:kirimin:20151003233744j:plain

ディスプレイを2枚置いてもプラス机一つ分くらいのスペースがあるので、ご飯を食べたりMacBookを開いたり絵を描いたりするのも快適。
ちなみにディスプレイはデスクトップPC用だけど、片方のディスプレイはMacBookとの併用で、もう片方のディスプレイはWiiUとの併用です。

僕が買ったのはこれだけど「L字 デスク」でググると色々出てくるので適当なので良さそう。

item.rakuten.co.jp

机、椅子に比べると安いので不便を感じているのなら買った方がいいと思う。
でも古い机を捨てるのが一番大変そう。
(僕は部屋にまだ余裕があったので古い机は棚として活躍しています)

シルバーウィーク進捗

9連休でした。

外出

実家に帰って3泊した
年々田舎の景色が恋しくなっている気がする。

旅行で軽井沢へ行った
ずっと雨で残念だったけど温泉入ったりして最高だった。

ゲーム

ロロナのアトリエ(3DS版)買った
やっぱりアトリエ面白いのでVita買ってやりまくりたい。

Cities Skylines After Dark買った
もう誰もSkylinesやってない…。

ハッピーエンドは欲しくない読んだ
増田のやつ。

帰ってきたヒトラー読んだ
映画化すると聞いたので。

ルワンダ中央銀行総裁日記読んでる
話題になっていたので。まだ1/3くらいだけどたしかに面白い。

技術

クックパッドのインターン資料のiOSアプリ開発入門をやった
とても簡潔で分かりやすくて良かった。(説明端折ってる部分もあるので完全にiOS初見だと出来ない)
iOS開発、ようやく初歩的な事は出来るようになってきたけど、SwiftよりもStoryboardとかAutoLayoutとかXcodeGUIに混乱してなかなか進まなくてストレスが溜まる。
いい参考書教えてほしい。

その他

25歳になった
20歳からの5年間は本当に刺激的で面白いものだったので、次の5年間も退屈なものにならないように日々精進したい。

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代の頃は常に眠くて本当にぼーっとした人間だったんだけど、カフェイン取り始めてからようやくうたた寝せずに仕事が出来るようになったので、カフェイン止めると仕事失いそうなんだけど、元々そういう体質なのであれば無理やり身体動かしてるという事なので命削っている感ある。
健康を金で買いたい…。