implement viewmodel, domain layer and view layer for userlist

This commit is contained in:
Carlos Martinez
2020-02-03 13:01:55 -03:00
parent 7d4432278e
commit 259955ed5d
26 changed files with 305 additions and 76 deletions

View File

@@ -15,6 +15,9 @@ android {
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
buildConfigField "String", "DB_NAME", '"friendlists.db"'
buildConfigField "String", "BASE_ENDPOINT", '"https://jsonplaceholder.typicode.com/"'
}
buildTypes {

View File

@@ -1,6 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.hako.friendlists">
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:name=".MainApplication"
android:allowBackup="true"

View File

@@ -1,7 +1,8 @@
package com.hako.friendlists
import android.app.Application
import com.hako.base.di.baseModule
import com.hako.friendlist.di.userlistModules
import com.hako.friendlists.di.appModules
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin
import timber.log.Timber
@@ -24,7 +25,8 @@ class MainApplication : Application() {
modules(
listOf(
baseModule
appModules,
userlistModules
)
)
}

View File

@@ -0,0 +1,18 @@
package com.hako.friendlists.di
import androidx.room.Room
import com.hako.base.domain.database.DatabaseClient
import com.hako.base.domain.network.RemoteClient
import com.hako.friendlists.BuildConfig
import org.koin.dsl.module
val appModules = module {
// Room database
single { Room.databaseBuilder(get(), DatabaseClient::class.java, BuildConfig.DB_NAME).build() }
factory { get<DatabaseClient>().userDao() }
factory { get<DatabaseClient>().albumDao() }
factory { get<DatabaseClient>().photoDao() }
// Retrofit
single { RemoteClient(BuildConfig.BASE_ENDPOINT) }
}

View File

@@ -12,8 +12,6 @@ android {
minSdkVersion build_versions.min_sdk
targetSdkVersion build_versions.target_sdk
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
buildConfigField "String", "DB_NAME", '"friendlists.db"'
}
compileOptions {

View File

@@ -1,16 +0,0 @@
package com.hako.base.di
import androidx.room.Room
import com.hako.base.BuildConfig
import com.hako.base.room.BaseDatabase
import org.koin.dsl.module
val baseModule = module {
// Room database
single { Room.databaseBuilder(get(), BaseDatabase::class.java, BuildConfig.DB_NAME).build() }
factory { get<BaseDatabase>().userDao() }
factory { get<BaseDatabase>().albumDao() }
factory { get<BaseDatabase>().photoDao() }
}

View File

@@ -0,0 +1,33 @@
package com.hako.base.domain
sealed class Either<out L, out R> {
data class Left<out L>(val a: L) : Either<L, Nothing>()
data class Right<out R>(val b: R) : Either<Nothing, R>()
val isRight get() = this is Right<R>
val isLeft get() = this is Left<L>
fun <L> left(a: L) = Left(a)
fun <R> right(b: R) = Right(b)
fun either(fnL: (L) -> Any, fnR: (R) -> Any): Any =
when (this) {
is Left -> fnL(a)
is Right -> fnR(b)
}
}
fun <A, B, C> ((A) -> B).c(f: (B) -> C): (A) -> C = {
f(this(it))
}
fun <T, L, R> Either<L, R>.flatMap(fn: (R) -> Either<L, T>): Either<L, T> =
when (this) {
is Either.Left -> Either.Left(
a
)
is Either.Right -> fn(b)
}
fun <T, L, R> Either<L, R>.map(fn: (R) -> (T)): Either<L, T> = this.flatMap(fn.c(::right))

View File

@@ -0,0 +1,5 @@
package com.hako.base.domain
interface UseCase <T> {
fun execute(onSuccess: (List<T>) -> Unit, onError: (Throwable) -> Unit, onLoading: () -> Unit)
}

View File

@@ -0,0 +1,17 @@
package com.hako.base.domain.database
import androidx.room.Database
import androidx.room.RoomDatabase
import com.hako.base.domain.database.dao.AlbumDao
import com.hako.base.domain.database.dao.PhotoDao
import com.hako.base.domain.database.dao.UserDao
import com.hako.base.domain.database.entities.AlbumEntity
import com.hako.base.domain.database.entities.PhotoEntity
import com.hako.base.domain.database.entities.UserEntity
@Database(entities = [UserEntity::class, AlbumEntity::class, PhotoEntity::class], version = 1, exportSchema = false)
abstract class DatabaseClient : RoomDatabase() {
abstract fun userDao(): UserDao
abstract fun albumDao(): AlbumDao
abstract fun photoDao(): PhotoDao
}

View File

@@ -1,10 +1,10 @@
package com.hako.base.room.dao
package com.hako.base.domain.database.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.hako.base.room.entities.AlbumEntity
import com.hako.base.domain.database.entities.AlbumEntity
@Dao
interface AlbumDao {

View File

@@ -1,10 +1,10 @@
package com.hako.base.room.dao
package com.hako.base.domain.database.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.hako.base.room.entities.PhotoEntity
import com.hako.base.domain.database.entities.PhotoEntity
@Dao
interface PhotoDao {

View File

@@ -1,10 +1,10 @@
package com.hako.base.room.dao
package com.hako.base.domain.database.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.hako.base.room.entities.UserEntity
import com.hako.base.domain.database.entities.UserEntity
@Dao
interface UserDao {
@@ -18,6 +18,9 @@ interface UserDao {
@get:Query("SELECT * FROM ${UserEntity.TABLE_NAME}")
val all: List<UserEntity>
@Query("SELECT * FROM ${UserEntity.TABLE_NAME}")
fun getAllUsers(): List<UserEntity>
@Query("SELECT COUNT(*) FROM ${UserEntity.TABLE_NAME}")
fun count(): Int

View File

@@ -1,4 +1,4 @@
package com.hako.base.room.entities
package com.hako.base.domain.database.entities
import androidx.room.Entity
import androidx.room.Index

View File

@@ -1,4 +1,4 @@
package com.hako.base.room.entities
package com.hako.base.domain.database.entities
import androidx.room.Entity
import androidx.room.Index

View File

@@ -1,4 +1,4 @@
package com.hako.base.room.entities
package com.hako.base.domain.database.entities
import androidx.room.Entity
import androidx.room.Index

View File

@@ -0,0 +1,35 @@
package com.hako.base.domain.network
import com.hako.base.BuildConfig
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
import timber.log.Timber
class RemoteClient(endpoint: String) {
private val logger = HttpLoggingInterceptor(object : HttpLoggingInterceptor.Logger {
override fun log(message: String) {
Timber.d(message)
}
}).setLevel(getLoggerLevel())
private val client = OkHttpClient.Builder()
.addInterceptor(logger)
.build()
private val retrofit: Retrofit = Retrofit.Builder()
.baseUrl(endpoint)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.client(client)
.build()
fun <T> getClient(api: Class<T>): T = retrofit.create(api)
private fun getLoggerLevel() = when (BuildConfig.DEBUG) {
true -> HttpLoggingInterceptor.Level.BASIC
false -> HttpLoggingInterceptor.Level.NONE
}
}

View File

@@ -0,0 +1,7 @@
package com.hako.base.domain.network
sealed class RequestStatus {
object Ready : RequestStatus()
object Loading : RequestStatus()
object Errored : RequestStatus()
}

View File

@@ -0,0 +1,13 @@
package com.hako.base.extensions
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
fun <T> LiveData<T>.observeNonNull(owner: LifecycleOwner, func: (T) -> Unit) {
observe(owner, Observer {
it?.let {
func(it)
}
})
}

View File

@@ -1,17 +0,0 @@
package com.hako.base.room
import androidx.room.Database
import androidx.room.RoomDatabase
import com.hako.base.room.dao.AlbumDao
import com.hako.base.room.dao.PhotoDao
import com.hako.base.room.dao.UserDao
import com.hako.base.room.entities.AlbumEntity
import com.hako.base.room.entities.PhotoEntity
import com.hako.base.room.entities.UserEntity
@Database(entities = [UserEntity::class, AlbumEntity::class, PhotoEntity::class], version = 1, exportSchema = false)
abstract class BaseDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
abstract fun albumDao(): AlbumDao
abstract fun photoDao(): PhotoDao
}

View File

@@ -0,0 +1,17 @@
package com.hako.friendlist.di
import com.hako.base.domain.network.RemoteClient
import com.hako.friendlist.domain.datasource.UserlistDatasource
import com.hako.friendlist.domain.datasource.UserlistRemoteApi
import com.hako.friendlist.domain.usecase.GetUsers
import com.hako.friendlist.viewmodel.UserlistViewmodel
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
val userlistModules = module {
factory { get<RemoteClient>().getClient(UserlistRemoteApi::class.java) }
factory { UserlistDatasource() }
factory { GetUsers(get()) }
viewModel { UserlistViewmodel() }
}

View File

@@ -0,0 +1,13 @@
package com.hako.friendlist.domain.datasource
import com.hako.friendlist.model.User
import io.reactivex.Single
import org.koin.core.KoinComponent
import org.koin.core.get
class UserlistDatasource : KoinComponent, UserlistRemoteApi {
private val api: UserlistRemoteApi = get()
override fun getUsers(): Single<List<User>> = api.getUsers()
}

View File

@@ -0,0 +1,11 @@
package com.hako.friendlist.domain.datasource
import com.hako.friendlist.model.User
import io.reactivex.Single
import retrofit2.http.GET
interface UserlistRemoteApi {
@GET("/users")
fun getUsers(): Single<List<User>>
}

View File

@@ -0,0 +1,43 @@
package com.hako.friendlist.domain.usecase
import com.hako.base.domain.UseCase
import com.hako.base.domain.database.dao.UserDao
import com.hako.friendlist.domain.datasource.UserlistDatasource
import com.hako.friendlist.model.UserViewable
import com.hako.friendlist.model.toUserEntity
import com.hako.friendlist.model.toUserViewable
import io.reactivex.Single
import io.reactivex.schedulers.Schedulers
import org.koin.core.KoinComponent
import org.koin.core.get
class GetUsers(private val dao: UserDao) : KoinComponent,
UseCase<UserViewable> {
private val api: UserlistDatasource = get()
override fun execute(
onSuccess: (List<UserViewable>) -> Unit,
onError: (Throwable) -> Unit,
onLoading: () -> Unit
) {
Single.fromCallable { dao.getAllUsers() }
.subscribeOn(Schedulers.io())
.doOnError { onError(it) }
.doOnSuccess { dbUsers ->
if (dbUsers.isEmpty() || dbUsers.count() == 0) {
api.getUsers()
.doOnSuccess {
dao.saveAll(it.map { user -> user.toUserEntity() })
onSuccess(dao.getAllUsers().map { user -> user.toUserViewable() })
}
.doOnSubscribe { onLoading() }
.subscribeOn(Schedulers.io())
.subscribe({}, { onError(it) })
} else {
onSuccess(dbUsers.map { it.toUserViewable() })
}
}
.subscribe()
}
}

View File

@@ -6,14 +6,20 @@ import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import com.hako.base.domain.network.RequestStatus
import com.hako.base.extensions.observeNonNull
import com.hako.base.extensions.toast
import com.hako.friendlist.model.UserViewable
import com.hako.friendlist.viewmodel.UserlistViewmodel
import com.hako.friendlist.widget.UserlistAdapter
import com.hako.friendlist_userlist.R
import kotlinx.android.synthetic.main.fragment_userlist.*
import org.koin.androidx.viewmodel.ext.android.viewModel
import timber.log.Timber
class UserlistFragment : Fragment() {
private val viewModel: UserlistViewmodel by viewModel()
private val chatAdapter by lazy { UserlistAdapter() }
override fun onCreateView(
@@ -23,37 +29,37 @@ class UserlistFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setRecycler()
setObservers()
viewModel.fetchUsers()
}
private fun setObservers() {
viewModel.data.observeNonNull(this) {
it.either(::handleFetchError, ::handleFetchSuccess)
}
viewModel.requestStatus.observeNonNull(this) {
when (it) {
RequestStatus.Ready -> { context?.toast("Ready") }
RequestStatus.Loading -> { context?.toast("Loading") }
RequestStatus.Errored -> { context?.toast("Errored") }
}
}
}
private fun handleFetchError(throwable: Throwable) {
context?.toast("Could't get data")
Timber.e(throwable)
}
private fun handleFetchSuccess(users: List<UserViewable>) {
chatAdapter.addAll(users)
}
private fun setRecycler() {
fragment_userlist_recycler_container.apply {
layoutManager = LinearLayoutManager(context)
adapter = chatAdapter.apply {
addAll(
listOf(
UserViewable(1, "Carlos Martinez", "carlitos"),
UserViewable(2, "Carlos Martinez", "carlitos"),
UserViewable(3, "Carlos Martinez", "carlitos"),
UserViewable(4, "Carlos Martinez", "carlitos"),
UserViewable(5, "Carlos Martinez", "carlitos"),
UserViewable(6, "Carlos Martinez", "carlitos"),
UserViewable(7, "Carlos Martinez", "carlitos"),
UserViewable(8, "Carlos Martinez", "carlitos"),
UserViewable(9, "Carlos Martinez", "carlitos"),
UserViewable(10, "Carlos Martinez", "carlitos"),
UserViewable(11, "Carlos Martinez", "carlitos"),
UserViewable(12, "Carlos Martinez", "carlitos"),
UserViewable(13, "Carlos Martinez", "carlitos"),
UserViewable(14, "Carlos Martinez", "carlitos"),
UserViewable(15, "Carlos Martinez", "carlitos"),
UserViewable(16, "Carlos Martinez", "carlitos"),
UserViewable(17, "Carlos Martinez", "carlitos"),
UserViewable(18, "Carlos Martinez", "carlitos"),
UserViewable(19, "Carlos Martinez", "carlitos"),
UserViewable(20, "Carlos Martinez", "carlitos")
)
)
onItemClick = {
context.toast(it.realName)
}

View File

@@ -2,7 +2,7 @@ package com.hako.friendlist.model
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import com.hako.base.room.entities.UserEntity
import com.hako.base.domain.database.entities.UserEntity
import kotlinx.android.parcel.Parcelize
@Parcelize
@@ -20,9 +20,9 @@ data class UserViewable(
val realName: String,
val userName: String,
var isFavorite: Boolean = false
) {
fun User.toUserViewable() = UserViewable(this.id, this.realName, this.userName)
)
fun User.toUserEntity() = UserEntity(this.id, this.realName, this.userName, this.email, this.phone, this.website)
fun UserEntity.toUserViewable() = UserViewable(this.id, this.realName, this.userName)
}

View File

@@ -0,0 +1,36 @@
package com.hako.friendlist.viewmodel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.hako.base.domain.network.RequestStatus
import com.hako.base.domain.network.RequestStatus.Ready
import com.hako.base.domain.network.RequestStatus.Loading
import com.hako.base.domain.network.RequestStatus.Errored
import com.hako.base.domain.Either
import com.hako.friendlist.domain.usecase.GetUsers
import com.hako.friendlist.model.UserViewable
import org.koin.core.KoinComponent
import org.koin.core.get
class UserlistViewmodel : ViewModel(), KoinComponent {
val data = MutableLiveData<Either<Throwable, List<UserViewable>>>()
val requestStatus = MutableLiveData<RequestStatus>()
private val getUsers: GetUsers = get()
fun fetchUsers() {
getUsers.execute(
onSuccess = {
requestStatus.postValue(Ready)
data.postValue(Either.Right(it))
},
onLoading = {
requestStatus.postValue(Loading)
},
onError = {
requestStatus.postValue(Errored)
data.postValue(Either.Left(it))
})
}
}