みんからきりまで

きりみんです。

Androidの通知を全部Slackに流してPCでも検知する

ずっと家にいるとあまりスマホを見ないのでLINEとかメールとかゲームの通知とかわりと気付かなくて面倒な事がある。
僕は一人Slackチームを作ってメモなどに利用しているので、そこにスマホの通知を全部流せたら便利だと思った。

IFTTTでそういうのあるかなと思ったけどパッと見た感じ無さそうだった。
サービスごとに個別で頑張ってもいいんだけどそれも面倒なのでもう全部流したい。
探せばそういうアプリありそうだけどよく分からない個人アプリに個人情報垂れ流すのも怖いので自分で作った。

通知の取得方法はこちらを参考にした。

techbooster.org

あとはSlackのWebhookに取得した内容をどんどん投げるだけ。
だいたいこんな感じ。自分が使うだけなので超適当。

    @Override
    public void onNotificationPosted(StatusBarNotification sbn) {
        if(sbn.isOngoing()){
            return;
        }
        String message = sbn.getPackageName() + "\n" + sbn.getNotification().tickerText;
        try {
            post("https://hooks.slack.com/services/xxxxxxxxxxxxxxx", "{\"text\":\"" + message + "\"}");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    String post(String url, String json) throws IOException {
        RequestBody body = RequestBody.create(JSON, json);
        Request request = new Request.Builder()
                .url(url)
                .post(body)
                .build();
        Response response = client.newCall(request).execute();
        return response.body().string();
    }

で、こうなった。

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

とりあえずLINEに気付かない事はなくなりそう。
ただ、なんかアプリによって連続で取得したりテキストが取得出来なかったりするのでイマイチ。
Twitter公式アプリがツイートする度に通知出したりするのでブラックリスト作った方が良さそう。

関係ないけどAndroid Wearはもう一年くらい使ってない。

Android用はてブクライアントMitsumineの2.5.0をリリースした話とか

Mitsumine 2.5.0をリリースしました。
Mitsumineは趣味で開発しているAndroid用のはてなブックマーククライアントアプリです。
久しぶりのバージョンアップなので、開発にあたってのいろいろな話を書いてみようと思います。

Mitsumineについて

とりあえず宣伝しておきます。

play.google.com

Mitsumineは主に自分が使いやすいはてぶアプリとして開発しています。
多分一番のヘビーユーザーは僕だと思う。
はてブを快適に眺めるために必要な機能は一通り実装されています。
特に一覧画面に表示される情報は公式アプリよりも多くなっていて、これは僕が出来るだけ記事を開かずにどんどん流し読みしながら面白そうな記事を探したいからです。
一方でアプリ内ブラウザなどの自分に不要な機能は実装していません。

https://i.gyazo.com/4abefe309e4739222e020fd0176179e1.gif

バージョン2.5.0では前々から放置していた一部URLの読み込みに失敗するバグの修正の他、ブコメについたはてなスターの表示などを実装しました。

Mitsumineと公式アプリ

元々Mitsumineは公式のはてなブックマークアプリがまだあまりメンテナンスされていなかった頃に作ったアプリで、当時の公式アプリはスクロールするだけでクラッシュしたりと自分が使うのに不便すぎたので、自分用アプリとして作成しました。
しかし、最近ははてなにもAndroidエンジニアが増えてきたのか、公式アプリも頻繁にアップデートされるようになってきたようです。(めでたい事ですね)
なのでMitsumineはそろそろ積極的に開発する必要が無くなってきたかなーと思っています。自分が欲しい機能も一通り実装されているので、今後はバグ修正などのアップデートだけになるかもしれません。

まあ、僕は公式アプリよりMitsumineの方が見やすいので使い続けるんだけど。

実装の話

Mitsumineは僕が新しいライブラリや設計を試すためのサンドボックス的なプロジェクトでもあったりします。
例えばMitsumineのコードはKotlinにハマった時に全てKotlinに書き換えていて、今でもKotlinで開発しています。

そんな流れで去年の秋くらいからはMVP設計への全面書き換えを行っていたのですが、すでにコード量がかなり多くなっているMitsumineを全面的にMVPへ書き換える作業は思いのほかダルく、またMVP設計についても書けば書くほど「あれ、これ意味ないんじゃ」「もっとこうした方がいいな」と考えがまとまらず、結局半年近くもひたすら機能とは関係ないリファクタリング作業をやっているというわけの分からない状態になってしまいました。
たとえ趣味であっても、あまり大きいアプリに全面書き換えのような事をすると途中でモチベが死んで辛いという知見を得ました。

ただ、散々試行錯誤したおかげでMVP設計について自分の中でそれなりに妥協点が見つかったような気がします。

MVPについて

元々はCleanArchitectureを参考にして
・View
・Presenter
・UseCase
・Repository
の4レイヤーに分けるという設計でやっていたのですが、だんだんそれだと冗長なだけであまりメリットがない気がしてきて、最終的にはUseCaseを廃止して
・View
・Presenter
・Repository
の3レイヤーになりました。

この場合、ロジックは全てPresenterのレイヤーに収まる事になり、ViewやデータなどAndroidの仕様に依存する部分のみ別レイヤーに分ける事になります。
そしてViewとRepositoryをMockに差し替えたPresenterのテストを書く事で、とりあえずインプットとアウトプットを監視するテストをJUnitで走らせる事が出来ます。(あとは必要に応じてモデルクラスのテストも書く)
PresenterのテストだけだとViewレイヤーでのバグ(リスナーのセットし忘れやViewのレイアウト崩れなど)までは検知できませんが、大きなコストを掛けずにAndroidで書くテストとしてこんなものかなという気がしてます。

Mockitoを利用したPresenterのテスト例

class FeedPresenterTest {

    lateinit var viewMock: FeedView
    lateinit var repositoryMock: AbstractFeedRepository
    val presenter = FeedPresenter()
    val resultMock = listOf(Feed(title = "mock1"), Feed(title = "mock2"))

    @Before
    fun setup() {
        
        viewMock = mock()
        repositoryMock = mock()
        whenever(repositoryMock.requestFeed()).thenReturn(Observable.from(resultMock))
    }

    @Test
    @JvmName(name = "onCreate時にフィードを読み込みセットする")
    fun onCreateTest() {
        presenter.onCreate(viewMock, repositoryMock)
        verify(viewMock, times(1)).initViews()
        verify(viewMock, times(1)).showRefreshing()
        verify(repositoryMock, times(1)).requestFeed()

        verify(viewMock, times(1)).setFeed(resultMock)
        verify(viewMock, times(1)).dismissRefreshing()
    }

    @Test
    @JvmName(name = "PullToRefresh時にフィードを更新する")
    fun onRefreshTest() {
        presenter.onCreate(viewMock, repositoryMock)
        presenter.onRefresh()
        verify(viewMock, times(1)).clearAllItem()
        verify(viewMock, times(2)).showRefreshing()
        verify(repositoryMock, times(2)).requestFeed()

        verify(viewMock, times(2)).setFeed(resultMock)
        verify(viewMock, times(2)).dismissRefreshing()
    }

    @Test
    @JvmName(name = "フィードデータ取得失敗時にインジケータを停止する")
    fun onErrorTest() {
        whenever(repositoryMock.requestFeed()).thenReturn(Observable.error(Exception()))
        presenter.onCreate(viewMock, repositoryMock)
        verify(viewMock, never()).setFeed(anyList())
        verify(viewMock, times(1)).dismissRefreshing()
    }

    @Test
    @JvmName(name = "アイテムクリック時にURLをブラウザで開く")
    fun onItemClick() {
        presenter.onCreate(viewMock, repositoryMock)
        presenter.onItemClick(Feed(linkUrl = "http://test"))
        verify(viewMock, times(1)).sendUrlIntent("http://test")
    }

    @Test
    @JvmName(name = "アイテム長押し時にコメント一覧を開く")
    fun onItemLongClick() {
        val feed = Feed()
        feed.linkUrl = "http://test"
        feed.entryLinkUrl = "http://entry"

        // ブラウザで開く
        whenever(repositoryMock.isUseBrowserSettingEnable).thenReturn(true)
        presenter.onCreate(viewMock, repositoryMock)
        presenter.onItemLongClick(feed)
        verify(viewMock, times(1)).sendUrlIntent("http://entry")
        verify(viewMock, never()).startEntryInfoView("http://test")

        // ネイティブで開く
        whenever(repositoryMock.isUseBrowserSettingEnable).thenReturn(false)
        presenter.onItemLongClick(feed)
        verify(viewMock, times(1)).sendUrlIntent("http://entry")
        verify(viewMock, times(1)).startEntryInfoView("http://test")
    }

    @Test
    @JvmName(name = "シェアボタン押下と長押しでタイトルを付けるかを切り替える")
    fun onFeedShareClick() {
        val feed = Feed()
        feed.title = "title"
        feed.linkUrl = "http://test"
        feed.entryLinkUrl = "http://entry"

        // 押下
        // タイトル入り設定
        whenever(repositoryMock.isShareWithTitleSettingEnable).thenReturn(true)
        presenter.onCreate(viewMock, repositoryMock)
        presenter.onFeedShareClick(feed)
        verify(viewMock, times(1)).sendShareUrlWithTitleIntent("title", "http://test")
        verify(viewMock, never()).sendShareUrlIntent("title", "http://test")

        // タイトル無し設定
        whenever(repositoryMock.isShareWithTitleSettingEnable).thenReturn(false)
        presenter.onFeedShareClick(feed)
        verify(viewMock, times(1)).sendShareUrlWithTitleIntent("title", "http://test")
        verify(viewMock, times(1)).sendShareUrlIntent("title", "http://test")

        // 長押し
        // タイトル入り設定
        `when`(repositoryMock.isShareWithTitleSettingEnable).thenReturn(true)
        val presenter = FeedPresenter()
        presenter.onCreate(viewMock, repositoryMock)
        presenter.onFeedShareLongClick(feed)
        verify(viewMock, times(1)).sendShareUrlWithTitleIntent("title", "http://test")
        verify(viewMock, times(2)).sendShareUrlIntent("title", "http://test")

        // タイトル無し設定
        `when`(repositoryMock.isShareWithTitleSettingEnable).thenReturn(false)
        presenter.onFeedShareLongClick(feed)
        verify(viewMock, times(2)).sendShareUrlWithTitleIntent("title", "http://test")
        verify(viewMock, times(2)).sendShareUrlIntent("title", "http://test")
    }
}
class EntryInfoPresenterTest {

    lateinit var viewMock: EntryInfoView
    lateinit var repositoryMock: EntryInfoRepository
    lateinit var contextMock: Context
    lateinit var resultMock: EntryInfo
    val presenter = EntryInfoPresenter()

    @Before
    fun setup() {
        viewMock = mock()
        repositoryMock = mock()
        contextMock = mock()

        val bookmarks = listOf(
                Bookmark("test1", listOf("TagA"), "", "comment", "", emptyList()),
                Bookmark("test2", emptyList(), "", "", "", emptyList()),
                Bookmark("test3", listOf("TagB", "TagC"), "", "comment", "", emptyList()),
                Bookmark("test4", listOf("TagB"), "", "", "", emptyList())
        )
        resultMock = EntryInfo("testA", 4, "http://sample", "http://thum", bookmarks)
        whenever(repositoryMock.requestEntryInfoApi(any(), any())).thenReturn(Observable.just(resultMock))
    }

    @Test
    @JvmName(name = "onCreate時にページ情報を取得し表示する")
    fun onCreateTest() {
        whenever(repositoryMock.isLogin()).thenReturn(false)
        presenter.onCreate(viewMock, repositoryMock, "http://sample", contextMock)
        verify(viewMock, times(1)).initActionBar()
        verify(repositoryMock, times(1)).requestEntryInfoApi(contextMock, URLEncoder.encode("http://sample", "utf-8"))

        // 取得したものが設定される
        verify(viewMock, times(1)).setEntryInfo(resultMock)
        // 非ログイン時は対象ページのブクマ登録Fragmentは設定されない
        verify(viewMock, never()).setRegisterBookmarkFragment("http://sample")
        // コメントありは2件
        verify(viewMock, times(1)).setCommentCount("2")
        // タグは多い順にカンマ区切り
        Assert.assertEquals(resultMock.tagListString(), "TagB, TagA, TagC")
        verify(viewMock, times(1)).setViewPagerSettings(currentItem = 1, offscreenPageLimit = 2)
    }

    @Test
    @JvmName(name = "ログイン時にはブクマ登録Fragmentが追加される")
    fun onNextTestWithLogin() {
        whenever(repositoryMock.isLogin()).thenReturn(true)
        presenter.onCreate(viewMock, repositoryMock, "http://sample", contextMock)
        verify(viewMock, times(1)).setEntryInfo(resultMock)
        verify(viewMock, times(1)).setRegisterBookmarkFragment("http://sample")
    }
}

コード全体はGitHubにあげているので興味があればどうぞ。(試行錯誤しまくってたのでチグハグになっていますが)

github.com

Kotlinの練習にオセロ作って遊んだ

AndroidのリハビリとKotlinでのリスト操作の練習のために雑なオセロゲームを作った。

https://i.gyazo.com/9d889a7725433631c384fe93e6fffb4e.gif

アルゴリズムは適当だけどKotlinの練習なので出来るだけfor文やvarを使わず関数型プログラミングっぽい感じで書いてみた。Kotlinだとリスト操作がめっちゃやりやすくてたのしい。
そこそこきれいに書けた気がするけど、特に結果をキャッシュして使いまわしたりせず同じ計算処理を何度も行ったりしてるのでパフォーマンス的には微妙そう。

class OseroGame() {

    val BOARD_SIZE = 8
    // 初期配置石
    private val CENTER_LEFT_UP = Place(BOARD_SIZE / 2 - 1, BOARD_SIZE / 2 - 1, Stone.BLACK)
    private val CENTER_LEFT_UNDER = Place(BOARD_SIZE / 2 - 1, BOARD_SIZE / 2, Stone.WHITE)
    private val CENTER_RIGHT_UP = Place(BOARD_SIZE / 2, BOARD_SIZE / 2 - 1, Stone.WHITE)
    private val CENTER_RIGHT_UNDER = Place(BOARD_SIZE / 2, BOARD_SIZE / 2, Stone.BLACK)

    /** 盤の状態を2次元配列で保持 */
    val boardStatus = arrayOfNulls<List<Place>>(BOARD_SIZE).mapIndexed { x, list -> arrayOfNulls<Place>(BOARD_SIZE).mapIndexed { y, place -> Place(x, y, Stone.NONE) } }

    fun getInitialPlaces() = listOf(CENTER_LEFT_UP, CENTER_LEFT_UNDER, CENTER_RIGHT_UP, CENTER_RIGHT_UNDER)

    /** 指定された場所に石を置けるか */
    fun canPut(place: Place) = boardStatus[place.x][place.y].stone == Stone.NONE && getCanChangePlaces(place).isNotEmpty()

    /** 石を置ける全ての場所 */
    fun getAllCanPutPlaces(color: Stone) = boardStatus.flatMap { it }.filter { canPut(Place(it.x, it.y, color)) }

    /** 次のターンで置ける場所がまだ存在するか */
    fun canNext(color: Stone): Boolean = getAllCanPutPlaces(color).isNotEmpty()

    /** 石の数を数える */
    fun countStones(color: Stone) = boardStatus.flatMap { it }.count { it.stone == color }

    /** ゲームが終了しているか */
    fun isGameOver() = !canNext(Stone.BLACK) && !canNext(Stone.WHITE)

    /** ひっくり返せる石のリストを取得 */
    fun getCanChangePlaces(target: Place): List<Place> {
        return searchChangePlacesRight(target)
                .plus(searchChangePlacesLeft(target))
                .plus(searchChangePlacesUp(target))
                .plus(searchChangePlacesUnder(target))
                .plus(searchChangePlacesUpperLeft(target))
                .plus(searchChangePlacesDownRight(target))
                .plus(searchChangePlacesUpperRight(target))
                .plus(searchChangePlacesDownLeft(target))
    }

    /** 置いた石から右方向にひっくり返せる石のリストを返す */
    private fun searchChangePlacesRight(target: Place): List<Place> {
        if (target.x + 1 > BOARD_SIZE - 1) return emptyList()

        val rightPlaces = boardStatus.drop(target.x + 1) // targetより右の列だけ抽出
                .map { it[target.y] } // targetの行だけ取り出す
        return getInsidePlaces(target, rightPlaces)
    }

    /** 置いた石から左方向にひっくり返せる石のリストを返す */
    private fun searchChangePlacesLeft(target: Place): List<Place> {
        if (target.x == 0) return emptyList()

        val leftPlaces = boardStatus
                .take(target.x) // targetより左の列だけを抽出
                .map { it[target.y] } // targetの行だけ取り出す
                .reversed() // 左方向だと辿りにくいのでリスト反転
        return getInsidePlaces(target, leftPlaces)
    }

    /** 置いた石から下方向にひっくり返せる石のリストを返す */
    private fun searchChangePlacesUnder(target: Place): List<Place> {
        if (target.y + 1 > BOARD_SIZE - 1) return emptyList()

        val underPlaces = boardStatus[target.x].drop(target.y + 1) // targetより下の行だけ抽出
        return getInsidePlaces(target, underPlaces)
    }

    /** 置いた石から上方向にひっくり返せる石のリストを返す */
    private fun searchChangePlacesUp(target: Place): List<Place> {
        if (target.y == 0) return emptyList()

        val upPlaces = boardStatus[target.x]
                .take(target.y) // targetより上の行だけ抽出
                .reversed() // 上方向だと辿りにくいのでリスト反転
        return getInsidePlaces(target, upPlaces)
    }

    /** 置いた石から左上方向にひっくり返せる石のリストを返す */
    private fun searchChangePlacesUpperLeft(target: Place): List<Place> {
        if (target.x == 0 || target.y == 0) return emptyList()

        val upperLeftPlaces = boardStatus.flatMap { it }
                .filter { it.x < target.x && it.y < target.y } // targetより左上だけ抽出
                .filter { it.x - it.y == target.x - target.y } // 斜めのライン上だけ抽出
                .reversed()
        return getInsidePlaces(target, upperLeftPlaces)
    }

    /** 置いた石から右下方向にひっくり返せる石のリストを返す */
    private fun searchChangePlacesDownRight(target: Place): List<Place> {
        if (target.x + 1 > BOARD_SIZE - 1 || target.y + 1 > BOARD_SIZE - 1) return emptyList()

        val downRightPlaces = boardStatus.flatMap { it }
                .filter { it.x > target.x && it.y > target.y }
                .filter { it.x - it.y == target.x - target.y } // 斜めのライン上だけ抽出
        return getInsidePlaces(target, downRightPlaces)
    }

    /** 置いた石から右上方向にひっくり返せる石のリストを返す */
    private fun searchChangePlacesUpperRight(target: Place): List<Place> {
        if (target.x + 1 > BOARD_SIZE || target.y == 0) return emptyList()

        val upperRightPlaces = boardStatus.flatMap { it }
                .filter { it.x > target.x && it.y < target.y }
                .filter { it.x + it.y == target.x + target.y }
        return getInsidePlaces(target, upperRightPlaces)
    }

    /** 置いた石から左下方向にひっくり返せる石のリストを返す */
    private fun searchChangePlacesDownLeft(target: Place): List<Place> {
        if (target.x == 0 || target.y + 1 > BOARD_SIZE - 1) return emptyList()

        val downLeftPlaces = boardStatus.flatMap { it }
                .filter { it.x < target.x && it.y > target.y }
                .filter { it.x + it.y == target.x + target.y }
                .reversed()
        return getInsidePlaces(target, downLeftPlaces)
    }

    /** targetを始点として挟めている範囲を判定して返す */
    private fun getInsidePlaces(target: Place, places: List<Place>): List<Place> {
        // 最初に見つかった自分の石
        val endPoint = places.indexOfFirst { it.stone == target.stone }
        // 挟めていなければ終わり
        if (endPoint == -1) return emptyList()
        //挟んでいる範囲を抽出
        val insidePlaces = places.take(endPoint)
        // 挟んでいる範囲が全て相手の石なら反転範囲として返す
        if (insidePlaces.all { it.stone == target.stone.other() }) {
            return insidePlaces.map { Place(it.x, it.y, target.stone) }
        }
        return emptyList()
    }
}

一応簡単なAIも以下のページを参考に実装してみた。

オセロ(リバーシ)の作り方(アルゴリズム) ~石の位置による評価~

盤面重み付けして計算するだけだけど、それなりにちゃんと戦ってくれて面白い。

class AIStrong : OseroAI {

    val boardRatings = arrayOf(
            arrayOf(30, -12, 0, -1, -1, 0, -12, 30),
            arrayOf(-12, -15, -3, -3, -3, -3, -15, -12),
            arrayOf(0, -3, 0, -1, -1, 0, -3, -1),
            arrayOf(-1, -3, -1, -1, -1, -1, -3, -1),
            arrayOf(-1, -3, -1, -1, -1, -1, -3, -1),
            arrayOf(0, -3, 0, -1, -1, 0, -3, -1),
            arrayOf(-12, -15, -3, -3, -3, -3, -15, -12),
            arrayOf(30, -12, 0, -1, -1, 0, -12, 30)
    )

    override fun computeNext(game: OseroGame, color: Stone): Place {
        return game.boardStatus.flatMap { it }
                .filter { game.canPut(Place(it.x, it.y, color)) }
                .maxBy { checkScore(it) + game.getCanChangePlaces(it).map { checkScore(it) }.sum() }!!
    }

    private fun checkScore(place: Place) = boardRatings[place.x][place.y]
}

アルゴリズムとかゲームの計算処理とか全然知識がないので勉強してみたい。
作ったアプリは今更雑なオセロゲームなんか出しても需要が全くないと思うのでリリースはしないですが、コード全体はGitHubにあげているのでご確認ください。

github.com

桜が満開の京都ほかに一人旅してきた

kirimin.hatenablog.com

の続きです。
休暇に入ったので早速念願の一人旅に出掛けてきた。

5泊6日で京都・大阪・尾道・博多を巡ってきました。修学旅行も行かなかったので、関東より西側に行くのは人生初。
丁度桜が満開の一番いい時期に重なったのでとにかく最高だった。
ただし、休暇が確定した1ヶ月前の時点ではもう京都のホテルが全く取れない状態だったので大阪のカプセルホテルに泊まって京都に通うという面白い感じになった。

以下はInstagramに上げた写真の抜粋です。

きりみん (@kirimin) • Instagram photos and videos

1日目

適当に新幹線に乗り昼過ぎに京都へ到着。
鴨川沿いにふらふらと下鴨神社まで歩いて戻ってきた。

ビッグサイトっぽい

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

京都にありがちなやつ!

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

デルタ!!!!!!!!!

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

はるばる来たぜ下鴨〜

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

下鴨

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

夜は京都線で大阪のカプセルホテルへ。
1日目からガッツリ歩いたせいで足がめっちゃ痛くなった。

2日目

2日目は銀閣寺から哲学の道を歩き、その後は繁華街でまったりして清水寺を鑑賞した。 足の痛みが寝ても治らなかったのでなかなかしんどかった。

満開!!!

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

鳥!!!!!

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

北白川たまこ

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

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

北白川あんこ

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

今日はいい天気

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

ミーハーなので舞台撮った

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

3日目

3日目は嵐山を歩いた。とにかく足が痛くて5分歩いて5分休むみたいな感じだった。

にぎやか

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

しゅごいいいいい

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

中に入ったら静かだった(人は多い)

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

展望台

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

例のやつ2

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

建物

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

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

4日目

4日目は大阪まわる予定だったけど台風みたいな天気だった上に足の痛みが限界で一歩も歩けないみたいな感じだったので、ネットカフェに半日くらい居たあと梅田スカイビルに登ったりした。

かぜつよい、しぬ

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

5日目

5日目は午前中に大阪城に行って、午後は尾道の山に登った。

ムムッ

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

うむうむ

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

尾道展望

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

尾道展望3

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

険しい

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

6日目

最終日は午前中にしまなみ海道をレンタサイクルで走り、夕方に博多で福岡タワーなどに行き夜の飛行機で帰ってきた。

連絡船!

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

海の上!

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

しまなみ海道!

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

めちゃ景色よい

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

めっちゃかっこよくない?

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

すごい、海の向こうが見える

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

日本海だ!!!!

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

屋台通り

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

飛行機の景色、すごかった。タワーとか展望台とはレベルが違う(あたりまえ)

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

感想など

ずっと行きたかった京都に桜が満開の一番良い時期に行けてとにかく最高だった。
平日でも外国人観光客の数が凄まじかった。京都のバスは満員電車状態で最悪なので出来るだけ歩くか地下鉄などを利用するのが良いと思った。

大阪のカプセルホテルはホテルというより日雇い労働者の寮みたいな感じだったけどそれはそれで面白かった。2連泊くらいまでならいいけどさすがに4連カプセルホテルはパーソナルスペースの無さや布団のかたさなどで消耗してくるので良くないと思った。

飛行機に乗った事が無かったので帰りは飛行機にしたけど、移動手段として考えると飛行機はちょっと面倒くさいなと思った。新幹線は本当に楽。
ただ、飛行機からの景色は本当にすごくて、アトラクションとしては乗って損はないなと思った。

フリーランスなので長期休暇を取るぞ!

フリーランスになって最初に請け負ったお仕事が3月末で無事に契約満了となったので、せっかくなので3ヶ月くらい休暇を取る事にした。
フリーだと長期休暇が取りやすいので最高。

お仕事の思い出振り返り

某情報サービス会社さんで2年半近くお世話になっていました。
フリーランスと言ってもエージェント経由での常駐業務なので、フリーランスというか派遣みたいなものです。

最初の2年弱くらいはWebサービスのクライアントアプリ開発チームに入って保守やエンハンス開発を行っていました。
入った時は数人のチームでの小規模な開発でしたが、だんだんチームが大きくなりアプリも要件もどんどん複雑化していき、最終的にアプリのエンジニアだけで15人以上いて自分が一番古株みたいな感じだったので、技術的にもチームワーク的にもどう変化に対応しスケールして行くかみたいな事を嫌でも考えさせられた。
僕の業務内容も実装から次第に見積もりや調査、設計、レビュー、ドキュメント整備などなどが多くなりなかなかコーディングが出来なくなったりした。
それでも思い返すとGitなどの各種ツールやライブラリの導入推進や、大規模改修案件での設計など随分面白い事をやらせて貰えたなぁという気がします。
あと、非常に大きなサービスでの高い品質を求められる開発体制や、某社のスピード感と妥協のなさを両立させた仕事の進め方などに触れ大変勉強になった。

後半の半年くらいはアプリ開発部署内の技術基盤的なチームに移り、Androidの新しい機能やライブラリの調査、開発環境の整備、標準ルールの策定なんかをやっていました。
職場ではQiita:Team大好きおじさんみたいな感じでひたすら情報共有やポエムを書きなぐっていた。
これもなかなか仕事でやらせて貰える事じゃないので貴重な経験でした。

職場で友人もたくさん出来てとても居心地が良かったのですが、長期休暇が取りたかったのと、せっかくフリーランスになったので、もっと色々な環境でガリガリ実装するような仕事を経験してみたかったのもあって3月末で離れる事になりました。

しんどくなった時に辞めやすいようにという理由もあってフリーランスになったのに、今までの人生で一番長く同じ場所に毎朝通った2年半でした。(学校もロクに行ってなかったし会社も1年で辞めたし)
最後の日には寄せ書きを貰ったりもしてとても有り難かった。

今後の予定

3ヶ月くらいは仕事をせずに、趣味のコードを書いたり本を読んだりして平和に過ごす予定です。
働いている間にすっかりコーディングから離れてしまったので、休暇中にガッツリ勘を取り戻したいですね。あとAndroid以外も色々と勉強して出来るようになりたい。
とは言えあんまり休んでいるとお金が無くなってしまうので、その後はまたフリーランスとしてお仕事を探してアプリの開発をしたいと思っています。

もし、Androidのお仕事(特にKotlin案件や週三勤務、リモートなど)があるよ!という方がいらっしゃったらTwitter(@kirimin)か何かで連絡して頂きお話聞かせていただけたら嬉しいです。