Android Page 3 Flow读sql数据库媒体文件,Kotlin

Android Page 3 Flow读sql数据库媒体文件,Kotlin

基于Kotlin的Android媒体文件浏览应用实现方案。核心功能包括:

  1. 使用Room数据库存储媒体文件信息(MediaEntity),并通过Paging 3库实现分页加载
  2. 采用Coil 3图片加载库处理媒体文件显示,配置了2GB内存和磁盘缓存
  3. 实现权限管理,包括读写外部存储和访问媒体文件的权限
  4. 采用MVVM架构,通过ViewModel管理数据,Flow进行异步数据流处理
  5. 使用RecyclerView展示媒体文件网格视图,支持图片和视频缩略图显示

关键技术点包括Room数据库操作、Paging分页加载、Coil图片加载以及协程处理异步任务。应用通过分页方式高效加载大量媒体文件,同时利用缓存机制提升图片加载性能。

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" />
XML 复制代码
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 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.Disposable
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): Disposable? {
        return mImageLoader?.enqueue(request)
    }

    fun loader(): ImageLoader {
        return mImageLoader!!
    }
}
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 androidx.paging.PagingSource
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("DELETE FROM media")
    suspend fun clearAll()

    @Query("SELECT * FROM media ORDER BY dateAdded DESC")
    fun pagingSource(): PagingSource<Int, MediaEntity>

    @Query("SELECT * FROM media WHERE type = :type ORDER BY dateAdded DESC")
    fun pagingSourceByType(type: String): PagingSource<Int, MediaEntity>
}
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 复制代码
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.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import coil3.asDrawable
import coil3.request.Disposable
import coil3.request.ImageRequest
import coil3.request.SuccessResult
import coil3.size.Scale
import com.ppdemo.coil.MyCoilMgr

class MediaPagingAdapter(val mCtx: Context) : PagingDataAdapter<MediaEntity, MediaPagingAdapter.MediaViewHolder>(DIFF) {
    companion object {
        const val TAG = "fly/MediaAdapter"

        val DIFF = object : DiffUtil.ItemCallback<MediaEntity>() {
            override fun areItemsTheSame(oldItem: MediaEntity, newItem: MediaEntity): Boolean {
                return oldItem.id == newItem.id
            }

            override fun areContentsTheSame(oldItem: MediaEntity, newItem: MediaEntity): Boolean {
                return oldItem == newItem
            }
        }
    }

    private var disposable: Disposable? = null

    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) {
        holder.imageView.setImageResource(android.R.drawable.gallery_thumb)

        val item = getItem(position) ?: return

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

                override fun onStart(request: ImageRequest) {
                    holder.imageView.setImageResource(android.R.drawable.ic_menu_gallery)
                }
            }).build()

        disposable = MyCoilMgr.INSTANCE.enqueue(req)
    }

    class MediaViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val imageView: ImageView = itemView.findViewById(R.id.iv)
    }
}
Kotlin 复制代码
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import kotlinx.coroutines.flow.Flow

class MediaRepository(
    private val mediaDao: MediaDao
) {

    fun getMediaPager(): Flow<PagingData<MediaEntity>> {
        return Pager(
            config = PagingConfig(
                pageSize = 150,          // 每页 150 条
                initialLoadSize = 150,   // 首次也读取 150 条
                prefetchDistance = 50,   // 提前预取
                enablePlaceholders = false
            ),
            pagingSourceFactory = { mediaDao.pagingSource() }
        ).flow
    }
}
Kotlin 复制代码
enum class MediaType {
    IMAGE,
    VIDEO
}
Kotlin 复制代码
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.PagingData
import androidx.paging.cachedIn
import kotlinx.coroutines.flow.Flow

class MediaViewModel(application: Application) : AndroidViewModel(application) {

    private val db = AppDatabase.getInstance(application)
    private val repository = MediaRepository(
        db.mediaDao()
    )

    val mediaPagingFlow: Flow<PagingData<MediaEntity>> =
        repository.getMediaPager().cachedIn(viewModelScope)
}
Kotlin 复制代码
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

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

        const val GRID = 6
        const val IMG_SIZE = 300
    }

    private var recyclerView: RecyclerView? = null
    private var adapter: MediaPagingAdapter? = 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, GRID)

        adapter = MediaPagingAdapter(this)
        recyclerView?.adapter = adapter
    }

    override fun onResume() {
        super.onResume()

        collectPagingData()
    }

    private fun collectPagingData() {
        lifecycleScope.launch(Dispatchers.IO) {
            viewModel?.mediaPagingFlow?.collect { pagingData ->
                adapter?.submitData(pagingData)
            }
        }
    }
}
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)
    }
}

相关:https://blog.csdn.net/zhangphil/article/details/160559292

相关推荐
九皇叔叔1 小时前
MySQL ORDER BY 性能优化详解:Using filesort、联合索引、ASC/DESC 与覆盖索引
android·adb
bytebitx2 小时前
MacOS 编译腾讯 Mars XLog 及使用(已16KB对齐)
kotlin·mac·apk
OriginCoding2 小时前
用 AI Agent 协作完成一个 Android TOTP 应用:从需求边界到 v1.0.0
android·ai编程
杉氧4 小时前
React 灵魂:深入剖析 Hooks 与副作用(Side Effects)陷阱
android·前端·react native
又见情义5 小时前
RK3568 + RTL8211F 网络唤醒(WOL)功能适配全记录
android·网络·驱动开发
用户0934077735146 小时前
HarmonyOS WPS Open SDK 实践:registerApp 鉴权与就绪门禁
android·typescript·harmonyos
jike_20266 小时前
会议离线录音APP:无网络记录能力对比
android·智能手机·语音识别
always_TT7 小时前
【Python 字符串格式化:format() 方法】
android·开发语言·python
恋猫de小郭7 小时前
AI 时代,一个优化 Flutter 的重复代码工具 Deslop
android·前端·flutter
RisunJan7 小时前
Android 面试知识点整理
android·面试·职场和发展