Classe selada Kotlin para sucesso e tratamento de erros
Manipular erros nunca foi fácil para a linguagem de programação Java, pois a falta de suporte do compilador Java.
Conteudo
Um caso de uso bem conhecido
A classe selada não é nova em outras linguagens de programação, como C # e Scala. A classe lacrada fornece uma maneira de organizar nosso código para parecer muito mais agradável e fácil de trabalhar.
@WorkerThread suspend fun fetchPosts() { val response = httpClient.posts() withContext(Dispatchers.Main) { when (response) { is ResultOf.Success -> { // objeto de desestruturação de dados val (posts) = response // atualiza a vista adapter.submitList(posts) } is ResultOf.Failure -> { val (message, throwable) = response; handlePostsFailure(messsage, throwable) // logar mensagem de erro para logcat Timber.e(throwable, message) } } } }
Como você pode ver no exemplo acima, não há necessidade de lançar objetos manualmente. O compilador Kotlin sabe que tipo de dados deve ser.
Classes seladas funcionam bem com LiveData e ViewModels
Muitas vezes, usamos o LiveData para armazenar dados de APIs ou bancos de dados, no entanto, os dados dessas operações de E / S podem causar falha, portanto, a melhor maneira de lidar com esses erros e respostas bem-sucedidas é usar classes seladas.
class PostViewModel: ViewModel() { // Serviço de retrofit private val postService: PostService = HttpClient.get().postService() // para uso interno private val _posts = MutableLiveData<ResultOf<Post>>() // Exponha ao mundo exterior val posts: LiveData<ResultOf<Post>> = _posts @UiThread fun fetchPostsFromApi() { viewModelScope.launch(Dispatchers.IO) { try { val response = postService.getAllPosts() _posts.postValue(ResultOf.Success(response.data)) } catch (ioe: IOException) { _posts.postValue(ResultOf.Failure("[IO] error please retry", ioe)) } catch (he: HttpException) { _posts.postValue(ResultOf.Failure("[HTTP] error please retry", he)) } } } }
Depois disso, podemos finalmente observá-lo em nossas Atividades ou Fragmentos.
class PostListFragment: Fragment() { // `viewModels` é uma extensão do fragment-ktx private val viewModel: PostViewModel by viewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { // apenas um ListAdapter normal de RecyclerView val adapter = PostListAdapter() // defina o adaptador para a vista binding.recyclerView.adapter = adapter // observar o resultado do ViewModel viewModel.posts.observe(viewLifecycleOwner, Observer { result -> when (result) { // não há necessidade de conversão de tipo, o compilador já sabe que is ResultOf.Success -> { adapter.submitList(result.value) } // aqui também is ResultOf.Failure -> { showErrorMessage(result.message ?: "Unknown error message") } } }) viewModel.fetchPostsFromApi() } }
Analise esses códigos de amostra, podemos estar pensando que é a melhor maneira de trabalhar, mas há mais que podemos fazer para melhorá-los usando funções de extensão Kotlin.
Livre-se da palavra-chave `when`
‘when’ em Kotlin é ótimo para verificar as condições, mas parece enfadonho quando o usamos repetidamente para nosso objeto ResultOf. Portanto, a maneira de corrigir isso é criar funções de extensão.
inline fun <reified T> ResultOf<T>.doIfFailure(callback: (error: String?, throwable: Throwable?) -> Unit) { if (this is ResultOf.Failure) { callback(message, throwable) } } inline fun <reified T> ResultOf<T>.doIfSuccess(callback: (value: T) -> Unit) { if (this is ResultOf.Success) { callback(value) } }
E aqui nosso novo resultado
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { viewModel.posts.observe(viewLifecycleOwner, Observer {result -> result.doIfSuccess {items -> adapter.submit(items) } result.doIfFailure {message, throwable -> showErrorMessage(message ?: "Mensagem de erro desconhecida") } }) }
doIfSuccess e doIfFailure são funções embutidas, portanto, não há sobrecarga de tempo de execução. Eles serão removidos durante a compilação no bytecode Java.
Transformações `ResultOf`
Atualmente, o ResultOf não suporta o mapeamento de seu valor em outra coisa. Por exemplo, queremos obter a primeira postagem da lista de postagens no objeto ResultOf. Então, vamos escrever outra função de extensão para isso.
inline fun <reified T, reified R> ResultOf<T>.map(transform: (T) -> R): ResultOf<R> { return when (this) { is ResultOf.Success -> ResultOf.Success(transform(value)) is ResultOf.Failure -> this } }
Ao criar essa função de extensão, podemos finalmente mapear o estado de sucesso em outra coisa que preferirmos.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { viewModel.posts.observe(viewLifecycleOwner, Observer {result -> result.map { it.posts.first() }.doIfSuccess { post -> addToSomewhere(post) } }) }
Valor padrão quando ocorre falha
Às vezes, queremos garantir que sempre obteremos um resultado bem-sucedido, mesmo que haja uma exceção. Para fazer isso, precisamos fornecer um valor padrão.
inline fun <T> ResultOf<T>.withDefault(value: () -> T): ResultOf.Success<T> { return when (this) { is ResultOf.Success -> this is ResultOf.Failure -> ResultOf.Success(value()) } }
Agora podemos especificar nosso valor padrão.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { viewModel.posts.observe(viewLifecycleOwner, Observer {result -> val (items) = result.withDefault { postsFromDb() } adapter.submitList(items) }) }
Ao definir o valor padrão, temos apenas um estado de sucesso para lidar.
Conclusão
A classe selada Kotlin é poderosa para lidar com diferentes estados de chamadas de E/S, mas este é apenas um caso de uso para ela. Existem muitos outros casos de uso que podemos usar para melhorar a legibilidade e a consistência de nossos códigos.