クローディアにはエンジニアとしてお世話になりました
クローディアとは
2012年4月6日から2017年12月30日の期間。372文字投稿できる国産ミニブログなど言われていましたが、実際のユーザーの利用方法としては小規模なTwitterでした。 そこのサービスのユーザーとの思い出も色々とあるのですが、アプリ開発の思い出などを少し…
アプリ開発の思い出
ここのサードパーティーのAndroidアプリの「クローイド」とiOSの「Icors」を開発してかれこれ多くのユーザーに?使っていただきました。(現在はストアから掲載を落としております)
ネイティブアプリ開発者としてここのアプリを作るのは非常に良い教材でした。
auth認証の実装やらアクセストークンの管理、トークンは1時間で切れるので適宜リフレッシュする必要がったり…
システム的にもほぼTwitterと似たような機能を持ち合わせているので、自然とTwitterライクなUIを作ることになります。
しかしSDKなど一切存在しないので、すべて自分で実装しなければならないところが勉強に非常に適しているところでした。
Androidで新しい機能や便利そうなライブラリが出たら、クローイドに入れて実際の業務で使えそうかどうか実験していたことも多々あります。
またマテリアルデザインが出始めの頃はFABを入れたりReceyclerView入れたり、BottomSheetBehavior入れてみたり最新のUIのキャッチアップにも使わせてもらいました。
kotlinが流行りだしたときはkotlinでリプレースをし、MVPやMVVMなどクラス設計が話題になり始めたときにも実際に導入してどんな雰囲気になるか試したり、会社の新卒エンジニアでアプリ開発に配属された人にはクローディアのアプリを作ってもらって、Android開発のいろはを学んでもらったりしました。
Icrosは自分の初めてのiOSアプリ開発で、中のソースコードもUIも出来栄えとかはクソみたいなものでした…そしてアップルとの審査との戦いそのあたりは以前のブログにでも…
masahide318.hatenablog.jp
このように、現在アプリ開発者としてそこそこマシな人間になれているのも何かあれば実際に試せる環境があったからです。
ユーザーのフィードバックもダイレクトに来ます。嬉しいことを言ってくれる人もいたり、厳しいことを言ってくる人もいましたが、全部参考にさせていただいてました。
広告や課金などの要素は一切入れてなかったのですが、Amazonのほしいものリストを公開するとみんな色々と送ってくれたので実際の現物支給の額でいえばなかなかのものになってました。(あまりクローディアの住民には言えない)
最後に
大晦日に完全ポエムの投稿でしたが、開発者としてはちょうどいいAPIがそろった絶妙なサービスだったのでこれから何かアプリの動作を試したいときはどうしようかなぁと思っているところです。やっぱりマストドンかな
クローディアの新サービスも開発中らしいのでまたAPIが公開されたらアプリを作りたいと思います。
長い間実験場としてありがとうございました。
社内でGoogle Home使ってハッカソンしました
タイトル通り1日エンジニアの業務をストップしてハッカソンしました
作ったものは大したことないんですが、3人チームでメンバーの一人がGoogle Homeを持っていたのでそれを使っての開発が楽しかったので振り返りを
作ったもの
「我々には仕事より大切なものがある」をテーマにGoogle Homeでアイドルやアーティストのライブに遅刻しないように、逐一Google Homeに喋ってもらおうと頑張りました。
ひたすらググって出てきた参考ソースをほぼコピペしてだけなのですが…
喋ってもらうもの
- 公式ブログ
- 公式アカウントのツイート
- 特定ワードのツイート
- 「残り時間教えて!」と聞くと、ライブまでの残り時間教えてくれる
- etc…
使用したライブラリなど
Google Homeに指定した文言を喋ってもらうのには
github.com ツイートの取得には
以上2つお世話になりました。
また独自の音声コマンド「残り時間教えて」を実行するのにIFTTTを使いました
これらの3つはGoogle Homeでググるといっぱい先人の経験があるのでおかげさまで楽できました
それらを組み合わせてこんな感じになりました
基本的にツイートを喋ってもらう分には問題無い作りです。StreamingAPIで流れてくるテキストをgoogle-home-notifierにcurlを投げつければいいだけなので
ただ独自の音声コマンド追加して何かさせるというのがGoogle Homeあまり向いてないんですかね…こんなめんどくさいことするとは思わなかったです。
- 「残り時間教えて」って言うとGoogleHomeが反応
- IFTTTのApplet実行し、特定のワードをTwitterに投稿
- 特定のワードがStreamingAPIで流れてきて、ローカルPCで取得
- 残り時間計算して、google-home-notifierにcurlなげて計算結果しゃべってもらう
なんじゃこりゃ…ハッカソンなので許してください
ハマったところ
google-home-notifierを使うためにGoogle Homeの固定IPを取得する必要があったのですが、スマホの端末とGoogle Homeとの接続が同一ネットワーク内にあるにも関わらずうまく接続できないということがありました。
結果社内のwifiが悪いということが判明して手持ちのiPhoneでテザリングして繋いだらあっさりセットアップできたのでなんとかなりました。
改善点
- やはりgoogle-home-notifierを常駐しておく用のラズパイなり何かが欲しかった
- もっと別のいいやり方や連携サービス使えたかもしれないが知識が足りなかった
まとめ
CloundFirestoreの基本的なCRUD使い方メモ
Android開発においていわゆるRepository層にcloud firestoreを使うときのメモです。
昨日のNANAMemoのときのデータストアに活躍してもらいました。 その時の使用した様子を特に詳しい説明もなく書いてます。
RxJavaと使うと相性がよさそうですね。
gradleにfirestore追加
AndroidStudioのToolメニューからfirebaseの連携をした後にgradleにfiresotreを追加します。
だいたい以下のような感じになります
dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" implementation 'com.android.support:appcompat-v7:26.1.0' implementation 'com.android.support.constraint:constraint-layout:1.0.2' implementation "com.google.firebase:firebase-firestore:11.6.0" //firestore追加 testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.1' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' } apply plugin: 'com.google.gms.google-services'
CRUDサンプル
data class User( val name: String = "", val age: Long, val email: String = "", val createdAt: Date = Date() )
class UsersRepository(val db: FirebaseFirestore = FirebaseFirestore.getInstance()) { private val path = "users" fun findAll(onSuccess: (List<User>) -> Unit, onError: (Exception?) -> Unit) { db.collection(path).get().addOnCompleteListener { if (it.isSuccessful) { onSuccess(it.result.toObjects(User::class.java)) } else { onError(it.exception) } } } fun findOne(id: String, onSuccess: (User) -> Unit, onError: (Exception?) -> Unit) { db.collection(path).document(id).get().addOnCompleteListener { if (it.isSuccessful) { onSuccess(it.result.toObject(User::class.java)) } else { onError(it.exception) } } } fun create(user: User, onResult: (Boolean) -> Unit?) { db.collection(path).add(user).addOnCompleteListener { onResult(it.isSuccessful) } } fun update(id: String, user: User, onResult: (Boolean) -> Unit?) { db.collection(path).document(id).set(user).addOnCompleteListener { onResult(it.isSuccessful) } } fun delete(id: String, onResult: (Boolean) -> Unit?) { db.collection(path).document(id).delete().addOnCompleteListener { onResult(it.isSuccessful) } } }
上記のやりたかというか、firestoreのtoObjectメソッドは結構融通が利かず、key名と変数名が一致していないといけません。
また型にも厳しく、Intをもたせることが出来ません
さらにidの情報はaddOnCompleteListenerのDocumentSnapShotが持ってるので、idがわかりません。
なので独自のマッピングメソッドを用意してあげてもいいかもしれません
Userのentityにidをもたせると、次はfirestoreにaddしたときにkeyのid情報と、userのドキュメントidとで情報が重複しますがあまり気にしない。
data class User( val id: String? = null, val name: String = "", val age: Int, val email: String = "", val createdAt: Date = Date() ) { companion object { fun mapping(result: DocumentSnapshot): User { return User( result.id, result.getString("name"), result.getLong("age").toInt(), result.getString("email"), result.getDate("createdAt") ) } fun mapping(result: QuerySnapshot): List<User> { val users = mutableListOf<User>() result.forEach { users.add(mapping(it)) } return users.toList() } } }
fun findAll(onSuccess: (List<User>) -> Unit, onError: (Exception?) -> Unit) { db.collection(path).get().addOnCompleteListener { if (it.isSuccessful) { onSuccess(User.mapping(it.result)) //こんな感じで置き換える } else { onError(it.exception) } } }
Presenterから使う
class SamplePresenter(val mainView: MainView,val usersRepository: UsersRepository = UsersRepository()){ fun loadUsers(){ usersRepository.findAll({ mainView.showUser(it) },{ mainView.showError() }) } }
RxJavaを使うVersion
class UsersRepository(val db: FirebaseFirestore = FirebaseFirestore.getInstance()) { private val path = "users" fun findAll(): Single<List<User>> { return Single.create<List<User>> { emitter -> db.collection(path).get().addOnCompleteListener({ if (it.isSuccessful) { emitter.onSuccess(User.mapping(it.result)) } else { emitter.onError(Throwable(it.exception)) } }) } } fun findOne(id: String): Single<User> { return Single.create<User> { emitter -> db.collection(path).document(id).get().addOnCompleteListener({ if (it.isSuccessful) { emitter.onSuccess(User.mapping(it.result)) } else { emitter.onError(Throwable(it.exception)) } }) } } }
class SamplePresenter(val mainView: MainView, val usersRepository: UsersRepository = UsersRepository()) { fun loadUsers() { usersRepository.findAll().observeOn(AndroidSchedulers.mainThread()).subscribeOn(Schedulers.io()).doOnSuccess { mainView.showUser(it) }.doOnError { mainView.showError() }.subscribe() } }
水樹奈々の思い出を記録するNANAMemoアプリ公開しました
アプリはこちらです
play.google.com
なぜつくったのか?
ファンクラブイベント7においてセトリハンターというコーナーが話題になりました。
その場で抽選で選ばれた人に次のライブで歌ってもらいたい歌をリクエストできるという企画で自分が歌ってほしい歌を確定で歌ってもらえる。ファンにとってこれほど嬉しいものはありません。
その時みんな自分が好きな歌を選んでくれ−っと思ったはずです。私もそうです。願わくば誰か「Independent Love Song」か「宝物」か「あの日夢見た願い」を選んでくれ−って思いました。
そしてひらめきました。みんなの思い入れのある歌の思い入れを記録してみんなで見れればいいのではないかと。自分の好きな歌が他の人がどんな思い入れがあるか知れたら楽しいではないかと
ということで開発期間約1週間で、とりあえずガーッと作りました。
ざっくりした構成
基本的にデータストアはfirebaseのCloud Firestoreにもたせてます。初めてNoSQLをがっつり使ったのでどういうデータ構成にするか悩みました。そのことについてはまた後日書きたいと思います。
ニュース記事は自分のsakuraサーバーでphpによるスクレイピングをしてMySQLにデータを格納。その後JavaのバッチがFirebaseにデータを送るという形になってます。
ログイン機構もFirebaseに任せていますしTwitterログインしかさせてないのでメールアドレスなど個人情報も一切持ってないので安心して使ってください。
今後の開発予定
- CDやアルバム情報に画像を載せたかったのですがやはり著作権的にNGなので載せれなかったのでなんとかして華やかさをもたせたい。
- iOS版を開発する
- コメント数など表示する
- ライブBDの情報も追加して、ライブの思い出も残せる場を作る
- ニュース記事を増やす
とりあえず直近でこの辺りを計画してますが、果たしてどうなることやら…とりあえず頑張ります…
何かご意見ご感想があればsarahahやtwitterで言っていただければ頑張ります
AndroidでMVPするときのPresenterの責務
Androidのクラス設計でMVPやMVVMが流行っています。
自分もMVPのクラス設計が導入しやすく好きなのですが、実際にやろうとすると何をPresenterに書き何をView(ActivityやFragment)に書くべきなのか指針がなくて
なんとなくMVPになることが多いです。そこで個人的にどういう風にMVPに分けるべきかメモしておきます
個人的に注意するポイントは以下の三点です
- PresenterにViewの具象クラスを引き数として与えない
- PresenterにAndroid特有のクラスを引き数として与えない
- Presenterはテスタブルな状態にする
PresenterにViewの具象クラスを引き数として与えない
個人的にNGなコードは以下のようなPresenterが出てきたときです
class MainViewPresenter(val mainView: MainActivity) { fun doSomething() { } }
PresenterのコンストラクタでActivityやFragmentをそのまま受け取るようなパターンです。
PresenterがガッツリViewと依存してしまいますし、AcitivityやFragmentを受け取ることでテストコードでmockにしにくいし、これをやってしまうとView側でできることとPresenter側で出来ることの境目がなくなってしまいます
FragmentからfindViewByIdすればレイアウトは操作できてしまうし、Contextを取り出せばやりたいほうだいです。すべてのロジックをPresenterに書けてしまいまうためチームメンバーによって何を書くかバラバラになってしまいます。
ViewとPresenterはInterfaceを通して余計なことができないように縛りましょう
interface MainViewable{ fun bindUserData(user:User) }
//MainViewableのInterfaceを渡すことで余計なことをさせない class MainViewPresenter(val mainView:MainViewable) { fun doSomething() { } }
class MainActivity : AppCompatActivity(),MainViewable{ val presenter:MainViewPresenter by lazy { MainViewPresenter(this) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) presenter.doSomething() } override fun bindUserData(user:User) { //Viewにデータを反映させる } }
PresenterにAndroid特有のクラスを引き数として与えない
例えばこういうPresenterです。
class MainViewPresenter(val context: Context,val pref:SharedPreferences) { fun doSomething() { } }
ContextやSharedPreferencesなどAndroid特有のクラスを渡してしまうものです。
これをやってしまうとActivityやFragmentを引き数として渡すのと同じでUnitテスト時にmockを差し込みにくくなります。
またContextを渡してしまうと上記で書いた通りPresenter側でほぼなんでも出来るようになってしまうのでViewとPresenterの責務が曖昧になりがちです。
SharedPreferencesなどはそれをラップしたクラスを作ってそれを渡すといいでしょう。Contextはそもそも渡さないようにしましょう。
Contextが必要なクラスが多くあると思いますが、それは上記のSharedPreferenceServiceのようにContextを扱えるView側でクラスを生成しそれをPresenterへ引き数として渡すようにしましょう。
class SharedPreferencesService(private val context: Context) { val pref: SharedPreferences by lazy { context.getSharedPreferences("sample", Context.MODE_PRIVATE) } fun apply(key: String, value: String) { pref.edit().putString(key, value).apply() } }
//ラップしたクラスにすることで簡単にmockにかえJUnitを回しやすくなる class MainViewPresenter(val context: Context,val pref:SharedPreferencesService) { fun doSomething() { } }
Presenterはテスタブルな状態にする
基本的にPresenterにAndroid特有の関心事を持ち込まなければJUnitによるテストが実行可能になっているはずです。
Presenterは高速にテスト回せる状態にし、ActivityやFragmentはAndroidUnitTestによる少し重めのテストという風に住み分けします。
PresenterがJUnitによるテストコードを回せなくなったとき、一度立ち止まって本当にPresenterでやるべき処理か立ち止まるいいきっかけになります。
ソースレビューするときもレビュー観点としておくことで、メンバー間によるプログラムの差異を減らせると思います。
最後に
そもそもMVPってAndroidのコードがFragmentやActivityにビジネスロジックとViewの操作が集中してマッチョになってしまうのでビジネスロジック部分を分けたいという欲求から生まれたものなはずなのに
そのビジネスロジック部分にFragmentやContextが出てきてマッチョになっていくのは何か違う気がすると思います。なんのためのPresenterなのか?それを一度考えましょう
ちなみにadapterの操作をどこで行うか?というのが悩みどころですが、あれもAndroid特有のクラスなので自分は「View側で操作する」でいいと思います。
sharedPreference同様にapdaterをラップしたクラスを用意しそこで操作するようにすればPresenterに渡してもいいかもしれません。
ものすごく雑にまとめたので実際のプロダクトでやるともしかしたらうまくいかない部分が出てくるかもしれませんが、基本方針としてPresenterをJUnitでテスタブルな状態にするということを守っていれば
クラス間の責務は適切にわけられているのではないでしょうか?
その他にMVPの責務の分け方の考えなど意見がある方は是非教えてください。自分もこれが最適とは思っていないので……
Androidのクラス設計ってなんでこんなに難しいんでしょうかね。
capistranoでcomposer installしてからrsync
capistranoでphpのプロジェクトをdeployしようとして
通常のdeploy方式だとdeploy先のサーバーでgit pullしてからcomposer installになってしまうがそうではなくて
deployサーバーでgit pullしてcomposer installしてそれをdeploy先のサーバーにrsyncでファイルを送りたかった
ググったらすぐに見つかるかなぁと思っていたが思いの外ピンポイントな情報がなかったのでメモ
capistrano-bundle_rsyncを使います GemFileの内容としてはだいたいこんな感じ
source "https://rubygems.org" gem 'capistrano', '3.8.1' gem 'capistrano-rbenv' gem 'capistrano-bundle_rsync'
bundle install –path vendor_bundle でbundle installしましょう。pathを指定しないとデフォルトでvendorになるので、phpのcomposerのvendorと被って余計なものがデプロイされてしまいます。なのでbundleではインストールパスを指定して後でそのパスはrsyncから外すようにします。 後はcapistrano-bundle_rsyncのREADMEに従ってCapfileやconfigファイルを設定します
コピペですがCapfileは
# Load DSL and Setup Up Stages require 'capistrano/setup' # Includes default deployment tasks require 'capistrano/deploy' # capistrano-3.3.3 - 3.6.1 require 'capistrano/bundle_rsync' # capistrano-3.7+ require 'capistrano/bundle_rsync/plugin' install_plugin Capistrano::BundleRsync::Plugin # Loads custom tasks from `lib/capistrano/tasks' if you have any defined. Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r }
config/deployにvagrantにdeployする想定のlocal.rbを作ります
server "vagrant" set :stage, :local role :web, "vagrant"
問題のdeploy.rbです
# config valid only for current version of Capistrano lock "3.8.1" set :branch, ENV['BRANCH'] || 'master' set :keep_releases, 2 set :scm, :bundle_rsync set :application, "sample" set :repo_url, "gitrepo" set :rsync_options, '-az --delete --delete-excluded --exclude=vendor_bundle' set :deploy_to, "/var/www/html/example" # rsync前にcomposer installする task :pre_rsync_action do config = Capistrano::BundleRsync::Config run_locally do execute ("cd #{config.local_release_path} && php composer.phar install --no-dev") end end before "bundle_rsync:rsync_release", "pre_rsync_action"
rsync_optionでbundle installしたディレクトリはrsyncから外しています。
capistrano-bundle_rsyncはいろいろなタイミングでフックポイントがあるのでrsyncが実行される前にcomposer installを実行するようにしています。
rsyncされるディレクトリーはcapistranoがカレントディレクトリの下に「.local_repo」というディレクトリを作り、さらにその下にできる「releases/{datetime}」ディレクトリがrsyncされるディレクトリです。
そのrsyncされるパスを取得するのがdeploy.rbの「config.local_release_path」の部分です
ここまでできたら後はgitにGemfile.lockをPushして「bundle exec cap local deploy」を実行するだけです。
今回はcomposer installしただけですが、その他にも設定ファイルなどをこのタイミングでそのstageに合わせてコピーするなど色々できます。
ごちゃごちゃ書きましたが結果としてはcapistrano-bundle_rsyncのREADME通りに書いてdeploy.rbだけ少し改良するだけで良いという話です
kotlinのjavascirptで非同期で取得したデータをDOMにバインドするサンプル
kotlinがAndroidに公式サポートされて盛り上がっておりますね。
一応Androidエンジニアやっている?いた?のでkotlinは少し追っていたのですが、今ではWebエンジニアとしての方が強くなってきたので
あえてkotlinでjavascriptやってみよう思い立ってDelegate.Observableを使って、非同期で取得したデータをhtmlのDOMにバインドできないかなぁと思ってやってみたら楽しくなってきたのでメモ
kotlinでjavascriptの準備
チュートリアルにそって準備しました kotlinlang.org
適当にhtmlを用意
<!DOCTYPE html> <html lang="en"> <head> <script src="../kjs/out/production/kjs/lib/kotlin.js"></script> <script src="../kjs/out/production/kjs/kjs.js"></script> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div> <input id="sampleText" type="text" value="text" title="sampleText"> <input id="sampleCheckBox" type="checkbox" title="checkBox"> <button id="sampleButton">sampleButton</button> </div> <script> new kjs.SampleRequest().request(); </script> </body> </html>
divタグの中のDOMのvalueやらcheckboxのdisableの切り替えをAPIを叩いて帰ってきたJSONレスポンスをdata classに変換してバインドする
みたいなものを想定しています。無邪気にid指定しているのは許してください。nameでもclassでも取得できるのでそこは後でお好みで変換してください。
scriptタグ内のkjs.SampleRequest().request()で非同期でGETリクエストを投げてJSONを受けってデータバインドします
バインドするdata class
data class SampleEntity(val inputText: String, val disabled: Boolean, val buttonText: String)
inputTextをDOMのtextのvalueに
buttonTextをDOMのButtonのtextContentに
disableをcheckBoxのdisableに
バインドする想定のdata class
バインドするDOMのテンプレートクラス
object SampleTemplate { var entity by Delegates.observable<SampleEntity?>(null) { _, _, new -> new?.let { inputText.value = new.inputText checkBox.disabled = new.disabled button.textContent = new.buttonText } } val inputText: HTMLInputElement = document.getElementById("sampleText") as HTMLInputElement val checkBox: HTMLInputElement = document.getElementById("sampleCheckBox") as HTMLInputElement val button: HTMLButtonElement = document.getElementById("sampleButton") as HTMLButtonElement }
それぞれのDOMの情報をメンバ変数に持って
Delegate.observableを利用してSampleEntityがsetされたらDOMに値を入れるようにしています
GETのRequestを投げるクラス
class SampleRequest { fun request(){ val req = XMLHttpRequest() req.onloadend = fun(event:Event){ SampleTemplate.entity = JSON.parse<SampleEntity>(req.responseText) } req.open("GET","http://localhost:63342/kjs/json_text.txt",true) req.send() } }
XMLHttpRequestで非同期でGETリクエストを投げて、取得したJSONをSampleEntityに変換してSampleTemplateのentityにセットしています。
適当なJSONのファイル用意します
{"inputText":"テキストはいる","disabled":true,"buttonText":"hoge"}
buildしてブラウザでチェックします
ディレクトリ構成はこんな感じですごく適当においてます
IntelliJからindex.htmlをopenすると「http://localhost:63342/kjs/index.html」みたいなURLで開きます
すると
から
にtextの内容やcheckboxがdisable状態になったりと、データが反映されます。
今回は恐らく最小の構成?でバーっと書いてみましたが、案外使えそうな気がしています。
おまけ
チュートリアルにあるようにdebugの設定をすると、kotlinのソース上でちゃんとブレイクポイント貼れるのでかなりデバッグが助かります。 vue.jsやほかのjsのライブラリとうまく組み合わせたパターンなど色々と試してみたいですね。