美文网首页
JetPack知识点实战系列十三:Kotlin Flow项目实战

JetPack知识点实战系列十三:Kotlin Flow项目实战

作者: chonglingliu | 来源:发表于2020-11-27 16:09 被阅读0次

    前一章节我们讲解了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<T>?

    回答:可以。可以使用(suspend () -> T).asFlow(): Flow<T>这个Buildersuspend函数转换成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操作,后面会介绍。

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

    代码解释:

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

    相关文章

      网友评论

          本文标题:JetPack知识点实战系列十三:Kotlin Flow项目实战

          本文链接:https://www.haomeiwen.com/subject/bwztwktx.html