Merge pull request #5 from hakodeveloper/feature/albumlist

Albumlist Implementation
This commit is contained in:
Carlos Martinez
2020-02-04 20:46:28 -03:00
committed by GitHub
32 changed files with 401 additions and 75 deletions
+1
View File
@@ -0,0 +1 @@
/build
+6
View File
@@ -0,0 +1,6 @@
apply plugin: 'com.android.library'
apply from: '../core.gradle'
dependencies {
implementation project(':base')
}
View File
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
+2
View File
@@ -0,0 +1,2 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.hako.albumlist" />
@@ -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() }
}
@@ -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>>
}
@@ -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()
}
}
@@ -0,0 +1,80 @@
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)
}
}
}
}
}
@@ -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)
@@ -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))
})
}
}
@@ -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)
}
}
}
@@ -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>
@@ -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="Album name" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -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>
@@ -0,0 +1,3 @@
<resources>
<string name="app_name">albumlist</string>
</resources>
+1
View File
@@ -49,4 +49,5 @@ android {
dependencies { dependencies {
implementation project(":base") implementation project(":base")
implementation project(":userlist") implementation project(":userlist")
implementation project(":albumlist")
} }
@@ -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)
}
}
@@ -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
) )
) )
} }
@@ -6,4 +6,6 @@
<include app:graph="@navigation/userlist_navigation" /> <include app:graph="@navigation/userlist_navigation" />
<include app:graph="@navigation/albumlist_navigation" />
</navigation> </navigation>
+1
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>
+1
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>
@@ -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)
}
}
@@ -1,5 +0,0 @@
package com.hako.base.domain
interface UseCase <T> {
fun execute(onSuccess: (List<T>) -> Unit, onError: (Throwable) -> Unit, onLoading: () -> Unit)
}
@@ -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
-1
View File
@@ -1,6 +1,5 @@
buildscript { buildscript {
apply from: 'versions.gradle' apply from: 'versions.gradle'
ext.kotlin_version = deps.kotlin.version
repositories { repositories {
google() google()
+1 -1
View File
@@ -1,2 +1,2 @@
include ':app', ':base', ':userlist' include ':app', ':base', ':userlist', ':albumlist'
rootProject.name='Friendlists' rootProject.name='Friendlists'
@@ -1,7 +1,6 @@
package com.hako.userlist.di 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.UserlistDatasource
import com.hako.userlist.domain.datasource.UserlistRemoteApi import com.hako.userlist.domain.datasource.UserlistRemoteApi
import com.hako.userlist.domain.usecase.GetUsers import com.hako.userlist.domain.usecase.GetUsers
import com.hako.userlist.viewmodel.UserlistViewmodel import com.hako.userlist.viewmodel.UserlistViewmodel
@@ -10,7 +9,6 @@ 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 { UserlistDatasource() }
factory { GetUsers(get()) } factory { GetUsers(get()) }
viewModel { UserlistViewmodel() } viewModel { UserlistViewmodel() }
@@ -1,13 +0,0 @@
package com.hako.userlist.domain.datasource
import com.hako.userlist.model.User
import io.reactivex.Single
import org.koin.core.KoinComponent
import org.koin.core.get
class UserlistDatasource : KoinComponent, UserlistRemoteApi {
private val api: UserlistRemoteApi = get()
override fun getUsers(): Single<List<User>> = api.getUsers()
}
@@ -1,8 +1,7 @@
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.UserlistDatasource import com.hako.userlist.domain.datasource.UserlistRemoteApi
import com.hako.userlist.model.UserViewable import com.hako.userlist.model.UserViewable
import com.hako.userlist.model.toUserEntity import com.hako.userlist.model.toUserEntity
import com.hako.userlist.model.toUserViewable import com.hako.userlist.model.toUserViewable
@@ -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: UserlistDatasource = 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() })
@@ -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)
} }
@@ -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" />