7

I'm trying to read a list of objects from the database and mapping it to another type of list.

// Returns either a Failure or the expected result
suspend fun getCountries(): Either<Failure, List<CountryItem>> {
    // Get the result from the database
    val result = countryLocalDataSource.getCountries()

    // Left means Failure
    if (result.isLeft) {
        // Retrieve the error from the database
        lateinit var error: Failure
        result.either({
            error = it
        }, {})

        // Return the result
        return Either.Left(error)
    }

    // The database returns a List of Country objects, we need to map it to another object (CountryItem)
    val countryItems: MutableList<CountryItem> = mutableListOf()

    // Iterate the Country List and construct a new List of CountryItems
    result.map { countries -> {
        countries.forEach {
            // Assign some values from an Enum (localized string resources)
            val countryEnumValue = Countries.fromId(it.id)
            countryEnumValue?.let { countryIt ->
                val countryStringNameRes = countryIt.nameStringRes;

                // Create the new CountryItem object (@StringRes value: Int, isSelected: Bool)
                countryItems.add(CountryItem(countryStringNameRes, false))
            }
        }
    } }

    // Because this is a success, return as Right with the newly created List of CountryItems
    return Either.Right(countryItems)
}

For the sake of readability I didn't included the whole Repository or the DAO classes and I have left comments in the code snippet above.

In a nutshell: I'm using Kotlin's Coroutines for accessing the database in a separated thread and I'm handling the response on the UI Thread. Using the Either class in order to return two different results (failure or success).

The above code works, however It's too ugly. Is this the right approach to deliver the result?

What I'm trying to do is to refactor the code above.

The whole problem is caused by the two different object types. The Database Data Source API is returning an Either<Failure, List<Country>>, meanwhile the function is expected to return an Either<Failure, List<CountryItem>>.

I can't deliver a List<CountryItem> directly from the Database Data Source API, because Android Studio doesn't let me compile the project (entities implementing interfaces, compile error, etc.). What I'm trying to achieve is to map the Either result in a nicer way.

Zbarcea Christian
  • 9,367
  • 22
  • 84
  • 137

3 Answers3

4

Try using Kotlin's Result

So in your case you can write something like:

return result.mapCatching { list: List<Country> -> /*return here List<CountryItem>>*/ }

And for checking result call:

result.fold(
    onSuccess = {...},
    onFailure = {...}
)

In order to invoke a constructor you should call Result.success(T) or Result.failure(Throwable)

Unfortunately, you'll also need to suppress use-as-return-type warnings How to

krsnk
  • 267
  • 1
  • 10
2

You can simplify by checking the type of Either and accessing the value directly. In your case:

access Left via result.a -> Failure

access Right via result.b -> List<Country>

ex:

when (result) {
    is Either.Left -> {
        val failure: Failure = result.b
        ...
    }
    is Either.Right -> {
        val countries: List<Country> = result.b
        ...
    }
}

An alternative is to use the either() function (normally this is called fold()):

result.either(
    { /** called when Left; it == Failure */ }, 
    { /** called when Right; it == List<Country> */ }
)
triad
  • 20,407
  • 13
  • 45
  • 50
  • can you clarify on how to replace either class with fold? – A_rmas Apr 20 '20 at 15:51
  • fold is better than using when-is. If you call `result.asRight()` in the `Either.Left block`, or `result.asLeft()` in the `Either.Right` block, you'll throw an exception. You can avoid that possibility by using fold like: `result.fold({ leftValue -> ... }, { rightValue -> ... }) – mithunc Jul 06 '22 at 21:32
1

Assume your Country class is defined as follow:

data class Country(val name: String) {}

and your CountryItem class is defined as follow:

data class CountryItem(private val name: String, private val population: Int) {}

and your CountryLocalDataSource class with a method getCountries() like this:

class DataSource {
    suspend fun getCountries(): Either<Exception, List<Country>> {
        return Either.Right(listOf(Country("USA"), Country("France")))
        //return Either.Left(Exception("Error!!!"))
    }
}

then the answer to your question would be:

suspend fun getCountryItems(): Either<Exception, List<CountryItem>> {        
    when (val countriesOrFail = DataSource().getCountries()) {
        is Either.Left -> {
            return Either.Left(countriesOrFail.a)
        }
        is Either.Right -> {
            val countryItems = countriesOrFail.b.map {
                CountryItem(it.name, 1000)
            }
            return Either.Right(countryItems)
        }        
    }    
}

To call your getCountryItems(), here is an example:

suspend fun main() {
    when (val countriesOrFail = getCountryItems()) {
        is Either.Left -> {
            println(countriesOrFail.a.message)
        }
        is Either.Right -> {
            println(countriesOrFail.b)       
        }
    }
}

Here's the sample code in the playground: https://pl.kotl.in/iiSrkv3QJ

A note about your map function:

I'm guessing you don't actually need the result to be a MutableList<CountryItem> but you had to define so because you want to add an element as you iterate through the input list List<Country>.

Perhaps the following is the case: If you have a List<Country> with 2 elements like in the example, and you want to map so that the result becomes a List<CountryItem> with also 2 corresponding elements, then you don't need to call forEach inside a fun that gets passed to the higher-order function map. But this may be an entirely new question.

Kevin Le - Khnle
  • 10,579
  • 11
  • 54
  • 80