Blog Infos
Author
Published
Topics
Published

Not long ago, I went through a project with a common problem in almost every applications, which is Error View Handling.

Every application has its own scenario, and on this article, I will describe to you the way I have approached mine.

The goal here is to have a way to display a custom error to the user in a Fragment. Seems simple, uh? Well, there are many ways to do it, but not all are scalable and easy to maintain and extend.

I will show you how I have approached this problem, using SOLID, keeping scalability in mind. Props to Uncle Bob!

As always, a sample project is on my GitHub, feel free to check. (Good option if you are lazy! Stay here if you are curious to learn 😁)

As of pre-requisites, you’ll need:

  • Android Studio 4.2.1
  • Kotlin plugin 1.5.0

If you build and run the app, you’ll see this:

So, we want to display a custom error. There may be a lot of different errors, for example:

  • Not Found
  • Internet
  • Server
  • etc

On this specific scenario, the behavior is the following:

There are specific states observed [Observables], and each of these can have one or more listeners [Observers]. Whenever any state changes, the Observers get notified. [Observer Pattern]

One of the detected states is the kind of error that may be happening.

As soon as this Error State changes, the Error Fragment is shown with the respective labels and images. Notice that errors share the same layout and only change the content.

Add the different errors handling logic inside ErrorFragment itself

Use something like Static Factory Method, which creates object instances based on supplied arguments (or even without them) and you can call them from anywhere without the need of having an instance of a class that contains them.

It is usually used in these cases, to re-use the same Fragment, displaying different content.

  • Create an Enum class with the different error types;
  • Pass the error type to a newInstance() function inside companion object;
  • Create the Fragment passing the error type to the arguments bundle;

However… Our goal is to display the error. Views must be dumb, and all the work of knowing what error to display must be done out of them. Their only responsibility is to display something to the user.

If we ended up following any of these approaches, EVERY time we wanted to add a new error, we would need to come back here and change something, which could break something.

This means these approaches are not scalable, and do not respect Open Closed Principle.

The only thing that ErrorFragment needs to know, is that it needs to display an Error. It should not care of what type of error it is. In programming terms, we can represent this using an abstraction.

If we create an Interface representing an Error, we can create as many different Errors as we want. Then, these different errors are aware of their own implementation, and ErrorFragment will be receiving them using the Static Factory Method.

This way, we can achieve the Single Responsibility Principle on ErrorFragment. The only thing that it needs to do, is to display the error.

interface ErrorView : Parcelable {
fun setupView(binding: FragmentErrorBinding)
}
view raw solid_1.kt hosted with ❤ by GitHub

The ErrorView interface, represents an Error to be displayed in ErrorFragment. It needs to extend Parcelable to be passed as a bundle argument to the fragment.

We will be using a specific Kotlin Plugin to avoid the need to implement certain functions due to extend from Parcelable (Parcelize annotation).

Go to your build.gradle (app) file, and add the following line inside the pluginsblock:

id 'kotlin-parcelize'

If you cannot find the plugins block, add instead the following on the top of the same file:

apply plugin: 'kotlin-parcelize'

You should be ok now.

The function setupView() receives FragmentErrorBinding as an argument to be used when building the error view. As mentioned before, in this scenario all errors share the same layout and only the content is changed.

This is the base abstraction that we need, and is what we will provide to ErrorFragment.

I am using ViewBinding

interface ErrorViewWithWarning : ErrorView {
var warning: Snackbar?
fun showWarning(root: View)
fun dismissWarning()
}
view raw solid_2.kt hosted with ❤ by GitHub

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

,

Error Handling Beyond the Standard Library

When building applications it’s easy to focus on the happy path and forget about error scenarios. However, in the real world failures happen, and dealing with them is as important as the happy pah. Kotlin…
Watch Video

Error Handling Beyond the Standard Library

Stojan Anastasov
Android Engineer
FP Enthusiast

Error Handling Beyond the Standard Library

Stojan Anastasov
Android Engineer
FP Enthusiast

Error Handling Beyond the Standard Library

Stojan Anastasov
Android Engineer
FP Enthusiast

Jobs

After having the ErrorView abstraction, we can also start to define different types of error abstractions. For example, there are some errors where a Snackbar can be shown. As long as they implement the main ErrorView abstraction, it is fine.

Let’s have as examples the previously mentioned error types:

  • Not Found
  • Internet
  • Server
TabLayoutMediator(mTabLayout, mViewPager) { tab, position ->
tab.text = it[position].title
}.attach()

As you can see, it’s pretty straightforward to extend our implementation, without changing the ErrorFragment.

We can add new errors, simply by implementing the ErrorView abstraction, or ErrorViewWithWarning.

@Parcelize
class InternetError(private val showWarning: Boolean) : ErrorViewWithWarning {
@IgnoredOnParcel
override var warning: Snackbar? = null
override fun setupView(binding: FragmentErrorBinding) {
binding.apply {
errorTitle.setText(R.string.key_error)
errorDescription.setText(R.string.key_internet_error)
errorImage.setImageResource(R.drawable.ic_internet)
}
if (showWarning) {
showWarning(binding.root)
}
}
override fun showWarning(root: View) {
warning = Snackbar.make(root, "Internet error!", Snackbar.LENGTH_SHORT)
warning?.show()
}
override fun dismissWarning() {
warning?.dismiss()
warning = null
}
}
view raw solid_4.kt hosted with ❤ by GitHub

In this error, we also want to have the choice to display a Snackbar, so we implement ErrorViewWithWarning. The IgnoredOnParcel annotation is used to ignore that variable while passing our error to the ErrorFragment.

@Parcelize
class ServerError : ErrorView {
override fun setupView(binding: FragmentErrorBinding) {
binding.apply {
errorTitle.setText(R.string.key_error)
errorDescription.setText(R.string.key_server_error)
errorImage.setImageResource(R.drawable.ic_server)
}
}
}
view raw solid_5.kt hosted with ❤ by GitHub

I think you already understood how easy it is now to add new errors with their own implementation! :]

class ErrorFragment : Fragment() {
private var _binding: FragmentErrorBinding? = null
private val binding get() = _binding!!
companion object {
private const val ERROR_KEY = "errorKey"
fun newError(error: ErrorView): ErrorFragment {
return ErrorFragment().apply {
arguments = Bundle().apply {
putParcelable(ERROR_KEY, error)
}
}
}
}
private val error: ErrorView? by lazy { arguments?.getParcelable(ERROR_KEY) }
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentErrorBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
error?.setupView(binding)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
view raw solid_6.kt hosted with ❤ by GitHub

Companion Object has the newError() static function [Static Factory Method], which allows to create a new ErrorFragment based on any ErrorView.

The error is obtained from the fragment arguments in the error variable and setupView() is called inside onViewCreated() to handle how it is displayed.

Every time a new error state is dispatched, the state is handled from outside of the ErrorFragment, and then the error type is chosen:

  • ErrorFragment.newError(NotFoundError())
  • ErrorFragment.newError(InternetError(showWarning = false))
  • ErrorFragment.newError(ServerError())

Let’s walk through the principles, one by one, and inspect they are respected with this implementation:

Gather together the things that change for the same reasons. Separate things that change for different reasons.

ErrorFragment has a Single Responsibility, which is to display the error.

Different Errors implementations may change, so they must be kept separated from each other and from ErrorFragment.

A Module should be open for extension but closed for modification.

Our Error handling is open for extension, since we can keep adding different kinds of errors, and is closed for modification, because we will never have to touch ErrorFragment again when adding new error types.

A program that uses an interface must not be confused by an implementation of that interface.

ErrorFragment can receive both ErrorView and ErrorViewWithWarning abstractions, it does not matter.

Keep interfaces small so that users don’t end up depending on things they don’t need.

If an error doesn’t need to show a warning, it can implement only ErrorView, so it wont depend on things it doesn’t need.

Depend in the direction of abstraction. High level modules should not depend upon low level details.

ErrorFragment depends on an ErrorView abstraction, instead of depending in concrete implementations.

If you want to check the code in a more cleaner way, feel free to check my GitHub. There is an usage example with Jetpack Navigation.

If you want to extend your knowledge on Jetpack Compose, check the following resources:

If you enjoyed it, and if this helped you, you can also consider paying me a coffee. :]

Thank you!

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
These last weeks, I had the chance to think about the best error handling…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu