Kotlin and Android: Beyond the Basics with Sealed Classes [FREE]

栏目: IT技术 · 发布时间: 4年前

内容简介:Managing UI state is one of the most important things you can do as an app developer. Done wrong, state management causes problems like poor error handling, hard-to-read code and a lack of separation between UI and business logic.With Kotlin,In this tutori

Managing UI state is one of the most important things you can do as an app developer. Done wrong, state management causes problems like poor error handling, hard-to-read code and a lack of separation between UI and business logic.

With Kotlin, sealed classes make it simpler to manage state.

In this tutorial, you’ll learn the advantages of Kotlin sealed classes and how to leverage them to manage states in your Android apps.

You’ll do this by building the RickyCharacters app, which displays a list of characters from the television show. You’ll download the character data from The Rick And Morty API and manage states in the app by using the Retrofit library to make network calls.

Note: This tutorial assumes you have experience with developing for Android in Kotlin and know the basics of sealed classes.

If you’re unfamiliar with Kotlin, take a look at our Kotlin For Android: An Introduction tutorial.

To brush up on your Android skills, check out our Android and Kotlin tutorials .

You can also learn more about sealed classes in our Kotlin Sealed Classes tutorial.

Getting Started

Download the begin project by clicking the Download Materials button at the top or bottom of the tutorial.

Extract the zip file and open the begin project in Android Studio by selecting Open an existing Android Studio project from the welcome screen. Navigate to the begin directory and click Open .

Once the Gradle sync finishes, build and run the project using the emulator or a device. You’ll see this screen appear:

Kotlin and Android: Beyond the Basics with Sealed Classes [FREE]

You probably expected some Rick and Morty Characters on the home screen, but that feature isn’t ready yet. You’ll add the logic to fetch the character images later in this tutorial.

Advantages of Sealed Classes

Sealed classes are a more powerful extension of enums. Here are some of their most powerful features.

Multiple Instances

While an enum constant exists only as a single instance, a subclass of a sealed class can have multiple instances. That allows objects from sealed classes to contain state.

Look at the following example:

sealed class Months {
    class January(var shortHand: String) : Months()
    class February(var number: Int) : Months()
    class March(var shortHand: String, var number: Int) : Months()
}

Now you can create two different instances of February . For example, you can pass the 2019 as an argument to first instance, and 2020 to second instance, and compare them.

Inheritance

You can’t inherit from enum classes, but you can from sealed classes.

Here’s an example:

sealed class Result {
    data class Success(val data : List<String>) : Result()
    data class Failure(val exception : String) : Result()
}

Both Success and Failure inherit from the Result sealed class in the code above.

Kotlin 1.1 added the possibility for data classes to extend other classes, including sealed classes.

Architecture Compatibility

Sealed classes are compatible with commonly-used app architectures, including:

  • MVI
  • MVVM
  • Redux
  • Repository pattern

This ensures that you don’t have to change your existing app architecture to leverage the advantages of sealed classes.

“When” Expressions

Kotlin lets you use when expressions with your sealed classes. When you use these with the Result sealed class, you can parse a result based on whether it was a success or failure.

Here’s how this might look:

when (result) {
  is Result.Success -> showItems(result.data)
  is Result.Failure -> showError(result.exception)
}

This has a few advantages. First, you can check the type of result using Kotlin’s is operator. By checking the type, Kotlin can smart-cast the value of result for you for each case.

If the result is a success, you can access the value as if it’s Result.Success . Now, you can pass items without any typecasting to showItems(data: List ) .

If the result was a failure, you display an error to the user.

Another way you can use the when expression is to exhaust all the possibilities for the Result sealed class type.

Typically, a when expression must have an else clause. In the example above, however, there are no other possible types for Result . The compiler and IDE know you don’t need an else clause.

Look what happens when you add an InvalidData object to the sealed class:

sealed class Result {
    data class Success(val data : List<String>) : Result()
    data class Failure(val exception : String) : Result()
    object InvalidData : Result()
}

The IDE now shows an error on the when statement because you haven’t handled the InvalidData branch of Result .

Kotlin and Android: Beyond the Basics with Sealed Classes [FREE]

The IDE knows you didn’t cover all your cases here. It even knows which possible types result could be, depending on the Result sealed class. The IDE offers you a quick fix to add the missing branches.

Kotlin and Android: Beyond the Basics with Sealed Classes [FREE]

Note : You only make a when expression exhaustive if you use it as an expression, you use its return type or you call a function on it.

Managing State in Android

Fasten your seat belt, as you’re about to travel back in time to a multiverse where sealed classes did not exist. Get ready to see how Rick and Morty survived back then.

Classical State Management

Your goal is to fetch a list of characters for the “Rick and Morty” television show from The Rick And Morty API and display them in a Recycler view.

Open CharactersFragment.kt in com.raywenderlich.android.rickycharacters.ui.views.fragments.classicalway and you’ll see the following code:

class CharactersFragment : Fragment(R.layout.fragment_characters) {
  // 1
  private val apiService = ApiClient().getClient().create(ApiService::class.java)
  private lateinit var charactersAdapter: CharactersAdapter

  override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    // 2
    charactersAdapter = CharactersAdapter {character ->
      displayCharacterDetails(character)
    }
    recyclerViewMovies.adapter = charactersAdapter
    fetchCharacters()
    swipeContainer.setOnRefreshListener {
      fetchCharacters()
    }
  }

  // 3
  private fun displayCharacterDetails(character: Character) {
    val characterFragmentAction =
        CharactersFragmentDirections.actionCharactersFragmentToCharacterDetailsFragment(
            character)
    findNavController().navigate(characterFragmentAction)
  }

To explain the code above:

  1. You have two variables at the very top. One represents the retrofit service class and the other represents the recyclerview adapter class.
  2. Inside onViewCreated , you initialize the adapter class and set the adapter for the Recycler view, which displays the list of characters. There’s a call to fetchCharacters and you also set a refresh listener to the SwipeRefeshLayout that calls the fetchCharacters on refresh.
  3. displayCharacterDetails(character: Character) is responsible for navigating to the CharacterDetailsFragment and showing character details once you tap on each character.

Below displayCharactersDetails(character: Characters) , there’s more code responsible for requesting the characters and displaying the appropriate UI.

// 1
private fun fetchCharacters() {
    //TODO 1 Make a get characters Request

    //TODO 2 Catch errors with else statement

    //TODO 3 Catch errors with try-catch statement

    //TODO 4 Catch HTTP error codes

    //TODO 5 Add refresh dialog

    //TODO 6 Handle null response body
  }

  // 2
  private fun showCharacters(charactersResponseModel: CharactersResponseModel?) {
    charactersAdapter.updateList(charactersResponseModel!!.results)
  }

  // 3
  private fun handleError(message : String) {
    errorMessageText.text = message
  }
 
  // 4
  private fun showEmptyView() {
    emptyViewLinear.show()
    recyclerViewMovies.hide()
    hideRefreshDialog()
  }

  // 5
  private fun hideEmptyView() {
    emptyViewLinear.hide()
    recyclerViewMovies.show()
    hideRefreshDialog()
  }

  // 6
  private fun showRefreshDialog() {
    swipeContainer.isRefreshing = true
  }
  
  // 7
  private fun hideRefreshDialog() {
    swipeContainer.isRefreshing = false
  }

Here’s an explanation for the code above:

  1. fetchCharacters() is responsible for fetching the characters from the api and handling the response. There’s a couple of //TODOs here, which you’ll address one by one in this tutorial.
  2. showCharacters(charactersResponseModel: CharactersResponseModel?) takes CharacterResponseModel as an argument. This is a data class representing the response from the Rick and Morty API. The function sends the list of characters to the CharactersAdapter .
  3. handleError(message : String) takes a String as an argument and sets the message to a TextView .
  4. showEmptyView() hides the Recycler view and shows the empty view. This is a linear layout with an image and an error text view for displaying the error message. Notice recyclerViewMovies.hide() uses an extension function from com.raywenderlich.android.rickycharacters.utils.extensions.kt . This is also where you find the show() extension function.
  5. hideEmptyView() hides the empty view and shows the Recycler view.
  6. showRefreshDialog() sets the refresh property of SwipeRefreshLayout to true .
  7. hideRefreshDialog sets the refresh property of SwipeRefreshLayout to false .

With the code in this class explained, we’re ready to begin coding. In the next section, you’ll add begin to request characters from the Rick and Morty API. Time to get schwifty!

Requesting the Characters

Start by replacing the first TODO in fetchCharacters() with the following:

lifecycleScope.launchWhenStarted {
  val response = apiService.getCharacters()
  val charactersResponseModel = response.body()
  if (response.isSuccessful){
    hideEmptyView()
    showCharacters(charactersResponseModel)
  }
}

Note: lifecycleScope throws an error when added, because it needs an imported class to work To fix this, just import the missing class.

This function has a couple of important components:

  • First, there’s lifecycleScope.launchWhenStarted{} , a CoroutineScope from the architecture components that’s lifecycle-aware. It launches a coroutine to perform operations on the background thread that let you make a network call using Retrofit. The project has the Retrofit part already set up for you, along with all its required classes and interfaces.
  • Inside the lifecycleScope , you make the network call. You use response , which calls getCharacters() from Retrofit’s ApiService class to get a list of characters charactersResponseModel derives its value from the response body of the network call.
  • Finally, you check if the response is successful and call showCharacters(charactersResponseModel) .

Note: Take a look at the Coroutines documentation for more info about Coroutines.

Build and run and you’ll see the list of Rick and Morty characters:

Kotlin and Android: Beyond the Basics with Sealed Classes [FREE]

Hurray, the app runs as expected. But there’s a problem: With this kind of approach, this is what happens when an error occurs:

Kotlin and Android: Beyond the Basics with Sealed Classes [FREE]

In classic state management, errors completely crash your app.

Addressing Errors

Your next thought might be to catch all errors with an else statement.

To try this, add the following catchall else statement for //TODO 2 :

else {
 handleError("An error occurred")
}

To reproduce an error, navigate to data/network/ApiService.kt and make the following change to the @GET call:

@GET("/api/character/rrr")

Notice the addition at the end of the path. Your app won’t like that.

Now, build and run and the app will show errors:

Kotlin and Android: Beyond the Basics with Sealed Classes [FREE]

Though you might think that else has rescued you, a closer look reveals some errors that else doesn’t handle – and they crash the app.

The errors that you have not yet handled are HTTP errors, network exceptions and null responses from the API.

Your next step will be to handle these kinds of errors.

Using a Try-Catch Statement to Catch Errors

If else isn’t robust enough to catch all the errors, maybe a try-catch statement might do the trick? You’ll try that next.

For //TODO 3 , modify the entire code in fetchCharacters with a try-catch . The code in the method should look as follows:

lifecycleScope.launchWhenStarted {
    try {
      val response = apiService.getCharacters()
      val charactersResponseModel = response.body()
      if (response.isSuccessful) {
        hideEmptyView()
        showCharacters(charactersResponseModel)
      } else {
        handleError("An error occurred")       
      }
      
    } catch (error: IOException) {
      showEmptyView()
      handleError(error.message!!)
    }
  }

Note: IOException throws an error when added, just import the missing class to fix this.

Here, the try-catch makes sure the app no longer crashes. It displays an error instead.

Let’s try this out. Make sure your device has no internet connection, then build and run the app. Swipe down to begin requesting characters, shortly, you’ll see the following:

Kotlin and Android: Beyond the Basics with Sealed Classes [FREE]

The app no longer crashes when there’s no internet connection. Instead, it displays the Unable to resolve host error message.

However, you have only addressed one type of error, making it hard to know what’s gone wrong, especially for the case of HTTP errors. You’ll address that problem in the next section.

Handling HTTP Errors

Continue testing your states and and you’ll realize that there are several different HTTP errors that help you know what the problem is.

With the current approach, you won’t see what’s causing the problem because you’ll only get the generic error message: “An error occurred”.

Kotlin and Android: Beyond the Basics with Sealed Classes [FREE]

To catch more of the HTTP errors, replace the code within the else branch in fetchCharacters with this:

showEmptyView()
 when(response.code()) {
   403 -> handleError("Access to resource is forbidden")
   404 -> handleError("Resource not found")
   500 -> handleError("Internal server error")
   502 -> handleError("Bad Gateway")
   301 -> handleError("Resource has been removed permanently")
   302 -> handleError("Resource moved, but has been found")
   else -> handleError("All cases have not been covered!!")
 }

The code block has a when statement, which takes response.code() from the network call as a parameter. It has some specific cases – 403, 404, 500, 502, 301 and 302 – plus the default else , in case the code isn’t specified. For each case, you handle the error with the appropriate message for each HTTP code.

Navigate to data/network/ApiService.kt again and change the end of the @GET call:

@GET("/api/character/rrr")

Notice the addition at the end of the path. Again, your app won’t like that.

Build and run and you’ll see an Internal server error message instead of the generic error message:

Kotlin and Android: Beyond the Basics with Sealed Classes [FREE]

Congratulations, you can now catch HTTP errors and show the user what exactly went wrong instead of a generic message.

Now that you’ve handled your error message nicely, your next step will be to make the download experience more transparent to the user.

Note : Make sure you go back and undo the change on ApiService.kt to remove the addition that causes errors.

Indicating Download Progress

It’s not a great experience to click on an option to download data and get no response until the network call finishes. To make your app more user-friendly, your next step is to show a progress dialog so that users can know that the app is fetching data.

You’ll now address //TODO 5 by adding showRefreshDialog() below fetchCharacters() inside OnViewCreated . Your code should look like the following:

lifecycleScope.launchWhenStarted {
    try {
      val response = apiService.getCharacters()
      val charactersResponseModel = response.body()
      if (response.isSuccessful) {
        hideEmptyView()
        showCharacters(charactersResponseModel)
      } else {
        showEmptyView()
        when(response.code()) {
          403 -> handleError("Access to resource is forbidden")
          404 -> handleError("Resource not found")
          500 -> handleError("Internal server error")
          502 -> handleError("Bad Gateway")
          301 -> handleError("Resource has been removed permanently")
          302 -> handleError("Resource moved, but has been found")
          else -> handleError("All cases have not been covered!!")
        }
      }
    } catch (error: IOException) {
      showEmptyView()
      handleError(error.message!!)
    }
  }

  showRefreshDialog()

Build and run to see the loading icon.

Kotlin and Android: Beyond the Basics with Sealed Classes [FREE]

You’re almost done, but you have one more case to handle before your app is ready to go.

Handling Null Responses

As you finish managing state the classic way, you need to handle what happens when the success response is null. Handling this case is your //TODO 6 .

To address this, add a null check on the on successful response by replacing if (response.isSuccessful) {} with this:

if (response.body() != null) {
  hideEmptyView()
  showCharacters(charactersResponseModel)
} else {
  showEmptyView()
  handleError("No characters found")
}

In the code above, you check if response.body() is null . If it’s not, hide the empty view and show the characters. If it is, you handle the error with a “No characters found” message.

The final results looks like this:

private fun fetchCharacters() {
  lifecycleScope.launchWhenStarted {
    try {
      val response = apiService.getCharacters()
      val charactersResponseModel = response.body()
      if (response.isSuccessful) {
        if (response.body() != null) {
          hideEmptyView()
          showCharacters(charactersResponseModel)
        } else {
          showEmptyView()
          handleError("No characters found")
        }

      } else {
        showEmptyView()
        when (response.code()) {
          403 -> handleError("Access to resource is forbidden")
          404 -> handleError("Resource not found")
          500 -> handleError("Internal server error")
          502 -> handleError("Bad Gateway")
          301 -> handleError("Resource has been removed permanently")
          302 -> handleError("Resource moved, but has been found")
          else -> handleError("All cases have not been covered!!")
        }
      }
    } catch (error : IOException) {
      showEmptyView()
      handleError(error.message!!)
    }
  }

Problems With Classical State Management

As you went implementing state using the classical approach, you may have noticed some problems with it. Including:

fetchCharacters

Luckily, Kotlin gives you a better way to handle states.

Simplifying States With Sealed Classes

Sealed classes can eliminate the problems associated with the old way of managing state.

Modeling States With Sealed Classes

With sealed classes, you think about all the possible states before you start to code. You then keep the end result in mind when you start to code.

In this example, you need to follow these states:

  • Success with a list of characters.
  • Invalid data – no characters found.
  • Generic error state.
  • Network exceptions – errors caused by network failure.
  • HTTP errors that represent HTTP error codes. There can be more than one.

So with these states in mind, navigate to data ▸ states . Right-click and create a new Kotlin class named NetworkState . Then add the following code:

sealed class NetworkState {
  data class Success(val data : CharactersResponseModel) : NetworkState()
  object InvalidData : NetworkState()
  data class Error(val error : String) : NetworkState()
  data class NetworkException(val error : String) : NetworkState()
  sealed class HttpErrors : NetworkState() {
    data class ResourceForbidden(val exception: String) : HttpErrors()
    data class ResourceNotFound(val exception: String) : HttpErrors()
    data class InternalServerError(val exception: String) : HttpErrors()
    data class BadGateWay(val exception: String) : HttpErrors()
    data class ResourceRemoved(val exception: String) : HttpErrors()
    data class RemovedResourceFound(val exception: String) : HttpErrors()
  }
}

The types in the sealed class are data classes, objects and another sealed class representing the HTTP error states.

Now that you have all the states ready, you’re ready to apply the states to your network call.

Applying the States to Your App

Navigate to ui ▸ views ▸ fragments ▸ sealedclassway . Open StateCharactersFragment.kt Replace getCharacters with the following:

private fun getCharacters() {
  lifecycleScope.launchWhenStarted {
    showRefreshDialog()
    val charactersResult = fetchCharacters()
    handleCharactersResult(charactersResult)
  }
}

Here you’re setting up the Fragment to show it’s in a loading state, via showRefreshDialog , whilst the app makes a network request through fetchCharacters() . You handle the result of the request through handleCharactersResult .

Next, you need to write the fetchCharacters method to request the characters from the API. Update the fetchCharacters like so:

private suspend fun fetchCharacters() : NetworkState {
    return try {
      val response = apiService.getCharacters()
      if (response.isSuccessful) {
        if (response != null) {
          NetworkState.Success(response.body()!!)
        } else {
          NetworkState.InvalidData
        }
      } else {
        when(response.code()) {
          403 -> NetworkState.HttpErrors.ResourceForbidden(response.message())
          404 -> NetworkState.HttpErrors.ResourceNotFound(response.message())
          500 -> NetworkState.HttpErrors.InternalServerError(response.message())
          502 -> NetworkState.HttpErrors.BadGateWay(response.message())
          301 -> NetworkState.HttpErrors.ResourceRemoved(response.message())
          302 -> NetworkState.HttpErrors.RemovedResourceFound(response.message())
          else -> NetworkState.Error(response.message())
        }
      }

    } catch (error : IOException) {
      NetworkState.NetworkException(error.message!!)
    }
}

After adding this, make sure to import the NetworkState and IOException classes.

There are a few differences here from the classical approach. The function has a suspend keyword, which means it can pause and resume later when you have a response from the server.

All the condition checks are the same as before, but now instead of handling the results, you are assigning variables to NetworkState depending on the response.

For a success response, you check if the response is null or not. If it’s not, you set the state as NetworkState.Success(response.body()!!) .

Notice the success state takes the response body and sets the state to NetworkState.InvalidData if its null . If the response is not successful, you handle the error along with the HTTP errors.

For HTTP errors, you set the state to NetworkState.HttpErrors , depending on the error code. For normal errors, you set the state to NetworkState.Error(response.message()) . In the catch block, you set the state to NetworkState.NetworkException(error.message!!) .

Notice all these states have variables that can have more than one value, which is one of the advantages of sealed states.

Also, this function only deals with fetching data and updating the states, it contains no business logic or UI logic. This helps the method be focused on doing one thing and makes the code much more readable.

To display the results, add the following function to the fragment just below fetchCharacters() :

private fun handleCharactersResult(networkState: NetworkState) {
    return when(networkState) {
      is NetworkState.Success -> showCharacters(networkState.data)
      is NetworkState.HttpErrors.ResourceForbidden -> handleError(networkState.exception)
      is NetworkState.HttpErrors.ResourceNotFound -> handleError(networkState.exception)
      is NetworkState.HttpErrors.InternalServerError -> handleError(networkState.exception)
      is NetworkState.HttpErrors.BadGateWay -> handleError(networkState.exception)
      is NetworkState.HttpErrors.ResourceRemoved -> handleError(networkState.exception)
      is NetworkState.HttpErrors.RemovedResourceFound -> handleError(networkState.exception)
      is NetworkState.InvalidData -> showEmptyView()
      is NetworkState.Error -> handleError(networkState.error)
      is NetworkState.NetworkException -> handleError(networkState.error)
    }
}

This function takes NetworkState as an argument and uses a when expression as a return statement to give the exhaustive advantage. Since each state exists independent of the others, you handle all the possible states.

Displaying the Character Details

Now you’ve converted the network call code to use sealed classes, let’s hook up the UI. Since fetchCharacters is a suspend function call, we’ll need to wrap calls to it in a coroutine. In getCharacters wrap the calls to getCharacters() in lifecycle scope coroutine like so:

lifecycleScope.launchWhenStarted {
  hideEmptyView()
  showRefreshDialog()
  val charactersResult = getCharacters()
  handleCharactersResult(charactersResult)
}

Also in onViewCreated , add a lifecycleScope within the refresh listener:

swipeContainer.setOnRefreshListener {
  lifecycleScope.launchWhenStarted {
    getCharacters()
  }
}

Also add the lifecycle import when prompted.

Here, you call showRefreshDialog() to show the refresh dialog whilst the app fetches the characters from the api. characterResult calls getCharacters() to make a network request to fetch the characters, then the next line calls handleCharactersResult(charactersResult) with the characterResult variable.

Finally, we need to hook up our UI to use the new Fragment, as it’ll continue to use CharactersFragment instead of our shiny new StateCharactersFragment . Navigate to res ▸ navigation and open main_nav_graph.xml .

Kotlin and Android: Beyond the Basics with Sealed Classes [FREE]

Choose the Text tab and change startDestination at the navigation tags with stateCharactersFragment .

<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main_nav_graph"
    app:startDestination="@id/stateCharactersFragment">

Now, build and run.

Behold, the app runs well, all states properly managed. The character list displays properly:

Kotlin and Android: Beyond the Basics with Sealed Classes [FREE]

The character detail screen works, as well:

Kotlin and Android: Beyond the Basics with Sealed Classes [FREE]

Congratulations! You’ve completed your app. In the process, you learned about the features and advantages of sealed classes and how to use them in Android to tame states.

Where to Go From Here?

You can download the begin and end projects by using the Download Materials button at the top or bottom of the tutorial.

There’s much you can do with sealed classes in state management. If you want to dive deeper into the subject, you can read our Kotlin Sealed Classes tutorial.

This tutorial touched on several other concepts that were outside the scope of this article.

To learn more about coroutines in Kotlin check out our Kotlin Coroutines Tutorial for Android: Getting Started tutorial.

You can also take a deeper look at how to create clean architecture for your apps in our Clean Architecture for Android tutorial.

We hope you enjoyed this Beyond the Basics with Sealed Classes tutorial. If you have any questions, comments or awesome modifications to this project app, please join the forum discussion below.


以上所述就是小编给大家介绍的《Kotlin and Android: Beyond the Basics with Sealed Classes [FREE]》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Mashups Web 2.0开发技术—— 基于Amazon.com

Mashups Web 2.0开发技术—— 基于Amazon.com

萨拉汉 / 吴宏泉 / 清华大学 / 2008-1 / 48.00元

《MashupsWeb2.0开发技术(基于Amazon.Com) 》介绍了mashup的底层技术,并且第一次展示了如何创建mashup的应用程序。Amazon.com与Web服务强势结合,拓展了Internet的应用范围,使得开发人员可以把Amazon的数据和其他的可利用资源自由地结合起来创建功能丰富的全新应用程序,这种应用程序叫做mashup。 《MashupsWeb2.0开发技术(基于A......一起来看看 《Mashups Web 2.0开发技术—— 基于Amazon.com》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

随机密码生成器
随机密码生成器

多种字符组合密码

SHA 加密
SHA 加密

SHA 加密工具