VRヘッドセットのViveを買ったので感想とか書いていこうと思う。
そもそもViveとは
ViveはHTCが出しているPC用VRヘッドセットで、Oculus Riftのライバルのような製品。
以前はVRといえばOculusという感じだったけど最近はOculusの動向が怪しくて相対的にViveの評価が上がっている気がする。
VRヘッドセットのViveを買ったので感想とか書いていこうと思う。
ViveはHTCが出しているPC用VRヘッドセットで、Oculus Riftのライバルのような製品。
以前はVRといえばOculusという感じだったけど最近はOculusの動向が怪しくて相対的にViveの評価が上がっている気がする。
Annictという素敵なアニメ視聴記録サービスがある。
このAnnictのAPIが公開されていたので、試しに使ってみようと思いクライアントアプリっぽいのを作ってみた。
実は僕は元々Animetickという別のサービスでアニメの視聴管理をやってたんだけど、Annictの方が多機能っぽいしAPIも公開されているのでそちらに移行しようと思ってる。
ただ、Annictは多機能な分Animetickと比べると視聴登録がサクサク出来なかったりするところがあったので、サッと開いて今日のアニメを確認して、シュッと視聴済みチェックをしていける感じにしたい。
それと、Animetickにもアプリがあるけど通知機能が無いようだったので、放送時間のリマインド通知機能は付けたい。
...というわけで3日くらい黙々とコードを書いてだいたいこんな感じのアプリになった。
まだ開発中だけど、自分が使う必要最低限の機能が出来たのでとりあえずBETA版という事でリリースした。
需要がありそうだったらもっと色々機能付けたりしていきたい。
コードも一応公開してます。ちょっと雑にガーッと書いたので恥ずかしいですが。
github.com
KotlinとRetrofitが便利だったのであんまり何も書かなくてもアプリが出来上がった気がする。
最近ずっとJavaScriptをやってた。
JavaScriptをやってる理由は色々ある。
僕は仕事ではずっとJavaとAndroidで、他の言語やプラットフォームは入門的な事は色々やって来たが、ちゃんと出来ると言えるものが他にないので単純にもっといろんな事が出来るようになりたい。
JavaScriptが出来ればWebでちょっとしたサイトやサービスを作ったりしやすくなるし、ElectronやReactNativeなどWebの技術を他の分野で使用する流れがあるので色々と便利そうだ。
はてブやQiitaでよく読むフロントエンド関連の記事をもっと楽しめるようになりたいというのもある。
あと、Androidの世界でも日々いろいろな思想や技術が入ってきていて、そういうものに触れた時にAndroidだけやっていてはもっと広い視点で技術の動向を追っている人にはキャッチアップの速度でも理解度でも敵わないなと感じる事がある。
もちろん何にでも手を出して器用貧乏になるのもよくないのだろうけど、僕もそろそろもう少しAndroid以外の世界も知った方がいい気がしたのだ。
JavaScript以前に、そもそも僕はHTMLとCSSすらなんとなく見よう見まねで書ける程度にしか理解していない。
非エンジニアのイラストレーターなどが自サイトをオシャレなレイアウトで構築しているのを見るだけで悔しくなるほどHTMLにコンプレックスを抱えている。
一応専門学校時代にテキストサイト全盛時代のようなシブいHTML入門を教わったが、文字の色を変えたりtableを組み立てたりは出来てもヘッダーやサイドメニューのような複雑なレイアウトが一体どういう仕組みで組み立てられているのかが全く検討も付かない。
そこで、Amazonで適当に評判が良さそうなHTML5の入門書を買って入門し直す事にした。
最初に買ったのはHTML5&CSS3レッスンブックという本。
まずはこの本を写経しながら読んだ。ちょっと初歩過ぎる気もしたが基礎を疎かにするとハマるので簡単そうなところから始めた。
結果的にこの本はめちゃくちゃ良本で、HTMLを全く知らない人でも分かるようによくある個人サイト風レイアウトの構築手順を分かりやすく解説している。
内容はとても簡潔ながら洗練されていて、写経しながらでも数時間で読み終わるボリュームながら、HTMLをCSSをきれいに分離した(多分)実践的な知識が身につくようになっている。
というかこの本を一冊読んだら「Viewを横に並べる方法」さえ理解すればヘッダーもサイドメニューも特別難しいレイアウトじゃない事が分かって今までのコンプレックスは一体なんだったんだみたいな気持ちになった。
ところでこの本ではfloatを使ってViewを並べているが、最近ではflexboxというもっと簡単な仕組みがあるそうですね。ぐぬぬ…。
続けて同じシリーズのHTML5&CSS3デザインブックという本を買った。
HTML5&CSS3デザインブック (ステップバイステップ形式でマスターできる)
こちらは上記のレッスンブックが理解出来たくらいの人を対象に、もう少し凝ったブログ風レイアウトや会社のフロントページ風レイアウトなどの構築手順をレスポンシブデザインの解説も交えながら紹介している。
CSSのコード量が増えて写経が大変だったけど実家に帰ってダラダラしながら3日くらいで読了した。
これでなんとなく簡単なWebページくらいなら自前で構築出来るようになった気がしたので、Materializeというテンプレートライブラリでぐちゃぐちゃと書いてたプロフィールサイトを全て自前のHTMLとCSSで書きなおした。
ようやくこれでJavaScriptを勉強する準備が整ったわけだけど、JavaScriptの技術は入り組んでいて技術書も非エンジニア向けのものからガチ勢向けのものまで、更に古い情報も混じっていたりするので何をベースに学習するのか結構迷った。
そもそも僕にはJavaScriptをWebサイト制作にどう組み込んでいくのかもよく分かっていない。
そこで、まずはあまりライブラリや周辺技術に依存せず、JavaScriptをガッツリ使ったWebアプリの基礎を覚えようと思い、シングルページWebアプリケーションという本を選んだ。
シングルページWebアプリケーション ―Node.js、MongoDBを活用したJavaScript SPA
この本はAngularJSのようなフレームワークを使用せず、昔ながらのJavaScriptとjQueryだけを使って、頑張ってオブジェクトにスコープを閉じ込めオブジェクト指向でのMVCのようなものを作りanchorで状態を切り替えるSPA(チャット機能)を作りこんでいくという内容だ。
オライリーなのであまり初心者向けの内容ではないが、ちゃんと読み進める上で必要なJavaScriptでのスコープやクロージャの解説も最初に書かれているし、分かりにくい部分はしつこいくらいに何度も説明しているので意外にハードルが低い本だった。
ただ、かなり堅実な実装で、しかも途中からは本来の趣向とはあまり関係のないチャット機能を頑張って作りこんで行くのでコード量がめちゃくちゃ多く、とにかく写経がしんどかった。(というか途中からは配布されているサンプルをコピペした)
なかなか読むのに骨が折れたが大変なだけあって一冊でJavaScriptでのWebアプリ作成方法がかなりイメージ出来るようになったので良かった。
シングルページWebアプリケーションではDOM操作を全てjQueryで行っていて、読んでいて意味の分からない部分があったのでjQueryの入門書を買おうかと思ったんだけど、どうも非エンジニア向けっぽいのが多くて良さそうな本が見つからなかった。
Webでも回りくどい解説サイトばかりで困ったが、こちらのサイトが非常に簡潔に必要な事だけが書かれていて有り難かった。
シングルページWebアプリケーションはJavaScriptの入門書ではないので、あまり細かいJavaScriptの説明などは書いていない。
実は手元に4年くらい前に買って投げ出したパーフェクトJavaScriptという本があるので、再読した。
パーフェクトJavaScript (PERFECT SERIES 4)
この本は少し古いけどJavaScriptの言語仕様についてかなり細かく書かれており、読めば読むほどJavaScriptが嫌いになってくる素敵な本だ。
JavaScriptがなんとなく書けるけどもっと理解を深めたいという僕みたいなパターンでは非常に良い本だったが、初めてJavaScriptに触れる人は読むとJavaScriptが苦手になってしまうかもしれない。
なんとなくWebアプリの作り方が分かったのでとりあえず何か練習で作ってみようと思い、オセロゲームを作り始めた。
ついでに現在主流のnpmとbrowserifyを使ってみようと思って色々と調べ始めた。
この辺りは書籍にはあまりまとまっていないようなので主にQiitaなどの記事を参考にした。
browserifyでビルドするようにしたらシングルページWebアプリケーションで覚えたやり方では通用しない事が分かったので、更に色々と調べながらファイルを全てモジュール化して読み込むように書き換えた。
春からはじめるモダンJavaScript / ES2015 - Qiita
npm とか bower とか一体何なんだよ!Javascript 界隈の文脈を理解しよう - Qiita
旧石器時代のJavaScriptを書いてる各位に告ぐ、現代的なJavaScript超入門 Section2 ~CommonJSモジュールと仲良くなろう~ - Qiita
旧石器時代のJavaScriptを書いてる各位に告ぐ、現代的なJavaScript超入門 Section4 ~Gulpで処理を自動化しよう~ - Qiita
世間ではjQueryを捨てて行く流れのようなので、いきなりReact.jsやvue.jsなどのフレームワークを使おうか迷ったけど、今回は練習だし、jQueryの事が分かっていないとjQueryを捨てられないのでjQueryベースで開発する事にした。
同じ理由でES2015も今回は使わなかった。
オセロ自体は特に詰まる事もなくサクッと実装出来たけど、何も考えないで書いたら思いっきりViewとModelが結合してcssのクラス名でロジックを判定するようなシブい設計になってしまった。
MVCっぽく書き直そうかとも思ったけど、わざわざ書かなくても書き方は明らかで単純作業になってしまうし、そんな事をするよりはES2015とフレームワークを使って書き直した方がマシだと思ったのでやめた。
というわけで完成したジェークエリーオセロがこちらになります。
ルールとか適当だけど一応AIも実装して遊べるようになっているので遊んでみてください。スマホでもできるよ。
コードもあります。
kirimin.me/game/osero at master · kirimin/kirimin.me · GitHub
まだ入門しただけだけど一応JavaScriptで簡単なものなら作れる感じになってよかった。(小並感)
次はReact.jsやりたいしもっとJavaScriptに深入りしたいけど、サーバーサイドも出来るようになりたいしiOSもやりたいので悩ましい。
落ち着いて1つずつ覚えていこう。
ずっと家にいるとあまりスマホを見ないのでLINEとかメールとかゲームの通知とかわりと気付かなくて面倒な事がある。
僕は一人Slackチームを作ってメモなどに利用しているので、そこにスマホの通知を全部流せたら便利だと思った。
IFTTTでそういうのあるかなと思ったけどパッと見た感じ無さそうだった。
サービスごとに個別で頑張ってもいいんだけどそれも面倒なのでもう全部流したい。
探せばそういうアプリありそうだけどよく分からない個人アプリに個人情報垂れ流すのも怖いので自分で作った。
通知の取得方法はこちらを参考にした。
あとは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(); }
で、こうなった。
とりあえずLINEに気付かない事はなくなりそう。
ただ、なんかアプリによって連続で取得したりテキストが取得出来なかったりするのでイマイチ。
Twitter公式アプリがツイートする度に通知出したりするのでブラックリスト作った方が良さそう。
関係ないけどAndroid Wearはもう一年くらい使ってない。
Mitsumine 2.5.0をリリースしました。
Mitsumineは趣味で開発しているAndroid用のはてなブックマーククライアントアプリです。
久しぶりのバージョンアップなので、開発にあたってのいろいろな話を書いてみようと思います。
とりあえず宣伝しておきます。
Mitsumineは主に自分が使いやすいはてぶアプリとして開発しています。
多分一番のヘビーユーザーは僕だと思う。
はてブを快適に眺めるために必要な機能は一通り実装されています。
特に一覧画面に表示される情報は公式アプリよりも多くなっていて、これは僕が出来るだけ記事を開かずにどんどん流し読みしながら面白そうな記事を探したいからです。
一方でアプリ内ブラウザなどの自分に不要な機能は実装していません。
バージョン2.5.0では前々から放置していた一部URLの読み込みに失敗するバグの修正の他、ブコメについたはてなスターの表示などを実装しました。
元々Mitsumineは公式のはてなブックマークアプリがまだあまりメンテナンスされていなかった頃に作ったアプリで、当時の公式アプリはスクロールするだけでクラッシュしたりと自分が使うのに不便すぎたので、自分用アプリとして作成しました。
しかし、最近ははてなにもAndroidエンジニアが増えてきたのか、公式アプリも頻繁にアップデートされるようになってきたようです。(めでたい事ですね)
なのでMitsumineはそろそろ積極的に開発する必要が無くなってきたかなーと思っています。自分が欲しい機能も一通り実装されているので、今後はバグ修正などのアップデートだけになるかもしれません。
まあ、僕は公式アプリよりMitsumineの方が見やすいので使い続けるんだけど。
Mitsumineは僕が新しいライブラリや設計を試すためのサンドボックス的なプロジェクトでもあったりします。
例えばMitsumineのコードはKotlinにハマった時に全てKotlinに書き換えていて、今でもKotlinで開発しています。
そんな流れで去年の秋くらいからはMVP設計への全面書き換えを行っていたのですが、すでにコード量がかなり多くなっているMitsumineを全面的に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にあげているので興味があればどうぞ。(試行錯誤しまくってたのでチグハグになっていますが)