EpoxyとGroupieを使ってみてた所感
こんにちは!ふぇりです。
今回は EpoxyとGroupieを軽く触ってみて思ったこと感じたことを言葉にして残しておきたいと思います。
両者ともRecyclerViewのライブラリで、一般にGroupieはDataBindingを使わなかったり、軽いアプリなどを作る際に使う。
EpoxyはDataBindingを使うときに便利とか
modelを自動生成してくれるから大きいアプリでもそこまでファイルが増えたりしない。
みたいなことを言われがちだと思います。
私自身Groupieしか触ったことなくて、その違いがいまいち自分の中で、飲み込めていなかったので、EpoxyでDataBindingを使ったアプリ実際に作ってみました。
https://github.com/felix925/AnimalPreview
そこで、感じた大きな違いは色々ありました。
・RecyclerViewのxmlがGroupieは従来のRecyclerViewでEpoxyはEpoxyRecyclerViewという点
・GroupieはRecyclerViewのadapterを便利にしてくれるイメージ Epoxyは根本からRecyclerViewをより短く簡潔に書けるようにしたイメージ
・Groupieはやろうと思えばModelを色々いじれる EpoxyはControlerから直で弄るというよりは、BindingAdapterとかを使って、極力データを渡すことに注力してるイメージ
ほぼほぼ自分の使ってみてのイメージですが、、、まず、1つ目のRecyclerViewのxmlがGroupieは従来のRecyclerViewでEpoxyはEpoxyRecyclerViewという点では、xmlでの宣言がそもそも違うといった部分です!
ここで結構感じるものがあって、Groupieは既存のものをより使いやすくする。Epoxyは既存のものを拡張して独自に作って使いやすくするみたいな方向性があるのかななんて感じました。
2つ目は、便利にする範囲的な部分だと思います。GroupieはAdapterにGroupieを用いて作った ItemやAdapterを繋いであげる感じで、既存のRecyclerViewから乗り換える際は便利だと感じました。
また、Epoxyではそもそも横スクロールと縦スクロールを共存させたりといった部分をできるような設計(?)になっているようで(airbnbのアプリのような)根本的なRecyclerViewそのものを使いやすくしたイメージです。RecyclerViewから乗り換える時は規模にもよりますが、Groupieよりは工数がかかりそう?
3つ目は、Adapter(Controller)でできる範囲みたいな話になります。
浅い使い方なので、もしかしたら使う方法があるかもしれませんが、書いていきます。
Groupieはbindingを使って色々ごにょごにょできるのですが(FragmentでDataBindingを使うようにImageViewとかにGlideで画像を直接ロードさせたりビジネスロジックマシマシにしたり)
Epoxyはデータの引き渡しに注力しているようで、DataBindingでデータを渡す最低限の部分を見せているようなイメージです。
画像を読み込む際などは、BindingAdapterを使って読み込むなどの対策が必要なのかなといった所感です。
EpoxyはGroupieでいうItemを作らなくていいといった点はすごく便利だと思いますし、
GroupieはItem内でbindingであれこれできたり、既存のRecyclerViewからの乗り換えが楽みたいな部分も魅力的です。
結局どちらが優れている劣っているという話ではなくて、リスト表示する際にMPChartを使ってチャート表示したいとか色々要件は無限に出てくると思うので、
どちらが自分、チームメンバーにとって使いやすいか、メンテナンスしやすいかブラックボックスな部分をどれだけ減らせるかみたいなところを考えて採用していくのがベストなのかなと思います。(これはこのライブラリなどに限った話ではないかもですが。)
まだまだ深い部分の理解が足りていないので、深く理解できた際に、再度もっと踏み込んだ内容を書いていきたいなと思っています。
Room,多対多で詰まったので生成されたJavaコード覗いてみた
こんばんは。ふぇりです。
多対多のエンティティを生成中に
FOREIGN KEY constraint failed (code 787)
が出てしまい、そのエラーが中間テーブルを登録する際に発生しており、
いろんなサイト漁ったものの解決策がいまいちだったので、
生成されたJavaコード読むことにしました。
(読めない人間が読むので、勉強向きではないです)_(_><)⊃
なので、ついでに他の部分も読んでみて、Roomが実際にどんなコードを書いてくれているのかを感覚的に掴めたらいいなって思います。
Roomの生成コードは以下のディレクトリに潜ると見れると思います。
build/generated/source/kapt/debug/モジュール名/
いつも書くようなDataBaseをみてみるとこんな感じかなって思います。
@Database(
entities = [
UserEntity::class
],
version = 5
)
@TypeConverters(DateConverter::class)
abstract class LocalDataBase : RoomDatabase() {
abstract fun userEntityDao(): UserEntityDao
}
生成コードの冒頭をみてみます。
@Override
protected SupportSQLiteOpenHelper createOpenHelper(DatabaseConfiguration configuration) {
final SupportSQLiteOpenHelper.Callback _openCallback = new RoomOpenHelper(configuration, new RoomOpenHelper.Delegate(5) {
@Override
public void createAllTables(SupportSQLiteDatabase _db) {
_db.execSQL("CREATE TABLE IF NOT EXISTS `user_item` (`user_id` TEXT NOT NULL, `user_name` TEXT NOT NULL, `user_thumbnail` TEXT NOT NULL, PRIMARY KEY(`user_id`))");
createOpenHelperなるものをOverrideしていて、_openCallbackにRoomOpenHelperに設定とバージョンを渡してインスタンス生成して代入してますね。
createAllTablesで必要なテーブルを作ってくれてるみたいですね...!
'カラム名' 型 null許容かどうか みたいな形でカラムが追加されて、プライマリキーも登録されていますね。
カラム名を指定しなかった場合どうやって登録されるかみたいですね....(今度みます)
型 null許容かだけで追加されるのでしょうか
結構飛ばしてDaoの部分見てみます。
@Override
public UserEntityDao userEntityDao() {
if (_userEntityDao != null) {
return _userEntityDao;
} else {
synchronized(this) {
if(_userEntityDao == null) {
_userEntityDao = new UserEntityDao_Impl(this);
}
return _userEntityDao;
}
}
}
_userEntityDao(生成コード内でのUserEntityDaoのインスタンス)がnull出ないならインスタンスを返して、nullならシンクロナイズド(非同期(?))でインスタンス生成して返す形になっていますね。
要求された時に生成するのはメモリなどにも優しそうですね。
そして問題の中間テーブルを読んでみて今回は終わりたいと思います。
public final class TodoWithLabelDao_Impl implements TodoWithLabelDao {
private final RoomDatabase __db;
private final EntityInsertionAdapter<TodoWithLabel> __insertionAdapterOfTodoWithLabel;
private final DateConverter __dateConverter = new DateConverter();
public TodoWithLabelDao_Impl(RoomDatabase __db) {
this.__db = __db;
this.__insertionAdapterOfTodoWithLabel = new EntityInsertionAdapter<TodoWithLabel>(__db) {
@Override
public String createQuery() {
return "INSERT OR REPLACE INTO `todo_with_label` (`todo_id_with`,`label_id_with`) VALUES (?,?)";
}
@Override
public void bind(SupportSQLiteStatement stmt, TodoWithLabel value) {
stmt.bindLong(1, value.getTodoId());
stmt.bindLong(2, value.getLabelId());
}
};
}
もっといい名前を考えて命名したいと思います(自戒)
TodoとLabelの中間テーブルなんですが、なかなか読めない部分が多いです...
EntityInsertionAdapter<>でTodoWithLabelが覆われていますね。
なんの役割をしているのでしょうか、みてみましょう。
(長いのでcommand+Bしてね!)
データベースにインサートする役割とかを担っている(?)
insertが配列かそうじゃないかで処理が分かれていて、一つのエンティティごとにInsertQueryとEntityを結びつけていますね。
EntityとDB側のクエリの間を取り持っている(?)
エラーが発生した箇所を見ます。
@Override
public void insertLabel(final TodoWithLabel todoWithLabel) {
__db.assertNotSuspendingTransaction();
__db.beginTransaction();
try {
__insertionAdapterOfTodoWithLabel.insert(todoWithLabel);//ココ
__db.setTransactionSuccessful();
} finally {
__db.endTransaction();
}
}
private final EntityInsertionAdapter<TodoWithLabel> __insertionAdapterOfTodoWithLabel;
さっきのEntityInsertionAdapterのinsertで怒られている。
TodoWithLabelはInt型のidを持つカラムが2個でそれぞれがForeignKeyでTodoとLabelと結びついてる形になっています。
今回渡した値はTodoWithLabel(todo_id = 1, label_id = 1)で、それぞれあたいは追加済みで、idだけ投げたのが行けなかったのでしょうか。(多分そう)
TodoWithLabel(Todo, Label)で渡せるようにしてみましょうかね...
根本的に何が問題だったのかはまだ理解できていないので、ForeignKeyをまず理解することが先決なのかなって思っています。(内部の仕組み)
今日は頭がもう働かないので寝ようと思います。
解決できなくて悔しいですが.....早起きしたいと思います(8時間睡眠)
Android Roomで1対nの表現方法とそれをネストする方法
こんにちは。ふぇりです。
この記事では、Room初学者(同志)を対象としており、1:nのデータ構造の表現方法および、それらをネストする方法について解説(?)共有(?)します。
最近Roomを触っていて、その中で、1:nのものをさらに1:nでネストするというデータ構造を作ることになりました。
私が探した範囲内では、日本語でこのようなデータ構造をRoomで表す方法についての記事が見つからなかったので、その実装方法を説明したいと思います。
まず前提として、1:nというのは、
一つのものに対して複数のものが相対すること。対応関係にある二項のうち、片方は単独・単一であり、他方は多数であるという構成になっている状況を意味する語。多くの場合は「一対一」、「多対多」との対比で用いられる語。
ウェブリオ辞書様より引用
うむ、わかりづらい。
一つのオブジェクトに対して、複数のオブジェクトが紐づいているようなイメージでいいかなと思います。
例えば、悟●が持っているドラ●ンボールなどですかね(謎)
●空という一つのオブジェクトに、所持しているドラゴ●ボールのデータが紐づいているようなイメージです。
1:n
悟空 → ドラゴンボール
Roomですと、その実装は主に中間テーブルを生成するというものになります。
その前に、まず各々のエンティティを実装していきます。
まず、悟空側(ユーザー側)は次のようにします。
@Entity(tableName = "user_item")
data class UserEntity(
@PrimaryKey
@ColumnInfo(name = "user_id")
val id:String
)
今回はidのみを持つこととします。
テーブル名はuser_itemでプライマリキーはuser_idとします。
次にドラゴンボール側を実装していきます。
@Entity(tableName = "dragonball_item",
foreignKeys = arrayOf(
ForeignKey(
entity = UserEntity::class,
parentColumns = arrayOf("user_id"),
childColumns = arrayOf("dragonball_id"),
onDelete = ForeignKey.CASCADE //ユーザー削除と共に子も削除
))
)
data class DragonballEntity(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "project_id")
val id: Int,
@ColumnInfo(name = "author_id")
val author: String
)
少しややこしいのが増えました。
foreignKeysという奴が増えてます。
こいつは外部のエンティティのキーを参照して、自身のエンティティを関連づける役割といったイメージを持っていただけるといいと思いますb
・entityは結びつける対象のクラス
・parentColumnsは結びつける親のカラム名
・childColumnsは結びつけられる側のカラム名
・onDeleteは親が削除された際の扱い
を示しています。parentColumnsとchildColumnsは同じ値を保持します。なので、1:nのn側(ドラゴンボール側)が1側(悟空側)のカラムの値を知っていて、それと紐づけ(内部で結合テーブルを作っている?)を行ってくれています。
つぎはこれらの中間テーブルを実装します。
data class UserWithDragonball(
@Embedded
val user: UserEntity,
@Relation(parentColumn = "user_id",entityColumn = "dragonball_id")
val DragonballList: List<DragonballEntity>
)
1側はEmbeddedアノテーションをつけて、n側はRelationアノテーションを使用します。
これらの内部の仕組みはまだ理解していないので、理解し次第記事をあげるか更新します。
Daoは次のようになります。
@Dao
interface UserEntityDao {
@Transaction
@Query("SELECT * FROM user_item")
fun getUserWithDragonball():List<UserWithDragonball>
}
一通り、これで1:nの表現ができ、DaoのQueryでuser_itemを全て取ってきた際に、
ユーザーとドラゴンボールが紐づいたリストが返されて、1:nの関係がデータ構造で表現できます。
(ここまでお疲れ様でした....)
次に、1:n:nとでもいうべきか1対nのネストを実装していきます。
先ほどの実装には少し不満点がありますね。
みなさんお分かりの通り、ドラゴンボールの星が再現されてません!!!(なんてこった)
ドラゴンボールとドラゴンボールの中の星は1:nの関係ですね!!(白目)
これを表現してあげると、
ユーザー : ドラゴンボール = 1 : n
となるので、1:nの中にさらに1:nを作る(ネストする)ことになります。
さてやっていきましょう。
まずは、ドラゴンボールの星の中のエンティティを実装していきます。
@Entity(tableName = "star_item",
foreignKeys = arrayOf(
ForeignKey(
entity = DragonballEntity::class,
parentColumns = arrayOf("dragonball_id"),
childColumns = arrayOf("star_id"),
onDelete = ForeignKey.CASCADE
)
)
)
data class StarEntity(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "star_id")
val id: Int
}
これは上の部分で行ったように、今回はユーザーではなくドラボンボールに結びつけています。
上の部分でも行ったように、次はドラゴンボールとドラゴンボールの中の星の中間テーブルを作りましょう。
data class DragonballWithStar(
@Embedded
val dragonball: DragonballEntity,
@Relation(parentColumn = "dragonball_id",entityColumn = "star_id")
val todoList: List<StarEntity>
)
困りました、ネストしたいのにこのままではユーザーとドラゴンボールのテーブル、ドラゴンボールとその中の星のテーブルで独立していて、繋がっていることを表現できていません。
ユーザーとドラゴンボールのテーブルを以下のように変更しましょう。
data class UserWithProject(
@Embedded
val user: UserEntity,
@Relation(parentColumn = "user_id",entityColumn = "dragonball_id",
entity = DragonballEntity::class)
val DragonballAndStar: List<DragonballWithStar>
)
ユーザーとドラゴンボールの中間テーブルにドラゴンボールとスターの中間テーブル教えて繋げるのはいいけど、entityってなんやねん!って思った方も多いと思います。
その理由としては、そもそもuserはドラゴンボールとスターの中間テーブルの存在を知らないため、知らんがな!とエラーを吐きます。
それを防ぐため、私は、ドラゴンボールのエンティティです、怪しいものではございませんと伝えることによってエラーを防ぎます。
これによって、1対nをネストすることができました。
これによって、 ユーザーを全て取ってきた際に、ドラゴンボールの星までアクセスができるようになります。
書き終えて思ったこと。
1対nのネストの例えって難しいですね(白目)
あと記事をかくと自分の理解できていない部分がわかっていい感じです。
無料のIvyの授業を英語だからと諦めてる方へ
こんにちは。
ハーバード大学などの授業を無料で受けられるという事をTwitterで拡散されているのをみて、すごい面白そうだと思う反面、英語だから難しいだろうなと私も考えていました。
ですが、サイトに潜ってedXというプロバイダに入った際に授業の下に字幕をtxtでダウンロードできることに気づきました。
これを用いると、音声で授業を受けることはできないかもしれませんが、
英語の授業と照らし合わせて受けることができると考えたため、英語は苦手だがインプットを増やして成長させたいという方にこの方法を提案します。
ダウンロードしたtxtは5000字に収まっていなかったため、サイト上の翻訳でも文字数をオーバーするため諦めていましたが、
https://translate.google.co.jp/?hl=ja#view=home&op=docs&sl=auto&tl=ja
ここで先ほどの授業の字幕のtxtファイルをアップロードしてあげると、綺麗な日本語ではありませんが、Google様が翻訳してくれます!
なので、この翻訳後のものを見ながら、映像と照らし合わせて授業を受けられます!
コロナで色々学習の機会は失ってしまいましたがここで知見を得て成長できるようにお互い頑張っていけたらなと思います!
古いプロジェクトにViewBindingをGradleエラーで導入できない方へ
こんにちは。
今日ViewBindingを自作しているアプリに導入しようとしたところGradleさんに
ERROR:Could not find method viewBinding() for arguments
とエラーを吐かれました。
英語が読める方は以下のサイトから解決方法を知ることができます。
https://stackoverflow.com/questions/57947991/error-could-not-find-method-viewbinding-for-arguments
今回は軽く何故このエラーが起きたのかを分析して次このようなことで時間を使わないよう備忘録として残したいと思います。
まず、はじめにViewBindingは
https://developer.android.com/topic/libraries/view-binding
注: ビュー バインディングを利用できるのは、Android Studio 3.6 Canary 11 以降に限られます。
と注意書きされています。
今回私はAndroidStudio 4.0を使っていたので、問題ないだろうと思いそのまま進めましたが、そういう問題ではなく、
classpath 'com.android.tools.build:gradle:n.n.n
の部分(nは任意の数)をAndroidStudio3.6で導入された値(?) 3.6.0にあげて、
gradke-wrapper.propertiesのdistributionUrlの部分も対応したURL
(https\://services.gradle.org/distributions/gradle-5.6.4-all.zip)
に変更したことから、Android Studio 3.6 Canary 11以降のバージョンのAndroidStudioでアプリをビルドしたら使えるのではなく(考えてみればそうだが...)
Android Studio 3.6 Canary 11以降でアプリ作成時などに用いられているGradleのバージョンでViewBindingを利用できるということになります。
知見
・普段何も意識することなくライブラリやコルーチンをimplementationして使っていた
Gradle自体のバージョンをどんな風にあげるか知るきっかけになった。
https://services.gradle.org/distributions/
・ここ(distributionUrlのファイル名の前の部分)に飛んでみると現時点で6.3までリリースされていて、かなり高い頻度で更新されていることがわかった。
・distriburionとはなんぞと思って翻訳をかけた際、配布や配給という意味があるらしく、URLを入力していることから、Gradleの何かしらを取ってきていると思われる。
それによって、オフラインでGradleがキャッシュなどなしで動作しないのかがわかった。
理解したいこと
・Gradle.zipの中に何があって、どんな仕組みで動いているのかを解剖する
Dagger2の備忘録
こんにちは。
Dagger2をいろんな記事を読んでも内部動作は理解できたのですが、コードを書けなかった私が思う詰まるポイントをざっとまとめて備忘録としたいと思います。
私自身はまだ@Component、@Conponent.Factory,@Singleton,@Provides,@Moduleしか触れておらず、深い部分の理解はまだ浅いと思われますので、初学の際の一助に慣れればを思います。
まずはじめに、Dagger2のDI(Dependency injection 訳:依存性の注入)は当初思っていたより魔法のようなものではなかったということです。
@Injectを書けばDIしてくれる、
@Inject constructor(val HogeRepository: HogeRepository)のように書けば、HogeRepositoryとそれに必要なHogeModelなど必要なものも全てDIしてくれる。インスタンス製造機だといったような認識は誤っていました。
コードが動く程度に理解した範囲でお伝えすると、Dagger2はあらかじめどのインスタンスが必要になった(@Injectが現れた)際にどのように提供するかを定義しておいて、それを提供する部分をライブラリが担っているというイメージでした。
@InjectがDIしてほしいよっていう意思表示で@Providesが提供方法、それを塊で分ける@Modulesそしてそれを統括する@Componentというイメージです。
提供方法を定義 提供物のグループ分け 提供方法統括
↓ ↓ ↓
@Provides ------> @Modules --------------> @Component
提供方法統括本部 製造機 -> @Conponent.Factory
みたいな認識で使っています。
以下のコードを用いて説明させていただきます。
App
class App: Application() {
lateinit var appComponent: AppComponent
override fun onCreate() {
super.onCreate()
appComponent = DaggerAppComponent.create()
}
}
AppComponent
@Singleton
@Component(
modules = [
DataBaseModule::class,
PresenterModule::class
]
)
interface AppComponent {
@Component.Factory
interface Factory{
fun create():AppComponent
}
fun inject(fragment: HogeFragment): HogeFragment
}
PresenterModule
@Module
class PresenterModule {
@Provides
fun provideHogePresenter(
repository: HogeRepository
): HogePresenter = HogePresenter(repository)
}
DataBaseModule
@Module
class DataBaseModule {
@Singleton
@Provides
fun provideHogeRepository(): HogeRepository = HogeRepository()
HogeFragment
class ListViewFragment {
@Inject lateinit var presenter:HogePresenter
override fun onAttach(context: Context) {
(activity!!.application as App).appComponent.inject(this)
super.onAttach(context)
}
....//省略
}
以上を説明すると、
1. AppComponentをActivityで作っています。
2. AppComponentにはどのFragmentにDIを行うかが定義されています。
3. それを対象となっているHogeFragmentから呼び出してDIしてとお願いします。((activity!!.application as App).appComponent.inject(this)で)
4. 対象を@Injectで指定し要求されたPresenterの提供方法をModulesからライブラリが探してきます。
5. そこで、PresenterModulesにそれが定義されている事を見つけます。
6. そのPresenterはRepositoryを要求していて、それはLocal,Remoteそれぞれを要求します。
7.それらの提供方法を全て探して、@Providesで定義した方法に従い注入していく
そういった流れになっています。
この流れを追うと、決して自動で作ってくれるものではなく、
定義したものを引っ張ってきてそれを自動でやってくれるものだということがわかりました。
ここまでが私の理解になっています。
軽く概要や動く仕組みを認知していただいたあとは以下のような記事が道を示してくださると思いますので、リンクを貼らせていただきます。
https://qiita.com/m-dove/items/767c4bfaeee53caefc4d
Fragment Over Flow
こんにちは。
最近更新できていなかったわけですが、書いていきたいと思います。
今日アプリを作成していて、Fragmentにアニメーションつけた際に気づいてしまったことがあります。
Fragmentの遷移にnavigationを使っているのですが、フラグメントが重なって行っている。
どういうことかと言いますと、Fragmentを理解していれば当たり前のことではあるんですが、Fragmentの管理はStackのような管理になっていて、LIFOのデータ構造になっています。
navigateで遷移する際には、遷移先のフラグメントを新しく生成しています。
画面を戻る際にメインフラグメントにnavigateをしてしまうとメインフラグメントを新たに生成してスタックに積み上げていくことになってしまいます。
わかりやすく説明すると、メインとサブ二つのフラグメントがあるとします。
スタックの初めの中身はメインだけです。()の中身はスタックの中身とします。
(メイン)
そこでサブにnavigateします。
(メイン → サブ)
サブで処理を終えた後メインにnavigateすると
(メイン → サブ → メイン)
となってメインが2個存在することになります。
これをN回繰り返すとN+1個メインが生成されます。地獄です。
これを起こさないためには、遷移先から遷移元に戻る際はpopBackStack()を使用しましょう。
データ更新されなくなったが?
という問題が発生することが考えられます。
要はサブで更新した内容がメインで表示されていないと言った内容です。
これは今まで新しくメインを生成したために更新されていたが、以前のメインを表示するようにしたために昔のデータ表示のままと言ったことから生まれるものです。
これに関しては簡単で、FragmentにはonResume()という関数(?)が存在しています。
これはライフサイクルを勉強していただくとわかることなのですが、Fragmentを再表示した際に呼び出される関数となっています。
ここにデータをアーキテクチャやデータフローを考えて適切にリロードする処理を書いてあげることで、アーキテクチャを守って、スタックマシマシから守ってコードをかけます。
これでStack Over FlowいいえFragment Over Flowなコードを卒業しましょう(自戒)