Android sql查媒体数据封装room Dao构造AndroidViewModel,RecyclerView宫格展示,Kotlin

Android sql查媒体数据封装room Dao构造AndroidViewModel,RecyclerView宫格展示,Kotlin

Android媒体数据管理的实现方案,主要包含以下技术要点:

  1. 使用Room数据库存储媒体数据,通过MediaDao接口提供CRUD操作,支持按类型、关键字查询媒体文件。

  2. 采用ViewModel架构,通过MediaViewModel管理数据加载逻辑,配合LiveData实现UI自动更新。

  3. 实现RecyclerView宫格展示(8列布局),使用Coil图片加载库高效加载和缓存媒体缩略图(支持2GB内存/磁盘缓存)。

  4. 通过MediaStore获取系统媒体文件,区分图片和视频类型,并按添加时间排序。

  5. 包含完整的权限声明,支持Android存储访问框架。

  6. 采用Kotlin协程实现异步操作,避免主线程阻塞。

该方案实现了媒体数据的本地持久化、高效加载和灵活展示,适用于相册类应用开发。

XML 复制代码
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
    <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
Kotlin 复制代码
plugins {
    id 'com.google.devtools.ksp' version '2.3.6'
}


dependencies {
   
    implementation("io.coil-kt.coil3:coil:3.4.0")
    implementation("io.coil-kt.coil3:coil-core:3.4.0")


    def paging_version = "3.4.2"
    implementation "androidx.paging:paging-runtime:$paging_version"



    def room_version = "2.8.4"
    implementation("androidx.room:room-runtime:$room_version")

    // If this project uses any Kotlin source, use Kotlin Symbol Processing (KSP)
    // See KSP Quickstart to add KSP to your build
    ksp "androidx.room:room-compiler:$room_version"

    // If this project only uses Java source, use the Java annotationProcessor
    // No additional plugins are necessary
    annotationProcessor("androidx.room:room-compiler:$room_version")

    // optional - Kotlin Extensions and Coroutines support for Room
    implementation("androidx.room:room-ktx:$room_version")

    // optional - Paging 3 Integration
    implementation "androidx.room:room-paging:$room_version"
}
Kotlin 复制代码
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase

@Database(entities = [MediaEntity::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
    abstract fun mediaDao(): MediaDao

    companion object {
        @Volatile
        private var INSTANCE: AppDatabase? = null

        fun getInstance(context: Context): AppDatabase {
            return INSTANCE ?: synchronized(this) {
                INSTANCE ?: Room.databaseBuilder(
                    context.applicationContext,
                    AppDatabase::class.java,
                    "media_db"
                ).build().also { INSTANCE = it }
            }
        }
    }
}
Kotlin 复制代码
import android.content.Context
import android.os.Environment
import android.util.Log
import coil3.EventListener
import coil3.ImageLoader
import coil3.decode.Decoder
import coil3.disk.DiskCache
import coil3.disk.directory
import coil3.fetch.Fetcher
import coil3.imageDecoderEnabled
import coil3.memory.MemoryCache
import coil3.request.CachePolicy
import coil3.request.ImageRequest
import coil3.request.Options
import java.io.File


class MyCoilMgr {
    companion object {

        const val TAG = "fly/MyCoilMgr"

        val INSTANCE by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { MyCoilMgr() }
    }

    private var mImageLoader: ImageLoader? = null

    private constructor() {
        Log.d(TAG, "constructor")
    }

    fun init(ctx: Context): ImageLoader {
        if (mImageLoader != null) {
            return mImageLoader!!
        }

        Log.d(TAG, "初始化ImageLoader")

        //初始化加载器。
        mImageLoader = ImageLoader.Builder(ctx)
            .imageDecoderEnabled(true)
            .memoryCachePolicy(CachePolicy.ENABLED)
            .memoryCache(initMemoryCache())
            .diskCachePolicy(CachePolicy.ENABLED)
            .diskCache(initDiskCache())
            .eventListener(object : EventListener() {
                override fun fetchStart(request: ImageRequest, fetcher: Fetcher, options: Options) {
                    //Log.d(TAG, "fetchStart ${request.data}")
                }

                override fun decodeStart(request: ImageRequest, decoder: Decoder, options: Options) {
                    //Log.d(TAG, "decodeStart ${request.data}")
                }
            })
            .components {

            }.build()

        return mImageLoader!!
    }

    private fun initMemoryCache(): MemoryCache {
        //内存缓存。
        val memoryCache = MemoryCache.Builder()
            .maxSizeBytes(1024 * 1024 * 1024 * 2L) //2GB
            .build()
        return memoryCache
    }

    private fun initDiskCache(): DiskCache {
        //磁盘缓存。
        val diskCacheFolder = Environment.getExternalStorageDirectory()
        val diskCacheName = "fly_disk_cache"

        val cacheFolder = File(diskCacheFolder, diskCacheName)
        if (cacheFolder.exists()) {
            Log.d(TAG, "${cacheFolder.absolutePath} exists")
        } else {
            if (cacheFolder.mkdir()) {
                Log.d(TAG, "${cacheFolder.absolutePath} create OK")
            } else {
                Log.e(TAG, "${cacheFolder.absolutePath} create fail")
            }
        }

        val diskCache = DiskCache.Builder()
            .maxSizeBytes(1024 * 1024 * 1024 * 2L) //2GB
            .directory(cacheFolder)
            .build()

        Log.d(TAG, "cache folder = ${diskCache.directory.toFile().absolutePath}")

        return diskCache
    }


    fun enqueue(request: ImageRequest) {
        mImageLoader?.enqueue(request)
    }

    fun loader(): ImageLoader {
        return mImageLoader!!
    }
}
Kotlin 复制代码
import android.content.Context
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.recyclerview.widget.RecyclerView
import coil3.asDrawable
import coil3.request.ImageRequest
import coil3.request.SuccessResult
import coil3.size.Scale
import com.ppdemo.coil.MyCoilMgr

class MediaAdapter(val mCtx: Context) : RecyclerView.Adapter<MediaAdapter.MediaViewHolder>() {
    companion object {
        const val TAG = "fly/MediaAdapter"
    }

    private val data = mutableListOf<MediaEntity>()

    fun setData(list: List<MediaEntity>) {
        data.clear()
        data.addAll(list)

        Log.d(TAG, "setData ${list.size}")
        notifyDataSetChanged()
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MediaViewHolder {
        val view = LayoutInflater.from(mCtx).inflate(R.layout.item_media, parent, false)
        return MediaViewHolder(view)
    }

    override fun onBindViewHolder(holder: MediaViewHolder, position: Int) {
        val item = data[position]

        val req = ImageRequest.Builder(mCtx)
            .data(item.uri)
            .size(400)
            .scale(Scale.FIT)
            .listener(object : ImageRequest.Listener {
                override fun onSuccess(request: ImageRequest, result: SuccessResult) {
                    holder.imageView.setImageDrawable(result.image.asDrawable(mCtx.resources))
                }
            })
            .build()

        MyCoilMgr.INSTANCE.enqueue(req)
    }

    override fun getItemCount(): Int {
        return data.size
    }

    class MediaViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val imageView: ImageView = itemView.findViewById(R.id.iv)
    }
}
Kotlin 复制代码
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query

@Dao
interface MediaDao {

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertAll(list: List<MediaEntity>)

    @Query("SELECT * FROM media ORDER BY dateAdded DESC")
    suspend fun getAllMedia(): List<MediaEntity>

    @Query("SELECT * FROM media WHERE type = :type ORDER BY dateAdded DESC")
    suspend fun getMediaByType(type: String): List<MediaEntity>

    @Query("SELECT * FROM media WHERE name LIKE '%' || :keyword || '%' ORDER BY dateAdded DESC")
    suspend fun searchByName(keyword: String): List<MediaEntity>

    @Query("DELETE FROM media")
    suspend fun clearAll()
}
Kotlin 复制代码
import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "media")
data class MediaEntity(
    @PrimaryKey
    val id: Long,                  // MediaStore._ID
    val uri: String,               // 文件 uri
    val name: String,              // 文件名
    val path: String?,             // 可选,部分系统可能拿不到真实路径
    val mimeType: String?,
    val size: Long,
    val duration: Long = 0L,       // 视频时长,图片为0
    val dateAdded: Long,
    val type: String               // IMAGE / VIDEO
)
Kotlin 复制代码
class MediaRepository(
    private val mediaDao: MediaDao,
    private val mediaStoreDataSource: MediaStoreDataSource
) {

    suspend fun refreshLocalMedia() {
        val images = mediaStoreDataSource.queryImages()
        val videos = mediaStoreDataSource.queryVideos()

        mediaDao.clearAll()
        mediaDao.insertAll(images + videos)
    }

    suspend fun getAllMedia(): List<MediaEntity> {
        return mediaDao.getAllMedia()
    }

    suspend fun getImages(): List<MediaEntity> {
        return mediaDao.getMediaByType(MediaType.IMAGE.name)
    }

    suspend fun getVideos(): List<MediaEntity> {
        return mediaDao.getMediaByType(MediaType.VIDEO.name)
    }

    suspend fun search(keyword: String): List<MediaEntity> {
        return mediaDao.searchByName(keyword)
    }
}
Kotlin 复制代码
import android.content.ContentUris
import android.content.Context
import android.provider.MediaStore

class MediaStoreDataSource(private val context: Context) {

    suspend fun queryImages(): List<MediaEntity> {
        val list = mutableListOf<MediaEntity>()

        val collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
        val projection = arrayOf(
            MediaStore.Images.Media._ID,
            MediaStore.Images.Media.DISPLAY_NAME,
            MediaStore.Images.Media.MIME_TYPE,
            MediaStore.Images.Media.SIZE,
            MediaStore.Images.Media.DATE_ADDED,
            MediaStore.Images.Media.DATA // 新系统可能为 null 或不可用
        )

        val sortOrder = "${MediaStore.Images.Media.DATE_ADDED} DESC"

        context.contentResolver.query(
            collection,
            projection,
            null,
            null,
            sortOrder
        )?.use { cursor ->
            val idCol = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
            val nameCol = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME)
            val mimeCol = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.MIME_TYPE)
            val sizeCol = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.SIZE)
            val dateCol = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_ADDED)
            val pathCol = cursor.getColumnIndex(MediaStore.Images.Media.DATA)

            while (cursor.moveToNext()) {
                val id = cursor.getLong(idCol)
                val name = cursor.getString(nameCol) ?: ""
                val mimeType = cursor.getString(mimeCol)
                val size = cursor.getLong(sizeCol)
                val dateAdded = cursor.getLong(dateCol)
                val path = if (pathCol >= 0) cursor.getString(pathCol) else null

                val uri = ContentUris.withAppendedId(collection, id).toString()

                list.add(
                    MediaEntity(
                        id = id,
                        uri = uri,
                        name = name,
                        path = path,
                        mimeType = mimeType,
                        size = size,
                        duration = 0L,
                        dateAdded = dateAdded,
                        type = MediaType.IMAGE.name
                    )
                )
            }
        }

        return list
    }

    suspend fun queryVideos(): List<MediaEntity> {
        val list = mutableListOf<MediaEntity>()

        val collection = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
        val projection = arrayOf(
            MediaStore.Video.Media._ID,
            MediaStore.Video.Media.DISPLAY_NAME,
            MediaStore.Video.Media.MIME_TYPE,
            MediaStore.Video.Media.SIZE,
            MediaStore.Video.Media.DURATION,
            MediaStore.Video.Media.DATE_ADDED,
            MediaStore.Video.Media.DATA
        )

        val sortOrder = "${MediaStore.Video.Media.DATE_ADDED} DESC"

        context.contentResolver.query(
            collection,
            projection,
            null,
            null,
            sortOrder
        )?.use { cursor ->
            val idCol = cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID)
            val nameCol = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DISPLAY_NAME)
            val mimeCol = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.MIME_TYPE)
            val sizeCol = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.SIZE)
            val durationCol = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION)
            val dateCol = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATE_ADDED)
            val pathCol = cursor.getColumnIndex(MediaStore.Video.Media.DATA)

            while (cursor.moveToNext()) {
                val id = cursor.getLong(idCol)
                val name = cursor.getString(nameCol) ?: ""
                val mimeType = cursor.getString(mimeCol)
                val size = cursor.getLong(sizeCol)
                val duration = cursor.getLong(durationCol)
                val dateAdded = cursor.getLong(dateCol)
                val path = if (pathCol >= 0) cursor.getString(pathCol) else null

                val uri = ContentUris.withAppendedId(collection, id).toString()

                list.add(
                    MediaEntity(
                        id = id,
                        uri = uri,
                        name = name,
                        path = path,
                        mimeType = mimeType,
                        size = size,
                        duration = duration,
                        dateAdded = dateAdded,
                        type = MediaType.VIDEO.name
                    )
                )
            }
        }

        return list
    }
}
Kotlin 复制代码
enum class MediaType {
    IMAGE,
    VIDEO
}
Kotlin 复制代码
import android.app.Application
import android.util.Log
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

class MediaViewModel(application: Application) : AndroidViewModel(application) {
    companion object {
        const val TAG = "fly/MediaViewModel"
    }

    private val db = AppDatabase.getInstance(application)

    private val repository = MediaRepository(
        db.mediaDao(),
        MediaStoreDataSource(application)
    )

    val mediaList = MutableLiveData<List<MediaEntity>>()
    val loading = MutableLiveData(false)

    fun refreshMedia() {
        viewModelScope.launch(Dispatchers.IO) {
            loading.postValue(true)

            Log.d(TAG, "load data begin")
            val t = System.currentTimeMillis()
            val result = repository.getAllMedia()
            Log.d(TAG, "load data end ${result.size} time cost=${System.currentTimeMillis() - t}ms")

            mediaList.postValue(result)

            loading.postValue(false)
        }
    }

    fun loadImages() {
        viewModelScope.launch(Dispatchers.IO) {
            mediaList.postValue(repository.getImages())
        }
    }

    fun loadVideos() {
        viewModelScope.launch(Dispatchers.IO) {
            mediaList.postValue(repository.getVideos())
        }
    }

    fun search(keyword: String) {
        viewModelScope.launch(Dispatchers.IO) {
            mediaList.postValue(repository.search(keyword))
        }
    }
}
Kotlin 复制代码
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView


class PageActivity : AppCompatActivity()  {
    companion object{
        const val TAG="fly/PageActivity"
    }

    private var recyclerView: RecyclerView?=null
    private var adapter: MediaAdapter?=null
    private var viewModel: MediaViewModel?=null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_page)

        viewModel = ViewModelProvider(this)[MediaViewModel::class.java]

        recyclerView = findViewById(R.id.rv)
        recyclerView?.layoutManager = GridLayoutManager(this, 8) // 4列,接近8宫格视觉

        adapter = MediaAdapter(this)
        recyclerView?.adapter = adapter

        viewModel?.mediaList?.observe(this) { list ->
            Log.d(TAG,"observe ${list.size}")
            adapter?.setData(list)
        }

        viewModel?.refreshMedia()
    }
}
Kotlin 复制代码
import android.app.Application
import com.ppdemo.coil.MyCoilMgr

class MyApp : Application(){
    companion object {
        const val TAG = "fly/MyApp"
    }

    override fun onCreate() {
        super.onCreate()
        MyCoilMgr.INSTANCE.init(this)
    }
}

item_media.xml:

XML 复制代码
<?xml version="1.0" encoding="utf-8"?>
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/iv"
    android:layout_width="match_parent"
    android:layout_height="120px"
    android:scaleType="centerCrop" />
相关推荐
半摆烂日常7 小时前
低代码平台API对接实践:接口鉴权与数据同步的完整实现
android·低代码·rxjava
事圆则缓8 小时前
volatile 使用场景全解析:它能保证什么,又解决不了什么
android
蒲公英内测分发9 小时前
智能投影仪参加海外展会,未上架的 Android 配套 App 怎么让客户限时试用?
android
ii_best9 小时前
按键精灵手机端开发安卓版实战:手写一个可最小化、可拖动的「运行日志悬浮窗」(附完整 源码 + 踩坑记录)
android·ios·智能手机·自动化·ai编程·按键精灵
终端安全笔记9 小时前
安卓做 MDM 管控,先分清三种注册入口
android·安全·智能手机
乌萨达10 小时前
手机模拟器安卓怎么用?电脑安装应用、玩手游与账号同步指南
android·智能手机·电脑·雷电模拟器
美狐美颜SDK开放平台10 小时前
Android与iOS直播APP美颜有什么区别?视频美颜SDK开发详解
android·ios·音视频·视频美颜sdk
春猿火12 小时前
某市-2026【网安·论道】misc-afterimage_note-wp
android
恋猫de小郭13 小时前
Shopify 回应为什么从 RN 回到原生,为什么不用 KMP ?
android·前端·flutter
fundroid15 小时前
Android AI 开发,真正拉开差距的是工程闭环
android·ai·大模型·agent