需求概述

查找功用是许多APP都会要点维护的功用,由于查找功用可以很好的帮助用户找到自己需求的内容,电商APP上,用户可以运用查找功用快速找到自己想要的商品,社交App上,用户可以运用查找功用快速找到对应的好友和内容以及运用浏览器快速查找自己想要知道的问题答案等…..。所以查找功用的运用频率是很高的,所以查找功用的用户体会也就适当重要,假设查找功用仅仅供给查找的话,有点美中不足,可是假设能加上查找记载的办理就很好了。现在基本上一切的带有查找的APP都会带有查找记载的功用。试想下假设没有查找记载的功用,用户查找完自己想要的东西后,假设再次进入到查找页还想查找之前的内容就需求再次输入对应的关键字,关键字短还好,假设关键字很长,或许输入的是链接,那用户输入起来就太麻烦了。所以查找记载功用的重要性不言而喻。本文便是要完成一个查找记载的办理功用,包含显现查找记载,删去单条查找记载,删去悉数查找记载的功用同时完成一个查找页面。

功用展现

Android JetPack Compose+Room----完成查找记载功用

如上面的gif图展现的相同,查找界面有一个输入框,在输入框中输入咱们的关键词点击查找,这是就会发生一个查找记载,这些查找记载会以sqlite数据库的方法保存起来,当再次打开查找界面后就会显现之前的查找记载。用户可以点击查找记载开端查找,也可以删去不想要的查找记载,或许是铲除一切的查找记载。查找页面中有些细节需求重视下,刚进入查找页面的时分会默许拉起键盘,输入框中的铲除查找内容按钮是当有输入内容的时分才展现,否则不展现,查找小图标的色彩也是有输入内容的时分才会显现得更加明晰。这些功用会在代码完成的部分化说

完成查找功用运用的技能

1.Android Jetpack room

由于查找记载需求耐久化存储到手机里边,咱们可以挑选文件,shared perference,Sqlite数据库,这儿挑选Sqlite数据库无疑是最合适的,可是见过许多的小伙伴却在数据库和文件以及SP中挑选了文件和SP,原因肯定是和Sqlite数据库的运用比较繁琐,乃至还涉及到数据库晋级的问题。面试的时分许多小伙伴肯定都会被问到数据库晋级的问题。由于数据库的晋级假设处理欠好,就会导致APP闪退,所以许多小伙伴挑选了更为稳妥的方法。可是Android jetpack 的ROOM出现后,这一切都变得简略了,Room库在 SQLite 上供给了一个笼统层,充分利用 SQLite 的强壮功用的同时,可以流畅地访问数据库。Room 供给针对 SQL 查询的编译时验证并供给便利注解,可最大极限削减重复和简单出错的样板代码而且还简化了数据库迁移晋级。可以说十分的好用。查找记载挑选它耐久化也十分便利,由于查找记载会涉及到排序,删去,限制查找记载的条数,逻辑删去等功用,运用sqlite数据库无疑是最佳挑选。

2.Android JetPack Compose

Compose是Android 推出的新一代UI结构,是一种声明式的UI结构,本文涉及的查找功用的界面悉数都由Compose开发,Compose依据Kotlin的DSL语言 做界面的UI描绘,UI表达能力丝毫不逊色于XML。运用Compose,咱们再也不用写XML布局和findViewByID了。主张读者去了解下Compose UI。

代码完成

编写查找界面

查找界面主要便是包含一个输入框,回来按钮,和展现查找记载的部分,先界说查找界面的Composable函数,而且界说好对应的事情回调,这样做的好处是可以让咱们的程序符合单向数据流的结构,咱们让数据单向流向UI,而UI的更新经过事情的方法来告诉数据源头更新数据,而数据源头更新数据后,由于Compose的State是一种依据观察者模式的状态,所以当State状态更新的时分,UI会主动重组更新。所以咱们需求界说一个SearchHistoryState,如下所示:

@Stable
data class SearchHistoryState(
    var history_id: Long = -1L,
    var history_title: String = "",
    var history_content:  String = "",
    var createTime: Long = System.currentTimeMillis(),
    var isDelete:  String = "0"
)

查找界面以回调的方法向调用者供给现在查找界面中履行的操作,这样做可以使咱们的查找界面的复用性更高,也让查找界面的责任更加单一,不用承当数据的更新操作。

@Composable
fun SearchPage(
    searchHistoryList: MutableList<SearchHistoryState>,
    onBackClick: () -> Unit,
    onSearchTrigger: (String) -> Unit,
    onClearAllClick: () -> Unit,
    onDeleteClick: (SearchHistoryState) -> Unit,
    onHistoryItemClick: (SearchHistoryState) -> Unit
) {
    ....
}

查找界面也很简略页面分化如图所示:

Android JetPack Compose+Room----完成查找记载功用
一个纵向布局,绿色框中是一个横向布局,包含查找框和一个回来按钮,在赤色框里边便是一个纵向布局,包含显现近期浏览,悉数删去按钮的头部分和展现索记载的列表部分,界面的代码就不贴了,太多了,文章结束会给源码:

接入Room完成查找功用的办理

引进依赖

// zoom sqlite jetpack组件
    val roomVersion = "2.6.1"
    implementation("androidx.room:room-runtime:$roomVersion")
    implementation("androidx.room:room-ktx:$roomVersion")
    implementation("androidx.room:room-paging:$roomVersion")
    ksp("androidx.room:room-compiler:$roomVersion")

留意这儿的KSP需求引进对应的插件

plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
    id("com.google.devtools.ksp").version("1.9.20-1.0.14") // 需求与Android的构建插件的版别相对应
}

界说包结构

如下图所示,咱们先界说几个包,便利后面咱们编写对应的代码,dao用来放操作数据库的方法,entitiy用于寄存咱们的界说的实体类,repository是咱们办理dao的操作的类,调用者可以经过它获取各种dao操作接口去操作对应的表数据。

Android JetPack Compose+Room----完成查找记载功用

咱们首要应该界说的是entity,查找记载的entity如下所示:

@Keep //避免混淆的时分将咱们的实体类混淆,导致无法找到
@Entity(tableName = "t_search_history") // 界说sqlite数据库中表的姓名,后面操作查找记载时就操作这个表
data class SearchHistory(
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "id")
    val id: Long = 0L,
    @ColumnInfo(name = "title")
    val title: String,
    @ColumnInfo(name = "webUrl")
    val webUrl: String,
    @ColumnInfo(name = "create_time")
    val createTime: Long = 0L,
    // 是否现已删去,0表明未删去,1表明已删去
    @ColumnInfo(name = "isDelete")
    var isDelete: String = "0",
)

留意:咱们在界说entity的类时,会映射成数据库中的表,这儿就会涉及的到刺进数据的记载时的id主动生成的问题,咱们界说ID的时分需求将其默许值界说为0,而不是其他的

 @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "id")
    val id: Long = 0L,

假设界说成-1或许是其他的会导致无法刺进记载,由于ID没有自增

界说操作表的Dao类

对数据进行查询,删去,更新等操作咱们界说一个Dao类来完成,代码如下所示:

@Dao
interface SearchHistoryDao {
// 限制查找记载为10条,这便是运用sqlite数据库的优势之一。可以随意变换显现的条数,而且可以经过时间排序。
    @Query("select * from t_search_history where isDelete = 0 order by create_time desc limit 10")
    fun getAllHistory():MutableList<SearchHistory>
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertHistory(history: SearchHistory):Long
    @Query("delete from t_search_history")
    suspend fun clearAll()
    @Transaction
    @Query("select * from t_search_history where id=:id")
    suspend fun findSearchHistoryById(id:Long):SearchHistory?
    @Update
    suspend fun update(searchHistory: SearchHistory)
}

界说数据库的基础装备

界说完entity和Dao类后,咱们就可以开端界说数据库的对应装备了,之所以将这步放到entity和dao之后是由于这步需求用到entity和dao,代码如下所示:

@Database(
    version = 1,//数据库的版别,这儿在数据库迁移晋级的时分需求改变
    entities = [
        SearchHistory::class,
    ] // 和表彼此映射的实体类
)
abstract class AppSqliteDataBase:RoomDatabase(){
    abstract fun searchHistoryDao():SearchHistoryDao // 界说获取Dao操作类的笼统方法。
}
// 数据库的初始化装备类,运用Room数据库时咱们需求先初始化它。在咱们的Application中调用AppDB.init(Context)
// 就可以初始化数据库了
class AppDB{
    companion object{
        fun init(context: Context):AppSqliteDataBase{
            val databaseBuilder = Room.databaseBuilder(
                context = context,
                klass = AppSqliteDataBase::class.java,// 数据库装备类
                name = "SearchDB" // 数据库的姓名
            ).apply {
                fallbackToDestructiveMigration()
            }
            return databaseBuilder.build()
        }
    }
}

界说数据库的Dao办理类

咱们的项目中可能会有许多的数据库表,每个表都会有一个Dao操作接口类,所以咱们需求一个类去办理这些接口,这便是咱们的Repository类。如下所示:

class SearchHistoryRepository(private val db: AppSqliteDataBase) {
    /**
     * 获取查找列表
     */
    fun getSearchHistoryList(): MutableList<SearchHistory> {
        return db.searchHistoryDao().getAllHistory()
    }
    /**
     * 新增查找历史记载
     */
    suspend fun insertHistory(searchHistory: SearchHistory) {
        val oldHistory = db
            .searchHistoryDao()
            .findSearchHistoryById(searchHistory.id)
        if (oldHistory != null) {
            db.searchHistoryDao().update(searchHistory)
        } else {
            db.searchHistoryDao().insertHistory(searchHistory)
        }
    }
    /**
     * 经过ID删去历史记载
     */
    suspend fun deleteById(id: Long) {
        val searchHistory = db.searchHistoryDao().findSearchHistoryById(id)
        if (searchHistory != null) {
            // 将删去的标志更新成1,表明现已删去
            searchHistory.isDelete = "1"
            db.searchHistoryDao().update(searchHistory)
        }
    }
    /**
     * 更新历史记载
     */
    suspend fun updateHistory(searchHistory: SearchHistory) {
        db.searchHistoryDao().update(searchHistory)
    }
    /**
     * 铲除历史记载
     */
    suspend fun clearAllHistory() {
        db.searchHistoryDao().clearAll()
    }
}

运用

界说好了对应的接口后,咱们就可以在ViewModel中运用了。

class SearchHistoryViewModel(db: AppSqliteDataBase) : ViewModel() {
    private val TAG = "SearchHistoryViewModel"
    var searchHistoryRepo: SearchHistoryRepository = SearchHistoryRepository(db = db)
    var searchHistoryStateList = mutableStateListOf<SearchHistoryState>() // 运用
    // compose的StateAPI,当数据更新时,界面会主动重组更新
    fun loadHistoryList() {
        Log.d(TAG, "loadHistoryList")
       viewModelScope.launch(Dispatchers.IO) {
           searchHistoryRepo.getSearchHistoryList().forEach { searchHistory: SearchHistory ->
               Log.d(TAG,"loadHistoryList: $searchHistory")
               val searchHistoryState = SearchHistoryState(
                   history_id = searchHistory.id,
                   history_title = searchHistory.title,
                   history_content = searchHistory.webUrl,
                   createTime = searchHistory.createTime,
                   isDelete = searchHistory.isDelete
               )
               searchHistoryStateList.add(searchHistoryState)
           }
       }
    }
    fun deleteHistory(searchHistoryState: SearchHistoryState) {
        Log.d(TAG, "deleteHistory: $searchHistoryState")
        viewModelScope.launch(Dispatchers.IO) {
            searchHistoryStateList.remove(searchHistoryState)
            searchHistoryStateList.sortBy { it.createTime }
            searchHistoryRepo.deleteById(searchHistoryState.history_id)
        }
    }
    fun addHistory(searchHistoryState: SearchHistoryState) {
        Log.d(TAG, "deleteHistory: $searchHistoryState")
        if(searchHistoryStateList.size == 10){
            searchHistoryStateList.removeLast()
        }
        viewModelScope.launch(Dispatchers.IO) {
            searchHistoryStateList.add(searchHistoryState)
            searchHistoryStateList.sortBy { it.createTime }
            val searchHistory = SearchHistory(
                title = searchHistoryState.history_title,
                webUrl = searchHistoryState.history_content,
                createTime = searchHistoryState.createTime
            )
            searchHistoryRepo.insertHistory(searchHistory)
        }
    }
    fun clearAllHistory() {
        Log.d(TAG, "clearAllHistory")
        searchHistoryStateList.clear()
        viewModelScope.launch {
            searchHistoryRepo.clearAllHistory()
        }
    }
    fun updateHistory(searchHistoryState: SearchHistoryState){
        viewModelScope.launch {
            val searchHistory = SearchHistory(
                title = searchHistoryState.history_title,
                webUrl = searchHistoryState.history_content,
                createTime = searchHistoryState.createTime
            )
            searchHistoryRepo.updateHistory(searchHistory)
        }
    }
}

在Activity中,初始化ViewModel依据查找页面中触发的事情去做对应的查找记载操作。

  SearchPage(searchHistoryViewModel.searchHistoryStateList,
                    onBackClick = { finish() },
                    onSearchTrigger = { url ->
                        if (url.isNotEmpty()) {
                            val searchHistoryState = SearchHistoryState(
                                history_title = url,
                                history_content = url,
                                createTime = System.currentTimeMillis()
                            )
                            searchHistoryViewModel.addHistory(searchHistoryState)
                        }
                    },
                    onClearAllClick = {
                        searchHistoryViewModel.clearAllHistory()
                    },
                    onDeleteClick = { searchHistoryState ->
                        Log.d(TAG, "onDeleteClick=>searchHistoryState: $searchHistoryState")
                        searchHistoryViewModel.deleteHistory(searchHistoryState)
                    },
                    onHistoryItemClick = { searchHistoryState ->
                        Log.d(TAG, "onHistoryItemClick=>searchHistoryState: $searchHistoryState")
                        val content = searchHistoryState.history_content
                        val searchHistory = SearchHistoryState(
                            history_title = content,
                            history_content = content,
                            createTime = System.currentTimeMillis()
                        )
                        searchHistoryViewModel.updateHistory(searchHistory)
                    }
                )

数据库晋级

数据库晋级便是咱们发布了app的第一个版别,这个版别上只要查找记载的数据库表t_searchhistory,然后咱们打算发布app的第二个版别,在第二个版别上咱们新增了数据库的表t_test,或许是修改了t_searchhistory的字段,这时假设用户更新咱们的app第二个版别时,由于数据库中没有咱们新增的第二张表,这就会导致出现下面的异常导致APP直接闪退。

Android JetPack Compose+Room----完成查找记载功用
所以需求咱们做数据库的晋级迁移,当用户装置咱们第二个app版别时,咱们将更新的表更新到用户的本地数据库中,咱们在项目中新建一个TestEntity演示数据库的迁移晋级,界说的进程和咱们的查找记载的界说进程相同,不同的点在于。咱们需求新建一个Migration类去办理咱们的晋级版别,如下所示:

val MIGRATION_1_2 = object : Migration(1,2){
    override fun migrate(db: SupportSQLiteDatabase) {
        db.execSQL("CREATE TABLE IF NOT EXISTS `t_test` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT 
        NULL, `name` TEXT NOT NULL)")
    }
}

比方咱们新增了一张表,就像上面的写法相同。有读者可能会决定Sqlite语句的写法有难度,还简单错,这儿有个很好的办法,由于Room是运用注解去生成代码的,所以咱们界说好咱们的功用后,构建下项目,然后去到生成的代码中复制对应的Sqlite代码就可以了。比方本例中生成的代码如下:

Android JetPack Compose+Room----完成查找记载功用
然后便是装备AppSqliteDataBase,装备对应的晋级策略和版别号,如下所示:

@Database(
    version = 2,//数据库的版别晋级到2
    entities = [
        SearchHistory::class,
        TestEntity::class
    ] // 和表彼此映射的实体类
)
abstract class AppSqliteDataBase:RoomDatabase(){
    abstract fun searchHistoryDao():SearchHistoryDao
    abstract fun testEntityDao():TestDao
}
class AppDB{
    companion object{
        fun init(context: Context):AppSqliteDataBase{
            val databaseBuilder = Room.databaseBuilder(
                context = context,
                klass = AppSqliteDataBase::class.java,
                name = "SearchDB"
            ).apply {
                fallbackToDestructiveMigration()
                addMigrations( // 数据库晋级迁移
                    MIGRATION_1_2 // 将咱们的新版APP的新增的数据库操作装备到这儿就可以了
                )
            }
            return databaseBuilder.build()
        }
    }
}

为了验证咱们的数据库是否晋级成功,咱们在SearchHistoryViewModel的loadHistoryList中参加如下的测试代码:

   fun loadHistoryList() {
        Log.d(TAG, "loadHistoryList")
       viewModelScope.launch(Dispatchers.IO) {
           searchHistoryRepo.getSearchHistoryList().forEach { searchHistory: SearchHistory ->
               Log.d(TAG,"loadHistoryList: $searchHistory")
               val searchHistoryState = SearchHistoryState(
                   history_id = searchHistory.id,
                   history_title = searchHistory.title,
                   history_content = searchHistory.webUrl,
                   createTime = searchHistory.createTime,
                   isDelete = searchHistory.isDelete
               )
               searchHistoryStateList.add(searchHistoryState)
           }
           searchHistoryRepo.insertTest(TestEntity(name = "walt"))
           searchHistoryRepo.insertTest(TestEntity(name = "zhong"))
           searchHistoryRepo.insertTest(TestEntity(name = "007"))
           searchHistoryRepo.getTestList().forEach {
               Log.d(TAG,"result: $it")
           }
       }
    }

运行成果如下表明咱们数据库晋级成功了。完整的比如请参阅源码。

Android JetPack Compose+Room----完成查找记载功用

源码地址

为了便利读者了解Room的运用,在此贴上源码,主张读者下载源码自己动手完成一遍,后面遇到相关的需求时就可以快速搞定了。这个仓库我今后涉及到jetpack的运用时都会更新,欢迎读者克隆更新,彼此参阅学习。有问题欢迎谈论区沟通。 查找记载功用的源码