create module and implement it

This commit is contained in:
Carlos Martinez
2020-02-04 21:51:45 -03:00
parent 885978495e
commit c1efba5d63
31 changed files with 421 additions and 58 deletions

View File

@@ -3,7 +3,7 @@ package com.hako.albumlist.domain.usecase
import com.hako.albumlist.domain.datasource.AlbumlistRemoteApi import com.hako.albumlist.domain.datasource.AlbumlistRemoteApi
import com.hako.albumlist.model.AlbumViewable import com.hako.albumlist.model.AlbumViewable
import com.hako.albumlist.model.toAlbumEntity import com.hako.albumlist.model.toAlbumEntity
import com.hako.albumlist.model.toUserViewable import com.hako.albumlist.model.toAlbumViewable
import com.hako.base.domain.database.dao.AlbumDao import com.hako.base.domain.database.dao.AlbumDao
import io.reactivex.Single import io.reactivex.Single
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
@@ -28,13 +28,13 @@ class GetAlbum(private val dao: AlbumDao) : KoinComponent {
api.getAlbums(userId) api.getAlbums(userId)
.doOnSuccess { .doOnSuccess {
dao.saveAll(it.map { album -> album.toAlbumEntity() }) dao.saveAll(it.map { album -> album.toAlbumEntity() })
onSuccess(dao.getAlbums(userId).map { album -> album.toUserViewable() }) onSuccess(dao.getAlbums(userId).map { album -> album.toAlbumViewable() })
} }
.doOnSubscribe { onLoading() } .doOnSubscribe { onLoading() }
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.subscribe({}, { onError(it) }) .subscribe({}, { onError(it) })
} else { } else {
onSuccess(dbAlbum.map { it.toUserViewable() }) onSuccess(dbAlbum.map { it.toAlbumViewable() })
} }
} }
.subscribe() .subscribe()

View File

@@ -20,5 +20,5 @@ data class AlbumViewable(
fun Album.toAlbumEntity() = AlbumEntity(this.id, this.userId, this.title) fun Album.toAlbumEntity() = AlbumEntity(this.id, this.userId, this.title)
fun AlbumEntity.toUserViewable() = AlbumViewable(this.id, this.userId, this.title) fun AlbumEntity.toAlbumViewable() = AlbumViewable(this.id, this.userId, this.title)

View File

@@ -17,10 +17,10 @@ class AlbumlistViewmodel : ViewModel(), KoinComponent {
val data = MutableLiveData<Either<Throwable, List<AlbumViewable>>>() val data = MutableLiveData<Either<Throwable, List<AlbumViewable>>>()
val requestStatus = MutableLiveData<RequestStatus>() val requestStatus = MutableLiveData<RequestStatus>()
private val getUsers: GetAlbum = get() private val getAlbum: GetAlbum = get()
fun fetchAlbums(userId: Int) { fun fetchAlbums(userId: Int) {
getUsers.execute( getAlbum.execute(
userId, userId,
onSuccess = { onSuccess = {
requestStatus.postValue(Ready) requestStatus.postValue(Ready)

View File

@@ -20,7 +20,7 @@ class AlbumlistAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
var onItemClick: (AlbumViewable) -> Unit = { } var onItemClick: (AlbumViewable) -> Unit = { }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder =
UserViewHolder( AlbumViewHolder(
LayoutInflater LayoutInflater
.from(parent.context) .from(parent.context)
.inflate(R.layout.item_album_card, parent, false), .inflate(R.layout.item_album_card, parent, false),
@@ -37,12 +37,12 @@ class AlbumlistAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onBindViewHolder(viewholder: RecyclerView.ViewHolder, position: Int) = override fun onBindViewHolder(viewholder: RecyclerView.ViewHolder, position: Int) =
when (viewholder) { when (viewholder) {
is UserViewHolder -> viewholder.bind(items[position]) is AlbumViewHolder -> viewholder.bind(items[position])
else -> throw NoWhenBranchMatchedException("Undefined viewholder") else -> throw NoWhenBranchMatchedException("Undefined viewholder")
} }
} }
class UserViewHolder(private val view: View, class AlbumViewHolder(private val view: View,
private val onItemClick: (AlbumViewable) -> Unit) : private val onItemClick: (AlbumViewable) -> Unit) :
RecyclerView.ViewHolder(view) { RecyclerView.ViewHolder(view) {

View File

@@ -14,12 +14,15 @@
<TextView <TextView
android:id="@+id/item_album_card_album_name" android:id="@+id/item_album_card_album_name"
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="24dp" android:layout_marginStart="24dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:layout_marginEnd="24dp"
android:ellipsize="end"
android:textSize="18sp" android:textSize="18sp"
android:textStyle="bold" android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:text="Album name" /> tools:text="Album name" />

View File

@@ -9,6 +9,6 @@
android:id="@+id/albumlistFragment" android:id="@+id/albumlistFragment"
android:name="com.hako.albumlist.feature.AlbumlistFragment" android:name="com.hako.albumlist.feature.AlbumlistFragment"
tools:layout="@layout/fragment_albumlist" tools:layout="@layout/fragment_albumlist"
android:label="AlbumListFragment" /> android:label="AlbumlistFragment" />
</navigation> </navigation>

View File

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

View File

@@ -4,6 +4,7 @@ import android.app.Application
import com.hako.albumlist.di.albumListModules 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 com.hako.photolist.di.photoListModules
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin import org.koin.core.context.startKoin
import timber.log.Timber import timber.log.Timber
@@ -28,7 +29,8 @@ class MainApplication : Application() {
listOf( listOf(
appModules, appModules,
userlistModules, userlistModules,
albumListModules albumListModules,
photoListModules
) )
) )
} }

View File

@@ -2,10 +2,12 @@
<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/photolist_navigation">
<include app:graph="@navigation/userlist_navigation" /> <include app:graph="@navigation/userlist_navigation" />
<include app:graph="@navigation/albumlist_navigation" /> <include app:graph="@navigation/albumlist_navigation" />
<include app:graph="@navigation/photolist_navigation" />
</navigation> </navigation>

View File

@@ -1,24 +0,0 @@
package com.hako.base
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.base.test", appContext.packageName)
}
}

View File

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

View File

@@ -1,17 +0,0 @@
package com.hako.base
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
photolist/.gitignore vendored Normal file
View File

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

6
photolist/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
photolist/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.photolist" />

View File

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

View File

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

View File

@@ -0,0 +1,40 @@
package com.hako.photolist.domain.usecase
import com.hako.base.domain.database.dao.PhotoDao
import com.hako.photolist.domain.datasource.PhotolistRemoteApi
import com.hako.photolist.model.*
import io.reactivex.Single
import io.reactivex.schedulers.Schedulers
import org.koin.core.KoinComponent
import org.koin.core.get
class GetPhoto(private val dao: PhotoDao) : KoinComponent {
private val api: PhotolistRemoteApi = get()
fun execute(
albumId: Int,
onSuccess: (List<PhotoViewable>) -> Unit,
onError: (Throwable) -> Unit,
onLoading: () -> Unit
) {
Single.fromCallable { dao.getPhotos(albumId) }
.subscribeOn(Schedulers.io())
.doOnError { onError(it) }
.doOnSuccess { dbAlbum ->
if (dbAlbum.isEmpty() || dbAlbum.count() == 0) {
api.getPhotos(albumId)
.doOnSuccess {
dao.saveAll(it.map { album -> album.toPhotoEntity() })
onSuccess(dao.getPhotos(albumId).map { album -> album.toPhotoViewable() })
}
.doOnSubscribe { onLoading() }
.subscribeOn(Schedulers.io())
.subscribe({}, { onError(it) })
} else {
onSuccess(dbAlbum.map { it.toPhotoViewable() })
}
}
.subscribe()
}
}

View File

@@ -0,0 +1,75 @@
package com.hako.photolist.feature
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import com.hako.base.domain.network.RequestStatus
import com.hako.base.extensions.gone
import com.hako.base.extensions.observeNonNull
import com.hako.base.extensions.visible
import com.hako.photolist.R
import com.hako.photolist.model.PhotoViewable
import com.hako.photolist.viewmodel.PhotolistViewmodel
import com.hako.photolist.widget.PhotolistAdapter
import kotlinx.android.synthetic.main.fragment_photolist.*
import org.koin.androidx.viewmodel.ext.android.viewModel
import timber.log.Timber
class PhotolistFragment : Fragment() {
private val viewModel: PhotolistViewmodel by viewModel()
private val listAdapter by lazy { PhotolistAdapter() }
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View = inflater.inflate(R.layout.fragment_photolist, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setRecycler()
setObservers()
// TODO: Get album by bundle
viewModel.fetchPhotos(2)
}
private fun setObservers() {
viewModel.data.observeNonNull(this) {
it.either(::handleFetchError, ::handleFetchSuccess)
}
viewModel.requestStatus.observeNonNull(this) {
when (it) {
RequestStatus.Ready -> {
fragment_photolist_error_overlay.gone()
fragment_photolist_loading_overlay.gone()
}
RequestStatus.Loading -> {
fragment_photolist_error_overlay.gone()
fragment_photolist_loading_overlay.visible()
}
RequestStatus.Errored -> {
fragment_photolist_error_overlay.visible()
fragment_photolist_loading_overlay.gone()
}
}
}
}
private fun handleFetchError(throwable: Throwable) {
Timber.e(throwable)
}
private fun handleFetchSuccess(photos: List<PhotoViewable>) {
listAdapter.addAll(photos)
}
private fun setRecycler() {
fragment_photolist_recycler_container.apply {
layoutManager = LinearLayoutManager(context)
adapter = listAdapter
}
}
}

View File

@@ -0,0 +1,27 @@
package com.hako.photolist.model
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import com.hako.base.domain.database.entities.PhotoEntity
import kotlinx.android.parcel.Parcelize
@Parcelize
data class Photo(
@SerializedName("id") val id: Int,
@SerializedName("albumId") val albumId: Int,
@SerializedName("title") val title: String,
@SerializedName("url") val photoUrl: String,
@SerializedName("thumbnailUrl") val thumbnailUrl: String
) : Parcelable
data class PhotoViewable(
val id: Int,
val albumId: Int,
val title: String,
val photoUrl: String
)
fun Photo.toPhotoEntity() = PhotoEntity(this.id, this.albumId, this.title, this.photoUrl, this.thumbnailUrl)
fun PhotoEntity.toPhotoViewable() = PhotoViewable(this.id, this.albumId, this.title, this.photoUrl)

View File

@@ -0,0 +1,37 @@
package com.hako.photolist.viewmodel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
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.photolist.domain.usecase.GetPhoto
import com.hako.photolist.model.PhotoViewable
import org.koin.core.KoinComponent
import org.koin.core.get
class PhotolistViewmodel : ViewModel(), KoinComponent {
val data = MutableLiveData<Either<Throwable, List<PhotoViewable>>>()
val requestStatus = MutableLiveData<RequestStatus>()
private val getPhoto: GetPhoto = get()
fun fetchPhotos(albumId: Int) {
getPhoto.execute(
albumId,
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,50 @@
package com.hako.photolist.widget
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.hako.base.extensions.autoNotify
import com.hako.photolist.R
import com.hako.photolist.model.PhotoViewable
import kotlinx.android.synthetic.main.item_photo_card.view.*
import kotlin.properties.Delegates
class PhotolistAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var items by Delegates.observable(emptyList<PhotoViewable>()) { _, oldList, newList ->
autoNotify(oldList, newList) { old, new -> old.id == new.id }
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder =
PhotoViewHolder(
LayoutInflater
.from(parent.context)
.inflate(R.layout.item_photo_card, parent, false)
)
fun getItem(position: Int) = items[position]
fun addAll(list: List<PhotoViewable>) {
items = list
}
override fun getItemCount() = items.size
override fun onBindViewHolder(viewholder: RecyclerView.ViewHolder, position: Int) =
when (viewholder) {
is PhotoViewHolder -> viewholder.bind(items[position])
else -> throw NoWhenBranchMatchedException("Undefined viewholder")
}
}
class PhotoViewHolder(private val view: View) :
RecyclerView.ViewHolder(view) {
fun bind(photo: PhotoViewable) = with(view) {
item_photo_card_title.text = photo.title
// TODO load image
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

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_photolist_base">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/fragment_photolist_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_photolist_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_photolist_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,50 @@
<?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_photo_card_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:background="@drawable/bg_card"
android:elevation="2dp"
android:orientation="vertical">
<ImageView
android:id="@+id/item_photo_card_photo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/img_photo_placeholder"
android:contentDescription="Album photo" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/item_photo_card_footer_container"
android:layout_width="match_parent"
android:layout_height="70dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/item_photo_card_photo">
<TextView
android:id="@+id/item_photo_card_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="24dp"
android:ellipsize="end"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Album name" />
</androidx.constraintlayout.widget.ConstraintLayout>
</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/photolist_navigation"
app:startDestination="@id/photolistFragment">
<fragment
android:id="@+id/photolistFragment"
android:name="com.hako.photolist.feature.PhotolistFragment"
tools:layout="@layout/fragment_photolist"
android:label="PhotolistFragment" />
</navigation>

View File

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

View File

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

View File

@@ -3,12 +3,12 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/userlist_navigation" android:id="@+id/userlist_navigation"
app:startDestination="@id/userListFragment"> app:startDestination="@id/userlistFragment">
<fragment <fragment
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="UserListFragment" /> android:label="UserlistFragment" />
</navigation> </navigation>