Android 中的 Kotlin Flow 示例
Kotlin Flow 是 Kotlin 协程的最新成员之一。使用 Kotlin Flow,我们可以异步处理顺序执行的数据流。
我们将在本文中构建什么?
我们将构建一个简单的应用程序,从 API 获取一些数据并将其显示在屏幕上。这是一个演示 Kotlin 流程工作的简单应用程序。它将使用 MVVM 架构。
先决条件:
- 对安卓有很好的了解
- Kotlin 的知识
- MVVM 架构基础
- 改造库基础
- Kotlin 协程的基础知识
- 视图绑定基础
分步实施
第 1 步:创建一个新项目
要在 Android Studio 中创建新项目,请参阅如何在 Android Studio 中创建/启动新项目。请注意,选择Kotlin作为编程语言。
第 2 步:项目结构
我们将遵循一些模式来保存我们的文件。根据此项目结构创建文件夹和文件。使用将在本文后面解释。
第 3 步:添加所需的依赖项
导航到Gradle Scripts > build.gradle(Module:app)并在依赖项部分添加以下依赖项。
// Retrofit dependency
implementation ‘com.squareup.retrofit2:retrofit:2.9.0’
// json convertor factory
implementation ‘com.squareup.retrofit2:converter-gson:2.1.0’
// Coroutines(includes kotlin flow)
implementation ‘org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0’
// lifecycle components
implementation ‘androidx.lifecycle:lifecycle-extensions:2.2.0’
implementation “androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1”
implementation “androidx.lifecycle:lifecycle-runtime-ktx:2.3.1”
第 4 步:使用 activity_main.xml
导航到app > res > layout > activity_main.xml并将以下代码添加到该文件中。下面是activity_main.xml文件的代码。
XML
Kotlin
data class CommentModel(
val postId: Int?=null,
val id: Int?=null,
val email: String?=null,
val name:String?=null,
@SerializedName("body")
val comment: String?=null
)
Kotlin
import retrofit2.http.GET
import retrofit2.http.Path
interface ApiService {
// Get method to call the api ,passing id as a path
@GET("/comments/{id}")
suspend fun getComments(@Path("id") id: Int): CommentModel
}
Kotlin
// A helper class to handle states
data class CommentApiState(val status: Status, val data: T?, val message: String?) {
companion object {
// In case of Success,set status as
// Success and data as the response
fun success(data: T?): CommentApiState {
return CommentApiState(Status.SUCCESS, data, null)
}
// In case of failure ,set state to Error ,
// add the error message,set data to null
fun error(msg: String): CommentApiState {
return CommentApiState(Status.ERROR, null, msg)
}
// When the call is loading set the state
// as Loading and rest as null
fun loading(): CommentApiState {
return CommentApiState(Status.LOADING, null, null)
}
}
}
// An enum to store the
// current state of api call
enum class Status {
SUCCESS,
ERROR,
LOADING
}
Kotlin
import com.google.gson.GsonBuilder
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
object AppConfig {
// Base url of the api
private const val BASE_URL = "https://jsonplaceholder.typicode.com/"
// create retrofit service
fun ApiService(): ApiService =
Retrofit.Builder().baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create(GsonBuilder().create()))
.build()
.create(ApiService::class.java)
}
Kotlin
class CommentsRepository(private val apiService: ApiService) {
suspend fun getComment(id: Int): Flow> {
return flow {
// get the comment Data from the api
val comment=apiService.getComments(id)
// Emit this data wrapped in
// the helper class [CommentApiState]
emit(CommentApiState.success(comment))
}.flowOn(Dispatchers.IO)
}
}
Kotlin
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
class CommentsViewModel : ViewModel() {
// Create a Repository and pass the api
// service we created in AppConfig file
private val repository = CommentsRepository(
AppConfig.ApiService()
)
val commentState = MutableStateFlow(
CommentApiState(
Status.LOADING,
CommentModel(), ""
)
)
init {
// Initiate a starting
// search with comment Id 1
getNewComment(1)
}
// Function to get new Comments
fun getNewComment(id: Int) {
// Since Network Calls takes time,Set the
// initial value to loading state
commentState.value = CommentApiState.loading()
// ApiCalls takes some time, So it has to be
// run and background thread. Using viewModelScope
// to call the api
viewModelScope.launch {
// Collecting the data emitted
// by the function in repository
repository.getComment(id)
// If any errors occurs like 404 not found
// or invalid query, set the state to error
// State to show some info
// on screen
.catch {
commentState.value =
CommentApiState.error(it.message.toString())
}
// If Api call is succeeded, set the State to Success
// and set the response data to data received from api
.collect {
commentState.value = CommentApiState.success(it.data)
}
}
}
}
Kotlin
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity() {
// create a CommentsViewModel
// variable to initialize it later
private lateinit var viewModel: CommentsViewModel
// create a view binding variable
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// instantiate view binding
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// initialize viewModel
viewModel = ViewModelProvider(this).get(CommentsViewModel::class.java)
// Listen for the button click event to search
binding.button.setOnClickListener {
// check to prevent api call with no parameters
if (binding.searchEditText.text.isNullOrEmpty()) {
Toast.makeText(this, "Query Can't be empty", Toast.LENGTH_SHORT).show()
} else {
// if Query isn't empty, make the api call
viewModel.getNewComment(binding.searchEditText.text.toString().toInt())
}
}
// Since flow run asynchronously,
// start listening on background thread
lifecycleScope.launch {
viewModel.commentState.collect {
// When state to check the
// state of received data
when (it.status) {
// If its loading state then
// show the progress bar
Status.LOADING -> {
binding.progressBar.isVisible = true
}
// If api call was a success , Update the Ui with
// data and make progress bar invisible
Status.SUCCESS -> {
binding.progressBar.isVisible = false
// Received data can be null, put a check to prevent
// null pointer exception
it.data?.let { comment ->
binding.commentIdTextview.text = comment.id.toString()
binding.nameTextview.text = comment.name
binding.emailTextview.text = comment.email
binding.commentTextview.text = comment.comment
}
}
// In case of error, show some data to user
else -> {
binding.progressBar.isVisible = false
Toast.makeText(this@MainActivity, "${it.message}", Toast.LENGTH_SHORT).show()
}
}
}
}
}
}
它的用户界面如下所示:
第 5 步:使用 API
我们将使用 https://jsonplaceholder.typicode.com/comments API,当 id作为路径传递时,它会提供一些 JSON 数据。例如,https://jsonplaceholder.typicode.com/comments/2 给出包含一些随机数据的 JSON。我们将使用这些数据并使用kotlin flow 将其显示在屏幕上。打开模型 > CommentModel并创建一个模型类来解析从 API 接收到的数据。
我们需要为此响应创建一个数据类。在 CommentModel 中添加以下代码
科特林
data class CommentModel(
val postId: Int?=null,
val id: Int?=null,
val email: String?=null,
val name:String?=null,
@SerializedName("body")
val comment: String?=null
)
创建 API 接口
我们需要创建一个 API 接口来使用改造调用 API。打开network > ApiService并添加以下代码
科特林
import retrofit2.http.GET
import retrofit2.http.Path
interface ApiService {
// Get method to call the api ,passing id as a path
@GET("/comments/{id}")
suspend fun getComments(@Path("id") id: Int): CommentModel
}
让我们添加一些辅助类来处理 API 的加载或错误状态。打开网络 > CommentApiState。有关解释,请参阅代码中的注释。
科特林
// A helper class to handle states
data class CommentApiState(val status: Status, val data: T?, val message: String?) {
companion object {
// In case of Success,set status as
// Success and data as the response
fun success(data: T?): CommentApiState {
return CommentApiState(Status.SUCCESS, data, null)
}
// In case of failure ,set state to Error ,
// add the error message,set data to null
fun error(msg: String): CommentApiState {
return CommentApiState(Status.ERROR, null, msg)
}
// When the call is loading set the state
// as Loading and rest as null
fun loading(): CommentApiState {
return CommentApiState(Status.LOADING, null, null)
}
}
}
// An enum to store the
// current state of api call
enum class Status {
SUCCESS,
ERROR,
LOADING
}
打开utils > AppConfig并添加代码以创建 API 服务,该服务将用于进行 API 调用。
科特林
import com.google.gson.GsonBuilder
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
object AppConfig {
// Base url of the api
private const val BASE_URL = "https://jsonplaceholder.typicode.com/"
// create retrofit service
fun ApiService(): ApiService =
Retrofit.Builder().baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create(GsonBuilder().create()))
.build()
.create(ApiService::class.java)
}
App的API部分已经完成。现在我们需要处理 ViewModel 和存储库。
第 6 步:使用存储库
打开存储库 > CommentsRepository。添加以下代码。请参阅注释以获取解释。
科特林
class CommentsRepository(private val apiService: ApiService) {
suspend fun getComment(id: Int): Flow> {
return flow {
// get the comment Data from the api
val comment=apiService.getComments(id)
// Emit this data wrapped in
// the helper class [CommentApiState]
emit(CommentApiState.success(comment))
}.flowOn(Dispatchers.IO)
}
}
第 7 步:使用 ViewModel
打开ViewModel > CommentViewModel。添加以下代码。请参阅注释以获取解释。
科特林
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
class CommentsViewModel : ViewModel() {
// Create a Repository and pass the api
// service we created in AppConfig file
private val repository = CommentsRepository(
AppConfig.ApiService()
)
val commentState = MutableStateFlow(
CommentApiState(
Status.LOADING,
CommentModel(), ""
)
)
init {
// Initiate a starting
// search with comment Id 1
getNewComment(1)
}
// Function to get new Comments
fun getNewComment(id: Int) {
// Since Network Calls takes time,Set the
// initial value to loading state
commentState.value = CommentApiState.loading()
// ApiCalls takes some time, So it has to be
// run and background thread. Using viewModelScope
// to call the api
viewModelScope.launch {
// Collecting the data emitted
// by the function in repository
repository.getComment(id)
// If any errors occurs like 404 not found
// or invalid query, set the state to error
// State to show some info
// on screen
.catch {
commentState.value =
CommentApiState.error(it.message.toString())
}
// If Api call is succeeded, set the State to Success
// and set the response data to data received from api
.collect {
commentState.value = CommentApiState.success(it.data)
}
}
}
}
我们差不多完成了,我们现在需要从 view(MainActivity) 调用 API 并在屏幕上显示数据。
第 8 步:使用视图 (MainActivity.kt)
打开演示文稿 > MainActivity.kt。添加如下代码,解释见注释。
科特林
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity() {
// create a CommentsViewModel
// variable to initialize it later
private lateinit var viewModel: CommentsViewModel
// create a view binding variable
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// instantiate view binding
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// initialize viewModel
viewModel = ViewModelProvider(this).get(CommentsViewModel::class.java)
// Listen for the button click event to search
binding.button.setOnClickListener {
// check to prevent api call with no parameters
if (binding.searchEditText.text.isNullOrEmpty()) {
Toast.makeText(this, "Query Can't be empty", Toast.LENGTH_SHORT).show()
} else {
// if Query isn't empty, make the api call
viewModel.getNewComment(binding.searchEditText.text.toString().toInt())
}
}
// Since flow run asynchronously,
// start listening on background thread
lifecycleScope.launch {
viewModel.commentState.collect {
// When state to check the
// state of received data
when (it.status) {
// If its loading state then
// show the progress bar
Status.LOADING -> {
binding.progressBar.isVisible = true
}
// If api call was a success , Update the Ui with
// data and make progress bar invisible
Status.SUCCESS -> {
binding.progressBar.isVisible = false
// Received data can be null, put a check to prevent
// null pointer exception
it.data?.let { comment ->
binding.commentIdTextview.text = comment.id.toString()
binding.nameTextview.text = comment.name
binding.emailTextview.text = comment.email
binding.commentTextview.text = comment.comment
}
}
// In case of error, show some data to user
else -> {
binding.progressBar.isVisible = false
Toast.makeText(this@MainActivity, "${it.message}", Toast.LENGTH_SHORT).show()
}
}
}
}
}
}
现在运行应用程序,输入一些数字并点击搜索。输入 1-500 之间的任意数字,将返回成功状态。
输出:
从 GitHub 获取完整的项目。