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
albumlist/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

6
albumlist/build.gradle Normal file
View File

@@ -0,0 +1,6 @@
apply plugin: 'com.android.library'
apply from: '../core.gradle'
dependencies {
implementation project(':base')
}

View File

21
albumlist/proguard-rules.pro vendored Normal file
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

View File

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

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,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)
}
}
}
}
}

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="Album 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

@@ -0,0 +1,3 @@
<resources>
<string name="app_name">albumlist</string>
</resources>

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

@@ -6,4 +6,6 @@
<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 @@
buildscript { buildscript {
apply from: 'versions.gradle' apply from: 'versions.gradle'
ext.kotlin_version = deps.kotlin.version
repositories { repositories {
google() google()

View File

@@ -1,2 +1,2 @@
include ':app', ':base', ':userlist' include ':app', ':base', ':userlist', ':albumlist'
rootProject.name='Friendlists' rootProject.name='Friendlists'

View File

@@ -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() }

View File

@@ -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()
}

View File

@@ -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() })

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" />