たかぎとねこの忘備録

プログラミングに関する忘備録を自分用に残しときます。マサカリ怖い。

Androidではデータベースに画像のリソースIDを保存してはいけないらしい!

Androidアプリをコンパイルした後だったり、新しい画像をResource Managerに追加した後に、アプリで表示されていた画像がまったく異なる他の画像に置き換えられているケースが多々あった。

そろそろ対処しないとなーと思って色々調べていたら次のコメントを我々の拠り所スタックオーバーフローにて見つけてしまった。

Don't do this. There's a major problem with storing image ids in the database- the numbers aren't stable. Each time you recompile the app, the values can change. If you add or remove any resource, the entire mapping can change. It isn't a stable method of storing that data. Using the drawable's name and converting to id would be more stable.

– Gabe Sechan Mar 24, 2017 at 6:46 android - Storing image resource id in Sqlite database and retrieving it in int array - Stack Overflow

つまり、アプリを再コンパイルするたびにリソースIDの値は変わる可能性があるし、リソースを追加したり削除したりすると、マッピング全体が変わってしまう可能性がある。なので、データを保存する方法として、リソースIDをそのままデータベースに保存するのは誉められたものではないとのこと。

ではどうすれば良いのかというと、ここで推奨されているのはリソースのIDではなく、エントリー名を保存する方法。

他のスタックオーバーフローでも推奨されていた。

Best way to store Resource id in Database on Android - Stack Overflow

早速実装してみる。 まず、DrawableConverterインターフェースを実装する。

interface DrawableConverter {
    fun getResourceIdByEntryName(entryName: String): Int
    fun getResourceEntryNameByResourceId(resourceId: Int): String
}

getResourceIdByEntryName()では、リソースのエントリー名を受け取ってリソースIDに変換する。

getResourceEntryNameByResourceId()では、リソースIDを受け取ってエントリー名に変換する。

次にDrawableConverterImplクラスを実装する。

class DrawableConverterImpl @Inject constructor(
    @ApplicationContext private val context: Context,
) : DrawableConverter {
    override fun getResourceIdByEntryName(entryName: String): Int {
        return context.resources.getIdentifier(entryName, "drawable", context.packageName)
    }
    override fun getResourceEntryNameByResourceId(resourceId: Int): String {
        return context.resources.getResourceEntryName(resourceId)
    }
}

バインドを行う。

@Module
@InstallIn(SingletonComponent::class)
abstract class DataModule {
    ...

    @Binds
    abstract fun bindDrawableConverter(
        drawableConverter: DrawableConverterImpl
    ): DrawableConverter
}

そしてリポジトリにて、DrawableConverterの注入を行い、DAOから受け取ったエンティティをリポジトリ内でモデルに変換し、ViewModelなどでその内容を受け取るようにする。

@Singleton
class AlbumRepositoryImpl @Inject constructor(
    private val albumDao: AlbumDao,
    private val drawableConverter: DrawableConverter,
) : AlbumRepository {
    override suspend fun getAlbums(
        ioDispatcher: CoroutineDispatcher
    ): Result<List<Album>> {
        return withContext(ioDispatcher) {
            val albums = albumDao.getAlbums()
            Result.Success(albums.first().map {
                val imageResourceId = drawableConverter.getResourceIdByEntryName(it.imageEntryName)
                it.asExternalModel(imageResourceId)
            })
        }
    }

    override suspend fun insertAlbum(
        title: String,
        artist: String,
        imageResourceId: Int,
        ioDispatcher: CoroutineDispatcher
    ) {
        val entryName = drawableConverter.getResourceEntryNameByResourceId(imageResourceId)
        val albumEntity = getNewAlbumEntry(
            albumTitle = title,
            albumArtist = artist,
            albumImageEntryName = entryName,
        )
        return withContext(ioDispatcher) {
            albumDao.insert(albumEntity)
        }
    }
   ...
}

図解してみると、こんな感じ。

これにより、データベースに保存するときはDAOを経由してエントリー名を渡し、画面上に表示するときはリポジトリ内で受け取ったエントリー名をリソースIDに変換してからViewModelに渡すことができる。

その結果、リソースIDの意図せぬ変更を恐れることなく、データベースに対して画像関連の値を保存できるようになった。めでたしめでたし。