みんからきりまで

きりみんです。

2015年のAndroid開発はKotlinで決まりだったのか?

これはKotlin Advent Calendar 2015、13日目の記事です。
残念ながらポエム(ネタ)枠です。

Kotlinは流行っているのか

3月にこんな記事を書きました。 kirimin.hatenablog.com

この中で、「2015年のAndoird開発、選択肢の一つとしてKotlinはかなりアリなんじゃないでしょうか」などと適当な事を書きましたが、当時まだネタ言語感が強かったKotlinはその後どうだったのでしょうか。
日々Kotlin界隈をウォッチしている身としては考えるまでもないテーマですが、世の中にはそうじゃない人の方が多いと思うので、改めて最近のKotlin機運の高まりについて振り返ってみたいと思います。

Googleトレンドで確認

Kotlinがどのくらい流行っているのかGoogleトレンドを使って確認してみました。(雑)

まずはKotlin単体で見てみます。全世界だとプログラミング言語以外のKotlinも含まれてしまう気がするので日本国内に設定しています。

f:id:kirimin:20151212213355p:plain

それまで観測範囲外だったのが今年の4月頃から上がり始め10月辺りから一気に上昇していますね。
※ちなみに12月の数値は観測途中のため不安定です。

続いて他の言語との比較です。こちらも国内に絞っています。

f:id:kirimin:20151212213406p:plain

やはり他のライバル言語と比べると低いですが、順調に上昇していて比べられるレベルまでは来ています。

最後に日本以外の地域とも比べてみます。

f:id:kirimin:20151212213751p:plain

他の地域でも今年始め頃から上がり始めていますが、なんと日本が一番Kotlinトレンド力が高いという衝撃の結果に。
日本から世界にKotlinの可愛さを伝えていけると良いですね。

AdventCalendarでのKotlin人気

有志によって毎年開催されているKotlin AdventCalendar、その歩みを見てみましょう。
※ぜひ順番に開いてみてください。

■2012

Kotlin Advent Calendar 2012 (全部俺) : ATND

■2013

Kotlin Advent Calendar 2013 - Adventar

■2014

Kotlin Advent Calendar 2014 - Adventar

■2015

Kotlin Advent Calendar 2015 - Adventar

毎年どんどん賑やかになってゆき、今年のKotlin Advent Calendarはあっという間に全日埋まってしまいました。
また、今年はKotlin Advent Calendar以外にも各企業などのAdvent CalendarでもKotlinについて語られている記事が複数あります。

Yahoo! JAPAN Tech Advent Calendar 2015 1日目
次世代言語Kotlinを使ったAndroid開発とヤフーの新技術との向き合い方 - Yahoo! JAPAN Tech Blog

■CyberAgent エンジニア Advent Calendar 2015 2日目
新卒入社2年目エンジニアがGitHubにAndroidのライブラリを公開してみて感じたこと|サイバーエージェント 公式エンジニアブログ

ドワンゴ Advent Calendar 2015 2日目
今からKotlin - Qiita

■Fenrir Advent Calendar 2015 11日目
かわいいプログラミング言語 Kotlin を既存の Android プロジェクトに導入したい (フェンリル | デベロッパーズブログ)

最近ではKotlinを実際の業務で採用しているという話も聞くようになってきました。
Kotlinが個人的趣味や一部ウケに留まらず、(主にAndroid界隈で)注目を集めている事が分かりますね。

Kotlinのバージョン動向

Kotlinは今年も次々とバージョンアップを繰り返し、とうとう現在はbeta3になっています。
言語仕様も固まりつつあり、1.0のリリースはもう間近という話です。
1.0がリリースされれば突然言語仕様が大きく変わって対応を求められるようなリスクはかなり減るはずなので、一気に業務での導入も進むのではないでしょうか。

個人的にはどうだったか

僕個人にとっては2015年はKotlinが一番アツい一年でした。
残念ながら業務でKotlinを使うには至っていませんが、仕事の業務内容がコーディングから遠ざかりなかなか技術的なモチベーションが捻出出来ない中で、Kotlinのおかげで技術的に楽しい一年を過ごせた気がします。

個人で開発しているMitsumineというAndroidアプリを100%Kotlinに書き換え、Kotlin勉強会で発表したりもしました。

Kotlinの魅力

元々Kotlinに興味を持ったきっかけは、「RxJavaなどが流行ってるのにAndroidでJava8が使えないのが辛いけどScala導入は色々上手くいかなくて何か代替は無いのか」という消極的なものでしたが、Kotlinの仕様を知っていくにつれ次第に「Kotlinは他のどの言語にも負けないくらい素晴らしい魅力を持った言語なのでは?」という気持ちが芽生えてきました。

Optionalの仕様なんかはJava8、ScalaSwiftなんかと比べてもかなり洗練されていますし、Javaとの連携のしやすさや学習コストの低さ、落とし穴の少なさなどとにかく堅実で業務利用を強く意識した言語だと思います。
Kotlin用のライブラリやフレームワークが少ないのも、Javaの資産をそのまま問題なく使えるからという面もあるのかもしれません。

2016年のKotlinは

来年はおそらくKotlin1.0がリリースされると思うので、爆発的に導入が進むのではないかな、と妄想しています。

1月にはAndroidでKotlin勉強会というイベントもありますし、

connpass.com

DroidKaigi2016でもKotlinの話が聞けるようです。

droidkaigi.github.io

2016年のKotlinの活躍が今から楽しみですね!

うっかり4kモニタとグラボ買った

うっかりした

自宅ではずっとPCモニタとして21インチのフルHDモニタを2枚使っていたんだけど、仕事でMacBook Pro(Retina)を使っていたりスマホフルHDだったりして、だんだん画面の粗さが気になり始めてきた。
随分安くなってきたし4kにしてみたいなーと思っていたんだけど、4kモニタ買うならグラボも一緒に買う必要があってタイミングを伺っていた。

そんな時になんとなくBlackOps3を買ってみたらスペック不足で全然まともに動かず、グラボを買い換える機運が一気に高まってしまった。
それから1週間くらいずっとグラボと4kモニタについて調べていたら、とうとう我慢できなくなって木曜日の夜中にポチった。 土曜日の朝に届いた。

買ったモニタ

これです。

kakaku.com

まあ普通に一番売れ筋のものなんだけど。
スペックも評判も悪くなくて、とにかく安かったのでこれになった。
唯一ネットで散々酷評されている部分があって、画面設定の操作がボタンじゃなくてセンサーになっていて入力切替などの操作がめちゃくちゃやりづらいとの事。
僕はWiiUと繋いで頻繁に入力切替をするのでかなり迷ったんだけど、他に同価格帯で納得できる製品が無かったので結局これにした。

実際に使ってみて画質や動作も全く問題がないけど入力切替はやっぱり使いづらかった。指がセンサーに近付いただけで反応するので誤動作しまくりだし本当に信じられないくらい操作しにくくてどうしてこうなったんだという感じ。
でも練習すれば使えない程ではないので価格を考えれば許せる。
イカのマッチング中に素早く画面切替えてPC操作したりするのはちょっと厳しいけど。

買ったグラボ

これです。

kakaku.com

GTX960にするかGTX970にするかで散々迷って、更にどの製品にするかでまた散々迷った。
せっかく4kにしたのにほとんどのゲームが4kでプレイ出来ないとさすがに寂しいのでかなり奮発して970にした。
メーカーは特にこだわりがないので適当に安くてファンがあまりうるさく無さそうなのにした。

4k解像度の使用感

広い

きりみんさん(@kirimin)が投稿した写真 -

元々あった2枚のモニタと合わせてトリプルディスプレイになってしまった。

4kはやっぱり綺麗で、Retinaに慣れてしまっているので感動という事はないけど、27インチでもMacスマホと同じようにドットが気にならないレベルになったので安心感がある。
そのままだとUIが小さすぎるのでWindowsの設定で文字サイズを140%にした。
これでエクスプローラやブラウザは大きくなってくれるんだけど、アプリケーションによってはUIが豆粒みたいになってちょっと辛い。
27インチという画面サイズは大きすぎて見難いかと思ったけど、逆にこのくらい大きい方が見やすい事が分かった。

4kとフルHDの併用は思ったより難儀で、4k用にアプリの文字サイズなどを設定したウィンドウを隣のモニタに持っていくと600×800を思い出すようなサイズになってしまったり、拡大したウィンドウ幅を縮小させるのが面倒だったりする。
なんか半年後くらいにはもう一枚4kモニタを買ってそうな予感がする。

ゲームはほとんどのゲームが4k + 最高設定でも問題なく動作した。
意外だったのはあんなに重かったBO3で、4k + 最高設定で25FPSくらい出て、2kくらいだったらマルチも問題ないくらいサクサク動いてくれた。 一番感動したのはCites Skylinesで、元々AAが弱くてギザギザがかなり目立っていたのが全く気にならないレベルになった上に60FPSでサクサク動くようになって改めてじっくり街を眺めて楽しんだ。

https://i.gyazo.com/d609c5853fd9e01e88c2c9e1d757a237.png

4kはPCモニタとしてオススメ出来るか

今すぐ買った方がいい。というかあと2枚ほしい。

※みんなが買えば世のアプリがもっと4kに最適化されるはず。

追記

画面分割アプリ入れてスーパーハカーっぽくなった

きりみんさん(@kirimin)が投稿した写真 -

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年間も退屈なものにならないように日々精進したい。