7 Commits

Author SHA1 Message Date
18f53543d4 Add codemagic 2024-02-26 12:34:09 -03:00
Carlos Martinez
ee869dacfc Merge pull request #13 from hakodeveloper/fix-userlist-fragments
Fix Userlist Hierarchy
2020-08-08 01:41:33 -04:00
Carlos Martinez
0fe35da8ba Change: fragment hieranchy 2020-08-08 01:32:05 -04:00
Carlos Martinez
a3e61a8297 Merge pull request #12 from hakodeveloper/structure/change-app-icon
Change app icon
2020-02-14 15:25:05 -03:00
Carlos Martinez
e14458589b change icon 2020-02-14 15:18:04 -03:00
Carlos Martinez
984602ec7e Merge pull request #11 from hakodeveloper/structure/clean-up-code
Migrate to multi-database structure
2020-02-13 15:01:40 -03:00
Carlos Martinez
e61c123068 migrate to multidatabase structure and clean up code 2020-02-13 12:40:28 -03:00
65 changed files with 336 additions and 301 deletions

View File

@@ -1,8 +1,12 @@
apply plugin: 'com.android.library'
apply from: '../core.gradle'
android {
defaultConfig {
buildConfigField "String", "DB_NAME", '"albumlist.db"'
}
}
dependencies {
implementation project(':base')
testImplementation project(':testing')
androidTestImplementation project(':testing')
}

View File

@@ -1,5 +1,8 @@
package com.hako.albumlist.di
import androidx.room.Room
import com.hako.albumlist.BuildConfig
import com.hako.albumlist.domain.clients.LocalClient
import com.hako.albumlist.domain.datasource.AlbumlistRemoteApi
import com.hako.albumlist.domain.usecase.GetAlbum
import com.hako.albumlist.viewmodel.AlbumlistViewmodel
@@ -8,8 +11,12 @@ import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
val albumListModules = module {
single { Room.databaseBuilder(get(), LocalClient::class.java, BuildConfig.DB_NAME).build() }
factory { get<LocalClient>().albumDao() }
factory { get<RemoteClient>().getClient(AlbumlistRemoteApi::class.java) }
factory { GetAlbum(get()) }
factory { GetAlbum(get(), get()) }
viewModel { AlbumlistViewmodel() }
}

View File

@@ -0,0 +1,11 @@
package com.hako.albumlist.domain.clients
import androidx.room.Database
import androidx.room.RoomDatabase
import com.hako.albumlist.domain.datasource.AlbumDao
import com.hako.albumlist.model.AlbumEntity
@Database(entities = [AlbumEntity::class], version = 1, exportSchema = false)
abstract class LocalClient : RoomDatabase() {
abstract fun albumDao(): AlbumDao
}

View File

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

View File

@@ -1,6 +1,6 @@
package com.hako.albumlist.domain.datasource
import com.hako.albumlist.model.Album
import com.hako.albumlist.model.AlbumRemote
import io.reactivex.Single
import retrofit2.http.GET
import retrofit2.http.Query
@@ -10,5 +10,5 @@ interface AlbumlistRemoteApi {
@GET("/albums")
fun getAlbums(
@Query("userId") userId: Int
): Single<List<Album>>
): Single<List<AlbumRemote>>
}

View File

@@ -1,22 +1,18 @@
package com.hako.albumlist.domain.usecase
import com.hako.albumlist.domain.datasource.AlbumDao
import com.hako.albumlist.domain.datasource.AlbumlistRemoteApi
import com.hako.albumlist.model.AlbumViewable
import com.hako.albumlist.model.Album
import com.hako.albumlist.model.toAlbumEntity
import com.hako.albumlist.model.toAlbumViewable
import com.hako.base.domain.database.dao.AlbumDao
import io.reactivex.Single
import io.reactivex.schedulers.Schedulers
import org.koin.core.KoinComponent
import org.koin.core.get
class GetAlbum(private val dao: AlbumDao) : KoinComponent {
private val api: AlbumlistRemoteApi = get()
class GetAlbum(private val dao: AlbumDao, private val api: AlbumlistRemoteApi) {
fun execute(
userId: Int,
onSuccess: (List<AlbumViewable>) -> Unit,
onSuccess: (List<Album>) -> Unit,
onError: (Throwable) -> Unit,
onLoading: () -> Unit
) {

View File

@@ -7,7 +7,7 @@ import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import com.hako.albumlist.R
import com.hako.albumlist.model.AlbumViewable
import com.hako.albumlist.model.Album
import com.hako.albumlist.navigation.AlbumlistNavigation
import com.hako.albumlist.viewmodel.AlbumlistViewmodel
import com.hako.albumlist.widget.AlbumlistAdapter
@@ -72,7 +72,7 @@ class AlbumlistFragment : Fragment() {
Timber.e(throwable)
}
private fun handleFetchSuccess(users: List<AlbumViewable>) {
private fun handleFetchSuccess(users: List<Album>) {
listAdapter.addAll(users)
}

View File

@@ -1,24 +1,38 @@
package com.hako.albumlist.model
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import com.google.gson.annotations.SerializedName
import com.hako.base.domain.database.entities.AlbumEntity
import kotlinx.android.parcel.Parcelize
@Parcelize
data class Album(
data class AlbumRemote(
@SerializedName("id") val id: Int,
@SerializedName("userId") val userId: Int,
@SerializedName("title") val title: String
) : Parcelable
data class AlbumViewable(
@Entity(tableName = AlbumEntity.TABLE_NAME, indices = [Index(value = ["id"], unique = true)])
data class AlbumEntity(
@PrimaryKey
val id: Int,
val userId: Int,
val title: String
) {
companion object {
const val TABLE_NAME = "albums"
}
}
data class Album(
val id: Int,
val userId: Int,
val title: String
)
fun Album.toAlbumEntity() = AlbumEntity(this.id, this.userId, this.title)
fun AlbumRemote.toAlbumEntity() = AlbumEntity(this.id, this.userId, this.title)
fun AlbumEntity.toAlbumViewable() = AlbumViewable(this.id, this.userId, this.title)
fun AlbumEntity.toAlbumViewable() = Album(this.id, this.userId, this.title)

View File

@@ -3,7 +3,7 @@ package com.hako.albumlist.viewmodel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.hako.albumlist.domain.usecase.GetAlbum
import com.hako.albumlist.model.AlbumViewable
import com.hako.albumlist.model.Album
import com.hako.base.domain.network.RequestStatus
import com.hako.base.domain.network.RequestStatus.Ready
import com.hako.base.domain.network.RequestStatus.Loading
@@ -14,7 +14,7 @@ import org.koin.core.get
class AlbumlistViewmodel : ViewModel(), KoinComponent {
val data = MutableLiveData<Either<Throwable, List<AlbumViewable>>>()
val data = MutableLiveData<Either<Throwable, List<Album>>>()
val requestStatus = MutableLiveData<RequestStatus>()
private val getAlbum: GetAlbum = get()

View File

@@ -5,19 +5,19 @@ import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.hako.albumlist.R
import com.hako.albumlist.model.AlbumViewable
import com.hako.albumlist.model.Album
import com.hako.base.extensions.autoNotify
import kotlinx.android.synthetic.main.item_album_card.view.*
import kotlin.properties.Delegates
class AlbumlistAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var items by Delegates.observable(emptyList<AlbumViewable>()) { _, oldList, newList ->
private var items by Delegates.observable(emptyList<Album>()) { _, oldList, newList ->
autoNotify(oldList, newList) { old, new -> old.id == new.id }
notifyDataSetChanged()
}
var onItemClick: (AlbumViewable) -> Unit = { }
var onItemClick: (Album) -> Unit = { }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder =
AlbumViewHolder(
@@ -29,7 +29,7 @@ class AlbumlistAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
fun getItem(position: Int) = items[position]
fun addAll(list: List<AlbumViewable>) {
fun addAll(list: List<Album>) {
items = list
}
@@ -43,10 +43,10 @@ class AlbumlistAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
}
class AlbumViewHolder(private val view: View,
private val onItemClick: (AlbumViewable) -> Unit) :
private val onItemClick: (Album) -> Unit) :
RecyclerView.ViewHolder(view) {
fun bind(album: AlbumViewable) = with(view) {
fun bind(album: Album) = with(view) {
item_album_card_album_name.text = album.title
item_album_card_container.setOnClickListener {
onItemClick(album)

View File

@@ -16,7 +16,6 @@ android {
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
buildConfigField "String", "DB_NAME", '"friendlists.db"'
buildConfigField "String", "BASE_ENDPOINT", '"https://jsonplaceholder.typicode.com/"'
}
@@ -57,6 +56,4 @@ dependencies {
implementation project(":userlist")
implementation project(":albumlist")
implementation project(":photolist")
testImplementation project(':testing')
androidTestImplementation project(':testing')
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -1,7 +1,5 @@
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.base.navigation.NavigationRouter
import com.hako.friendlists.BuildConfig
@@ -11,12 +9,6 @@ import org.koin.androidx.viewmodel.dsl.viewModel
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

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

View File

@@ -1,17 +0,0 @@
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,17 +0,0 @@
package com.hako.base.domain.database.entities
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(tableName = AlbumEntity.TABLE_NAME, indices = [Index(value = ["id"], unique = true)])
data class AlbumEntity(
@PrimaryKey
val id: Int,
val userId: Int,
val title: String
) {
companion object {
const val TABLE_NAME = "albums"
}
}

View File

@@ -1,19 +0,0 @@
package com.hako.base.domain.database.entities
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(tableName = PhotoEntity.TABLE_NAME, indices = [Index(value = ["id"], unique = true)])
data class PhotoEntity(
@PrimaryKey
val id: Int,
val albumId: Int,
val title: String,
val photoUrl: String,
val thumbnailUrl: String
) {
companion object {
const val TABLE_NAME = "photos"
}
}

View File

@@ -1,21 +0,0 @@
package com.hako.base.domain.database.entities
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(tableName = UserEntity.TABLE_NAME, indices = [Index(value = ["id"], unique = true)])
data class UserEntity(
@PrimaryKey
val id: Int,
val realName: String,
val userName: String,
val email: String,
val phone: String,
val website: String,
val isFavorite: Boolean = false
) {
companion object {
const val TABLE_NAME = "users"
}
}

1
codemagic.yaml Normal file
View File

@@ -0,0 +1 @@

View File

@@ -31,3 +31,7 @@ android {
abortOnError false
}
}
dependencies {
kapt deps.room.compiler
}

View File

@@ -1,8 +1,12 @@
apply plugin: 'com.android.library'
apply from: '../core.gradle'
android {
defaultConfig {
buildConfigField "String", "DB_NAME", '"photolist.db"'
}
}
dependencies {
implementation project(':base')
testImplementation project(':testing')
androidTestImplementation project(':testing')
}

View File

@@ -1,6 +1,9 @@
package com.hako.photolist.di
import androidx.room.Room
import com.hako.base.domain.network.RemoteClient
import com.hako.photolist.BuildConfig
import com.hako.photolist.domain.clients.LocalClient
import com.hako.photolist.domain.datasource.PhotolistRemoteApi
import com.hako.photolist.domain.usecase.GetPhoto
import com.hako.photolist.viewmodel.PhotolistViewmodel
@@ -8,8 +11,12 @@ import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
val photoListModules = module {
single { Room.databaseBuilder(get(), LocalClient::class.java, BuildConfig.DB_NAME).build() }
factory { get<LocalClient>().photoDao() }
factory { get<RemoteClient>().getClient(PhotolistRemoteApi::class.java) }
factory { GetPhoto(get()) }
factory { GetPhoto(get(), get()) }
viewModel { PhotolistViewmodel() }
}

View File

@@ -0,0 +1,11 @@
package com.hako.photolist.domain.clients
import androidx.room.Database
import androidx.room.RoomDatabase
import com.hako.photolist.domain.datasource.PhotoDao
import com.hako.photolist.model.PhotoEntity
@Database(entities = [PhotoEntity::class], version = 1, exportSchema = false)
abstract class LocalClient : RoomDatabase() {
abstract fun photoDao(): PhotoDao
}

View File

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

View File

@@ -1,6 +1,6 @@
package com.hako.photolist.domain.datasource
import com.hako.photolist.model.Photo
import com.hako.photolist.model.PhotoRemote
import io.reactivex.Single
import retrofit2.http.GET
import retrofit2.http.Query
@@ -10,5 +10,5 @@ interface PhotolistRemoteApi {
@GET("/photos")
fun getPhotos(
@Query("albumId") albumId: Int
): Single<List<Photo>>
): Single<List<PhotoRemote>>
}

View File

@@ -1,20 +1,16 @@
package com.hako.photolist.domain.usecase
import com.hako.base.domain.database.dao.PhotoDao
import com.hako.photolist.domain.datasource.PhotoDao
import com.hako.photolist.domain.datasource.PhotolistRemoteApi
import com.hako.photolist.model.*
import io.reactivex.Single
import io.reactivex.schedulers.Schedulers
import org.koin.core.KoinComponent
import org.koin.core.get
class GetPhoto(private val dao: PhotoDao) : KoinComponent {
private val api: PhotolistRemoteApi = get()
class GetPhoto(private val dao: PhotoDao, private val api: PhotolistRemoteApi) {
fun execute(
albumId: Int,
onSuccess: (List<PhotoViewable>) -> Unit,
onSuccess: (List<Photo>) -> Unit,
onError: (Throwable) -> Unit,
onLoading: () -> Unit
) {

View File

@@ -11,7 +11,7 @@ import com.hako.base.extensions.gone
import com.hako.base.extensions.observeNonNull
import com.hako.base.extensions.visible
import com.hako.photolist.R
import com.hako.photolist.model.PhotoViewable
import com.hako.photolist.model.Photo
import com.hako.photolist.viewmodel.PhotolistViewmodel
import com.hako.photolist.widget.PhotolistAdapter
import kotlinx.android.synthetic.main.fragment_photolist.*
@@ -68,7 +68,7 @@ class PhotolistFragment : Fragment() {
Timber.e(throwable)
}
private fun handleFetchSuccess(photos: List<PhotoViewable>) {
private fun handleFetchSuccess(photos: List<Photo>) {
listAdapter.addAll(photos)
}

View File

@@ -1,12 +1,14 @@
package com.hako.photolist.model
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import com.google.gson.annotations.SerializedName
import com.hako.base.domain.database.entities.PhotoEntity
import kotlinx.android.parcel.Parcelize
@Parcelize
data class Photo(
data class PhotoRemote(
@SerializedName("id") val id: Int,
@SerializedName("albumId") val albumId: Int,
@SerializedName("title") val title: String,
@@ -14,14 +16,28 @@ data class Photo(
@SerializedName("thumbnailUrl") val thumbnailUrl: String
) : Parcelable
data class PhotoViewable(
@Entity(tableName = PhotoEntity.TABLE_NAME, indices = [Index(value = ["id"], unique = true)])
data class PhotoEntity(
@PrimaryKey
val id: Int,
val albumId: Int,
val title: String,
val photoUrl: String,
val thumbnailUrl: String
) {
companion object {
const val TABLE_NAME = "photos"
}
}
data class Photo(
val id: Int,
val albumId: Int,
val title: String,
val photoUrl: String
)
fun Photo.toPhotoEntity() = PhotoEntity(this.id, this.albumId, this.title, this.photoUrl, this.thumbnailUrl)
fun PhotoRemote.toPhotoEntity() = PhotoEntity(this.id, this.albumId, this.title, this.photoUrl, this.thumbnailUrl)
fun PhotoEntity.toPhotoViewable() = PhotoViewable(this.id, this.albumId, this.title, this.photoUrl)
fun PhotoEntity.toPhotoViewable() = Photo(this.id, this.albumId, this.title, this.photoUrl)

View File

@@ -8,13 +8,13 @@ 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.photolist.domain.usecase.GetPhoto
import com.hako.photolist.model.PhotoViewable
import com.hako.photolist.model.Photo
import org.koin.core.KoinComponent
import org.koin.core.get
class PhotolistViewmodel : ViewModel(), KoinComponent {
val data = MutableLiveData<Either<Throwable, List<PhotoViewable>>>()
val data = MutableLiveData<Either<Throwable, List<Photo>>>()
val requestStatus = MutableLiveData<RequestStatus>()
private val getPhoto: GetPhoto = get()

View File

@@ -6,7 +6,7 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.hako.base.extensions.autoNotify
import com.hako.photolist.R
import com.hako.photolist.model.PhotoViewable
import com.hako.photolist.model.Photo
import com.squareup.picasso.Picasso
import kotlinx.android.synthetic.main.item_photo_card.view.*
import org.koin.core.KoinComponent
@@ -15,7 +15,7 @@ import kotlin.properties.Delegates
class PhotolistAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var items by Delegates.observable(emptyList<PhotoViewable>()) { _, oldList, newList ->
private var items by Delegates.observable(emptyList<Photo>()) { _, oldList, newList ->
autoNotify(oldList, newList) { old, new -> old.id == new.id }
notifyDataSetChanged()
}
@@ -29,7 +29,7 @@ class PhotolistAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
fun getItem(position: Int) = items[position]
fun addAll(list: List<PhotoViewable>) {
fun addAll(list: List<Photo>) {
items = list
}
@@ -47,7 +47,7 @@ class PhotoViewHolder(private val view: View) :
private val picasso: Picasso by inject()
fun bind(photo: PhotoViewable) = with(view) {
fun bind(photo: Photo) = with(view) {
picasso.load(photo.photoUrl)
.placeholder(R.drawable.img_photo_placeholder)
.fit()

View File

@@ -1,6 +1,12 @@
apply plugin: 'com.android.library'
apply from: '../core.gradle'
android {
defaultConfig {
buildConfigField "String", "DB_NAME", '"userlist.db"'
}
}
dependencies {
implementation project(':base')
testImplementation project(':testing')

View File

@@ -1,9 +1,9 @@
package com.hako.userlist.feature
import com.hako.base.domain.database.dao.UserDao
import com.hako.base.domain.database.entities.UserEntity
import com.hako.userlist.domain.datasource.UserDao
import com.hako.userlist.domain.datasource.UserlistRemoteApi
import com.hako.userlist.model.User
import com.hako.userlist.model.UserEntity
import com.hako.userlist.model.UserRemote
import io.reactivex.Single
class MockUserDao(private val userList: List<UserEntity>) : UserDao {
@@ -22,8 +22,8 @@ class MockUserDao(private val userList: List<UserEntity>) : UserDao {
override fun nukeDatabase() {}
}
class MockUserApi(private val userList: List<User>) : UserlistRemoteApi {
class MockUserApi(private val userRemoteList: List<UserRemote>) : UserlistRemoteApi {
override fun getUsers() = Single.fromCallable { getAllUsers() }
private fun getAllUsers() = userList
private fun getAllUsers() = userRemoteList
}

View File

@@ -2,14 +2,14 @@ package com.hako.userlist.feature
import androidx.fragment.app.testing.launchFragmentInContainer
import androidx.test.platform.app.InstrumentationRegistry
import com.hako.base.domain.database.dao.UserDao
import com.hako.base.domain.database.entities.UserEntity
import com.hako.testing.isTextDisplayed
import com.hako.userlist.domain.datasource.UserDao
import com.hako.userlist.domain.datasource.UserlistRemoteApi
import com.hako.userlist.domain.usecase.GetFavoriteUsers
import com.hako.userlist.domain.usecase.GetUsers
import com.hako.userlist.domain.usecase.SetFavoriteStatus
import com.hako.userlist.model.User
import com.hako.userlist.model.UserEntity
import com.hako.userlist.model.UserRemote
import com.hako.userlist.model.toUserEntity
import com.hako.userlist.viewmodel.UserlistViewmodel
import org.junit.After
@@ -28,7 +28,7 @@ class UserlistFragmentTest {
startKoin {
InstrumentationRegistry.getInstrumentation().targetContext
modules(module {
factory { GetUsers(get()) }
factory { GetUsers(get(), get()) }
factory { GetFavoriteUsers(get()) }
factory { SetFavoriteStatus(get()) }
viewModel { UserlistViewmodel() }
@@ -92,7 +92,7 @@ class UserlistRobot {
}
private fun loadTwoBasicUsers() = listOf(
User(
UserRemote(
1,
"Marian Arriaga",
"mariancita",
@@ -100,7 +100,7 @@ class UserlistRobot {
"+56873912",
"www.test.com"
),
User(
UserRemote(
2,
"Carlos Martinez",
"carlitos",

View File

@@ -1,2 +1,2 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.hako.friendlist_userlist" />
package="com.hako.userlist" />

View File

@@ -1,6 +1,9 @@
package com.hako.userlist.di
import androidx.room.Room
import com.hako.base.domain.network.RemoteClient
import com.hako.userlist.BuildConfig
import com.hako.userlist.domain.clients.LocalClient
import com.hako.userlist.domain.datasource.UserlistRemoteApi
import com.hako.userlist.domain.usecase.GetFavoriteUsers
import com.hako.userlist.domain.usecase.GetUsers
@@ -10,8 +13,12 @@ import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
val userlistModules = module {
single { Room.databaseBuilder(get(), LocalClient::class.java, BuildConfig.DB_NAME).build() }
factory { get<LocalClient>().userDao() }
factory { get<RemoteClient>().getClient(UserlistRemoteApi::class.java) }
factory { GetUsers(get()) }
factory { GetUsers(get(), get()) }
factory { GetFavoriteUsers(get()) }
factory { SetFavoriteStatus(get()) }

View File

@@ -0,0 +1,11 @@
package com.hako.userlist.domain.clients
import androidx.room.Database
import androidx.room.RoomDatabase
import com.hako.userlist.domain.datasource.UserDao
import com.hako.userlist.model.UserEntity
@Database(entities = [UserEntity::class], version = 1, exportSchema = false)
abstract class LocalClient : RoomDatabase() {
abstract fun userDao(): UserDao
}

View File

@@ -1,10 +1,10 @@
package com.hako.base.domain.database.dao
package com.hako.userlist.domain.datasource
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.hako.base.domain.database.entities.UserEntity
import com.hako.userlist.model.UserEntity
@Dao
interface UserDao {

View File

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

View File

@@ -1,7 +1,7 @@
package com.hako.userlist.domain.usecase
import com.hako.base.domain.database.dao.UserDao
import com.hako.userlist.model.UserViewable
import com.hako.userlist.domain.datasource.UserDao
import com.hako.userlist.model.User
import com.hako.userlist.model.toUserViewable
import io.reactivex.Single
import io.reactivex.schedulers.Schedulers
@@ -9,7 +9,7 @@ import io.reactivex.schedulers.Schedulers
class GetFavoriteUsers(private val dao: UserDao) {
fun execute(
onSuccess: (List<UserViewable>) -> Unit,
onSuccess: (List<User>) -> Unit,
onEmpty: () -> Unit,
onError: (Throwable) -> Unit
) {

View File

@@ -1,21 +1,17 @@
package com.hako.userlist.domain.usecase
import com.hako.base.domain.database.dao.UserDao
import com.hako.userlist.domain.datasource.UserDao
import com.hako.userlist.domain.datasource.UserlistRemoteApi
import com.hako.userlist.model.UserViewable
import com.hako.userlist.model.User
import com.hako.userlist.model.toUserEntity
import com.hako.userlist.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 {
private val api: UserlistRemoteApi = get()
class GetUsers(private val dao: UserDao, private val api: UserlistRemoteApi) {
fun execute(
onSuccess: (List<UserViewable>) -> Unit,
onSuccess: (List<User>) -> Unit,
onError: (Throwable) -> Unit,
onLoading: () -> Unit,
onEmpty: () -> Unit

View File

@@ -1,6 +1,6 @@
package com.hako.userlist.domain.usecase
import com.hako.base.domain.database.dao.UserDao
import com.hako.userlist.domain.datasource.UserDao
import io.reactivex.Single
import io.reactivex.schedulers.Schedulers

View File

@@ -0,0 +1,105 @@
package com.hako.userlist.feature
import android.os.Bundle
import android.view.LayoutInflater
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.gone
import com.hako.base.extensions.observeNonNull
import com.hako.base.extensions.visible
import com.hako.base.navigation.NavigationRouter
import com.hako.base.navigation.ShowFabButton
import com.hako.userlist.model.User
import com.hako.userlist.viewmodel.UserlistViewmodel
import com.hako.userlist.widget.UserlistAdapter
import com.hako.userlist.R
import com.hako.userlist.navigation.UserlistNavigation
import kotlinx.android.synthetic.main.fragment_userlist.*
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import timber.log.Timber
abstract class BaseUserlistFragment : Fragment(), ShowFabButton {
val viewModel: UserlistViewmodel by viewModel()
private val listAdapter by lazy { UserlistAdapter() }
private val navigation: NavigationRouter by inject()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View = inflater.inflate(R.layout.fragment_userlist, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setRecycler()
setObservers()
doRequest()
}
abstract fun doRequest()
override fun fabButtonPressed(): () -> Unit = {
navigation.sendNavigation(UserlistNavigation.ClickedOnFab)
}
private fun setObservers() {
viewModel.userList.observeNonNull(this) {
it.either(::handleFetchError, ::handleFetchSuccess)
}
viewModel.requestStatus.observeNonNull(this) {
when (it) {
RequestStatus.Ready -> {
fragment_userlist_error_overlay.gone()
fragment_userlist_loading_overlay.gone()
fragment_userlist_empty_overlay.gone()
}
RequestStatus.Loading -> {
fragment_userlist_error_overlay.gone()
fragment_userlist_loading_overlay.visible()
fragment_userlist_empty_overlay.gone()
}
RequestStatus.Errored -> {
fragment_userlist_error_overlay.visible()
fragment_userlist_loading_overlay.gone()
fragment_userlist_empty_overlay.gone()
}
RequestStatus.Empty -> {
fragment_userlist_error_overlay.gone()
fragment_userlist_loading_overlay.gone()
fragment_userlist_empty_overlay.visible()
}
}
}
viewModel.emptyMessage.observeNonNull(this) {
fragment_userlist_empty_overlay.setLabel(it)
}
}
private fun handleFetchError(throwable: Throwable) {
Timber.e(throwable)
}
private fun handleFetchSuccess(users: List<User>) {
listAdapter.addAll(users)
}
private fun setRecycler() {
fragment_userlist_recycler_container.apply {
layoutManager = LinearLayoutManager(context)
adapter = listAdapter.apply {
onItemClick = {
navigation.sendNavigation(UserlistNavigation.ClickedOnUser(it.id, it.realName))
}
onFavoriteClick = {
viewModel.updateUserFavoriteStatus(it.id, !it.isFavorite)
}
}
}
}
}

View File

@@ -1,9 +1,7 @@
package com.hako.userlist.feature
class FavoriteUserlistFragment : UserlistFragment() {
override fun doRequest() {
viewModel.fetchFavoriteUsers()
}
class FavoriteUserlistFragment : BaseUserlistFragment() {
override fun doRequest() = viewModel.fetchFavoriteUsers()
override fun shouldShowFabButton() = false
}

View File

@@ -1,109 +1,7 @@
package com.hako.userlist.feature
import android.os.Bundle
import android.view.LayoutInflater
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.gone
import com.hako.base.extensions.observeNonNull
import com.hako.base.extensions.visible
import com.hako.base.navigation.NavigationRouter
import com.hako.base.navigation.ShowFabButton
import com.hako.userlist.model.UserViewable
import com.hako.userlist.viewmodel.UserlistViewmodel
import com.hako.userlist.widget.UserlistAdapter
import com.hako.friendlist_userlist.R
import com.hako.userlist.navigation.UserlistNavigation
import kotlinx.android.synthetic.main.fragment_userlist.*
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import timber.log.Timber
open class UserlistFragment : Fragment(), ShowFabButton {
val viewModel: UserlistViewmodel by viewModel()
private val listAdapter by lazy { UserlistAdapter() }
private val navigation: NavigationRouter by inject()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View = inflater.inflate(R.layout.fragment_userlist, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setRecycler()
setObservers()
doRequest()
}
open fun doRequest() {
viewModel.fetchUsers()
}
class UserlistFragment : BaseUserlistFragment() {
override fun doRequest() = viewModel.fetchUsers()
override fun shouldShowFabButton() = true
override fun fabButtonPressed(): () -> Unit = {
navigation.sendNavigation(UserlistNavigation.ClickedOnFab)
}
private fun setObservers() {
viewModel.userList.observeNonNull(this) {
it.either(::handleFetchError, ::handleFetchSuccess)
}
viewModel.requestStatus.observeNonNull(this) {
when (it) {
RequestStatus.Ready -> {
fragment_userlist_error_overlay.gone()
fragment_userlist_loading_overlay.gone()
fragment_userlist_empty_overlay.gone()
}
RequestStatus.Loading -> {
fragment_userlist_error_overlay.gone()
fragment_userlist_loading_overlay.visible()
fragment_userlist_empty_overlay.gone()
}
RequestStatus.Errored -> {
fragment_userlist_error_overlay.visible()
fragment_userlist_loading_overlay.gone()
fragment_userlist_empty_overlay.gone()
}
RequestStatus.Empty -> {
fragment_userlist_error_overlay.gone()
fragment_userlist_loading_overlay.gone()
fragment_userlist_empty_overlay.visible()
}
}
}
viewModel.emptyMessage.observeNonNull(this) {
fragment_userlist_empty_overlay.setLabel(it)
}
}
private fun handleFetchError(throwable: Throwable) {
Timber.e(throwable)
}
private fun handleFetchSuccess(users: List<UserViewable>) {
listAdapter.addAll(users)
}
private fun setRecycler() {
fragment_userlist_recycler_container.apply {
layoutManager = LinearLayoutManager(context)
adapter = listAdapter.apply {
onItemClick = {
navigation.sendNavigation(UserlistNavigation.ClickedOnUser(it.id, it.realName))
}
onFavoriteClick = {
viewModel.updateUserFavoriteStatus(it.id, !it.isFavorite)
}
}
}
}
}

View File

@@ -1,12 +1,14 @@
package com.hako.userlist.model
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import com.google.gson.annotations.SerializedName
import com.hako.base.domain.database.entities.UserEntity
import kotlinx.android.parcel.Parcelize
@Parcelize
data class User(
data class UserRemote(
@SerializedName("id") val id: Int,
@SerializedName("name") val realName: String,
@SerializedName("username") val userName: String,
@@ -15,14 +17,30 @@ data class User(
@SerializedName("website") val website: String
) : Parcelable
data class UserViewable(
@Entity(tableName = UserEntity.TABLE_NAME, indices = [Index(value = ["id"], unique = true)])
data class UserEntity(
@PrimaryKey
val id: Int,
val realName: String,
val userName: String,
val email: String,
val phone: String,
val website: String,
val isFavorite: Boolean = false
) {
companion object {
const val TABLE_NAME = "users"
}
}
data class User(
val id: Int,
val realName: String,
val userName: String,
var isFavorite: Boolean
)
fun User.toUserEntity() = UserEntity(this.id, this.realName, this.userName, this.email, this.phone, this.website)
fun UserRemote.toUserEntity() = UserEntity(this.id, this.realName, this.userName, this.email, this.phone, this.website)
fun UserEntity.toUserViewable() = UserViewable(this.id, this.realName, this.userName, this.isFavorite)
fun UserEntity.toUserViewable() = User(this.id, this.realName, this.userName, this.isFavorite)

View File

@@ -8,13 +8,13 @@ import com.hako.base.domain.network.RequestStatus.*
import com.hako.userlist.domain.usecase.GetFavoriteUsers
import com.hako.userlist.domain.usecase.GetUsers
import com.hako.userlist.domain.usecase.SetFavoriteStatus
import com.hako.userlist.model.UserViewable
import com.hako.userlist.model.User
import org.koin.core.KoinComponent
import org.koin.core.get
class UserlistViewmodel : ViewModel(), KoinComponent {
val userList = MutableLiveData<Either<Throwable, List<UserViewable>>>()
val userList = MutableLiveData<Either<Throwable, List<User>>>()
val favoriteError = MutableLiveData<Int>()
val emptyMessage = MutableLiveData<String>()
val requestStatus = MutableLiveData<RequestStatus>()

View File

@@ -5,20 +5,20 @@ import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.hako.base.extensions.autoNotify
import com.hako.userlist.model.UserViewable
import com.hako.friendlist_userlist.R
import com.hako.userlist.model.User
import com.hako.userlist.R
import kotlinx.android.synthetic.main.item_user_card.view.*
import kotlin.properties.Delegates
class UserlistAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var items by Delegates.observable(emptyList<UserViewable>()) { _, oldList, newList ->
private var items by Delegates.observable(emptyList<User>()) { _, oldList, newList ->
autoNotify(oldList, newList) { old, new -> old.id == new.id }
notifyDataSetChanged()
}
var onItemClick: (UserViewable) -> Unit = { }
var onFavoriteClick: (UserViewable) -> Unit = { }
var onItemClick: (User) -> Unit = { }
var onFavoriteClick: (User) -> Unit = { }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder =
UserViewHolder(
@@ -31,7 +31,7 @@ class UserlistAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
fun getItem(position: Int) = items[position]
fun addAll(list: List<UserViewable>) {
fun addAll(list: List<User>) {
items = list
}
@@ -46,12 +46,12 @@ class UserlistAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
class UserViewHolder(
private val view: View,
private val onItemClick: (UserViewable) -> Unit,
private val onFavoriteClick: (UserViewable) -> Unit
private val onItemClick: (User) -> Unit,
private val onFavoriteClick: (User) -> Unit
) :
RecyclerView.ViewHolder(view) {
fun bind(user: UserViewable) = with(view) {
fun bind(user: User) = with(view) {
item_user_card_real_name.text = user.realName
item_user_card_user_name.text = user.userName
if (user.isFavorite) {

View File

@@ -3,6 +3,7 @@ ext.deps = [:]
def versions = [:]
versions.kotlin = "1.3.41"
versions.gradle = "3.5.3"
versions.room = "2.2.3"
def deps = [:]
@@ -22,6 +23,11 @@ kotlin.version = "$versions.kotlin"
kotlin.std_lib = "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$versions.kotlin"
deps.kotlin = kotlin
def room = [:]
room.runtime = "androidx.room:room-runtime:$versions.room"
room.compiler = "androidx.room:room-compiler:$versions.room"
deps.room = room
ext.deps = deps
static def addRepos(RepositoryHandler handler) {