mirror of
https://github.com/imcarlost/Friendlists.git
synced 2026-04-10 02:46:54 -04:00
Merge pull request #8 from hakodeveloper/feature/favorites
Implement Favorites
This commit is contained in:
@@ -11,7 +11,8 @@
|
|||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="false"
|
android:supportsRtl="false"
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppTheme"
|
||||||
|
android:hardwareAccelerated="true">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".view.MainActivity"
|
android:name=".view.MainActivity"
|
||||||
|
|||||||
@@ -4,18 +4,21 @@ import android.os.Bundle
|
|||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.navigation.findNavController
|
import androidx.navigation.findNavController
|
||||||
import androidx.navigation.ui.NavigationUI.setupActionBarWithNavController
|
import androidx.navigation.ui.NavigationUI.setupActionBarWithNavController
|
||||||
import com.hako.base.extensions.observeNonNull
|
import com.hako.base.extensions.*
|
||||||
import com.hako.base.navigation.NavigationRouter
|
import com.hako.base.navigation.NavigationRouter
|
||||||
|
import com.hako.base.navigation.ShowFabButton
|
||||||
import com.hako.friendlists.BuildConfig
|
import com.hako.friendlists.BuildConfig
|
||||||
import com.hako.friendlists.R
|
import com.hako.friendlists.R
|
||||||
import com.hako.friendlists.viewmodel.NavigationViewmodel
|
import com.hako.friendlists.viewmodel.NavigationViewmodel
|
||||||
import com.squareup.picasso.Picasso
|
import com.squareup.picasso.Picasso
|
||||||
|
import kotlinx.android.synthetic.main.activity_main.*
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private val navController by lazy { findNavController(R.id.main_fragment_container) }
|
private val navController by lazy { findNavController(R.id.main_fragment_container) }
|
||||||
|
private val navHostFragment by lazy { findNavHostFragment(R.id.main_fragment_container) }
|
||||||
private val navRouter: NavigationRouter by inject()
|
private val navRouter: NavigationRouter by inject()
|
||||||
private val picasso: Picasso by inject()
|
private val picasso: Picasso by inject()
|
||||||
private val viewModel: NavigationViewmodel by viewModel()
|
private val viewModel: NavigationViewmodel by viewModel()
|
||||||
@@ -39,6 +42,28 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setupActionBarWithNavController(this, navController)
|
setupActionBarWithNavController(this, navController)
|
||||||
|
|
||||||
|
navHostFragment.registerOnFragmentViewCreated { currentFragment ->
|
||||||
|
initializeViews()
|
||||||
|
when (currentFragment) {
|
||||||
|
is ShowFabButton -> fabButtonBehaviour(currentFragment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initializeViews() {
|
||||||
|
main_fragment_fab.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fabButtonBehaviour(currentFragment: ShowFabButton) {
|
||||||
|
main_fragment_fab.setOnClickListener {
|
||||||
|
currentFragment.fabButtonPressed().invoke()
|
||||||
|
}
|
||||||
|
if (currentFragment.shouldShowFabButton()) {
|
||||||
|
main_fragment_fab.show()
|
||||||
|
} else {
|
||||||
|
main_fragment_fab.hide()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupPicasso() {
|
private fun setupPicasso() {
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ class NavigationViewmodel : ViewModel() {
|
|||||||
putString(FRAGMENT_TITLE, event.userName)
|
putString(FRAGMENT_TITLE, event.userName)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
is UserlistNavigation.ClickedOnFab -> navigate.postValue(
|
||||||
|
buildNavigation(R.id.action_userlistFragment_to_favoriteUserlistFragment)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,4 +15,14 @@
|
|||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
|
android:id="@+id/main_fragment_fab"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="end|bottom"
|
||||||
|
android:layout_margin="24dp"
|
||||||
|
android:src="@drawable/ic_heart"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<color name="colorPrimary">#37474F</color>
|
<color name="colorPrimary">#37474F</color>
|
||||||
<color name="colorPrimaryDark">#324047</color>
|
<color name="colorPrimaryDark">#324047</color>
|
||||||
<color name="colorAccent">#CBCFD1</color>
|
<color name="colorAccent">#D32F2F</color>
|
||||||
<color name="colorRed">#D32F2F</color>
|
<color name="colorRed">#D32F2F</color>
|
||||||
<color name="colorDarkGray">#333333</color>
|
<color name="colorDarkGray">#333333</color>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -15,12 +15,18 @@ interface UserDao {
|
|||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
fun saveAll(entities: List<UserEntity>)
|
fun saveAll(entities: List<UserEntity>)
|
||||||
|
|
||||||
|
@Query("UPDATE ${UserEntity.TABLE_NAME} SET isFavorite = :favorite WHERE id = :id")
|
||||||
|
fun saveFavorite(id: Int, favorite: Boolean): Int
|
||||||
|
|
||||||
@get:Query("SELECT * FROM ${UserEntity.TABLE_NAME}")
|
@get:Query("SELECT * FROM ${UserEntity.TABLE_NAME}")
|
||||||
val all: List<UserEntity>
|
val all: List<UserEntity>
|
||||||
|
|
||||||
@Query("SELECT * FROM ${UserEntity.TABLE_NAME}")
|
@Query("SELECT * FROM ${UserEntity.TABLE_NAME} ORDER BY id ASC")
|
||||||
fun getAllUsers(): List<UserEntity>
|
fun getAllUsers(): List<UserEntity>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM ${UserEntity.TABLE_NAME} WHERE isFavorite = 1 ORDER BY id ASC")
|
||||||
|
fun getFavoriteUsers(): List<UserEntity>
|
||||||
|
|
||||||
@Query("SELECT COUNT(*) FROM ${UserEntity.TABLE_NAME}")
|
@Query("SELECT COUNT(*) FROM ${UserEntity.TABLE_NAME}")
|
||||||
fun count(): Int
|
fun count(): Int
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ data class UserEntity(
|
|||||||
val userName: String,
|
val userName: String,
|
||||||
val email: String,
|
val email: String,
|
||||||
val phone: String,
|
val phone: String,
|
||||||
val website: String
|
val website: String,
|
||||||
|
val isFavorite: Boolean = true
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
const val TABLE_NAME = "users"
|
const val TABLE_NAME = "users"
|
||||||
|
|||||||
@@ -4,4 +4,5 @@ sealed class RequestStatus {
|
|||||||
object Ready : RequestStatus()
|
object Ready : RequestStatus()
|
||||||
object Loading : RequestStatus()
|
object Loading : RequestStatus()
|
||||||
object Errored : RequestStatus()
|
object Errored : RequestStatus()
|
||||||
|
object Empty : RequestStatus()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,3 +11,5 @@ fun <T> LiveData<T>.observeNonNull(owner: LifecycleOwner, func: (T) -> Unit) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Int.wasUpdated() = this > 0
|
||||||
@@ -1,6 +1,32 @@
|
|||||||
package com.hako.base.extensions
|
package com.hako.base.extensions
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
import androidx.annotation.IdRes
|
import androidx.annotation.IdRes
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import androidx.navigation.fragment.NavHostFragment
|
||||||
|
|
||||||
|
fun AppCompatActivity.findNavHostFragment(@IdRes id: Int) =
|
||||||
|
supportFragmentManager.findFragmentById(id) as NavHostFragment
|
||||||
|
|
||||||
|
fun NavHostFragment.registerOnFragmentViewCreated(
|
||||||
|
recursive: Boolean = true,
|
||||||
|
listener: (currentFragment: Fragment) -> Unit
|
||||||
|
) {
|
||||||
|
childFragmentManager
|
||||||
|
.registerFragmentLifecycleCallbacks(object : FragmentManager.FragmentLifecycleCallbacks() {
|
||||||
|
override fun onFragmentViewCreated(
|
||||||
|
fm: FragmentManager,
|
||||||
|
f: Fragment,
|
||||||
|
v: View,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
) {
|
||||||
|
super.onFragmentViewCreated(fm, f, v, savedInstanceState)
|
||||||
|
listener(f)
|
||||||
|
}
|
||||||
|
}, recursive)
|
||||||
|
}
|
||||||
|
|
||||||
fun buildNavigation(@IdRes id: Int, bundle: Bundle = Bundle()) = Pair(id, bundle)
|
fun buildNavigation(@IdRes id: Int, bundle: Bundle = Bundle()) = Pair(id, bundle)
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.hako.base.navigation
|
||||||
|
|
||||||
|
interface ShowFabButton {
|
||||||
|
fun shouldShowFabButton(): Boolean
|
||||||
|
|
||||||
|
fun fabButtonPressed(): () -> Unit = {}
|
||||||
|
}
|
||||||
23
base/src/main/java/com/hako/base/widgets/EmptyOverlay.kt
Normal file
23
base/src/main/java/com/hako/base/widgets/EmptyOverlay.kt
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package com.hako.base.widgets
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import com.hako.base.R
|
||||||
|
import com.hako.base.extensions.inflate
|
||||||
|
import kotlinx.android.synthetic.main.empty_overlay.view.*
|
||||||
|
|
||||||
|
class EmptyOverlay @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = 0
|
||||||
|
) : FrameLayout(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
|
init {
|
||||||
|
inflate(R.layout.empty_overlay, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setLabel(message: String) {
|
||||||
|
empty_overlay_label.text = message
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ import kotlinx.android.synthetic.main.like_button.view.*
|
|||||||
private const val LIKE_MIN_FRAME = 0
|
private const val LIKE_MIN_FRAME = 0
|
||||||
private const val LIKE_MAX_FRAME = 28
|
private const val LIKE_MAX_FRAME = 28
|
||||||
private const val LIKE_ANIM_SPEED = 1f
|
private const val LIKE_ANIM_SPEED = 1f
|
||||||
private const val DISLIKE_MIN_FRAME = 28
|
private const val DISLIKE_MIN_FRAME = 29
|
||||||
private const val DISLIKE_MAX_FRAME = 70
|
private const val DISLIKE_MAX_FRAME = 70
|
||||||
private const val DISLIKE_ANIM_SPEED = 2f
|
private const val DISLIKE_ANIM_SPEED = 2f
|
||||||
|
|
||||||
@@ -29,16 +29,24 @@ class LikeButton @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun dislike() {
|
fun dislike() {
|
||||||
like_button_animation_view.frame = LIKE_MIN_FRAME
|
like_button_animation_view.frame = DISLIKE_MAX_FRAME
|
||||||
}
|
}
|
||||||
|
|
||||||
fun playLike() {
|
fun play() {
|
||||||
|
if (like_button_animation_view.frame <= LIKE_MAX_FRAME){
|
||||||
|
playDislike()
|
||||||
|
} else {
|
||||||
|
playLike()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun playLike() {
|
||||||
like_button_animation_view.setMinAndMaxFrame(LIKE_MIN_FRAME, LIKE_MAX_FRAME)
|
like_button_animation_view.setMinAndMaxFrame(LIKE_MIN_FRAME, LIKE_MAX_FRAME)
|
||||||
like_button_animation_view.speed = LIKE_ANIM_SPEED
|
like_button_animation_view.speed = LIKE_ANIM_SPEED
|
||||||
like_button_animation_view.playAnimation()
|
like_button_animation_view.playAnimation()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun playDislike() {
|
private fun playDislike() {
|
||||||
like_button_animation_view.setMinAndMaxFrame(DISLIKE_MIN_FRAME, DISLIKE_MAX_FRAME)
|
like_button_animation_view.setMinAndMaxFrame(DISLIKE_MIN_FRAME, DISLIKE_MAX_FRAME)
|
||||||
like_button_animation_view.speed = DISLIKE_ANIM_SPEED
|
like_button_animation_view.speed = DISLIKE_ANIM_SPEED
|
||||||
like_button_animation_view.playAnimation()
|
like_button_animation_view.playAnimation()
|
||||||
|
|||||||
BIN
base/src/main/res/drawable-hdpi/ic_heart.png
Normal file
BIN
base/src/main/res/drawable-hdpi/ic_heart.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 778 B |
BIN
base/src/main/res/drawable-mdpi/ic_heart.png
Normal file
BIN
base/src/main/res/drawable-mdpi/ic_heart.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 457 B |
BIN
base/src/main/res/drawable-xhdpi/ic_heart.png
Normal file
BIN
base/src/main/res/drawable-xhdpi/ic_heart.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 890 B |
BIN
base/src/main/res/drawable-xxhdpi/ic_heart.png
Normal file
BIN
base/src/main/res/drawable-xxhdpi/ic_heart.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1023 B |
BIN
base/src/main/res/drawable-xxxhdpi/ic_heart.png
Normal file
BIN
base/src/main/res/drawable-xxxhdpi/ic_heart.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
33
base/src/main/res/layout/empty_overlay.xml
Normal file
33
base/src/main/res/layout/empty_overlay.xml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:background="@color/soft_background">
|
||||||
|
|
||||||
|
<com.airbnb.lottie.LottieAnimationView
|
||||||
|
android:id="@+id/empty_overlay_animation_view"
|
||||||
|
android:layout_width="200dp"
|
||||||
|
android:layout_height="200dp"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintVertical_bias="0.39"
|
||||||
|
app:lottie_autoPlay="true"
|
||||||
|
app:lottie_loop="true"
|
||||||
|
app:lottie_rawRes="@raw/empty_animation" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/empty_overlay_label"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="No tienes favoritos!"
|
||||||
|
android:textSize="24sp"
|
||||||
|
app:layout_constraintEnd_toEndOf="@+id/empty_overlay_animation_view"
|
||||||
|
app:layout_constraintStart_toStartOf="@+id/empty_overlay_animation_view"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/empty_overlay_animation_view" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
android:background="@color/soft_background">
|
android:background="@color/soft_background">
|
||||||
|
|
||||||
<com.airbnb.lottie.LottieAnimationView
|
<com.airbnb.lottie.LottieAnimationView
|
||||||
android:id="@+id/loading_overlay_animation_view"
|
android:id="@+id/network_error_overlay_animation_view"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:scaleType="centerInside"
|
android:scaleType="centerInside"
|
||||||
@@ -20,14 +20,14 @@
|
|||||||
app:lottie_rawRes="@raw/network_animation" />
|
app:lottie_rawRes="@raw/network_animation" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/textView"
|
android:id="@+id/network_error_overlay_label"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="16dp"
|
android:layout_marginTop="16dp"
|
||||||
android:text="Network error!"
|
android:text="Network error!"
|
||||||
android:textSize="24sp"
|
android:textSize="24sp"
|
||||||
app:layout_constraintEnd_toEndOf="@+id/loading_overlay_animation_view"
|
app:layout_constraintEnd_toEndOf="@+id/network_error_overlay_animation_view"
|
||||||
app:layout_constraintStart_toStartOf="@+id/loading_overlay_animation_view"
|
app:layout_constraintStart_toStartOf="@+id/network_error_overlay_animation_view"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/loading_overlay_animation_view" />
|
app:layout_constraintTop_toBottomOf="@+id/network_error_overlay_animation_view" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
1
base/src/main/res/raw/empty_animation.json
Normal file
1
base/src/main/res/raw/empty_animation.json
Normal file
File diff suppressed because one or more lines are too long
@@ -2,7 +2,9 @@ package com.hako.userlist.di
|
|||||||
|
|
||||||
import com.hako.base.domain.network.RemoteClient
|
import com.hako.base.domain.network.RemoteClient
|
||||||
import com.hako.userlist.domain.datasource.UserlistRemoteApi
|
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.GetUsers
|
||||||
|
import com.hako.userlist.domain.usecase.SetFavoriteStatus
|
||||||
import com.hako.userlist.viewmodel.UserlistViewmodel
|
import com.hako.userlist.viewmodel.UserlistViewmodel
|
||||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
@@ -10,6 +12,8 @@ import org.koin.dsl.module
|
|||||||
val userlistModules = module {
|
val userlistModules = module {
|
||||||
factory { get<RemoteClient>().getClient(UserlistRemoteApi::class.java) }
|
factory { get<RemoteClient>().getClient(UserlistRemoteApi::class.java) }
|
||||||
factory { GetUsers(get()) }
|
factory { GetUsers(get()) }
|
||||||
|
factory { GetFavoriteUsers(get()) }
|
||||||
|
factory { SetFavoriteStatus(get()) }
|
||||||
|
|
||||||
viewModel { UserlistViewmodel() }
|
viewModel { UserlistViewmodel() }
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.hako.userlist.domain.usecase
|
||||||
|
|
||||||
|
import com.hako.base.domain.database.dao.UserDao
|
||||||
|
import com.hako.userlist.model.UserViewable
|
||||||
|
import com.hako.userlist.model.toUserViewable
|
||||||
|
import io.reactivex.Single
|
||||||
|
import io.reactivex.schedulers.Schedulers
|
||||||
|
|
||||||
|
class GetFavoriteUsers(private val dao: UserDao) {
|
||||||
|
|
||||||
|
fun execute(
|
||||||
|
onSuccess: (List<UserViewable>) -> Unit,
|
||||||
|
onEmpty: () -> Unit,
|
||||||
|
onError: (Throwable) -> Unit
|
||||||
|
) {
|
||||||
|
Single.fromCallable { dao.getFavoriteUsers() }
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.doOnSuccess { dbUsers ->
|
||||||
|
if (dbUsers.isEmpty()) {
|
||||||
|
onEmpty()
|
||||||
|
} else {
|
||||||
|
onSuccess(dbUsers.map { it.toUserViewable() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.doOnError { onError(it) }
|
||||||
|
.onErrorReturn { emptyList() }
|
||||||
|
.subscribe()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,8 @@ class GetUsers(private val dao: UserDao) : KoinComponent {
|
|||||||
fun execute(
|
fun execute(
|
||||||
onSuccess: (List<UserViewable>) -> Unit,
|
onSuccess: (List<UserViewable>) -> Unit,
|
||||||
onError: (Throwable) -> Unit,
|
onError: (Throwable) -> Unit,
|
||||||
onLoading: () -> Unit
|
onLoading: () -> Unit,
|
||||||
|
onEmpty: () -> Unit
|
||||||
) {
|
) {
|
||||||
Single.fromCallable { dao.getAllUsers() }
|
Single.fromCallable { dao.getAllUsers() }
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
@@ -26,9 +27,13 @@ class GetUsers(private val dao: UserDao) : KoinComponent {
|
|||||||
if (dbUsers.isEmpty()) {
|
if (dbUsers.isEmpty()) {
|
||||||
api.getUsers()
|
api.getUsers()
|
||||||
.doOnSuccess {
|
.doOnSuccess {
|
||||||
|
if (it.isEmpty()) {
|
||||||
|
onEmpty()
|
||||||
|
} else {
|
||||||
dao.saveAll(it.map { user -> user.toUserEntity() })
|
dao.saveAll(it.map { user -> user.toUserEntity() })
|
||||||
onSuccess(dao.getAllUsers().map { user -> user.toUserViewable() })
|
onSuccess(dao.getAllUsers().map { user -> user.toUserViewable() })
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.doOnSubscribe { onLoading() }
|
.doOnSubscribe { onLoading() }
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.subscribe({}, { onError(it) })
|
.subscribe({}, { onError(it) })
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.hako.userlist.domain.usecase
|
||||||
|
|
||||||
|
import com.hako.base.domain.database.dao.UserDao
|
||||||
|
import io.reactivex.Single
|
||||||
|
import io.reactivex.schedulers.Schedulers
|
||||||
|
|
||||||
|
class SetFavoriteStatus(private val dao: UserDao) {
|
||||||
|
|
||||||
|
fun execute(
|
||||||
|
userId: Int,
|
||||||
|
favoriteStatus: Boolean,
|
||||||
|
onError: (userId: Int) -> Unit
|
||||||
|
) {
|
||||||
|
Single.fromCallable { dao.saveFavorite(userId, favoriteStatus) }
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.doOnError { onError(userId) }
|
||||||
|
.subscribe()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.hako.userlist.feature
|
||||||
|
|
||||||
|
class FavoriteUserlistFragment : UserlistFragment() {
|
||||||
|
override fun doRequest() {
|
||||||
|
viewModel.fetchFavoriteUsers()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun shouldShowFabButton() = false
|
||||||
|
}
|
||||||
@@ -9,9 +9,9 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||||||
import com.hako.base.domain.network.RequestStatus
|
import com.hako.base.domain.network.RequestStatus
|
||||||
import com.hako.base.extensions.gone
|
import com.hako.base.extensions.gone
|
||||||
import com.hako.base.extensions.observeNonNull
|
import com.hako.base.extensions.observeNonNull
|
||||||
import com.hako.base.extensions.toast
|
|
||||||
import com.hako.base.extensions.visible
|
import com.hako.base.extensions.visible
|
||||||
import com.hako.base.navigation.NavigationRouter
|
import com.hako.base.navigation.NavigationRouter
|
||||||
|
import com.hako.base.navigation.ShowFabButton
|
||||||
import com.hako.userlist.model.UserViewable
|
import com.hako.userlist.model.UserViewable
|
||||||
import com.hako.userlist.viewmodel.UserlistViewmodel
|
import com.hako.userlist.viewmodel.UserlistViewmodel
|
||||||
import com.hako.userlist.widget.UserlistAdapter
|
import com.hako.userlist.widget.UserlistAdapter
|
||||||
@@ -22,9 +22,9 @@ import org.koin.android.ext.android.inject
|
|||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
class UserlistFragment : Fragment() {
|
open class UserlistFragment : Fragment(), ShowFabButton {
|
||||||
|
|
||||||
private val viewModel: UserlistViewmodel by viewModel()
|
val viewModel: UserlistViewmodel by viewModel()
|
||||||
private val listAdapter by lazy { UserlistAdapter() }
|
private val listAdapter by lazy { UserlistAdapter() }
|
||||||
private val navigation: NavigationRouter by inject()
|
private val navigation: NavigationRouter by inject()
|
||||||
|
|
||||||
@@ -36,11 +36,21 @@ class UserlistFragment : Fragment() {
|
|||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
setRecycler()
|
setRecycler()
|
||||||
setObservers()
|
setObservers()
|
||||||
|
doRequest()
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun doRequest() {
|
||||||
viewModel.fetchUsers()
|
viewModel.fetchUsers()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun shouldShowFabButton() = true
|
||||||
|
|
||||||
|
override fun fabButtonPressed(): () -> Unit = {
|
||||||
|
navigation.sendNavigation(UserlistNavigation.ClickedOnFab)
|
||||||
|
}
|
||||||
|
|
||||||
private fun setObservers() {
|
private fun setObservers() {
|
||||||
viewModel.data.observeNonNull(this) {
|
viewModel.userList.observeNonNull(this) {
|
||||||
it.either(::handleFetchError, ::handleFetchSuccess)
|
it.either(::handleFetchError, ::handleFetchSuccess)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,17 +59,29 @@ class UserlistFragment : Fragment() {
|
|||||||
RequestStatus.Ready -> {
|
RequestStatus.Ready -> {
|
||||||
fragment_userlist_error_overlay.gone()
|
fragment_userlist_error_overlay.gone()
|
||||||
fragment_userlist_loading_overlay.gone()
|
fragment_userlist_loading_overlay.gone()
|
||||||
|
fragment_userlist_empty_overlay.gone()
|
||||||
}
|
}
|
||||||
RequestStatus.Loading -> {
|
RequestStatus.Loading -> {
|
||||||
fragment_userlist_error_overlay.gone()
|
fragment_userlist_error_overlay.gone()
|
||||||
fragment_userlist_loading_overlay.visible()
|
fragment_userlist_loading_overlay.visible()
|
||||||
|
fragment_userlist_empty_overlay.gone()
|
||||||
}
|
}
|
||||||
RequestStatus.Errored -> {
|
RequestStatus.Errored -> {
|
||||||
fragment_userlist_error_overlay.visible()
|
fragment_userlist_error_overlay.visible()
|
||||||
fragment_userlist_loading_overlay.gone()
|
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) {
|
private fun handleFetchError(throwable: Throwable) {
|
||||||
@@ -79,7 +101,7 @@ class UserlistFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onFavoriteClick = {
|
onFavoriteClick = {
|
||||||
context.toast(it.userName)
|
viewModel.updateUserFavoriteStatus(it.id, !it.isFavorite)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,10 +19,10 @@ data class UserViewable(
|
|||||||
val id: Int,
|
val id: Int,
|
||||||
val realName: String,
|
val realName: String,
|
||||||
val userName: String,
|
val userName: String,
|
||||||
var isFavorite: Boolean = false
|
var isFavorite: Boolean
|
||||||
)
|
)
|
||||||
|
|
||||||
fun User.toUserEntity() = UserEntity(this.id, this.realName, this.userName, this.email, this.phone, this.website)
|
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)
|
fun UserEntity.toUserViewable() = UserViewable(this.id, this.realName, this.userName, this.isFavorite)
|
||||||
|
|
||||||
|
|||||||
@@ -4,4 +4,5 @@ import com.hako.base.navigation.NavigationEvent
|
|||||||
|
|
||||||
sealed class UserlistNavigation : NavigationEvent {
|
sealed class UserlistNavigation : NavigationEvent {
|
||||||
data class ClickedOnUser(val userId: Int, val userName: String) : UserlistNavigation()
|
data class ClickedOnUser(val userId: Int, val userName: String) : UserlistNavigation()
|
||||||
|
object ClickedOnFab : UserlistNavigation()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,34 +3,65 @@ package com.hako.userlist.viewmodel
|
|||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import com.hako.base.domain.network.RequestStatus
|
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.base.domain.Either
|
||||||
|
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.GetUsers
|
||||||
|
import com.hako.userlist.domain.usecase.SetFavoriteStatus
|
||||||
import com.hako.userlist.model.UserViewable
|
import com.hako.userlist.model.UserViewable
|
||||||
import org.koin.core.KoinComponent
|
import org.koin.core.KoinComponent
|
||||||
import org.koin.core.get
|
import org.koin.core.get
|
||||||
|
|
||||||
class UserlistViewmodel : ViewModel(), KoinComponent {
|
class UserlistViewmodel : ViewModel(), KoinComponent {
|
||||||
|
|
||||||
val data = MutableLiveData<Either<Throwable, List<UserViewable>>>()
|
val userList = MutableLiveData<Either<Throwable, List<UserViewable>>>()
|
||||||
|
val favoriteError = MutableLiveData<Int>()
|
||||||
|
val emptyMessage = MutableLiveData<String>()
|
||||||
val requestStatus = MutableLiveData<RequestStatus>()
|
val requestStatus = MutableLiveData<RequestStatus>()
|
||||||
|
|
||||||
private val getUsers: GetUsers = get()
|
private val getUsers: GetUsers = get()
|
||||||
|
private val getFavoriteUsers: GetFavoriteUsers = get()
|
||||||
|
private val setFavoriteStatus: SetFavoriteStatus = get()
|
||||||
|
|
||||||
fun fetchUsers() {
|
fun fetchUsers() {
|
||||||
getUsers.execute(
|
getUsers.execute(
|
||||||
onSuccess = {
|
onSuccess = {
|
||||||
requestStatus.postValue(Ready)
|
requestStatus.postValue(Ready)
|
||||||
data.postValue(Either.Right(it))
|
userList.postValue(Either.Right(it))
|
||||||
},
|
},
|
||||||
onLoading = {
|
onLoading = {
|
||||||
requestStatus.postValue(Loading)
|
requestStatus.postValue(Loading)
|
||||||
},
|
},
|
||||||
onError = {
|
onError = {
|
||||||
requestStatus.postValue(Errored)
|
requestStatus.postValue(Errored)
|
||||||
data.postValue(Either.Left(it))
|
userList.postValue(Either.Left(it))
|
||||||
|
},
|
||||||
|
onEmpty = {
|
||||||
|
requestStatus.postValue(Empty)
|
||||||
|
emptyMessage.postValue("No se encontró ningún usuario")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fetchFavoriteUsers() {
|
||||||
|
getFavoriteUsers.execute(
|
||||||
|
onSuccess = {
|
||||||
|
requestStatus.postValue(Ready)
|
||||||
|
userList.postValue(Either.Right(it))
|
||||||
|
},
|
||||||
|
onError = {
|
||||||
|
requestStatus.postValue(Errored)
|
||||||
|
userList.postValue(Either.Left(it))
|
||||||
|
},
|
||||||
|
onEmpty = {
|
||||||
|
requestStatus.postValue(Empty)
|
||||||
|
emptyMessage.postValue("No tienes favoritos!")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateUserFavoriteStatus(userId: Int, status: Boolean) {
|
||||||
|
setFavoriteStatus.execute(userId, status,
|
||||||
|
onError = {
|
||||||
|
favoriteError.postValue(it)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,18 +44,26 @@ class UserlistAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class UserViewHolder(private val view: View,
|
class UserViewHolder(
|
||||||
|
private val view: View,
|
||||||
private val onItemClick: (UserViewable) -> Unit,
|
private val onItemClick: (UserViewable) -> Unit,
|
||||||
private val onFavoriteClick: (UserViewable) -> Unit) :
|
private val onFavoriteClick: (UserViewable) -> Unit
|
||||||
|
) :
|
||||||
RecyclerView.ViewHolder(view) {
|
RecyclerView.ViewHolder(view) {
|
||||||
|
|
||||||
fun bind(user: UserViewable) = with(view) {
|
fun bind(user: UserViewable) = with(view) {
|
||||||
item_user_card_real_name.text = user.realName
|
item_user_card_real_name.text = user.realName
|
||||||
item_user_card_user_name.text = user.userName
|
item_user_card_user_name.text = user.userName
|
||||||
|
if (user.isFavorite) {
|
||||||
|
item_user_card_like_button.like()
|
||||||
|
} else {
|
||||||
|
item_user_card_like_button.dislike()
|
||||||
|
}
|
||||||
item_user_card_container.setOnClickListener {
|
item_user_card_container.setOnClickListener {
|
||||||
onItemClick(user)
|
onItemClick(user)
|
||||||
}
|
}
|
||||||
item_user_card_like_button.setOnClickListener {
|
item_user_card_like_button.setOnClickListener {
|
||||||
|
item_user_card_like_button.play()
|
||||||
onFavoriteClick(user)
|
onFavoriteClick(user)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,4 +35,14 @@
|
|||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<com.hako.base.widgets.EmptyOverlay
|
||||||
|
android:id="@+id/fragment_userlist_empty_overlay"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -9,6 +9,20 @@
|
|||||||
android:id="@+id/userlistFragment"
|
android:id="@+id/userlistFragment"
|
||||||
android:name="com.hako.userlist.feature.UserlistFragment"
|
android:name="com.hako.userlist.feature.UserlistFragment"
|
||||||
tools:layout="@layout/fragment_userlist"
|
tools:layout="@layout/fragment_userlist"
|
||||||
android:label="Friendlist" />
|
android:label="Friendlist" >
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_userlistFragment_to_favoriteUserlistFragment"
|
||||||
|
app:destination="@id/favoriteUserlistFragment"
|
||||||
|
app:enterAnim="@anim/slide_in_right"
|
||||||
|
app:exitAnim="@anim/slide_out_left"
|
||||||
|
app:popEnterAnim="@anim/slide_in_left"
|
||||||
|
app:popExitAnim="@anim/slide_out_right"/>
|
||||||
|
</fragment>
|
||||||
|
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/favoriteUserlistFragment"
|
||||||
|
android:name="com.hako.userlist.feature.FavoriteUserlistFragment"
|
||||||
|
tools:layout="@layout/fragment_userlist"
|
||||||
|
android:label="Favoritos" />
|
||||||
|
|
||||||
</navigation>
|
</navigation>
|
||||||
Reference in New Issue
Block a user