駆け出しエンジニアぱかぱかの成長記録

引くほど忘れっぽい新卒2年目駆け出しSEぱかぱかの備忘録です。

【日記】コーヒーに胡椒を入れると美味いらしいよ

こんにちは!ぱかぱかです!
新年あけましておめでとうございます。
気づいたらなんとなく年が明けていました。
今年もよろしくおねがいいたします。

今日は特に技術的に関係ない記事を書きたいと思います。
本当はこういう記事もたくさん書いていきたいんですけどね…
記憶力がない私が後々振り返れるようにすることが大目的なので、内容はなんだっていいのです。

年末年始は地元に帰省をして、友だちと会ったり孤独のグルメを横目に見ながらコードと格闘したりして過ごしています。
なんかLiveDataを使った実装がずっとうまく行っていないんですよね…
この記事は後々書きたいと思いますが…

デスクワークのお供はいつもコーヒーの私。
ここで父親からまさかの提案を受けました。

「コーヒーに胡椒を入れると美味いらしいよ」

え、そんなわけある!?
ちょっと変わった食べ物があるとつい試してしまう私でも、ちょっと尻込み。

しかしものは試し、疑い半分で粒胡椒をゴリゴリ削ってコーヒーに入れてみました。

異色の組み合わせ…
黒い粒が胡椒です…

恐る恐る一口飲んでみると…これがなかなかうまい!?

スパイシーな胡椒の香りとコーヒーの香りが意外にも相性良し。
胡椒のかけらを噛むとさらに香りが強く広がります。
ピリピリした刺激もおもしろくて飽きが来ないです。
追い胡椒をさらにゴリゴリしてしまいました。

コーヒーに胡椒ってあるあるなのか?と思ってネットで検索してみると、意外と記事がヒットします。

スパイスを扱ってるエスビー食品のページにもレシピが乗っていました。
それは納得かも。
www.sbfoods.co.jp

しかしそれだけでなく、コーヒーのUCCでもちゃんとペッパー・スパイスコーヒーを取り上げています。
www.ucc.co.jp

粉よりも粗挽きの方が食感も楽しめるので個人的におすすめです。

これからも時々試しちゃいそうな予感…
人の提案には乗って見るものですね。

ではまた!

【日記】今年1年振り返り

こんにちは!ぱかぱかです!
誰もが書いていそうな内容になりますが私も残しておきます、今年1年の振り返りです。

今年1年の振り返り

今年も1年色んなことがあったと思いますが、記憶力が悪く過去を顧みるのが苦手なタイプなので簡単に…
一応普段私がこのブログに書いているような技術系の内容でいくつかピックアップすると、以下のような感じです。

・モバイルアプリ開発に携われた
・転職活動してみた
・資格2つとれた

1つ1つ振り返ってみます。

モバイルアプリ開発に携われた

ここは一番大きいですね。
今年のちょうど1月からiOSAndroidの両OSを扱うモバイルアプリのプロジェクトに配属されました。
BtoCでユーザも身近に多数いるような大手アプリの開発に携われたのは非常に良い経験になりました。
自分の開発したものが自分の友人も含めた一般ユーザに触ってもらえる経験って、エンジニアとしても貴重なのではないでしょうか。
(勝手にBtoBの業務系システムなど裏方の開発が主流のイメージがあります。)
やっぱり、やりがいは感じやすいですよね。

スキルについては新たにOS2つ、言語は4つ(Java, Kotlin, Swift, Objective-C)に触れることになりましたが、全く学習が追いつかずヒーヒー言っていました…
ただ、1週間スプリントのアジャイル開発ということもあり毎週新しい課題に挑戦させてもらい多種多様な経験ができたのが良かったです。

そして、OSや言語の多さにも気を取られがちなのですが、結局はアーキテクチャや設計理論など根幹となる考え方が大切になるんだということを学べた1年でもありました。
1週間以上かけて1つの機能をクラス設計を練ってあれこれ構成を考え直すような機会もあり、本当に拡張性や保守性の高い設計とは何なのかを色々考えられました。
このような力が今後も大切になってくるのでしょうか。
AIに頼めば一瞬でコードを書き上げてくれる時代がもうそこまで来ていますが、設計についてはどこまで考えてくれるようになるかは気になるところです。

AIに仕事を奪われるかもって考え出したら、今の時代何をやるのが正解かわからなくなってきますよね…
でもIT系の仕事を続けるからには、例え自分で手を動かさなくても、常識としてプログラミングの理論を根底に持った人間になりたいですね。

転職活動してみた

これは完全に一瞬の出来事でした。
radish-se.hatenablog.com
大手企業の社内SE休日選考会に試しに参加してみたら内定がもらえてしまいました。
ここの企業は行きたいという強い意志があるわけではなかったので辞退してしまいましたが、またタイミングを見計らって転職活動を再開したいです。
学びとしては、まだそんなに経験を積めているわけではないけど内定がもらえるほどに、この業界は人が足りていないのだなというところです。
その分、こんな私でもまだチャンスがありそうだなと。

今の会社では様々なプロジェクトで色々な案件を受託していますが、会社が全員バラバラの方向を見て仕事をしているのが何だか違和感があります。
一つの強みや目的を持った事業会社や自社サービスを持ったIT企業で仕事をしたいという気持ちは強くなる一方です。
そして二次請け三次請けのような構造にならない分、給料もいいし…

ただ、地方勤務も捨て難いのでどうにか地方からそのようなチャンスに挑戦できる求人がないかと時々漁っています。
今の会社にも恩があるので心苦しいのですが、そんなことを言い始めたら一生変われないのでそこは割り切りですね。

でも改めて、新卒の自分がうっかりIT業界に流れ込めたのは本当にラッキーだったなと思います。
新卒の時の就職活動は主にメーカーで色々探していたのですが、メーカー勤務だったらこのように選択肢が広がっていなかったのではないでしょうか。
休日も捧げるぐらい楽しいと思える分野に出会えたことにも感謝しなければですね。
これからも学ぶことを楽しみながら仕事をしていきたいと思います。

資格2つ取れた

今年は資格取得以外にもやりたいことがありだいぶ失速していましたが、以下を取得できました。

OSS-DB Silver
応用情報技術者

OSS-DB Silverは1月に取得したのですが、その後業務でPostgresに触れる機会もなかったので細かいところは忘れ去ってしまっています…
でも、DBの知識はどこでも共通なので活かせているはずです。
ただ、今のプロジェクトはモバイルということもありDBをゴリゴリに扱う機会が減ってしまったので、来年データベーススペシャリストの取得に向けた学習を通してDB関係のスキルを強化できたらいいなと思っています。

応用情報技術者については最近上げたばかりの記事に色々と書かせていただきました。
radish-se.hatenablog.com
結構時間を奪われましたが、普段学ぶこともないような分野も含めて幅広い知識をつけられたたので結果として取って良かったなと思える資格でした。
今後も広く浅くつけられた知識を色んな機会で深めて一過性のものにならないようにできたらと思っています。

来年の抱負

来年は以下を目標にしたいと思います。

転職活動

転職を実際にするか否かは正直決めきれていません…
来年度が新卒3年目なので、1社目で3年いっぱい経験を積んでから次に行く方がいいのかなとも考えたり。
ただ、第二新卒としての方が動き回りやすい点もあるかもなので、そこはお試し活動していく中で考えたいと思います。
短い人生なので後悔のないようにしなければですね。
色々求人を見つつ、チャンスがあれば面接等受けて良いご縁があれば転職してしまうのもありかと思っています。

アプリリリース

11月から作り始めたグルメノートというAndroidアプリですが、亀の歩みで開発を進めています。
業務で登場していないMVVMモデルやLiveData、ViewBinding、Roomなどの知識も学習しながらで遠回りになっていますが、ある程度形にしてリリースしてみたいですね。
その後は同様の構成でiOSに移植して、完全に手薄になっているiOS側のスキルも深めていきたいと思います。

資格取得

資格は第一優先ではないのですが、来年も2つくらい取れたらいいなと思っています。

AWS資格
ここは全然今まで触れる機会がないけど必要とされる知識だと思うので簡単めのものを1つくらい取っておきたいですね。
まだどれにするかは決まっていません。
それくらい知識が浅いです…
この前S3とアプリの連携で格闘して失敗したり色々苦戦していますが、どうにかアプリ開発にも取り入れていきたいです。

IPA高度資格
これはせっかく午前Iの免除が有効なうちにできれば取りたいというところです。
先ほども述べたように今のところはデータベーススペシャリストが有力候補ですかね…

まとめ

今年の3月から気まぐれに始めたブログでしたが、読んでくださってありがとうございます。
もう少し更新頻度とクオリティが上がるよう意識しつつ、来年も続けていきたいと思いますのでよろしくお願いいたします!

それでは良いお年を!

【Java】インナークラスってなんだっけ

こんにちは!ぱかぱかです!
今日は仕事納めということで1年の振り返りでも書きたいところだったのですが、業務でインナークラスの話になり、いまいちよくわかっていないなと思ったのでまとめておくことにしました。
(振り返りは年末ギリギリまで取っておくことにします笑)

なんか今までの記事の流れを見るとだいぶ唐突なのですが、鉄は熱いうちに打っておきましょう。
Java Goldでも黒本の第1章とか最初の方で出てくるような内容なので知ってはいるけど、いまいち使い所がよくわかっていないという…
SwiftやKotlinなど他の言語でも同じような概念が存在するようですが、今回はJavaベースでまとめておきます。

ネストクラス

ネストクラスとはずばり、クラスの中に定義されたクラスのことです。
外側のクラスのメンバとして、内側のクラスがいる状態になります。
ちなみに外側のクラスのことをエンクロージングクラスとも言います。

class Outer {
    class A {} // 非staticクラス → インナークラス
    static class B {} // staticクラス
}

ネストクラスにはstaticクラスおよび非staticクラスが定義できますが、特に非staticクラスのことをインナークラスと呼びます。
インナークラスにはさらにローカルクラスと匿名クラスが存在しますが、そちらは後の方にまとめたいと思います。

ネストクラスの分類

ネストクラスへのアクセス

ネストクラス内で定義したメンバはネストクラスをインスタンス化することで利用できます。
上のコード例でクラスをインスタンス化する場合は以下のような書き方になります。

非staticクラス(インナークラス)

外側クラス名.非staticクラス名 変数名 = new 外側クラス名().new 非staticクラス名();

Outer.A a = new Outer().new A();

staticクラス

外側クラス名.staticクラス名 変数名 = new 外側クラス名().staticクラス名();

Outer.B b = new Outer().B();

staticメンバなのでnewしなくても呼び出せるというお話で、書き方が違うだけですかね。

インナークラスの種類

非staticクラス(インナークラス)の種類にもまとめておきます。

ローカルクラス

ローカルクラスはあるクラスのメソッド内に定義したクラスのことです。
スコープはメソッド内で制限され、定義されているメソッド内でのみインスタンス化できます。

class Outer {
    void method() {
        class A {} // ローカルクラス
    }
}

匿名クラス

匿名クラスはクラス名を指定せずにクラス定義とインスタンス化を1つの式として記述したクラスのことです。
再利用されることがなく特定の場所のみで実装したい場合に使用します。

class Outer {
    void method() {
        new スーパークラス名またはインターフェース名() {} // 匿名クラス
    };
}

あまり意識していませんでしたが、SAM変換していないAndroidのsetOnClickListenerとかで匿名クラスが出てきてますかね。
(ネストクラスとは関係ないかもですが…)

View.onClickListenerインターフェイスを継承した匿名クラスを第1引数に取っていて、インターフェースのonClickメソッドをoverrideしています。

view.setOnClickListener(object: View.OnClickListener {
     override fun onClick(view: View?): Unit {
          
   }
})

ネストクラスのメリット

色々細かいルールは他にもあるのですがそこら辺は端折ってます。
今回知っておきたかったネストクラスのメリットとしては以下のようなところになります。

メリット

・使用場所が限定されその存在を外部から隠すことができる。
・ソース上で依存関係を表せる。
・外側のインスタンス化を強制できる。

使われる場所が限定されるようなクラスは内部クラスで持っておくというのも1つの手かもしれないですね。
ただあまり実装では使われているところを見かけません…
クラスを普通に分けておくのでもいいような気もしますが、依存関係を意識することを強制できるのがいいのかな…
結局使い方は復習できたけど、ここぞという使い所は掴めず…
何かいい例があったら教えていただきたいです。

短いですが今回はこれくらいにします。
ではまた!

【応用情報】合格体験記

こんにちは!ぱかぱかです!
今日は表題にもあるとおり2023年10月8日に受験した応用情報の合格体験記を書いておきたいと思います。

radish-se.hatenablog.com

以前の記事で受けた直後の感想を書き残していましたが、午後試験に自信が無かったのでかなりビビりながら試験結果を見ました…
結果はなんとか合格できました!

合格体験記というほどではないですが、学習方法等をメモしておきたいと思います。

受験結果

今回が1回目の受験になります。
正直、他にもやりたいことがある中で勉強時間を持って行かれて苦痛だったので、一発で合格できて本当に良かったです…

試験結果は以下のような感じでした。

午前:77.5点
午後:71.0点

なんとも微妙ですが、受かればなんでもOKですね。

午前試験

内訳はこんな感じ。

ストラテジ系:18.75 / 25点(75%)
マネジメント系:10.0 / 12.5点(80%)
テクノロジ系:48.75 / 62.5点(78%)

後述しますがひたすら応用情報技術者過去問ドットコムで過去問を解きまくっていたので、午前は見たことある問題ばかりで安心しました。
勉強が面倒で完全に捨てていた基礎理論の数学チックな問題も、ノリでなんとか乗り切ることができたので良かったです…

過去問でも全く見かけなかった問題がいくつかあり焦りましたが、これは他の受験者の方も一緒でしょうかね。

◆初耳すぎてビビった単語たち
(後述する応用情報技術者合格教本にも記載ありませんでした…そりゃ知らんよ…)

ヘテロジニアスマルチプロセッサ
・ユニファイドメモリ方式
・IaC
MOSトランジスタ
・マシンビジョン

午後試験

午後は技術的な問題が怖いので国語力で解けそうな分野に絞って対策していました。
当日受けたのは以下の分野です。
それぞれの点数は結果情報に記載されていませんでいた。

・情報セキュリティ(必須)
・経営戦略
・プロジェクトマネジメント
サービスマネジメント
・システム監査

午後試験は元々苦手だったのですが、本番の運だなと思ってそんなに対策していませんでした。
本番も改めて、これは対策しててもどうしようもないよなと感じましたね…国語力に尽きる…
午後にどれくらい集中力が残っているかが問題な気がします。
時間はだいぶ余ったので、見直しつつ記述のそれっぽさを上げたりして暇を潰しました。

勉強方法

勉強は以下の記事にもあるように6月の下旬から始めました。
一応丸3ヶ月くらいはやったことになります。

radish-se.hatenablog.com

でも応用情報一本に捧げていたというよりは、1日過去問30問解くというノルマだけこなしていたという感じですね。
使用した教材は以下です。

使用教材

応用情報技術者過去問ドットコム

www.ap-siken.com

こちらのサイトが結局一番お世話になりました…
「分野を指定して出題」の形式で基礎理論・アルゴリズムとプログラミング以外は全問解いたと思います。
この形式にすると重複問題が除かれているらしいので、効率よく学習ができます。
全2960問中1808問解答したみたいです。
重複した問題を解かずにこの問題数なので、カバー率的にはもっと高いはずです。
段位は基準がわかりませんが四段くらいになりました。


ちゃんと解き直していないので微妙な成績なのが恥ずかしい…

応用情報技術者合格教本

こちらは補足的に使っていました。
「合格教本に載っている」=「最低限覚えておいた方がいい」という認識だったので、過去問を解いていてこれはマニアック問題なのか覚えるべき内容なのかの判断材料にしていました。
最後、後述する「OneNoteに学んだ内容をまとめる」という作業をした際に、網羅性を確かめるためにこの教本の章立てを参考にしていました。
正直文章を黙々と読むだけでは頭に入ってこなかったので、わからない情報はネットで検索してわかりやすい記事を漁るということの方が多かったですね。

・Udemy「令和6年春:現役講師が教える【応用情報技術者試験 午前版】講座  合格に必要な知識の徹底解説+過去問題解説」

www.udemy.com

こちらは各章を学びはじめる前の導入として見ていました。
必要最低限の内容を説明していたので、ここで説明されていることは本当に重要なんだろうなの確認として役に立ちました。
結局出題は全然見かけなかったのですが、モジュール結合度のような覚えづらいところの語呂合わせが意外とありがたかったですね。
(内容結合〜データ結合と色々あって、内容結合が一番結合度強いよみたいなやつです。
NIKEが制すデータ」みたいな語呂合わせでした笑)
メモリ管理やネットワークなどわかりにくい分野も図や動画を使ってわかりやすく説明してくれており、理解の助けになりました。

学習の流れ

主に平日仕事終わった後と、予定のない休日に時間を見つけて勉強していました。

7~8月
・午前問題を各章ごとに学習

 Udemyの講座を見つつ教本をパラっと読んで、雰囲気を掴んだらひたすら午前過去問。
 応用情報技術者過去問ドットコムでノルマ午前過去問1日30問くらいをちまちまやり続けました。

9月
・初めて午後試験に手をつける

 結局午後の勉強がダルすぎて、解こうと思っている分野各5回分だけ解いて終了…
 あとは本番の運に任せる…

OneNoteに各章ごとに過去問や教本で学んだ知識を体系的にまとめ直す

 これが結構効いた気がします。
 過去問をただ解くだけだと知識が断片化してしまいますが、まとめ直すことで試験対策に留まらない今後に役立つ体系的な理解に繋がりました。

受験して良かったか

結論、良かったと思います。
アプリ開発の勉強など色々やりたいことがありそれが妨げられたのはストレスでしたが、どの知識もIT業界にいるなら必要な最低限の知識で実際に役立つ場面もちらほらありました。
実務では実感することが少ないのですが、先日技術のカンファレンスに参加した際にセキュリティからハードウェア、マネジメントに至るまで幅広い技術の話が登場するような場面で改めて学習した意義を感じられましたね。

ただ、受験後どんどんせっかく学んだ知識が抜け落ちていっているので、復習がてらブログにもちまちま記録を残せたらいいなと思っています。
せっかくOneNoteに色々調べた情報とともにまとめているので、ブログのネタが無い日に少しずつ放出しつつ記憶を蘇らせたいと思います。

次どうするか

今のところ新卒2年間で取得したIT系の資格は以下のような感じです。

応用情報技術者
基本情報技術者
Java Gold
Java Silver
OSS-DB Silver

会社のお金で色々受けさせてもらっているので、正直これ以上受けると会社に申し訳ないという気持ちがあります。
あとは他に資格をたくさん取っている人が少ないのであまり目立ちたくないという思いも…
ただ、自分はやはり試験というわかりやすい目標があった方がモチベが保てるタイプなのだなと感じることも多く…
せっかく応用情報を取ったので免除が適用されるうちに高度試験も取っておきたい気持ちもあります。
興味があるのは情報処理安全確保支援士データベーススペシャリストあたりですかね…
来年の秋くらいなら要検討ですが、次の春にという気は今のところなしです。
教材だけ買っておこうかな。
AWSの資格とSwiftの資格も気になっているので、モチベを見て決めたいですね。(結局資格取ろうとしている…)

でもしばらくは、あくまでアプリ開発を主軸に今後も学習していきたいと思いますよ!

ではまた!

【Android】RecyclerViewとViewbinding/DataBinding

お久しぶりです!ぱかぱかです!
また前回記事から時間が空いてしまいました…

今日は現在開発を進めている飲食店記録アプリ「グルメノート」で、Viewbindingと
DataBindingを使ってRecyclerViewに紐づける実装についてまとめておきます。
本当は店登録画面からお店の画像をアップロードしAWSのS3に保存するところを実装したかったのですが、AWSの認証関係がうまくいかず挫折しました…
またいつかリベンジします…

前回の記事

radish-se.hatenablog.com

Roomを使って店登録画面から店の情報をrestaurantsテーブルとvisit_recordsテーブルに登録できるようになった!

ViewBinding

今回はViewBindingとDataBindingが登場しますが、まずはViewBindingについてまとめていきます。
ViewBindingとはモデルオブジェクトとレイアウトを結びつける仕組みです。
findViewByIdしてコード側でView情報を取得せずとも、バインディングオブジェクトを通じて画面部品へアクセスできるようになります。

ちなみに今私がいるプロジェクトでは使われておらず、findViewByIdしてせっせと紐づけて実装しています。
findViewById()を使うとコードが煩雑になりますし、id指定誤りによるNullPointerExceptionや、データ型誤りによるClassCastExceptioinの危険性を孕んでいます。
これが原因でバグが発生した事例もありViewBindingに移行したほうがいいと思うのですが、なかなかな修正量になるので難しいところですね。

build.gradleでViewBindingオプションを設定

ViewBindingを利用するためにbuild.gradle(Module: app)にビルドオプションを設定します。
Android公式ページの記述に従いbuildFeaturesのviewBindingをtrueにします。
(公式で日本語のViewBindingのページを見ると定義の仕方が少し古くなってます。ちゃんとメンテされていないんですかね…)

View binding  |  Android Developers

android {
    ...
    buildFeatures {
        viewBinding true
    }
}

バインディングオブジェクトの利用

なんと、build.gradleに先ほどの数行を追加しただけで、もうバインディングオブジェクトが自動で生成されるようになります。
オブジェクト名はレイアウトxmlファイル名のキャメル記法 + "Binding"です。
例えば今回はRecyclerViewを配置している「fragment_visited_restaurant.xml」を使おうと思いますが、バインディングオブジェクト名は以下のように自動生成されています。

fragment_visited_restaurant.xml → FragmentVisitedRestaurantBinding

参考までに「fragment_visited_restaurant.xml」の中身です。
RecyclerViewがあるだけですね。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_height="wrap_content"
        android:layout_width="0dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

今回はこれを行った店リストを表示するVisitedRestaurantFragmentから利用したいと思います。
バインディングオブジェクトを取得する際はinflate()を使用します。
(FragmentVisitedRestaurantBindingにstaticメソッドとして自動生成されるようです)

Fragmentから利用する場合は引数が3個のinflate()を使用し、引数を以下のように指定します。

第1引数:onCreateView()メソッドの引数inflater
第2引数:onCreateView()メソッドの引数container
第3引数:false

レイアウトxmlファイルを元にinflateされた画面全体のインスタンス(contentView)はバインディングオブジェクトのrootで取得できるので、それを戻り値としてreturnします。
これで、ViewBindingで生成したRecyclerViewのレイアウトが表示されるようになります。

class VisitedRestaurantFragment: Fragment()  {
    lateinit var binding: FragmentVisitedRestaurantBinding

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = FragmentVisitedRestaurantBinding.inflate(inflater, container, false)

        return binding.root
    }
}

DataBinding

次はDataBindingについてまとめていきます。
ViewBindingだけではViewで表示する情報をコード側で設定する必要がありますが、ここにDataBindingを組み合わせるとレイアウト側がモデルから情報を受け取りViewに反映するところまで実現できます。
SpringでいうThymeleafみたいのものでしょうか。

build.gradleでDataBindingオプションを設定

DataBindingを利用するためにこちらもViewBindingと同様にbuild.gradle(Module: app)にビルドオプションを設定します。
Android公式ページの記述に従いbuildFeaturesのdataBindingをtrueにします。

Databinding  |  Android デベロッパー  |  Android Developers

android {
    ...
    buildFeatures {
        viewBinding true
        dataBinding true ←追加
    }
}

レイアウトファイルの修正

DataBindingでは、画面部品に反映させるデータをレイアウトファイルに設定する必要があります。
今度は行った店リストの各セルを表すレイアウトファイル「visited_restaurant_list_item.xml」を以下のように修正していきます。

①レイアウト全体をタグで囲む。
②先頭にタグを追加しメンバーを定義する。
ウィジェットの中でビューに反映したいデータを指定する。

<?xml version="1.0" encoding="utf-8"?>
<layout> ←①

    <data> ←②
        <variable
            name="visitedRestaurantItem"
            type="com.example.gourmetnote.VisitedRestaurantListItem"/>
    </data>

    <androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/list_item_card"
    ... >

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:id="@+id/restaurant_name"
            ...
            android:text="@{visitedRestaurantItem.restaurantName}" ←③
            ... />

...
</layout>

①のようにlayoutタグでラップするのはお決まりの記述になります。
②のdataタグの中でレイアウトで使用したいオブジェクトとそれを表す変数を定義します。
その変数を使って③のようにレイアウト内でデータにアクセスすることで値を表示することができます。

visited_restaurant_list_item.xmlの外観は以下のような感じで、店名・場所・ジャンル・登録日・メモなどにデータを表示したいので、それぞれ③のようにプレースホルダで設定していきます。


ViewModelとの紐付け

今度はAdapterでRecyclerViewとViewModelとを紐づける実装を修正します。

ViewModel

ViewModelは前回の記事でも登場しましたが、アクティビティに必要なデータを保持するクラスです。
この中でDBのデータをRepository経由で取り出してリストに詰めて持っておきたいと思うのですが、今回は記事が長くなりそうなので一旦固定値のvisitedRestaurantListを持たておきます。

class VisitedRestaurantViewModel(application: Application): AndroidViewModel(application) {
    val visitedRestaurantList = listOf(
        VisitedRestaurantListItem("吉野家",Evaluation.THREE_STAR,"東京","和食","2023/11/01","新メニューが美味しかった",
            ContextCompat.getDrawable(application, R.drawable.gyudon),true),
        VisitedRestaurantListItem("サイゼリヤ",Evaluation.FIVE_STAR,"名古屋","イタリアン","2023/11/01","安すぎ",ContextCompat.getDrawable(application, R.drawable.doria),true),
        VisitedRestaurantListItem("牛角",Evaluation.FOUR_STAR,"大阪","焼肉","2023/11/01","牛タンうまい",ContextCompat.getDrawable(application, R.drawable.yakiniku),false),
        VisitedRestaurantListItem("CoCo壱",Evaluation.THREE_STAR,"京都","カレー","2023/11/01","辛かった",ContextCompat.getDrawable(application, R.drawable.curry),true),
        VisitedRestaurantListItem("天下一品",Evaluation.ONE_STAR,"神戸","ラーメン","2023/11/01","味が濃い",ContextCompat.getDrawable(application, R.drawable.ramen),false),
        VisitedRestaurantListItem("コメダ珈琲",Evaluation.TWO_STAR,"広島","カフェ","2023/11/01","量多い",ContextCompat.getDrawable(application, R.drawable.komeda),false),
        VisitedRestaurantListItem("スシロー",Evaluation.THREE_STAR,"沖縄","寿司","2023/11/01","サーモン3皿食べた",ContextCompat.getDrawable(application, R.drawable.sushiro),true),
        VisitedRestaurantListItem("餃子の王将",Evaluation.FIVE_STAR,"札幌","中華","2023/11/01","餃子テイクアウトした",ContextCompat.getDrawable(application, R.drawable.gyoza),false),
        VisitedRestaurantListItem("やよい軒",Evaluation.THREE_STAR,"仙台","和食","2023/11/01","健康的",ContextCompat.getDrawable(application, R.drawable.yayoi),true),
        VisitedRestaurantListItem("マクドナルド",Evaluation.FOUR_STAR,"青森","ファストフード","2023/11/01","ポテト高くなってた",ContextCompat.getDrawable(application, R.drawable.mac),false)
    )
}

Adapter

Adapterの実装も修正します。
以前RecyclerViewを実装した時はRecyclerView.Adapterを継承していたのですが、今回はListAdapterを採用してみます。
ListAdapter は RecyclerView.Adapterを継承したクラスです。
RecyclerView.Adapterと異なりoverrideする関数が少なくリスト変数を保持する必要がないListAdapterを継承するのが一般的なようです。

VisitedRestaurantListItemBindingでvisited_restaurant_list_item.xmlに持たせたvisitedRestaurantItem変数に情報を渡すbind()を作り、onBindViewHolderの中で実行しています。

class VisitedRestaurantListAdapter(private val viewModel: VisitedRestaurantViewModel): ListAdapter<VisitedRestaurantListItem, VisitedRestaurantListAdapter.VisitedRestaurantCardViewHolder>(DiffCallBack) {
        class VisitedRestaurantCardViewHolder(private val binding: VisitedRestaurantListItemBinding): RecyclerView.ViewHolder(binding.root) {
            fun bind(item: VisitedRestaurantListItem) {
                binding.visitedRestaurantItem = item
            }
        }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VisitedRestaurantCardViewHolder {
        val layoutInflater = LayoutInflater.from(parent.context)
        return VisitedRestaurantCardViewHolder(VisitedRestaurantListItemBinding.inflate(layoutInflater,parent,false))
    }

    override fun getItemCount(): Int {
        return viewModel.visitedRestaurantList.count()
    }

    override fun onBindViewHolder(holder: VisitedRestaurantCardViewHolder, position: Int) {
        holder.bind(viewModel.visitedRestaurantList[position])
    }
}

ListAdapterではDiffUtil.ItemCallback型の変数をコンストラクタに渡す必要があるので、Adapterクラス内で定義しておきます。
これは2つの要素を比較するユーティリティクラスで、ListAdapterでは要素の追加・変更・削除を検知するのに使われます。
これで判定基準が適切かはさておき、ひとまず必要なので作成しておきました。

    private object DiffCallBack: DiffUtil.ItemCallback<VisitedRestaurantListItem>() {
        override fun areContentsTheSame(
            oldItem: VisitedRestaurantListItem,
            newItem: VisitedRestaurantListItem
        ): Boolean {
            return oldItem == newItem
        }

        override fun areItemsTheSame(
            oldItem: VisitedRestaurantListItem,
            newItem: VisitedRestaurantListItem
        ): Boolean {
            return oldItem.restaurantName == newItem.restaurantName
        }
    }

Fragmentの修正

RecyclerViewにlayoutManagerとadapterを設定してあげます。

class VisitedRestaurantFragment: Fragment()  {
    lateinit var binding: FragmentVisitedRestaurantBinding
    private val viewModel: VisitedRestaurantViewModel by viewModels()

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = FragmentVisitedRestaurantBinding.inflate(inflater, container, false)
        binding.recyclerView.layoutManager = LinearLayoutManager(context) ←追加
        binding.recyclerView.adapter = VisitedRestaurantListAdapter(viewModel) ←追加

        return binding.root
    }
}

完成

中身は色々いじったけど、完成品は何も変わらないんですよね〜笑
でも、少しずつあるべき姿に近づいていますよ!

次回はコードにベタ打ちしたリストではなくDBから取り出したデータを元に表示できるようにします。
あとはデータソースの更新に連動して画面表示も自動的に変化するLiveDataも取り入れていきたいと思います。

ではまた!

【Android】Roomで複数テーブルに対して処理する

こんにちは!ぱかぱかです!
今日は現在開発を進めている飲食店記録アプリ「グルメノート」で、店登録画面からデータベースにデータを登録する処理の続きです。
前回はRoomライブラリを使って行きたい店の情報をrestaurantsテーブルに対してのみ登録する処理を実装しました。
(余談ですが、restaurantsっていちいち長いんだよなぁ…)
今回は、行きたい店登録によって飲食店テーブルと訪問記録テーブルの両方に登録がされるというあるべき姿に修正していきたいと思います。

前回の記事

radish-se.hatenablog.com

Roomを使って店の名前をrestaurantsテーブルに登録できるようになった!

ER図の見直し

改めてDB処理を書こうと思い以前作ったER図を見直してみると、やっぱりなんか変では?となりました。


おかしいところ

・行きたい店・行った店・お気に入り店テーブルは多対多になってしまうユーザと飲食店を結ぶ中間テーブルのつもりで作ったけど、なんで中間テーブルがメモとかの情報を持ってるんだ?
・飲食店が場所テーブルやジャンルテーブルを外部キーで持ってるけど、ジャンルとか場所ってユーザ固有のものではないならわざわざDBで持たなくていいのでは?
 場所情報はGoogleマップAPIとかから取得した情報を持っておけばいいし、ジャンルはEnumとかでアプリ側で定義すればいいような。シーンも同じ。
・行きたい店・行った店・お気に入り店の状態が行き来するなら、共通の情報として管理して外部キーで行きたい店・行った店・お気に入り店特有の詳細を持てばいいのでは?

というわけで見直した結果、以下のようになりました。

いや、こんなシンプルでいいのか…なんか考えすぎて逆に気持ち悪い状況になってましたね。
飲食店が親テーブルで、行きたい店詳細・訪問記録・お気に入り店詳細を子テーブルとして持つ構成にしてます。
なんならもう1つの飲食店テーブルで行きたい店・行った店・お気に入り店の情報も持ってしまってもいいのかもしれませんが…
とりあえず一旦これで突き進むとします。

今回行う登録処理

今回実装したい処理の流れは以下のようになります。

①行った店登録画面に以下の情報を入力して「行った店登録」ボタンを押下する。
 店名、場所、ジャンル、評価、感想
②飲食店テーブル(restaurants)に入力した店名、場所、ジャンルが登録される。
 評価、感想は訪問記録テーブル(visit_records)に登録される。

1つの登録処理で2つのテーブルが更新されることになります。

Roomで複数テーブルの更新を行うには

まずRoomではエンティティクラス間のオブジェクト参照を許可していません。
理由は、UIスレッド上で遅延読み込みが発生するとパフォーマンスに問題が出るからとのこと。

Room を使用して複雑なデータを参照する  |  デベロッパー向け Android  |  Android Developers

「Room がオブジェクト参照をサポートしない理由を理解する」の箇所に説明が書かれています。
例えば今回であれば飲食店テーブルのエンティティオブジェクト内で訪問記録テーブルを参照するようにした場合、毎回訪問記録テーブルへの問い合わせが発生しメモリを食うことになります。
そういう構成にしてしまうと、訪問記録テーブルへの問い合わせが不要になった場合にもデータの読み込み方法を変更することが難しいため、無駄なメモリを食い続けることになってしまうそう。
モバイルアプリはとにかくメモリ節約を意識しなきゃいけないんですね。

そうならないように、Android公式では以下の2つのアプローチが提案されています。
オブジェクト間のリレーションを定義する  |  デベロッパー向け Android  |  Android Developers

  1. 埋め込みオブジェクトを持つ中間データクラスを使用
  2. マルチマップの戻り値の型を持つリレーショナルクエリメソッドを使用

それぞれにメリット・デメリットはありますが、公式ではマルチマップの戻り値の型アプローチを推奨しています。
SQLでゴニョゴニョした方が結局楽ということなのでしょう。

というわけで私もマルチマップの戻り値の型アプローチで進めていきたいと思います。

(ただし今回はInsertだけなのでSQLゴニョゴニョはしません。)

エンティティの作成

まずは粛々と、飲食店エンティティ(restaurants)と訪問記録エンティティ(visit_records)を作ります。

・飲食店エンティティ(restaurants)
前回作ったけどER図変更に伴いカラム構成を修正しました。

カラム名 データ型 内容
restaurant_id INTEGER 飲食店のID(主キー)
restaurant_name TEXT 飲食店の名前
place_name TEXT 場所名(Place Searchレスポンスのnameを格納)
genre_id INTEGER ジャンルのID(Enumで定義)
created_time TEXT 作成日時
updated_time TEXT 更新日時
@Entity(tableName = "restaurants")
class RestaurantsEntity(
    @PrimaryKey(autoGenerate = true) val restaurant_id: Int,
    val restaurant_name: String,
    val place: String,
    val genre: String,
    val created_at: String,
    val updated_at: String
    )

・訪問記録エンティティ(visit_records)

カラム名 データ型 内容
visit_record_id INTEGER 訪問記録のID(主キー)
restaurant_id INTEGER 飲食店のID(外部キー)
scene_id INTEGER シーンのID(Enumで定義)
visited_date TEXT 訪問日
evaluation_id INTEGER 評価のID(Enumで定義)
visited_note TEXT 行った感想
created_at TEXT 作成日時
updated_time TEXT 更新日時
@Entity(tableName = "visited_records")
class VisitedRecordsEntity(
    @PrimaryKey(autoGenerate = true) val visit_record_id: Int,
    var restaurant_id: Long,
    val scene: String,
    val visited_date: String,
    val evaluation: String,
    val visited_note: String,
    val created_at: String,
    val updated_time: String
)

各エンティティのDAOを作成

次に各エンティティのDAOを作成します。
それぞれにINSERTするメソッドだけとりあえず作成します。

・飲食店エンティティ(restaurants)

@Dao
interface RestaurantsEntityDAO {
    @Insert
    suspend fun insertRestaurants(restaurantsEntity: RestaurantsEntity): Long
}

@Insertメソッドでは渡すパラメータが1つの時、戻り値をLongで指定すると挿入したテーブルの主キーが返るそうです。
便利ですね。後ほどその主キーをもとに子テーブルの更新などを行います。

・訪問記録エンティティ(visit_records)

@Dao
interface VisitedRecordsDAO {
    @Insert
    suspend fun insertVisitedRecords(visitedRecordsEntity: VisitedRecordsEntity)
}

2つのテーブルのデータ処理を行うレポジトリの作成

前回の記事ではViewModelの中でデータの処理を色々書いてしまっていたのですが、あくまでViewModelはアクティビティに必要なデータを保持するクラスです。
本来はデータ処理をリポジトリクラスに分けておくことが望ましいので、今回はRestaurantVisitedRecordsRepositoryクラスを作成してそこに処理を記述することにします。

今回のINSERTの処理では、トランザクションで親テーブルを更新してから子テーブルを更新する感じで実装します。
@Transactionトランザクションを張ることができます。

class RestaurantVisitedRecordsRepository(application: Application) {
    private val _db: AppDatabase
    init {
        _db = AppDatabase.getDatabase(application)
    }

    @Transaction
    suspend fun insertRestaurantWithVisitedRecord(restaurant: RestaurantsEntity, visitedRecord: VisitedRecordsEntity) {
        val restaurantsEntityDAO = _db.createRestaurantsEntityDAO()
        val visitedRecordsEntityDAO = _db.createVisitedRecordEntityDAO()

        val restaurantId = restaurantsEntityDAO.insertRestaurants(restaurant)
        visitedRecord.restaurant_id = restaurantId
        visitedRecordsEntityDAO.insertVisitedRecords(visitedRecord)
    }
}

いざ実践

行った店登録画面に色々入力します。
ジャンル・シーン・評価はEnumクラスを作ってプルダウンで入力できるようにしました。

やばい…王将のジャンルをカフェにしてしまった…!笑

「行った店登録」ボタンを押下すると、飲食店テーブル(restaurants)と訪問記録テーブル(visit_records)の両方が更新されました。

飲食店テーブル(restaurants)には店名、場所、ジャンルを登録。

ちゃんとカフェで登録されている…笑

訪問記録テーブル(visit_records)には評価、感想を登録。

今日はここら辺にします。
次回は画像の登録も行えるようにしたいと思います。
行った店リストがDBの情報をもとに表示されるようにしたいですね。
ではまた!

【Android】Roomを使用したデータベース保存

こんにちは!ぱかぱかです!
今日は現在開発を進めている飲食店記録アプリ「グルメノート」で、店登録画面からデータベースにデータを登録する処理についてまとめていきたいと思います。
以前データベースを扱う実装をしたときは普通にSQLiteライブラリだけでデータを扱っていましたが、今回は勉強も兼ねてRoomライブラリを使用してみたいと思います。
Roomの他、ViewModelやコルーチンなど色々な内容が出てきて盛りだくさんになってしまいますがご承知おきください…

前回の記事

radish-se.hatenablog.com

前回はトップ画面からボタンを押して店登録画に遷移するところを作りました。

前回肝心の「行った店登録」ボタンを忘れていたので、追加しました。
デザインのダサさはさておき、ここから店の情報を登録できるようにしたいと思います!

要件の振り返り

店登録画面でやりたい操作は以下です。

・行きたい店の情報登録
・行った店の情報登録

要件が曖昧になってきているのでここで決めちゃいます。
(結局テキトー開発になっている…)

<行きたい店>
・店名・場所・ジャンル・メモ(リストに表示できる程度の文字数)を登録する。
・リスト上から行った店やお気に入り店に切り替えることが可能。

<行った店(訪問記録)>
・店名・場所・ジャンル・評価・シーン・感想を登録する。
・行った店を登録するというよりは訪問記録を登録するイメージ。
 1つの店に対して複数の登録を紐づられる。
・「店名」ではまだ登録していない店を新規で入力するか、行ったことのある店から選べるようにする。

データ構成の振り返り

暫定のなんちゃってER図は以下のようになっています。

一応前に以下の記事で考えたつもりなのですが、正直これでいいのかちょっと不安です。
これも勉強なので、作ってみて壁にぶち当たってから考えましょう…

radish-se.hatenablog.com

Roomについて

RoomはAndroidアプリがデータベースとやりとりする際に、その処理を自動化してくれるライブラリです。

従来のデータベース処理

従来のSQLiteを使ったデータベース処理は大昔に以下の記事で実装してみています。
radish-se.hatenablog.com

ここでは以下のオブジェクトが登場します。

SQLiteDatabaseオブジェクト SQLiteへのSQL実行を行なう。
ヘルパーオブジェクト SQLiteDatabaseオブジェクトの生成やデータベースそのものの管理を行なう。

しかしこのやり方では定型コードが多くなったり、オブジェクトの取得や解放を非同期で行う配慮が必要だったり、色々気をつけるべき点が出てきてしまいます。
今いるプロジェクトも普通にこの方法でやっているような。

Roomによるデータベース処理

Roomライブラリを利用すると、必要最小限のコードでデータベース処理を行えるようになります。
また、非同期処理も含めてデータベースとのやりとりを自動化してくれます。
Android公式でもRoomの使用が強く推奨されていました。
Room を使用してローカル データベースにデータを保存する  |  デベロッパー向け Android  |  Android Developers

Roomを使うためには以下のオブジェクト3点セットが必要になります。

エンティティ テーブル構造に対応したクラス。
DAO データ処理をまとめる。インターフェースとして定義する。
Room Database データベースそのものを管理。エンティティをもとにテーブルを作成したり、DAOインターフェースをもとにDAOインスタンスを生成したりする。

前Spring/Javaの案件にいた時にMybatisというライブラリを使っていたのですが、似た雰囲気を感じます。
DAOというワードを久しぶりに聞きました。

便利なものはじゃんじゃん活用しよう!ということで、早速始めていきます。

build.gradleにライブラリを設定

Roomを利用するためにbuild.gradle(Module)に依存関係を追加します。
Android公式ページから最新版の記述を拾ってきてコピペします。
2023年11月23日現在の最新バージョンは2.5.0のようです。

Room  |  Android デベロッパー  |  Android Developers

ただ、ここで問題発生!
ただコピペするだけでは以下エラーが発生してしまいました。

org.gradle.api.GradleScriptException: A problem occurred evaluating project ':app'.
...
Caused by: org.gradle.internal.metaobject.AbstractDynamicObject$CustomMessageMissingMethodException: Could not find method kapt() for arguments [androidx.room:room-compiler:2.5.0] on object of type org.gradle.api.internal.artifacts.dsl.dependencies.DefaultDependencyHandler
...

「kapt()が見つけられないよ〜」と言われてしましました。

ここで以下サイトを参考にさせていただきながらエラー解消。
Android 初心者が Room を使おうとしてハマったこと #Android - Qiita

Kaptがないからpluginにkotlin-kaptを追加してあげると、今度は次のksp()で「KSPがないよ〜」と言われます。
かといってpluginにcom.google.devtools.kspを追加してあげると今度は「KaptとKSPは重複してるよ〜」と言われてしまう。
よって、kaptのpluginとkaptに関連するimportを消して、kspのpluginだけ適用してあげると解決するみたいです。

ちなみにKaptやKSPはアノテーションプロセッサのこと。
KSPはKaptの進化系みたいなものなので、公式でもkaptからKSPへの移行が勧められていました。

kapt から KSP に移行する  |  Android デベロッパー  |  Android Developers

結局、今回Moduleのbuild.gradle追加したものは以下のようになりました。

plugins {
    id 'kotlin-android'
    id 'com.google.devtools.ksp'
}

dependencies {

    def room_version = "2.5.0"

    implementation "androidx.room:room-runtime:$room_version"
    ksp "androidx.room:room-compiler:$room_version"
    implementation "androidx.room:room-rxjava2:$room_version"
    implementation "androidx.room:room-rxjava3:$room_version"
    implementation "androidx.room:room-guava:$room_version"
    testImplementation "androidx.room:room-testing:$room_version"
    implementation "androidx.room:room-paging:$room_version"
    implementation "androidx.room:room-ktx:2.6.0" // 後々必要になったのでここも追加しています!
}

Room KTXはデータベース トランザクション向けのコルーチン サポートとのことで、後ほどの処理実装時に必要になったので追加しました。

また、KSPを使うためにProjectにbuild.gradle 構成ファイルで KSP プラグインも宣言する必要があります。

plugins {
    id 'com.google.devtools.ksp' version '1.8.10-1.0.9' apply false
}

ふう…ようやく次に進めます。

エンティティを定義する

次はテーブル構造を表すエンティティクラスを作っていきます。
律儀にテーブルを分けようとしているので、その数だけ作ります。
一旦ここでは飲食店エンティティの例だけ示します。

カラム名 データ型 内容
restaurant_id INTEGER 飲食店のID(主キー)
restaurant_name TEXT 飲食店の名前
place_id INTEGER 場所のID(外部キー)
genre_id INTEGER ジャンルのID(外部キー)
created_time TEXT 作成日時
updated_time TEXT 更新日時

SQLiteではDATE型とかがなく、日付時刻が単なる文字列として扱われるんですね。
とりあえずTEXTで「YYYY-MM-DD HH:MM:SS.SSS」みたいに管理するとします。

これをEntityクラスに起こすと以下のようになります。

@Entity(tableName = "restaurants")
class RestaurantsEntity(
    @PrimaryKey(autoGenerate = true) val restaurant_id: Int,
    val restaurant_name: String,
    val place_id: Int,
    val genre_id: Int,
    val created_at: String,
    val updated_at: String
)

ポイントは以下です。
・コンストラクタの引数としてカラムのプロパティを定義。
・クラスアノテーションとして@Entityをつける。
・主キーには@PrimaryKeyをつける。
 自動インクリメントにしたい場合は@PrimaryKey(autoGenerate = true)とする。
・Kotlinなら何もつけなくても@NonNull扱いになる。
(オプショナル型にするとそのカラムもnull許容になる。)

DAOインターフェースを定義する

次にDAOインターフェースの中でデータ処理を定義します。
とりあえず今回は簡単な検索の処理と挿入の処理を書いてみました。
(本当はテーブル色々結合して登録しなきゃいけないけど…)

@Dao
interface RestaurantsEntityDAO {
    @Query("SELECT * FROM restaurants WHERE restaurant_id = :id")
    suspend fun findById(id: Int): RestaurantsEntity
    @Insert
    suspend fun insert(restaurantsEntity: RestaurantsEntity)
}

ポイントは以下です。
・クラスアノテーションとして@Daoをつける。
・クエリを書く場合は@Queryを使用して@Query("クエリ")のように記述する。
・単純なINSERT/UPDATE/DELETEなら、@Insert・@Update・@Delete等を使う。

suspendをつけてコルーチンでうまいこと非同期処理をやる算段になっております。

Room Databaseの作成

最後にRoomDatabaseを作成します。

RoomDatabaseを継承した抽象クラスを作成しますが、クラス名は慣習的にAppDatabaseとすることが多いそうなのでそれに倣います。

@Database(entities = [RestaurantsEntity::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
    companion object {
        private var _instance: AppDatabase? = null
        fun getDatabase(context: Context): AppDatabase {
            if (_instance == null) {
                _instance = Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "gourmet_note_db").build()
            }
            return _instance!!
        }
    }
    abstract fun createRestaurantsEntityDAO(): RestaurantsEntityDAO
}

ポイントは以下です。
・クラスアノテーションとして@Databaseをつける。
 利用するエンティティクラスを配列として指定する。
・DAOインターフェースを戻り値とする抽象メソッドを記述する。

Room Databaseインスタンスの生成はリソースを消費するので、生成したインスタンスをアプリ内で再利用する必要があります。
インスタンスが存在しない場合のみにに実行されるようなコードになっています。

Roomを利用して登録処理を行う

最後にRoomを使ってデータベース処理を行ってみます。
ここで、登録画面で入力した情報をアクティビティのライフサイクルに影響されずに保持できるViewModelを使っていきます。
(ViewModelに関してはまた別の記事でまとめたいところ…)
Room Databaseのインスタンスの取得にはコンテキストが必要になるので、Applicationオブジェクトを利用できるAndroidViewModelというのを用います。

class RegistrationVisitedRestaurantViewModel(application: Application): AndroidViewModel(application) {
    private val _db: AppDatabase

    // 画面で持ちたいデータをプロパティ化
    var restaurantName = ""
    var place = ""
    var genre = ""
    var evaluation = ""
    var memo = ""

    // Room Databaseの作成
    init {
        _db =AppDatabase.getDatabase(application)
    }

    @RequiresApi(Build.VERSION_CODES.O)
    suspend fun addRestaurant(): Job {
        val restaurantsEntity = RestaurantsEntity(0, this.restaurantName, 1, 1, DateTime.getCurrentDateTime(), DateTime.getCurrentDateTime())
        // DAOオブジェクト取得
        val restaurantsEntityDAO = _db.createRestaurantsEntityDAO()
        // コルーチンスコープの準備
        val job = viewModelScope.launch {
            // restaurantsテーブルへの登録実行
            restaurantsEntityDAO.insert(restaurantsEntity)
        }
        // コルーチンスコープの戻り値をリターン
        return job
    }
}

最後に、登録画面のフラグメントで「行った店登録」ボタンを押した時の処理の中でViewModelに定義したaddRestaurant()を呼び出します。
コルーチンスコープの中で呼び出してあげる必要があります。

        val registrationButton = registVisitedRestFragment.findViewById<Button>(R.id.registration_button)
        registrationButton.setOnClickListener {
            _registrationVisitedRestaurantViewModel.restaurantName = etRestaurantName.text.toString()

            lifecycleScope.launch {
                _registrationVisitedRestaurantViewModel.addRestaurant()
            }
        }

いざ実践

今回は入力欄たくさんあるけどシンプルにrestaurantsテーブルの"restaurant_name"の登録だけを試します!
ジャンルエンティティや場所エンティティを作ってテーブル結合して…とやろうかと思いましたが、内容が長くなりすぎるので次回で…

モスバーガーと入力し登録ボタンを押すと…

ちゃんとrestaurantsテーブルに登録されました!
restaurant_idは勝手に自動採番されていますね。

次回は色々エンティティを作ってくっつけてあるべき姿で登録できるようにしたいと思います!
ではまた!