📜  Android 中的 Kotlin Flow 示例

📅  最后修改于: 2022-05-13 01:55:01.900000             🧑  作者: Mango

Android 中的 Kotlin Flow 示例

Kotlin Flow 是 Kotlin 协程的最新成员之一。使用 Kotlin Flow,我们可以异步处理顺序执行的数据流。

我们将在本文中构建什么?

我们将构建一个简单的应用程序,从 API 获取一些数据并将其显示在屏幕上。这是一个演示 Kotlin 流程工作的简单应用程序。它将使用 MVVM 架构。

先决条件:

  1. 对安卓有很好的了解
  2. Kotlin 的知识
  3. MVVM 架构基础
  4. 改造库基础
  5. Kotlin 协程的基础知识
  6. 视图绑定基础

分步实施

第 1 步:创建一个新项目

要在 Android Studio 中创建新项目,请参阅如何在 Android Studio 中创建/启动新项目。请注意,选择Kotlin作为编程语言。



第 2 步:项目结构

我们将遵循一些模式来保存我们的文件。根据此项目结构创建文件夹和文件。使用将在本文后面解释。

项目结构

第 3 步:添加所需的依赖项

导航到Gradle Scripts > build.gradle(Module:app)并在依赖项部分添加以下依赖项。

第 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 获取完整的项目。

想要一个更快节奏和更具竞争力的环境来学习 Android 的基础知识吗?
单击此处前往由我们的专家精心策划的指南,旨在让您立即做好行业准备!