Classe selada Kotlin para sucesso e tratamento de erros

Tempo de leitura: 3 minutes

Manipular erros nunca foi fácil para a linguagem de programação Java, pois a falta de suporte do compilador Java.

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.