アニメの通知アプリの制作詳細
AnimeCheckerの紹介
AnimeCheckerはアニメの放送時間をリマインドしてくれるアプリです。 アニメを視聴する時は、放送開始時間前にテレビをつけ、Twitterを開き、アニメを楽しみます。 既にアニメの放送時間を通知するアプリはありますが、多くは以下の問題があります。
- 通知の時間が固定されていて指定できない。
- 人によって、通知されてからテレビをつけるまでの時間は違います。
- 通知は1度のみ
- リマインダーとして使用されるなら、基本的に複数回通知べきです。
- アラームアプリのようなスヌーズ機能を備えるべきです。
これらの問題を解決するためにAnimeCheckerを作りました。
AnimeCheckerは以下のような特徴があります。
- 通知時間を選べる見逃し防止
- 通知時間を選択式にしました
- 可視性を低くしない為に最小限の選択肢に留めています
- アニメごと複数回に分けて、通知する
- 1つだけではなく、複数の通知を作成することができます
- 複数回通知して、今行っている作業の時間配分を考えることができます
- 少ないタップ操作
- 放送時間の指定には、時計を使用します
- 好きな通知をタップするだけで設定できます
機能の紹介
通知一覧画面
通知を設定したアニメを一覧で見ることができます。アプリを起動した後に移る画面です。 通知一覧画面から、通知の作成・編集・削除を行う画面に移動することができます。
通知時間を直感的な操作で簡単に設定
Chipをタップするだけです。また、複数通知を設定できます。
ハイライト
通知の例
2年前に作ったYoutubeでタイトルを通知する拡張機能をストア公開しました
2年前に作ったもの
参考程度にGooglePlayは設定で通知をONにすることができて、こんな感じ動作していた。
メンテする理由
正直うざかったので、切ってました。(放置) どうせメンテするなら、直したい->リファレンスを読む限りsilent:trueでできそう。
- 公開しないの?って言われたため
就活中に、公開しないのって言われた。それだけ
できた
Githubリポジトリ
この拡張機能の仕組みについて
ページリロードを検出し、読み込みが完了したら通知を出すようにしています。サムネイルはurlからmovieIDをsubstringしてvar thumbnailUrl = "http://i.ytimg.com/vi/" + movieID + "/default.jpg";
で取得しています。
chrome.notifications.create( getNotificationId(), { type : 'basic', iconUrl : thumbnailUrl, title : pageTitle, message : 'YouTitleの通知', silent : true, }, function(){})
タブの更新は
でこんな感じに...
chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) { //ページのURLを取得 var strUrl = String(tab.url); //通知の生成 //statusがcompleteになった時に通知 if (changeInfo.status == "complete" && isYoutubeTrack(strUrl)) { var thumbnailUrl = getThumbnailUrl(strUrl); var pageTitle= tab.title; //ページタイトルの余分な部分(末尾の" - Youtube")を削除する pageTitle=pageTitle.substr( 0, pageTitle.length-10); //通知開始 creatNotification(thumbnailUrl,pageTitle); } });
ちょっと疑問に感じたこと:onUpdated.addListener
は何故か5回通知される()
公開しました
5$払って公開
自己紹介
呼ばれ方
- KD
- ehuthon
SNS
- Twtter
- @ehuthon(趣味垢)
- @kd_gg12e(大学垢)
ブログや記事など
フロー&成果物
(2016/04)東海大学入学
(2016/9-2016/10)新入生教育用電卓アプリ
(2016/10-2017/4)Wifi-Connection:大学Wifiに自動接続、途中で挫折
(2017/05-2017/06)youTitle_extension:サムネイル付きで動画のタイトルを通知
(2017/06)AndroidSpinnerストレス発散
(2017/09-2017/10)spla-victor-calculation←ブログ、スプラトゥーンの画像面積測定
(2017/11)RecodeVoiceinbackgroud
(2017/12-2018/01)初めてのAndroidプログラミング書籍をやる
(2018/02)amazon_wishlist合宿用
(2018/02-2018/03)Python勉強期間
(2018/03)Raspberrypi購入
(2018/03)Qiitaを始める
(2018/05)OneDriveAPI_Java
(2018/05)slack_botSlackのコマンドで画像を出力、枚数指定、キーワード指定可能
(2018/05-2018/06) animeface←Qiita
(2018/06)サークル内でanimefaceのLT(資料:スライド)
(2018/05-2018/07)ImageDrive画像を検索&取得し、OneDriveにアップロードして、Windowsのデスクトップに1分刻みで更新
(2018/06-2018/07)Blocks Wordビル・ゲイツのゲームDONKEY.BASを目指したもの
(2018/09-2018/10)漫画を人に勧めるときために漫画足し算をやってみたかった話(途中経過)←Qiita
(2018/08-2019-01)WeekCkech
(2018/10)Kotlin イン アクションを読み始める
(2019/02)AtCoderの勉強
(2019/02-?)AtcoderChecker←ブログ
(2019/03-2019/04)java言語で学ぶデザインパターン入門を読む
(2019-04)WorkManagerやLivedataのリファレンスを読んだり、実装を追う( 成果:Qiita )
(2019/05) RecycleViewの実装を追う。fade-inを実装する。(成果: Qiita )
(2019/05) あるあるLT〜スマホアプリ開発エンジニア〜 Vol.4で登壇 ( 資料:RecyclerViewのAnimation.pptx - Google ドライブ )
(2019/07) 10日間でAndroidアプリ開発 アニメの通知アプリの制作詳細 - ehu-151’s diary
資格
2017年12月1日 普通運転免許 取得
2018年5月16日 基本情報技術者試験 合格
2018年8月2日 画像処理エンジニア検定 ベーシック 合格
AtCoderのABC問題のAC一覧をAndroidアプリで表示させた話(即席アプリ)
2019年2月26日現在
動機
AtCoderをやっていて、ABCのC問題の壁にぶち当たり、他の問題を解こうとしたのですが、自分のやった問題が分からない!!ということになりました。@kenkoooo さんのGitHub - kenkoooo/AtCoderProblems: Problem manager for AtCoder usersを使えばできそうだったので、やってみることにしました。
開発環境
- 言語:kotlin
- アーキテクチャ:MVVM(多分合ってるはず)
アーキテクチャはjetpackのガイドラインを参考にしました。 developer.android.com
アーキテクチャはしっかり考えたいところなのですが、即席なのであまり考えていません。 テストやったことないので、いつかやりないなぁ
package分け
API & DB
- HTTP client library:retorofit
- Json parser:moshi
- BDMS:Room
Submission APIを叩いています。帰ってきたレスポンスをmoshiでパースして、Repositotyに返しています。レスポンスのListはLiveDataで包んでいます。Fragment内でLivedata型を受け取り、変更があった時に、Viewの更新をしています。
APIのModel
class StaticsModel( @Json(name = "executionTime") val execution_time: String, val point: String, val result: String, @Json(name = "problemId") val problem_id: String, @Json(name = "userId") val user_id: String, @Json(name = "epochSecond") val epoch_second: String, @Json(name = "contestId") val contest_id: String, val id: String, val language: String, val length: String )
Service
interface StaticsServise { @GET("atcoder-api/results") fun getProblems(@Query("user") user: String): Call<List<StaticsModel>> }
DBのModel
@Entity data class RoomStatics( val execution_time: String?, val point: String, val result: String, val problem_id: String, val user_id: String, val epoch_second: String, val contest_id: String, @PrimaryKey val id: String, val language: String, val length: String )
DBのDao
@Dao interface RoomStaticsDao{ // get @Query("SELECT * FROM roomStatics ORDER BY epoch_second ASC") fun getAll(): List<RoomStatics> // insert @Insert(onConflict = OnConflictStrategy.REPLACE) fun insert(roomTask: RoomStatics) @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertAll(vararg roomTask: RoomStatics) // update @Update fun updateTasks(vararg roomTask:RoomStatics) // delete @Delete fun delete(roomTask: RoomStatics) }
Database
@Database(entities = arrayOf(RoomStatics::class), version = 1) @TypeConverters(DateTimeConverter::class) abstract class AppDatabase : RoomDatabase() { // DAOを取得する。 abstract fun roomTaskDao(): RoomStaticsDao }
Repository
APIでインターネットから情報を受け取るメソッドと、DBに保存するメソッドとDBから情報を取得するメソッドを実装しています。
class StaticsRepository { //Retrofitインターフェース private var staticsServise: StaticsServise init { //okhttpのclient作成 val interceptor = HttpLoggingInterceptor() interceptor.level = HttpLoggingInterceptor.Level.BODY val client = OkHttpClient.Builder().addInterceptor(interceptor).build() //クライアント生成 var retrofit = Retrofit.Builder() .baseUrl("https://kenkoooo.com/atcoder/") .addConverterFactory(MoshiConverterFactory.create()) .client(client) .build() staticsServise = retrofit.create(StaticsServise::class.java) } //APIにリクエストし、レスポンスをLiveDataで返す(StaticsModel) fun getReqListStatic(user: String): LiveData<List<StaticsModel>> { val data = MutableLiveData<List<StaticsModel>>() runBlocking { thread { data.postValue(staticsServise.getProblems(user).execute().body()) } } return data } fun saveToDB(context: Context, list: List<StaticsModel>){ thread { //save to room val db = Room.databaseBuilder(context, AppDatabase::class.java, "database-name").build() list.map { db.roomTaskDao().insert( RoomStatics( execution_time = it.execution_time, point = it.point, result = it.result, problem_id = it.problem_id, user_id = it.user_id, epoch_second = it.epoch_second, contest_id = it.contest_id, id = it.id, language = it.language, length = it.length ) ) } } } fun getDBData(context: Context): LiveData<List<StaticsModel>>{ val data= MutableLiveData<List<StaticsModel>>() runBlocking { thread { val db = Room.databaseBuilder(context, AppDatabase::class.java, "database-name").build() val result = db.roomTaskDao().getAll() val model = result.map { StaticsModel( execution_time = it.execution_time ?: "0", point = it.point, result = it.result, problem_id = it.problem_id, user_id = it.user_id, epoch_second = it.epoch_second, contest_id = it.contest_id, id = it.id, language = it.language, length = it.length ) } data.postValue(model) } } return data } }
ViewModel
Androidの画面で必要な情報を保持しておくためのクラスとして設計しました。LiveDataの集約の集まりといってもいいでしょうか。
class StaticsAllProblemsViewModel : ViewModel() { var apiData: LiveData<List<StaticsModel>>? = null var dbData: LiveData<List<StaticsModel>>? = null fun getListDataByAPI(user: String): LiveData<List<StaticsModel>> { return apiData ?: StaticsRepository().getReqListStatic(user) } fun save(context: Context, list: List<StaticsModel>) { StaticsRepository().saveToDB(context, list) } fun getDBData(context: Context): LiveData<List<StaticsModel>> { return dbData ?: StaticsRepository().getDBData(context) } }
Fragment
class StaticsAllProblemsFragment : Fragment() { lateinit var bindong: FragmentStaticsAllProblemsBinding lateinit var viewModel: StaticsAllProblemsViewModel ... override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { // Inflate the layout for this fragment bindong = DataBindingUtil.inflate<FragmentStaticsAllProblemsBinding>( inflater, R.layout.fragment_statics_all_problems, container, false ) //ViewModel初期化 viewModel = ViewModelProviders.of(activity!!) .get(StaticsAllProblemsViewModel::class.java) //wifiにつながっているなら、APIで取得 val wifiManager: WifiManager = context?.getSystemService(Context.WIFI_SERVICE) as WifiManager if (wifiManager.connectionInfo.supplicantState == SupplicantState.COMPLETED) { //有効なら setListDataByApiAndDB() } else { //無効なら setListDataByDB() } return bindong.root } // APIでデータをリクエスト&ローカルデータベースに保存&表示:wifi接続時推奨 fun setListDataByApiAndDB() { viewModel.getListDataByAPI("ehu_151").observe(this, Observer { //save viewModel.save(context!!, it) setListDataByDB() }) } // ローカルデータベースからデータを参照:wifi未接続時推奨 fun setListDataByDB() { viewModel.getDBData(context!!).observe(this, Observer { val group = it.filter { Regex("abc").containsMatchIn(it.contest_id) }.groupBy { it.contest_id } val res = group.map { var contestName = it.key var a_result = "" var b_result = "" var c_result = "" var d_result = "" for (model in it.value) { when (model.problem_id) { contestName + "_a" -> a_result = model.result contestName + "_b" -> b_result = model.result contestName + "_c" -> c_result = model.result contestName + "_d" -> d_result = model.result } } StaticsAllProblemEntity(contestName, a_result, b_result, c_result, d_result) } // ここでbindingやadapterに代入 bindong.staticsListView.adapter = StaticsAllProblemsAdapter(context!!, res.sortedByDescending { it.problemName }) }) } }
直したいところと反省点
DBとAPIのModelがほとんど一緒
個人的な意見ですが、ほとんど同じならば、インターフェースを作成したくなりますね。しかし、自分には上手い設計方法が分からなかったので、雑な作りになっています。
Fragmentがロジックを持ってしまっている
DBから受け取ったデータをViewに表示する用のEntityに変換するために、Fragmentで変換を行う形になってしまいました。MVPアーキテクチャをやったことがある自分としては、あまりよく感じていません。本当ならば、FragmentはViewにデータを渡すだけにしたいですね。その為に、UseCaseに変換ロジック負わせて、MVVM+CleanArchitectureの形にしてもいいかもしれませんね。
以上です。良い設計に改良出来たら、またブログを書こうと思います
ホワイトボードを買ったら、でかすぎた件
壁掛け専用かーww
届く前まではこのサイズの半分くらいかと思ってたんですよ。それがこのサイズ。
壁から外して机に置いて書く想定だったのに、壁で書くしかなくなっちゃた...
因みに商品はこれ、
プラス ホワイトボード 壁掛け 幅600×高さ450mm PWK-0604SS マーカー・イレーザーセット 428-169
- 出版社/メーカー: PLUS(プラス)
- メディア: オフィス用品
- この商品を含むブログを見る
まあ、このまま使いますよ...
次は、磁石で張り付くペンとイレイサーが欲しいな
【レビュー 】ブックストッパーで技術書が読みやすくなるのか?
こんにちは。ehu-151です。
今回は、技術書を読むときの便利アイテムとして、ブックストッパーを購入したのでレビューしたいと思います。
重さはスマホぐらいの質量です。クリップは先に丸みがあるので、紙が傷つくことはありません。
挟むページは大体100枚ほどが限界で30ページ挟むのがちょうどいいです。
下の写真は340ページある本です。真ん中のページあたりをクリップで挟みましたが、たるんでます。やはりページをしっかり開く系ではないようですね。
まだ満足してないので、ブックストッパーをまた買うかもしれません。
ではまた次回でー
【SF漫画2作品漫画紹介&機械学習】僕の好きな漫画
こんにちは、ehu-151です。
今回は自分の読んでいる漫画の紹介をさせていただきます。普段、プログラミングをやっているのにわざと脱線します。(ちゃんと後々、使う記事だから大丈夫。)
それでは紹介します。
はっぴぃヱンド(現在3巻まで)
1巻の表紙が物々しいww。そして右上に血がある上で「はっぴぃ」と名乗っています。全然説得力ないじゃん...。
まあ、とにかく、漫画のあらすじを読んでみましょう。
あらすじ
親の仕事の都合で
都会から田舎に引っ越してきた茜。
たった10人のクラスメートだけど、
とっても賑やかな学校生活!
魚釣りに行ったり、駄菓子屋で買い食いしたり、
自然に溢れた村で過ごす日々は毎日が新鮮で刺激的!
「こんな幸せな日常が永遠に続けばいい」と、
茜は心から思っていたのだが…!?
新進気鋭の鬼才が描く、
最高のしゅうまつのすごしかた!
3段落目までは、ほのぼの、日常系によくあるあらすじなのですが、4, 5段落目から不穏ですね。まずは、1話を読んでみてください。立ち読みで2話まで読めます。
どうでしょうか。「可愛い漫画」だと思いましたか?気になったらぜひ買ってみてください。
All You Need Is Kill(2巻 完結)
こちらはハリウッド映画化された漫画になります。死ぬとタイムリープするという現象にあいます。しかも、主人公は軍人で死と隣り合わせです。無事ループから抜け出すことができるのでしょうか?
通称ループものの部類になります。2巻ですが、短い時間でループものを読むときに最適です。
さいごに
機械学習で似ている漫画を予測しようという取り組みです。(失敗しましたが)