implement the album feature

This commit is contained in:
Carlos Martinez
2020-02-03 23:01:38 -03:00
parent 1d752803d7
commit 2411c1c84d
24 changed files with 367 additions and 98 deletions

View File

@@ -1,24 +0,0 @@
package com.hako.albumlist
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.hako.albumlist.test", appContext.packageName)
}
}

View File

@@ -0,0 +1,15 @@
package com.hako.albumlist.di
import com.hako.albumlist.domain.datasource.AlbumlistRemoteApi
import com.hako.albumlist.domain.usecase.GetAlbum
import com.hako.albumlist.viewmodel.AlbumlistViewmodel
import com.hako.base.domain.network.RemoteClient
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
val albumListModules = module {
factory { get<RemoteClient>().getClient(AlbumlistRemoteApi::class.java) }
factory { GetAlbum(get()) }
viewModel { AlbumlistViewmodel() }
}

View File

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

View File

@@ -0,0 +1,42 @@
package com.hako.albumlist.domain.usecase
import com.hako.albumlist.domain.datasource.AlbumlistRemoteApi
import com.hako.albumlist.model.AlbumViewable
import com.hako.albumlist.model.toAlbumEntity
import com.hako.albumlist.model.toUserViewable
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()
fun execute(
userId: Int,
onSuccess: (List<AlbumViewable>) -> Unit,
onError: (Throwable) -> Unit,
onLoading: () -> Unit
) {
Single.fromCallable { dao.getAlbums(userId) }
.subscribeOn(Schedulers.io())
.doOnError { onError(it) }
.doOnSuccess { dbAlbum ->
if (dbAlbum.isEmpty() || dbAlbum.count() == 0) {
api.getAlbums(userId)
.doOnSuccess {
dao.saveAll(it.map { album -> album.toAlbumEntity() })
onSuccess(dao.getAlbums(userId).map { album -> album.toUserViewable() })
}
.doOnSubscribe { onLoading() }
.subscribeOn(Schedulers.io())
.subscribe({}, { onError(it) })
} else {
onSuccess(dbAlbum.map { it.toUserViewable() })
}
}
.subscribe()
}
}

View File

@@ -0,0 +1,81 @@
package com.hako.albumlist.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.albumlist.R
import com.hako.albumlist.model.AlbumViewable
import com.hako.albumlist.viewmodel.AlbumlistViewmodel
import com.hako.albumlist.widget.AlbumlistAdapter
import com.hako.base.domain.network.RequestStatus
import com.hako.base.extensions.gone
import com.hako.base.extensions.observeNonNull
import com.hako.base.extensions.toast
import com.hako.base.extensions.visible
import kotlinx.android.synthetic.main.fragment_albumlist.*
import org.koin.androidx.viewmodel.ext.android.viewModel
import timber.log.Timber
class AlbumlistFragment : Fragment() {
private val viewModel: AlbumlistViewmodel by viewModel()
private val listAdapter by lazy { AlbumlistAdapter() }
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View = inflater.inflate(R.layout.fragment_albumlist, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setRecycler()
setObservers()
//TODO: Get user by bundle
viewModel.fetchAlbums(2)
}
private fun setObservers() {
viewModel.data.observeNonNull(this) {
it.either(::handleFetchError, ::handleFetchSuccess)
}
viewModel.requestStatus.observeNonNull(this) {
when (it) {
RequestStatus.Ready -> {
fragment_albumlist_error_overlay.gone()
fragment_albumlist_loading_overlay.gone()
}
RequestStatus.Loading -> {
fragment_albumlist_error_overlay.gone()
fragment_albumlist_loading_overlay.visible()
}
RequestStatus.Errored -> {
fragment_albumlist_error_overlay.visible()
fragment_albumlist_loading_overlay.gone()
}
}
}
}
private fun handleFetchError(throwable: Throwable) {
Timber.e(throwable)
}
private fun handleFetchSuccess(users: List<AlbumViewable>) {
listAdapter.addAll(users)
}
private fun setRecycler() {
fragment_albumlist_recycler_container.apply {
layoutManager = LinearLayoutManager(context)
adapter = listAdapter.apply {
onItemClick = {
context.toast(it.title)
}
}
}
}
}

View File

@@ -0,0 +1,24 @@
package com.hako.albumlist.model
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import com.hako.base.domain.database.entities.AlbumEntity
import kotlinx.android.parcel.Parcelize
@Parcelize
data class Album(
@SerializedName("id") val id: Int,
@SerializedName("userId") val userId: Int,
@SerializedName("title") val title: String
) : Parcelable
data class AlbumViewable(
val id: Int,
val userId: Int,
val title: String
)
fun Album.toAlbumEntity() = AlbumEntity(this.id, this.userId, this.title)
fun AlbumEntity.toUserViewable() = AlbumViewable(this.id, this.userId, this.title)

View File

@@ -0,0 +1,37 @@
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.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 org.koin.core.KoinComponent
import org.koin.core.get
class AlbumlistViewmodel : ViewModel(), KoinComponent {
val data = MutableLiveData<Either<Throwable, List<AlbumViewable>>>()
val requestStatus = MutableLiveData<RequestStatus>()
private val getUsers: GetAlbum = get()
fun fetchAlbums(userId: Int) {
getUsers.execute(
userId,
onSuccess = {
requestStatus.postValue(Ready)
data.postValue(Either.Right(it))
},
onLoading = {
requestStatus.postValue(Loading)
},
onError = {
requestStatus.postValue(Errored)
data.postValue(Either.Left(it))
})
}
}

View File

@@ -0,0 +1,55 @@
package com.hako.albumlist.widget
import android.view.LayoutInflater
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.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 ->
autoNotify(oldList, newList) { old, new -> old.id == new.id }
notifyDataSetChanged()
}
var onItemClick: (AlbumViewable) -> Unit = { }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder =
UserViewHolder(
LayoutInflater
.from(parent.context)
.inflate(R.layout.item_album_card, parent, false),
onItemClick
)
fun getItem(position: Int) = items[position]
fun addAll(list: List<AlbumViewable>) {
items = list
}
override fun getItemCount() = items.size
override fun onBindViewHolder(viewholder: RecyclerView.ViewHolder, position: Int) =
when (viewholder) {
is UserViewHolder -> viewholder.bind(items[position])
else -> throw NoWhenBranchMatchedException("Undefined viewholder")
}
}
class UserViewHolder(private val view: View,
private val onItemClick: (AlbumViewable) -> Unit) :
RecyclerView.ViewHolder(view) {
fun bind(album: AlbumViewable) = with(view) {
item_album_card_album_name.text = album.title
item_album_card_container.setOnClickListener {
onItemClick(album)
}
}
}

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:id="@+id/fragment_albumlist_base">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/fragment_albumlist_recycler_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.hako.base.widgets.LoadingOverlay
android:id="@+id/fragment_albumlist_loading_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" />
<com.hako.base.widgets.NetworkErrorOverlay
android:id="@+id/fragment_albumlist_error_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>

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/item_album_card_container"
android:layout_width="match_parent"
android:layout_height="70dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:background="@drawable/bg_card"
android:elevation="2dp"
android:orientation="vertical">
<TextView
android:id="@+id/item_album_card_album_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="16dp"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Real Name" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/albumlist_navigation"
app:startDestination="@id/albumlistFragment">
<fragment
android:id="@+id/albumlistFragment"
android:name="com.hako.albumlist.feature.AlbumlistFragment"
tools:layout="@layout/fragment_albumlist"
android:label="AlbumListFragment" />
</navigation>

View File

@@ -1,17 +0,0 @@
package com.hako.albumlist
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

View File

@@ -49,4 +49,5 @@ android {
dependencies { dependencies {
implementation project(":base") implementation project(":base")
implementation project(":userlist") implementation project(":userlist")
implementation project(":albumlist")
} }

View File

@@ -1,24 +0,0 @@
package com.hako.friendlists
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.hako.friendlists", appContext.packageName)
}
}

View File

@@ -1,6 +1,7 @@
package com.hako.friendlists package com.hako.friendlists
import android.app.Application import android.app.Application
import com.hako.albumlist.di.albumListModules
import com.hako.userlist.di.userlistModules import com.hako.userlist.di.userlistModules
import com.hako.friendlists.di.appModules import com.hako.friendlists.di.appModules
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
@@ -26,7 +27,8 @@ class MainApplication : Application() {
modules( modules(
listOf( listOf(
appModules, appModules,
userlistModules userlistModules,
albumListModules
) )
) )
} }

View File

@@ -2,8 +2,10 @@
<navigation xmlns:android="http://schemas.android.com/apk/res/android" <navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/main_navigation" android:id="@+id/main_navigation"
app:startDestination="@id/userlist_navigation"> app:startDestination="@id/albumlist_navigation">
<include app:graph="@navigation/userlist_navigation" /> <include app:graph="@navigation/userlist_navigation" />
<include app:graph="@navigation/albumlist_navigation" />
</navigation> </navigation>

View File

@@ -4,4 +4,5 @@
<color name="colorPrimaryDark">#324047</color> <color name="colorPrimaryDark">#324047</color>
<color name="colorAccent">#CBCFD1</color> <color name="colorAccent">#CBCFD1</color>
<color name="colorRed">#D32F2F</color> <color name="colorRed">#D32F2F</color>
<color name="colorDarkGray">#333333</color>
</resources> </resources>

View File

@@ -5,6 +5,7 @@
<item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item> <item name="colorAccent">@color/colorAccent</item>
<item name="android:windowBackground">@color/soft_background</item> <item name="android:windowBackground">@color/soft_background</item>
<item name="android:textColor">@color/colorDarkGray</item>
</style> </style>
</resources> </resources>

View File

@@ -1,17 +0,0 @@
package com.hako.friendlists
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

View File

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

View File

@@ -18,6 +18,9 @@ interface AlbumDao {
@get:Query("SELECT * FROM ${AlbumEntity.TABLE_NAME}") @get:Query("SELECT * FROM ${AlbumEntity.TABLE_NAME}")
val all: List<AlbumEntity> val all: List<AlbumEntity>
@Query("SELECT * FROM ${AlbumEntity.TABLE_NAME} WHERE userId = :userId ORDER BY id ASC")
fun getAlbums(userId: Int): List<AlbumEntity>
@Query("SELECT COUNT(*) FROM ${AlbumEntity.TABLE_NAME}") @Query("SELECT COUNT(*) FROM ${AlbumEntity.TABLE_NAME}")
fun count(): Int fun count(): Int

View File

@@ -1,6 +1,5 @@
package com.hako.userlist.domain.usecase package com.hako.userlist.domain.usecase
import com.hako.base.domain.UseCase
import com.hako.base.domain.database.dao.UserDao import com.hako.base.domain.database.dao.UserDao
import com.hako.userlist.domain.datasource.UserlistRemoteApi import com.hako.userlist.domain.datasource.UserlistRemoteApi
import com.hako.userlist.model.UserViewable import com.hako.userlist.model.UserViewable
@@ -11,12 +10,11 @@ import io.reactivex.schedulers.Schedulers
import org.koin.core.KoinComponent import org.koin.core.KoinComponent
import org.koin.core.get import org.koin.core.get
class GetUsers(private val dao: UserDao) : KoinComponent, class GetUsers(private val dao: UserDao) : KoinComponent {
UseCase<UserViewable> {
private val api: UserlistRemoteApi = get() private val api: UserlistRemoteApi = get()
override fun execute( fun execute(
onSuccess: (List<UserViewable>) -> Unit, onSuccess: (List<UserViewable>) -> Unit,
onError: (Throwable) -> Unit, onError: (Throwable) -> Unit,
onLoading: () -> Unit onLoading: () -> Unit
@@ -25,7 +23,7 @@ class GetUsers(private val dao: UserDao) : KoinComponent,
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.doOnError { onError(it) } .doOnError { onError(it) }
.doOnSuccess { dbUsers -> .doOnSuccess { dbUsers ->
if (dbUsers.isEmpty() || dbUsers.count() == 0) { if (dbUsers.isEmpty()) {
api.getUsers() api.getUsers()
.doOnSuccess { .doOnSuccess {
dao.saveAll(it.map { user -> user.toUserEntity() }) dao.saveAll(it.map { user -> user.toUserEntity() })

View File

@@ -22,7 +22,7 @@ import timber.log.Timber
class UserlistFragment : Fragment() { class UserlistFragment : Fragment() {
private val viewModel: UserlistViewmodel by viewModel() private val viewModel: UserlistViewmodel by viewModel()
private val chatAdapter by lazy { UserlistAdapter() } private val listAdapter by lazy { UserlistAdapter() }
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
@@ -63,13 +63,13 @@ class UserlistFragment : Fragment() {
} }
private fun handleFetchSuccess(users: List<UserViewable>) { private fun handleFetchSuccess(users: List<UserViewable>) {
chatAdapter.addAll(users) listAdapter.addAll(users)
} }
private fun setRecycler() { private fun setRecycler() {
fragment_userlist_recycler_container.apply { fragment_userlist_recycler_container.apply {
layoutManager = LinearLayoutManager(context) layoutManager = LinearLayoutManager(context)
adapter = chatAdapter.apply { adapter = listAdapter.apply {
onItemClick = { onItemClick = {
context.toast(it.realName) context.toast(it.realName)
} }

View File

@@ -19,6 +19,7 @@
android:layout_marginStart="24dp" android:layout_marginStart="24dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:textSize="18sp" android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:text="Real Name" /> tools:text="Real Name" />
@@ -27,7 +28,7 @@
android:id="@+id/item_user_card_user_name" android:id="@+id/item_user_card_user_name"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textSize="12sp" android:textSize="14sp"
app:layout_constraintStart_toStartOf="@+id/item_user_card_real_name" app:layout_constraintStart_toStartOf="@+id/item_user_card_real_name"
app:layout_constraintTop_toBottomOf="@+id/item_user_card_real_name" app:layout_constraintTop_toBottomOf="@+id/item_user_card_real_name"
tools:text="User Name" /> tools:text="User Name" />