JetPack知识点实战系列十三:Kotlin Flow项目实战-网络、数据库和UI的应用

前一章节我们讲解了Kotlin Flow的基本用法,这一节我们来实践将Kotlin Flow应用在Android应用中。

我们从三个方面进行讲解:

  1. 网络数据的请求
  2. 在编写UI界面中的使用
  3. 结合Room在数据库中的使用

MVVM架构中留给Flow的位置

我们再来看一下Google给我们规范的MVVM架构图:

Google架构图

MVVM架构中数据回流的方式主要是利用LiveData来实现:

LiveData

鉴于LiveData的功能很单一,我们可以将部分LiveData的实现方式替换成Kotlin Flow来实现。

这样就变成了如下的实现方式:

LiveData和Flow配合

本例中我们通过关键词搜索页面来介绍Flow的使用方式。

效果图
需求

  1. ToolBar上有一个输入框,从服务器端获取到一个最热关键词,输入框中输入关键词可以获取到关键词对应的关键词列表
  2. 页面中有一个热搜列表,数据从服务器端获取。
  3. 每个搜索过的关键词存入数据库,展示到搜索历史中,点击删除按钮搜索历史全部删除

网络数据请求

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?

回答:可以。可以使用(suspend () -> T).asFlow(): Flow<T>这个Buildersuspend函数转换成Flow

  • 疑问2:Retrofit的API接口能返回**Flow**吗?譬如定义为: fun getSearchDefaultWord(): Flow<SearchDefaultResponse>

回答:不可以。Retrofit API 是定义的 Interface,不是一个suspend函数,真正的实现类是Retrofit库去实现的。如果用的其他的请求库是有可能将返回值实现成Flow<T>的。

Repository

Repository层将返回值变为Flow

实现方式如下:

<!--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操作,后面会介绍。

我们先介绍最热搜索关键词和热门关键词列表两个请求的实现。

代码解释:

  1. 定义两个LiveData,返回值是Repository层释放的值。

区别:以前的实现是定义一个publicLiveDataprivateMutableLiveData,通过Flow的实现方式可以去掉MutableLiveData

  1. 在构造函数中将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()
    }
}

代码解释:

  1. SearchMainViewModel中定义一个值类型为StringMutableStateFlow
  2. 点击软键盘的搜索按钮的时候更改searchFlow的值
  3. 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 {
    // 搜索词相关的关键词列表展示
    ...
})

代码解释:

  1. 定义searchResult这个LiveData,它返回的是EditText的输入值相关的关键词列表数据。
  2. searchFlow经过一系列的中间操作,然后触发网络请求。

debounce:只有允许间隔超过500ms间隔才能触发,避免过多的请求

filter:只有关键词不为空才进行请求,避免空的输入值也请求

flatMapLatest:如果前面的请求没有完成,直接取消,然后开始先的请求

catch:捕获异常,释放空的List

  1. 网络请求得到的结果触发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>

将其他的代码也列出来:

<!--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项目中的应用基本上都介绍完了。

猜你喜欢

转载自blog.csdn.net/lcl130/article/details/110234975