From c232199492dcb8c286d0667e2beb586f05e3bb2c Mon Sep 17 00:00:00 2001 From: Hernand Azevedo Date: Sat, 9 Feb 2019 15:38:20 -0200 Subject: [PATCH] Adds Either class for livedata handling (#14) * :tada: Initial structure for the Either refactor * :recycle: Adds Either class for livedata handling --- .../com/hernandazevedo/moviedb/data/Logger.kt | 4 ++ .../moviedb/view/base/Either.kt | 45 +++++++++++++++++++ .../moviedb/view/base/Resource.kt | 44 ------------------ .../moviedb/view/details/DetailsActivity.kt | 36 ++++++++------- .../moviedb/view/details/DetailsViewModel.kt | 18 ++++---- .../moviedb/view/main/MainActivity.kt | 28 +++++------- .../moviedb/view/main/MainViewModel.kt | 16 +++---- .../moviedb/view/main/MainViewModelTest.kt | 6 +-- 8 files changed, 101 insertions(+), 96 deletions(-) create mode 100644 presentation/src/main/kotlin/com/hernandazevedo/moviedb/view/base/Either.kt delete mode 100644 presentation/src/main/kotlin/com/hernandazevedo/moviedb/view/base/Resource.kt diff --git a/data/src/main/kotlin/com/hernandazevedo/moviedb/data/Logger.kt b/data/src/main/kotlin/com/hernandazevedo/moviedb/data/Logger.kt index fc715af..a651b5e 100644 --- a/data/src/main/kotlin/com/hernandazevedo/moviedb/data/Logger.kt +++ b/data/src/main/kotlin/com/hernandazevedo/moviedb/data/Logger.kt @@ -18,6 +18,10 @@ object Logger { Timber.tag(UNIQUE_TAG).e(e) } + fun e( e: Throwable, msg: String) { + Timber.tag(UNIQUE_TAG).e(e, msg) + } + fun init() { if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) } diff --git a/presentation/src/main/kotlin/com/hernandazevedo/moviedb/view/base/Either.kt b/presentation/src/main/kotlin/com/hernandazevedo/moviedb/view/base/Either.kt new file mode 100644 index 0000000..0c8298d --- /dev/null +++ b/presentation/src/main/kotlin/com/hernandazevedo/moviedb/view/base/Either.kt @@ -0,0 +1,45 @@ +package com.hernandazevedo.moviedb.view.base + +/** + * Based on https://medium.com/@lupajz/you-either-love-it-or-you-havent-used-it-yet-a55f9b866dbe + */ +sealed class Either { + data class Left(val error: E) : Either() + data class Right(val value: V) : Either() + + fun right(value: V): Either = Either.Right(value) + fun left(value: E): Either = Either.Left(value) + + fun either(action: () -> V): Either = + try { right(action()) } catch (e: Exception) { left(e) } +} + +inline infix fun Either + .map(f: (V) -> V2): Either = when (this) { + is Either.Left -> this + is Either.Right -> Either.Right(f(this.value)) +} + +infix fun Either V2> + .apply(f: Either): Either = when (this) { + is Either.Left -> this + is Either.Right -> f.map(this.value) +} + +inline infix fun Either + .flatMap(f: (V) -> Either): Either = when (this) { + is Either.Left -> this + is Either.Right -> f(value) +} + +inline infix fun Either + .mapError(f: (E) -> E2): Either = when (this) { + is Either.Left -> Either.Left(f.invoke(error)) + is Either.Right -> this +} + +inline fun Either + .fold(e: (E) -> A, v: (V) -> A): A = when (this) { + is Either.Left -> e(this.error) + is Either.Right -> v(this.value) +} \ No newline at end of file diff --git a/presentation/src/main/kotlin/com/hernandazevedo/moviedb/view/base/Resource.kt b/presentation/src/main/kotlin/com/hernandazevedo/moviedb/view/base/Resource.kt deleted file mode 100644 index 050872c..0000000 --- a/presentation/src/main/kotlin/com/hernandazevedo/moviedb/view/base/Resource.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.hernandazevedo.moviedb.view.base - -class Resource(val status: Status, - val data: T?, - val throwable: Throwable?) { - companion object { - const val HASHCODE_MULTIPLIER = 31 - const val ZERO = 0 - fun success(data: T? = null): Resource { - return Resource(Status.SUCCESS, data, null) - } - - fun error(throwable: Throwable): Resource { - return Resource(Status.ERROR, null, throwable) - } - - fun error(data: T, throwable: Throwable?): Resource { - return Resource(Status.ERROR, data, throwable) - } - } - - override fun equals(other: Any?): Boolean { - - other as Resource<*> - return if (this === other) { - true - } else !(javaClass != other.javaClass || - status != other.status || - data != other.data || - throwable != other.throwable) - } - - override fun hashCode(): Int { - var result = status.hashCode() - result = HASHCODE_MULTIPLIER * result + (data?.hashCode() ?: ZERO) - result = HASHCODE_MULTIPLIER * result + (throwable?.hashCode() ?: ZERO) - return result - } -} - -enum class Status { - SUCCESS, - ERROR -} \ No newline at end of file diff --git a/presentation/src/main/kotlin/com/hernandazevedo/moviedb/view/details/DetailsActivity.kt b/presentation/src/main/kotlin/com/hernandazevedo/moviedb/view/details/DetailsActivity.kt index bed33c6..6326eb4 100644 --- a/presentation/src/main/kotlin/com/hernandazevedo/moviedb/view/details/DetailsActivity.kt +++ b/presentation/src/main/kotlin/com/hernandazevedo/moviedb/view/details/DetailsActivity.kt @@ -13,8 +13,7 @@ import com.hernandazevedo.moviedb.R import com.hernandazevedo.moviedb.data.Logger import com.hernandazevedo.moviedb.getFactoryViewModel import com.hernandazevedo.moviedb.view.base.BaseActivity -import com.hernandazevedo.moviedb.view.base.Resource -import com.hernandazevedo.moviedb.view.base.Status +import com.hernandazevedo.moviedb.view.base.fold import kotlinx.android.synthetic.main.activity_details.* import javax.inject.Inject @@ -35,6 +34,7 @@ class DetailsActivity : BaseActivity() { detailsViewModel = getFactoryViewModel { detailsViewModel } setContentView(R.layout.activity_details) subscribeToSearchMovie() + subscribeToFavoriteMovie() shareAction.setOnClickListener { setupShareAction() } populateInfo() setupFavAction() @@ -43,18 +43,24 @@ class DetailsActivity : BaseActivity() { private fun subscribeToSearchMovie() { detailsViewModel.responseGetMovieDetails.observe(this, - Observer> { - when (it?.status) { - Status.SUCCESS -> { - Logger.d("Success finding movie details") - fillMovieDetail(it.data) - } - Status.ERROR -> { - Logger.d("Error finding movie details") - showMessage("Error finding movie details") - it.throwable?.printStackTrace() - } - } + Observer { + it?.fold({ e: Throwable -> + Logger.e(e, "Left finding movie details") + showMessage("Left finding movie details") + }, {}) + }) + } + + private fun subscribeToFavoriteMovie() { + detailsViewModel.responseFavoriteAction.observe(this, + Observer { + it?.fold( + { e: Throwable -> + Logger.e(e, "Left finding movie details") + showMessage("Left finding movie details") + }, + {} + ) }) } @@ -81,7 +87,7 @@ class DetailsActivity : BaseActivity() { movieTitle.text = "# ${movie.title}" movieYear.text = year movieVoteAverage.text = "'-'" - favoriteButton.isChecked = favored ?: false + favoriteButton.isChecked = favored movieAdultImage.setImageDrawable(getDrawable(R.drawable.ic_clapperboard)) //TODO diff --git a/presentation/src/main/kotlin/com/hernandazevedo/moviedb/view/details/DetailsViewModel.kt b/presentation/src/main/kotlin/com/hernandazevedo/moviedb/view/details/DetailsViewModel.kt index 5b4f01a..055aaa5 100644 --- a/presentation/src/main/kotlin/com/hernandazevedo/moviedb/view/details/DetailsViewModel.kt +++ b/presentation/src/main/kotlin/com/hernandazevedo/moviedb/view/details/DetailsViewModel.kt @@ -10,7 +10,7 @@ import com.hernandazevedo.moviedb.domain.usecase.request.GetMovieDetailsRequest import com.hernandazevedo.moviedb.domain.usecase.request.SaveMovieToLocalDatabaseRequest import com.hernandazevedo.moviedb.util.UseCaseHandler import com.hernandazevedo.moviedb.view.base.BaseViewModel -import com.hernandazevedo.moviedb.view.base.Resource +import com.hernandazevedo.moviedb.view.base.Either class DetailsViewModel( val getMovieDetailsUseCase: BaseUseCase, @@ -18,8 +18,8 @@ class DetailsViewModel( val deleteMovieFromLocalDatabaseUseCase: BaseUseCase ) : BaseViewModel() { - val responseGetMovieDetails: MutableLiveData> = MutableLiveData() - val rsponseFavoriteAction: MutableLiveData> = MutableLiveData() + val responseGetMovieDetails: MutableLiveData> = MutableLiveData() + val responseFavoriteAction: MutableLiveData> = MutableLiveData() fun getMovieDetails(imdbID: String) { Logger.d("Starting getMovieDetails - $imdbID") @@ -29,10 +29,10 @@ class DetailsViewModel( useCaseExecute .subscribe({ Logger.d("Success searching movies for imdbID $imdbID") - responseGetMovieDetails.value = Resource.success(it) + responseGetMovieDetails.value = Either.Right(it) }, { - Logger.d("Error searching movies for imdbID $imdbID") - responseGetMovieDetails.value = Resource.error(it) + Logger.d("Left searching movies for imdbID $imdbID") + responseGetMovieDetails.value = Either.Left(it) })) } @@ -51,10 +51,10 @@ class DetailsViewModel( useCaseExecute .subscribe({ Logger.d("Success favoriteAction $checked") - responseGetMovieDetails.value = Resource.success() + responseFavoriteAction.value = Either.Right(Any()) }, { - Logger.d("Error favoriteAction $checked") - responseGetMovieDetails.value = Resource.error(it) + Logger.d("Left favoriteAction $checked") + responseFavoriteAction.value = Either.Left(it) })) } } \ No newline at end of file diff --git a/presentation/src/main/kotlin/com/hernandazevedo/moviedb/view/main/MainActivity.kt b/presentation/src/main/kotlin/com/hernandazevedo/moviedb/view/main/MainActivity.kt index 9610c81..52b0c8f 100644 --- a/presentation/src/main/kotlin/com/hernandazevedo/moviedb/view/main/MainActivity.kt +++ b/presentation/src/main/kotlin/com/hernandazevedo/moviedb/view/main/MainActivity.kt @@ -16,8 +16,7 @@ import com.hernandazevedo.moviedb.getFactoryViewModel import com.hernandazevedo.moviedb.view.Navigator import com.hernandazevedo.moviedb.view.adapter.MovieAdapter import com.hernandazevedo.moviedb.view.base.BaseActivity -import com.hernandazevedo.moviedb.view.base.Resource -import com.hernandazevedo.moviedb.view.base.Status +import com.hernandazevedo.moviedb.view.base.fold import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.activity_movies.* import javax.inject.Inject @@ -73,26 +72,21 @@ class MainActivity : BaseActivity() , NavigationView.OnNavigationItemSelectedLis } private fun subscribeToSearchMovie() { - mainViewModel.responseSearchMovie.observe(this, - Observer>> { - when (it?.status) { - Status.SUCCESS -> { - Logger.d("Success finding movie") - showMovies(it.data) - } - Status.ERROR -> { - Logger.d("Error finding movie") - showMessage("Error finding movie") - it.throwable?.printStackTrace() - } - } - }) + mainViewModel.responseSearchMovie.observe(this, Observer { + it?.fold(this::handleError, this::showMovies) + }) } - private fun showMovies(movieList: List?) { + private fun showMovies(movieList: List) { + Logger.d("Success finding movie") movieAdapter.setMovies(movieList) } + private fun handleError(e: Throwable) { + Logger.e(e, "Left finding movie") + showMessage("Left finding movie") + } + private fun setupRecyclersView() { moviesRecyclerView.layoutManager = layoutManager moviesRecyclerView.adapter = movieAdapter diff --git a/presentation/src/main/kotlin/com/hernandazevedo/moviedb/view/main/MainViewModel.kt b/presentation/src/main/kotlin/com/hernandazevedo/moviedb/view/main/MainViewModel.kt index 6000f8f..2ed9177 100644 --- a/presentation/src/main/kotlin/com/hernandazevedo/moviedb/view/main/MainViewModel.kt +++ b/presentation/src/main/kotlin/com/hernandazevedo/moviedb/view/main/MainViewModel.kt @@ -8,12 +8,12 @@ import com.hernandazevedo.moviedb.domain.usecase.base.BaseUseCase import com.hernandazevedo.moviedb.domain.usecase.request.SearchMovieRequest import com.hernandazevedo.moviedb.util.UseCaseHandler import com.hernandazevedo.moviedb.view.base.BaseViewModel -import com.hernandazevedo.moviedb.view.base.Resource +import com.hernandazevedo.moviedb.view.base.Either open class MainViewModel(val searchMovieUseCase: BaseUseCase>, val fetchMyFavoritesUseCase: BaseUseCase>) : BaseViewModel() { //Here could be an object created on presentation layer, but to be simple we are using the domain model. - val responseSearchMovie: MutableLiveData>> = MutableLiveData() + val responseSearchMovie: MutableLiveData>> = MutableLiveData() fun searchMovie(title: String) { Logger.d("Starting searchMovie - $title") @@ -23,10 +23,10 @@ open class MainViewModel(val searchMovieUseCase: BaseUseCase>> = MutableLiveData() - expectedResult.value = Resource.success(fakeMovieList) + val expectedResult: MutableLiveData>> = MutableLiveData() + expectedResult.value = Either.Right(fakeMovieList) mainViewModel.searchMovie(fakeTitleSearch) Assert.assertEquals(expectedResult.value, mainViewModel.responseSearchMovie.value) }