JetPack知识点实战系列十三:Kotlin Flow项目实战
前一章节我们讲解了Kotlin Flow的基本用法,这一节我们来实践将Kotlin Flow应用在Android应用中。
我们从三个方面进行讲解:
- 网络数据的请求
- 在编写UI界面中的使用
- 结合Room在数据库中的使用
MVVM架构中留给Flow的位置
我们再来看一下Google给我们规范的MVVM架构图:
Google架构图MVVM架构中数据回流的方式主要是利用LiveData来实现:
LiveData鉴于LiveData的功能很单一,我们可以将部分LiveData的实现方式替换成Kotlin Flow来实现。
这样就变成了如下的实现方式:
LiveData和Flow配合本例中我们通过关键词搜索页面来介绍Flow的使用方式。
效果图 需求- ToolBar上有一个输入框,从服务器端获取到一个最热关键词,输入框中输入关键词可以获取到关键词对应的关键词列表
- 页面中有一个热搜列表,数据从服务器端获取。
- 每个搜索过的关键词存入数据库,展示到搜索历史中,点击删除按钮搜索历史全部删除
网络数据请求
Retrofit API
这个页面有三个API请求,我们定义三个接口
<!--MusicApiService.kt-->
@GET(MusicApiConstant.SEARCH_DEFAULT_WORD)
suspend fun getSearchDefaultWord(): SearchDefaultResponse
@GET(MusicApiConstant.SEARCH_HOT_LIST)
suspend fun getSearchHotList(): SearchHostListResponse
@GET(MusicApiConstant.SEARCH_SUGGESTION)
suspend fun getSearchSuggest(@Query("keywords") keywords: String, @Query("type") type: String = "mobile"): SearchSuggestResponse
这个和以前的实现一样无异。您是否会有关于Flow的疑问?
我这里提出两个可能会有的的疑问:
- 疑问1: 网络请求的返回值是否可以为Flow<T>?
回答:可以。可以使用
(suspend () -> T).asFlow(): Flow<T>
这个Builder将suspend函数转换成Flow
- 疑问2:Retrofit的API接口能返回Flow<T>吗?譬如定义为:
fun getSearchDefaultWord(): Flow<SearchDefaultResponse>
回答:不可以。Retrofit API 是定义的 Interface,不是一个suspend函数,真正的实现类是Retrofit库去实现的。如果用的其他的请求库是有可能将返回值实现成
Flow<T>
的。
Repository
Repository层将返回值变为Flow<T>。
实现方式如下:
<!--SearchRepository.kt -->
object SearchRepository {
/* 搜索默认值 */
fun getSearchDefaultWord(): Flow<SearchDefaultResponse.SearchDefaultData?> {
return flow {
emit(MusicApiService.create().getSearchDefaultWord().data)
}
}
/* 搜索热点列表 */
fun getSearchHostList(): Flow<List<SearchHostListResponse.SearchDetail>> {
return flow {
MusicApiService.create().getSearchHotList().data?.let {
emit(it)
} ?: emit(listOf())
}
}
/* 搜索关键词的相关列表 */
fun getSearchSuggestion(keywords: String): Flow<List<SearchSuggestResponse.SearchSuggest>> {
return flow {
MusicApiService.create().getSearchSuggest(keywords).result?.get("allMatch")?.let {
emit(it)
} ?: emit(listOf())
}
}
}
ViewModel
ViewModel将Flow转换成LiveData
<!--SearchMainViewModel.kt-->
// 1
val keyword: LiveData<String>
val hotList: LiveData<List<SearchHostListResponse.SearchDetail>>
// 2
init {
keyword = liveData(timeoutInMs = 15000) {
SearchRepository.getSearchDefaultWord()
.catch {
emit("")
}
.collect {
defaultData = it
emit(it?.showKeyword ?: "")
}
}
// 3
hotList = liveData(timeoutInMs = 15000) {
SearchRepository.getSearchHostList()
.catch {
emit(listOf())
}
.collect {
emit(it)
}
}
}
搜索关键词的相关列表和输入框的操作有关,涉及UI操作,后面会介绍。
我们先介绍最热搜索关键词和热门关键词列表两个请求的实现。
代码解释:
- 定义两个LiveData,返回值是Repository层释放的值。
区别:以前的实现是定义一个
public
的LiveData和private
的MutableLiveData,通过Flow的实现方式可以去掉MutableLiveData。
- 在构造函数中将Flow捕获异常,然后通过
liveData{}
转换成LiveData。
liveData{}
的参数timeoutInMs = 15000 是给了一个超时时间。如果超时就会抛异常。,还可以通过Flow.asLiveData()
将LiveData转成Flow. 后面会使用。
Fragment
<!--MainSearchFragment.kt-->
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.keyword.observe(viewLifecycleOwner, Observer {
// 设置EditText的Hint
...
})
viewModel.hotList.observe(viewLifecycleOwner, Observer {
// 显示热点列表
...
})
}
Fragment中直接监听LiveData值的变化,更新界面。
UI相关 - 输入框中输入关键词
输入关键词需求为输入框中输入关键词,然后展示关键词相关的关键词列表。
EditText的Event转为Flow
我们首先可以将EditText的值的变化封装成StateFlow。
<!--SearchMainViewModel.kt-->
// 1. 定义一个String的MutableStateFlow
val searchFlow = MutableStateFlow("")
<!--MainSearchFragment.kt-->
edittext?.let {
it.setOnEditorActionListener { _, actionId, event ->
if ((actionId == EditorInfo.IME_ACTION_SEARCH)) {
// 2
viewModel.searchFlow.value = it.text.toString()
return@setOnEditorActionListener true
}
return@setOnEditorActionListener false
}
it.addTextChangedListener { _ ->
// 3
viewModel.searchFlow.value = it.text.toString()
}
}
代码解释:
- SearchMainViewModel中定义一个值类型为String的MutableStateFlow。
- 点击软键盘的搜索按钮的时候更改searchFlow的值
- EditText的文字变化的时候更改searchFlow的值
Flow值触发网络请求和UI刷新
<!--SearchMainViewModel.kt-->
// 1
val searchResult: LiveData<List<SearchSuggestResponse.SearchSuggest>>
init {
// 2
searchResult = searchFlow
.debounce(500)
.filter {
it.isNotEmpty()
}
.flatMapLatest {
SearchRepository.getSearchSuggestion(it)
}
.catch {
emit(listOf())
}
.asLiveData()
}
// 3
viewModel.searchResult.observe(viewLifecycleOwner, Observer {
// 搜索词相关的关键词列表展示
...
})
代码解释:
- 定义searchResult这个LiveData,它返回的是EditText的输入值相关的关键词列表数据。
- searchFlow经过一系列的中间操作,然后触发网络请求。
debounce:只有允许间隔超过500ms间隔才能触发,避免过多的请求
filter:只有关键词不为空才进行请求,避免空的输入值也请求
flatMapLatest:如果前面的请求没有完成,直接取消,然后开始先的请求
catch:捕获异常,释放空的List
- 网络请求得到的结果触发UI的更新
数据库
DataBase Migration
这一步不是必须的,为了博客的延续性,我们这里把Migration也列出来。
<!--MusicDatabase.kt-->
private class Migration2To3: Migration(2,3) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS search_history" +
"('search_keyword' VARCHAR NOT NULL PRIMARY KEY AUTO_INCREMENT, 'search_sequence' INTEGER NOT NULL)")
}
}
Dao
我们的Dao中只有查询会涉及到返回值,我们可以将将查询方法的返回值直接改成Flow<T>
。
fun getAllSearchHistory(): Flow<List<SearchHistory>>
将其他的代码也列出来:
<!--SearchHistoryDao.kt-->
@Dao
interface SearchHistoryDao {
/* 批量查询搜索历史 */
@Query("SELECT search_sequence, search_keyword FROM search_history ORDER BY search_sequence ASC;")
fun getAllSearchHistory(): Flow<List<SearchHistory>>
/* 插入搜索历史 */
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertSearchHistory(history: SearchHistory)
@Transaction
suspend fun insertSearchHistories(histories: List<SearchHistory>) {
for (history in histories) {
insertSearchHistory(history)
}
}
/* 批量删除搜索历史 */
@Query("DELETE FROM search_history")
suspend fun deleteAllSearchHistory()
/* 更新搜索历史 */
@Update(onConflict = OnConflictStrategy.REPLACE)
suspend fun updateSearchHistory(history: SearchHistory)
/* 批量更新搜索历史 */
@Transaction
suspend fun updateSearchHistories(histories: List<SearchHistory>) {
for (history in histories) {
updateSearchHistory(history)
}
}
}
ViewModel
<!--SearchMainViewModel.kt-->
// Dao
private var searchHistoryRepository: SearchHistoryRepository =
SearchHistoryRepository(MusicDatabase.getInstance(application).searchHistoryDao())
// 搜索历史的关键词字符串列表
val searchHistoryList: LiveData<List<String>>
@Volatile
// 记录下从数据库查询出来的数据库中的数据列表
private var _searchHistoryList: List<SearchHistory> = listOf()
init {
searchHistoryList = searchHistoryRepository.getAllShearchHistory()
.distinctUntilChanged() //确保有变化
.onEach { _searchHistoryList = it } // 记录下数据库的值
.map { value -> value.map { it.keyword } } // 转成字符串数组
.catch { println("$it") } //捕获异常
.asLiveData()
}
// 添加和修改搜索历史
@Synchronized fun addKeyWord(keyword: String) {
var latestIndex = _searchHistoryList.size
// 遍历
for (i in _searchHistoryList.indices) {
if (_searchHistoryList[i].keyword == keyword) {
// 置0
_searchHistoryList[i].sequence = 0
latestIndex = i
} else {
// 对应的元素之前的元素就后移一位
if (i < latestIndex) {
_searchHistoryList[i].sequence = _searchHistoryList[i].sequence + 1
}
}
}
if (latestIndex == _searchHistoryList.size) { // 属于增加的
val mList = mutableListOf<SearchHistory>()
for (item in _searchHistoryList) {
mList.add(item)
}
mList.add(SearchHistory(keyword, 0))
GlobalScope.launch { searchHistoryRepository.insertSearchHistories(mList) }
} else {
GlobalScope.launch { searchHistoryRepository.insertSearchHistories(_searchHistoryList) }
}
}
// 删除所有的搜索历史
@Synchronized fun deleteAll() {
GlobalScope.launch {
searchHistoryRepository.deleteAllSearchHistory()
}
}
Fragment
Fragment中监听数据库的变化
// 搜索历史相关
viewModel.searchHistoryList.observe(viewLifecycleOwner, Observer {
// 刷新列表
...
})
Flow在Android项目中的应用基本上都介绍完了。