mirror of
https://github.com/imcarlost/Friendlists.git
synced 2026-06-15 05:57:54 -04:00
Merge pull request #4 from hakodeveloper/feature/userlist
Userlist Implementation
This commit is contained in:
+5
-1
@@ -1,4 +1,4 @@
|
|||||||
apply from: 'versions.gradle'
|
apply from: '../versions.gradle'
|
||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
apply plugin: 'kotlin-android-extensions'
|
apply plugin: 'kotlin-android-extensions'
|
||||||
@@ -15,6 +15,9 @@ android {
|
|||||||
versionCode 1
|
versionCode 1
|
||||||
versionName "1.0"
|
versionName "1.0"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
|
buildConfigField "String", "DB_NAME", '"friendlists.db"'
|
||||||
|
buildConfigField "String", "BASE_ENDPOINT", '"https://jsonplaceholder.typicode.com/"'
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
@@ -45,4 +48,5 @@ android {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(":base")
|
implementation project(":base")
|
||||||
|
implementation project(":userlist")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,28 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="com.hako.friendlists">
|
package="com.hako.friendlists">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
android:name=".MainApplication"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
|
android:fullBackupContent="false"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="false"
|
||||||
android:theme="@style/AppTheme" />
|
android:theme="@style/AppTheme">
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".view.MainActivity"
|
||||||
|
android:screenOrientation="portrait">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.hako.friendlists
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import com.hako.userlist.di.userlistModules
|
||||||
|
import com.hako.friendlists.di.appModules
|
||||||
|
import org.koin.android.ext.koin.androidContext
|
||||||
|
import org.koin.core.context.startKoin
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
class MainApplication : Application() {
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
setupLogger()
|
||||||
|
setupDi()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupLogger() {
|
||||||
|
Timber.plant(Timber.DebugTree())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupDi() {
|
||||||
|
startKoin {
|
||||||
|
androidContext(this@MainApplication)
|
||||||
|
|
||||||
|
modules(
|
||||||
|
listOf(
|
||||||
|
appModules,
|
||||||
|
userlistModules
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.hako.friendlists.di
|
||||||
|
|
||||||
|
import androidx.room.Room
|
||||||
|
import com.hako.base.domain.database.DatabaseClient
|
||||||
|
import com.hako.base.domain.network.RemoteClient
|
||||||
|
import com.hako.friendlists.BuildConfig
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
val appModules = module {
|
||||||
|
// Room database
|
||||||
|
single { Room.databaseBuilder(get(), DatabaseClient::class.java, BuildConfig.DB_NAME).build() }
|
||||||
|
factory { get<DatabaseClient>().userDao() }
|
||||||
|
factory { get<DatabaseClient>().albumDao() }
|
||||||
|
factory { get<DatabaseClient>().photoDao() }
|
||||||
|
|
||||||
|
// Retrofit
|
||||||
|
single { RemoteClient(BuildConfig.BASE_ENDPOINT) }
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.hako.friendlists.view
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.navigation.findNavController
|
||||||
|
import com.hako.friendlists.R
|
||||||
|
|
||||||
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private val navController by lazy { findNavController(R.id.main_fragment_container) }
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_main)
|
||||||
|
setupNavigation()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupNavigation() {
|
||||||
|
navController.setGraph(R.navigation.main_navigation)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:id="@+id/main_fragment_container"
|
||||||
|
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:defaultNavHost="true"/>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?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"
|
||||||
|
android:id="@+id/main_navigation"
|
||||||
|
app:startDestination="@id/userlist_navigation">
|
||||||
|
|
||||||
|
<include app:graph="@navigation/userlist_navigation" />
|
||||||
|
|
||||||
|
</navigation>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<color name="colorPrimary">#008577</color>
|
<color name="colorPrimary">#37474F</color>
|
||||||
<color name="colorPrimaryDark">#00574B</color>
|
<color name="colorPrimaryDark">#324047</color>
|
||||||
<color name="colorAccent">#D81B60</color>
|
<color name="colorAccent">#CBCFD1</color>
|
||||||
|
<color name="colorRed">#D32F2F</color>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<!-- Base application theme. -->
|
|
||||||
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
||||||
<!-- Customize your theme here. -->
|
|
||||||
<item name="colorPrimary">@color/colorPrimary</item>
|
<item name="colorPrimary">@color/colorPrimary</item>
|
||||||
<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>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
ext.deps = [:]
|
|
||||||
|
|
||||||
def versions = [:]
|
|
||||||
versions.kotlin = "1.3.41"
|
|
||||||
versions.gradle = "3.5.3"
|
|
||||||
|
|
||||||
def deps = [:]
|
|
||||||
|
|
||||||
def project = [:]
|
|
||||||
project.kotlin = "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin"
|
|
||||||
project.gradle = "com.android.tools.build:gradle:$versions.gradle"
|
|
||||||
deps.project = project
|
|
||||||
|
|
||||||
def build_versions = [:]
|
|
||||||
build_versions.min_sdk = 21
|
|
||||||
build_versions.target_sdk = 29
|
|
||||||
build_versions.build_tools = "29.0.3"
|
|
||||||
ext.build_versions = build_versions
|
|
||||||
|
|
||||||
def kotlin = [:]
|
|
||||||
kotlin.version = "$versions.kotlin"
|
|
||||||
kotlin.std_lib = "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$versions.kotlin"
|
|
||||||
deps.kotlin = kotlin
|
|
||||||
|
|
||||||
ext.deps = deps
|
|
||||||
|
|
||||||
def addRepos(RepositoryHandler handler) {
|
|
||||||
handler.google()
|
|
||||||
handler.jcenter()
|
|
||||||
handler.maven { url 'https://oss.sonatype.org/content/repositories/snapshots' }
|
|
||||||
}
|
|
||||||
|
|
||||||
ext.addRepos = this.&addRepos
|
|
||||||
@@ -37,6 +37,8 @@ dependencies {
|
|||||||
api deps.androidx.lifecycle_ext
|
api deps.androidx.lifecycle_ext
|
||||||
api deps.androidx.lifecycle_viewmodel
|
api deps.androidx.lifecycle_viewmodel
|
||||||
api deps.androidx.recycler_view
|
api deps.androidx.recycler_view
|
||||||
|
api deps.androidx.navigation_fragment
|
||||||
|
api deps.androidx.navigation_ui
|
||||||
api deps.retrofit.runtime
|
api deps.retrofit.runtime
|
||||||
api deps.retrofit.gson
|
api deps.retrofit.gson
|
||||||
api deps.retrofit.rx
|
api deps.retrofit.rx
|
||||||
@@ -48,6 +50,7 @@ dependencies {
|
|||||||
api deps.rx.android
|
api deps.rx.android
|
||||||
api deps.okhttp_logging_interceptor
|
api deps.okhttp_logging_interceptor
|
||||||
api deps.timber
|
api deps.timber
|
||||||
|
api deps.lottie
|
||||||
//Testing
|
//Testing
|
||||||
api deps.testing.junit
|
api deps.testing.junit
|
||||||
api deps.testing.koin
|
api deps.testing.koin
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.hako.base.domain
|
||||||
|
|
||||||
|
sealed class Either<out L, out R> {
|
||||||
|
|
||||||
|
data class Left<out L>(val a: L) : Either<L, Nothing>()
|
||||||
|
data class Right<out R>(val b: R) : Either<Nothing, R>()
|
||||||
|
|
||||||
|
val isRight get() = this is Right<R>
|
||||||
|
val isLeft get() = this is Left<L>
|
||||||
|
|
||||||
|
fun <L> left(a: L) = Left(a)
|
||||||
|
fun <R> right(b: R) = Right(b)
|
||||||
|
|
||||||
|
fun either(fnL: (L) -> Any, fnR: (R) -> Any): Any =
|
||||||
|
when (this) {
|
||||||
|
is Left -> fnL(a)
|
||||||
|
is Right -> fnR(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <A, B, C> ((A) -> B).c(f: (B) -> C): (A) -> C = {
|
||||||
|
f(this(it))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T, L, R> Either<L, R>.flatMap(fn: (R) -> Either<L, T>): Either<L, T> =
|
||||||
|
when (this) {
|
||||||
|
is Either.Left -> Either.Left(
|
||||||
|
a
|
||||||
|
)
|
||||||
|
is Either.Right -> fn(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T, L, R> Either<L, R>.map(fn: (R) -> (T)): Either<L, T> = this.flatMap(fn.c(::right))
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.hako.base.domain
|
||||||
|
|
||||||
|
interface UseCase <T> {
|
||||||
|
fun execute(onSuccess: (List<T>) -> Unit, onError: (Throwable) -> Unit, onLoading: () -> Unit)
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.hako.base.domain.database
|
||||||
|
|
||||||
|
import androidx.room.Database
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import com.hako.base.domain.database.dao.AlbumDao
|
||||||
|
import com.hako.base.domain.database.dao.PhotoDao
|
||||||
|
import com.hako.base.domain.database.dao.UserDao
|
||||||
|
import com.hako.base.domain.database.entities.AlbumEntity
|
||||||
|
import com.hako.base.domain.database.entities.PhotoEntity
|
||||||
|
import com.hako.base.domain.database.entities.UserEntity
|
||||||
|
|
||||||
|
@Database(entities = [UserEntity::class, AlbumEntity::class, PhotoEntity::class], version = 1, exportSchema = false)
|
||||||
|
abstract class DatabaseClient : RoomDatabase() {
|
||||||
|
abstract fun userDao(): UserDao
|
||||||
|
abstract fun albumDao(): AlbumDao
|
||||||
|
abstract fun photoDao(): PhotoDao
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.hako.base.domain.database.dao
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import com.hako.base.domain.database.entities.AlbumEntity
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface AlbumDao {
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
fun save(entity: AlbumEntity)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
fun saveAll(entities: List<AlbumEntity>)
|
||||||
|
|
||||||
|
@get:Query("SELECT * FROM ${AlbumEntity.TABLE_NAME}")
|
||||||
|
val all: List<AlbumEntity>
|
||||||
|
|
||||||
|
@Query("SELECT COUNT(*) FROM ${AlbumEntity.TABLE_NAME}")
|
||||||
|
fun count(): Int
|
||||||
|
|
||||||
|
@Query("DELETE FROM ${AlbumEntity.TABLE_NAME}")
|
||||||
|
fun nukeDatabase()
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.hako.base.domain.database.dao
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import com.hako.base.domain.database.entities.PhotoEntity
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface PhotoDao {
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
fun save(entity: PhotoEntity)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
fun saveAll(entities: List<PhotoEntity>)
|
||||||
|
|
||||||
|
@get:Query("SELECT * FROM ${PhotoEntity.TABLE_NAME}")
|
||||||
|
val all: List<PhotoEntity>
|
||||||
|
|
||||||
|
@Query("SELECT COUNT(*) FROM ${PhotoEntity.TABLE_NAME}")
|
||||||
|
fun count(): Int
|
||||||
|
|
||||||
|
@Query("DELETE FROM ${PhotoEntity.TABLE_NAME}")
|
||||||
|
fun nukeDatabase()
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.hako.base.domain.database.dao
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import com.hako.base.domain.database.entities.UserEntity
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface UserDao {
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
fun save(entity: UserEntity)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
fun saveAll(entities: List<UserEntity>)
|
||||||
|
|
||||||
|
@get:Query("SELECT * FROM ${UserEntity.TABLE_NAME}")
|
||||||
|
val all: List<UserEntity>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM ${UserEntity.TABLE_NAME}")
|
||||||
|
fun getAllUsers(): List<UserEntity>
|
||||||
|
|
||||||
|
@Query("SELECT COUNT(*) FROM ${UserEntity.TABLE_NAME}")
|
||||||
|
fun count(): Int
|
||||||
|
|
||||||
|
@Query("DELETE FROM ${UserEntity.TABLE_NAME}")
|
||||||
|
fun nukeDatabase()
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.hako.base.domain.database.entities
|
||||||
|
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.Index
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
|
||||||
|
@Entity(tableName = AlbumEntity.TABLE_NAME, indices = [Index(value = ["id"], unique = true)])
|
||||||
|
data class AlbumEntity(
|
||||||
|
@PrimaryKey
|
||||||
|
val id: Int,
|
||||||
|
val userId: Int,
|
||||||
|
val title: String
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
const val TABLE_NAME = "albums"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.hako.base.domain.database.entities
|
||||||
|
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.Index
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
|
||||||
|
@Entity(tableName = PhotoEntity.TABLE_NAME, indices = [Index(value = ["id"], unique = true)])
|
||||||
|
data class PhotoEntity(
|
||||||
|
@PrimaryKey
|
||||||
|
val id: Int,
|
||||||
|
val albumId: Int,
|
||||||
|
val title: String,
|
||||||
|
val photoUrl: String,
|
||||||
|
val thumbnailUrl: String
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
const val TABLE_NAME = "photos"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.hako.base.domain.database.entities
|
||||||
|
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.Index
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
|
||||||
|
@Entity(tableName = UserEntity.TABLE_NAME, indices = [Index(value = ["id"], unique = true)])
|
||||||
|
data class UserEntity(
|
||||||
|
@PrimaryKey
|
||||||
|
val id: Int,
|
||||||
|
val realName: String,
|
||||||
|
val userName: String,
|
||||||
|
val email: String,
|
||||||
|
val phone: String,
|
||||||
|
val website: String
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
const val TABLE_NAME = "users"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.hako.base.domain.network
|
||||||
|
|
||||||
|
import com.hako.base.BuildConfig
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
|
import retrofit2.Retrofit
|
||||||
|
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
|
||||||
|
import retrofit2.converter.gson.GsonConverterFactory
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
class RemoteClient(endpoint: String) {
|
||||||
|
private val logger = HttpLoggingInterceptor(object : HttpLoggingInterceptor.Logger {
|
||||||
|
override fun log(message: String) {
|
||||||
|
Timber.d(message)
|
||||||
|
}
|
||||||
|
}).setLevel(getLoggerLevel())
|
||||||
|
|
||||||
|
private val client = OkHttpClient.Builder()
|
||||||
|
.addInterceptor(logger)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
private val retrofit: Retrofit = Retrofit.Builder()
|
||||||
|
.baseUrl(endpoint)
|
||||||
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
|
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
|
||||||
|
.client(client)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
fun <T> getClient(api: Class<T>): T = retrofit.create(api)
|
||||||
|
|
||||||
|
private fun getLoggerLevel() = when (BuildConfig.DEBUG) {
|
||||||
|
true -> HttpLoggingInterceptor.Level.BASIC
|
||||||
|
false -> HttpLoggingInterceptor.Level.NONE
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.hako.base.domain.network
|
||||||
|
|
||||||
|
sealed class RequestStatus {
|
||||||
|
object Ready : RequestStatus()
|
||||||
|
object Loading : RequestStatus()
|
||||||
|
object Errored : RequestStatus()
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.hako.base.extensions
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.widget.Toast
|
||||||
|
|
||||||
|
fun Context.toast(message: String) {
|
||||||
|
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.hako.base.extensions
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
|
||||||
|
fun <T> RecyclerView.Adapter<*>.autoNotify(oldList: List<T>, newList: List<T>, compare: (T, T) -> Boolean) {
|
||||||
|
val diff = DiffUtil.calculateDiff(object : DiffUtil.Callback() {
|
||||||
|
|
||||||
|
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||||
|
return compare(oldList[oldItemPosition], newList[newItemPosition])
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||||
|
return oldList[oldItemPosition] == newList[newItemPosition]
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getOldListSize() = oldList.size
|
||||||
|
|
||||||
|
override fun getNewListSize() = newList.size
|
||||||
|
})
|
||||||
|
|
||||||
|
diff.dispatchUpdatesTo(this)
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.hako.base.extensions
|
||||||
|
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
|
||||||
|
fun <T> LiveData<T>.observeNonNull(owner: LifecycleOwner, func: (T) -> Unit) {
|
||||||
|
observe(owner, Observer {
|
||||||
|
it?.let {
|
||||||
|
func(it)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.hako.base.extensions
|
||||||
|
|
||||||
|
import androidx.annotation.IdRes
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.navigation.fragment.NavHostFragment
|
||||||
|
|
||||||
|
fun AppCompatActivity.findNavHostFragment(@IdRes id: Int) =
|
||||||
|
supportFragmentManager.findFragmentById(id) as NavHostFragment
|
||||||
|
|
||||||
|
fun Fragment.findNavHostFragment(@IdRes id: Int) =
|
||||||
|
childFragmentManager.findFragmentById(id) as NavHostFragment
|
||||||
|
|
||||||
|
fun Fragment.findNavController(@IdRes id: Int) =
|
||||||
|
androidx.navigation.Navigation.findNavController(view?.findViewById(id) ?: viewNotFound(id, this))
|
||||||
|
|
||||||
|
private fun viewNotFound(@IdRes id: Int, fragment: Fragment): Nothing = throw IllegalStateException(
|
||||||
|
"View ID $id at '${fragment::class.java.simpleName}' not found."
|
||||||
|
)
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package com.hako.base.extensions
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.annotation.LayoutRes
|
||||||
|
|
||||||
|
fun View.enable() {
|
||||||
|
isEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun View.disable() {
|
||||||
|
isEnabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun View.visible() {
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
fun View.invisible() {
|
||||||
|
visibility = View.INVISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
fun View.transparent() {
|
||||||
|
alpha = 0f
|
||||||
|
}
|
||||||
|
|
||||||
|
fun View.opaque() {
|
||||||
|
alpha = 1f
|
||||||
|
}
|
||||||
|
|
||||||
|
fun View.gone() {
|
||||||
|
visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ViewGroup.inflate(@LayoutRes layout: Int, attachToRoot: Boolean = false): View =
|
||||||
|
LayoutInflater
|
||||||
|
.from(context)
|
||||||
|
.inflate(layout, this, attachToRoot)
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
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.like_button.view.*
|
||||||
|
|
||||||
|
private const val LIKE_MIN_FRAME = 0
|
||||||
|
private const val LIKE_MAX_FRAME = 28
|
||||||
|
private const val LIKE_ANIM_SPEED = 1f
|
||||||
|
private const val DISLIKE_MIN_FRAME = 28
|
||||||
|
private const val DISLIKE_MAX_FRAME = 70
|
||||||
|
private const val DISLIKE_ANIM_SPEED = 2f
|
||||||
|
|
||||||
|
class LikeButton @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = 0
|
||||||
|
) : FrameLayout(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
|
init {
|
||||||
|
inflate(R.layout.like_button, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun like() {
|
||||||
|
like_button_animation_view.frame = LIKE_MAX_FRAME
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dislike() {
|
||||||
|
like_button_animation_view.frame = LIKE_MIN_FRAME
|
||||||
|
}
|
||||||
|
|
||||||
|
fun playLike() {
|
||||||
|
like_button_animation_view.setMinAndMaxFrame(LIKE_MIN_FRAME, LIKE_MAX_FRAME)
|
||||||
|
like_button_animation_view.speed = LIKE_ANIM_SPEED
|
||||||
|
like_button_animation_view.playAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun playDislike() {
|
||||||
|
like_button_animation_view.setMinAndMaxFrame(DISLIKE_MIN_FRAME, DISLIKE_MAX_FRAME)
|
||||||
|
like_button_animation_view.speed = DISLIKE_ANIM_SPEED
|
||||||
|
like_button_animation_view.playAnimation()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
class LoadingOverlay @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = 0
|
||||||
|
) : FrameLayout(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
|
init {
|
||||||
|
inflate(R.layout.loading_overlay, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
class NetworkErrorOverlay @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = 0
|
||||||
|
) : FrameLayout(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
|
init {
|
||||||
|
inflate(R.layout.network_error_overlay, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item>
|
||||||
|
<shape >
|
||||||
|
<solid android:color="@color/card_background" />
|
||||||
|
<corners android:radius="4dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</layer-list>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<merge 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:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
tools:parentTag="android.widget.FrameLayout">
|
||||||
|
|
||||||
|
<com.airbnb.lottie.LottieAnimationView
|
||||||
|
android:id="@+id/like_button_animation_view"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:scaleType="centerInside"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:lottie_rawRes="@raw/like_animation" />
|
||||||
|
|
||||||
|
</merge>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?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/loading_overlay_animation_view"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:scaleType="centerInside"
|
||||||
|
app:lottie_autoPlay="true"
|
||||||
|
app:lottie_loop="true"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:lottie_rawRes="@raw/loading_animation" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -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/loading_overlay_animation_view"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:scaleType="centerInside"
|
||||||
|
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.48"
|
||||||
|
app:lottie_autoPlay="true"
|
||||||
|
app:lottie_loop="true"
|
||||||
|
app:lottie_rawRes="@raw/network_animation" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textView"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="Network error!"
|
||||||
|
android:textSize="24sp"
|
||||||
|
app:layout_constraintEnd_toEndOf="@+id/loading_overlay_animation_view"
|
||||||
|
app:layout_constraintStart_toStartOf="@+id/loading_overlay_animation_view"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/loading_overlay_animation_view" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="soft_background">#EEEEEE</color>
|
||||||
|
|
||||||
|
<color name="transparent">#00000000</color>
|
||||||
|
|
||||||
|
<color name="card_background">#FFFFFF</color>
|
||||||
|
</resources>
|
||||||
@@ -7,6 +7,7 @@ versions.androidx_core = "1.1.0"
|
|||||||
versions.androidx_constraint_layout = "1.1.3"
|
versions.androidx_constraint_layout = "1.1.3"
|
||||||
versions.androidx_lifecycle = "2.2.0"
|
versions.androidx_lifecycle = "2.2.0"
|
||||||
versions.androidx_recycler_view = "1.1.0"
|
versions.androidx_recycler_view = "1.1.0"
|
||||||
|
versions.androidx_navigation = "2.2.0"
|
||||||
versions.okhttp_logging_interceptor = "4.3.1"
|
versions.okhttp_logging_interceptor = "4.3.1"
|
||||||
versions.retrofit = "2.7.1"
|
versions.retrofit = "2.7.1"
|
||||||
versions.timber = "4.7.1"
|
versions.timber = "4.7.1"
|
||||||
@@ -18,6 +19,7 @@ versions.junit = "4.13"
|
|||||||
versions.test = "1.2.0"
|
versions.test = "1.2.0"
|
||||||
versions.test_ext = "1.1.1"
|
versions.test_ext = "1.1.1"
|
||||||
versions.espresso = "3.2.0"
|
versions.espresso = "3.2.0"
|
||||||
|
versions.lottie = "3.3.1"
|
||||||
|
|
||||||
def deps = [:]
|
def deps = [:]
|
||||||
|
|
||||||
@@ -33,6 +35,8 @@ androidx.constraint_layout = "androidx.constraintlayout:constraintlayout:$versio
|
|||||||
androidx.lifecycle_ext = "androidx.lifecycle:lifecycle-extensions:$versions.androidx_lifecycle"
|
androidx.lifecycle_ext = "androidx.lifecycle:lifecycle-extensions:$versions.androidx_lifecycle"
|
||||||
androidx.lifecycle_viewmodel = "androidx.lifecycle:lifecycle-viewmodel-ktx:$versions.androidx_lifecycle"
|
androidx.lifecycle_viewmodel = "androidx.lifecycle:lifecycle-viewmodel-ktx:$versions.androidx_lifecycle"
|
||||||
androidx.recycler_view = "androidx.recyclerview:recyclerview:$versions.androidx_recycler_view"
|
androidx.recycler_view = "androidx.recyclerview:recyclerview:$versions.androidx_recycler_view"
|
||||||
|
androidx.navigation_fragment = "androidx.navigation:navigation-fragment-ktx:$versions.androidx_navigation"
|
||||||
|
androidx.navigation_ui = "androidx.navigation:navigation-ui-ktx:$versions.androidx_navigation"
|
||||||
deps.androidx = androidx
|
deps.androidx = androidx
|
||||||
|
|
||||||
def retrofit = [:]
|
def retrofit = [:]
|
||||||
@@ -72,5 +76,6 @@ deps.testing = testing
|
|||||||
|
|
||||||
deps.okhttp_logging_interceptor = "com.squareup.okhttp3:logging-interceptor:$versions.okhttp_logging_interceptor"
|
deps.okhttp_logging_interceptor = "com.squareup.okhttp3:logging-interceptor:$versions.okhttp_logging_interceptor"
|
||||||
deps.timber = "com.jakewharton.timber:timber:$versions.timber"
|
deps.timber = "com.jakewharton.timber:timber:$versions.timber"
|
||||||
|
deps.lottie = "com.airbnb.android:lottie:$versions.lottie"
|
||||||
|
|
||||||
ext.deps = deps
|
ext.deps = deps
|
||||||
|
|||||||
@@ -39,7 +39,3 @@ detekt {
|
|||||||
task clean(type: Delete) {
|
task clean(type: Delete) {
|
||||||
delete rootProject.buildDir
|
delete rootProject.buildDir
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
}
|
|
||||||
|
|||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
apply from: '../versions.gradle'
|
||||||
|
apply plugin: 'com.android.library'
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
apply plugin: 'kotlin-android-extensions'
|
||||||
|
apply plugin: 'kotlin-kapt'
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdkVersion build_versions.target_sdk
|
||||||
|
buildToolsVersion build_versions.build_tools
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdkVersion build_versions.min_sdk
|
||||||
|
targetSdkVersion build_versions.target_sdk
|
||||||
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
|
testOptions {
|
||||||
|
unitTests.returnDefaultValues = true
|
||||||
|
}
|
||||||
|
|
||||||
|
lintOptions {
|
||||||
|
abortOnError false
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
include ':app', ':base'
|
include ':app', ':base', ':userlist'
|
||||||
rootProject.name='Friendlists'
|
rootProject.name='Friendlists'
|
||||||
|
|||||||
+1
-1
@@ -63,7 +63,7 @@ complexity:
|
|||||||
threshold: 150
|
threshold: 150
|
||||||
LongMethod:
|
LongMethod:
|
||||||
active: true
|
active: true
|
||||||
threshold: 20
|
threshold: 40
|
||||||
LongParameterList:
|
LongParameterList:
|
||||||
active: false
|
active: false
|
||||||
threshold: 6
|
threshold: 6
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
/build
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
apply plugin: 'com.android.library'
|
||||||
|
apply from: '../core.gradle'
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation project(':base')
|
||||||
|
}
|
||||||
Vendored
+21
@@ -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
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="com.hako.friendlist_userlist" />
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.hako.userlist.di
|
||||||
|
|
||||||
|
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.usecase.GetUsers
|
||||||
|
import com.hako.userlist.viewmodel.UserlistViewmodel
|
||||||
|
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
val userlistModules = module {
|
||||||
|
factory { get<RemoteClient>().getClient(UserlistRemoteApi::class.java) }
|
||||||
|
factory { UserlistDatasource() }
|
||||||
|
factory { GetUsers(get()) }
|
||||||
|
|
||||||
|
viewModel { UserlistViewmodel() }
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.hako.userlist.domain.datasource
|
||||||
|
|
||||||
|
import com.hako.userlist.model.User
|
||||||
|
import io.reactivex.Single
|
||||||
|
import retrofit2.http.GET
|
||||||
|
|
||||||
|
interface UserlistRemoteApi {
|
||||||
|
|
||||||
|
@GET("/users")
|
||||||
|
fun getUsers(): Single<List<User>>
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package com.hako.userlist.domain.usecase
|
||||||
|
|
||||||
|
import com.hako.base.domain.UseCase
|
||||||
|
import com.hako.base.domain.database.dao.UserDao
|
||||||
|
import com.hako.userlist.domain.datasource.UserlistDatasource
|
||||||
|
import com.hako.userlist.model.UserViewable
|
||||||
|
import com.hako.userlist.model.toUserEntity
|
||||||
|
import com.hako.userlist.model.toUserViewable
|
||||||
|
import io.reactivex.Single
|
||||||
|
import io.reactivex.schedulers.Schedulers
|
||||||
|
import org.koin.core.KoinComponent
|
||||||
|
import org.koin.core.get
|
||||||
|
|
||||||
|
class GetUsers(private val dao: UserDao) : KoinComponent,
|
||||||
|
UseCase<UserViewable> {
|
||||||
|
|
||||||
|
private val api: UserlistDatasource = get()
|
||||||
|
|
||||||
|
override fun execute(
|
||||||
|
onSuccess: (List<UserViewable>) -> Unit,
|
||||||
|
onError: (Throwable) -> Unit,
|
||||||
|
onLoading: () -> Unit
|
||||||
|
) {
|
||||||
|
Single.fromCallable { dao.getAllUsers() }
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.doOnError { onError(it) }
|
||||||
|
.doOnSuccess { dbUsers ->
|
||||||
|
if (dbUsers.isEmpty() || dbUsers.count() == 0) {
|
||||||
|
api.getUsers()
|
||||||
|
.doOnSuccess {
|
||||||
|
dao.saveAll(it.map { user -> user.toUserEntity() })
|
||||||
|
onSuccess(dao.getAllUsers().map { user -> user.toUserViewable() })
|
||||||
|
}
|
||||||
|
.doOnSubscribe { onLoading() }
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.subscribe({}, { onError(it) })
|
||||||
|
} else {
|
||||||
|
onSuccess(dbUsers.map { it.toUserViewable() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.subscribe()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package com.hako.userlist.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.toast
|
||||||
|
import com.hako.base.extensions.visible
|
||||||
|
import com.hako.userlist.model.UserViewable
|
||||||
|
import com.hako.userlist.viewmodel.UserlistViewmodel
|
||||||
|
import com.hako.userlist.widget.UserlistAdapter
|
||||||
|
import com.hako.friendlist_userlist.R
|
||||||
|
import kotlinx.android.synthetic.main.fragment_userlist.*
|
||||||
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
class UserlistFragment : Fragment() {
|
||||||
|
|
||||||
|
private val viewModel: UserlistViewmodel by viewModel()
|
||||||
|
private val chatAdapter by lazy { UserlistAdapter() }
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
|
||||||
|
): View = inflater.inflate(R.layout.fragment_userlist, container, false)
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
setRecycler()
|
||||||
|
setObservers()
|
||||||
|
viewModel.fetchUsers()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setObservers() {
|
||||||
|
viewModel.data.observeNonNull(this) {
|
||||||
|
it.either(::handleFetchError, ::handleFetchSuccess)
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.requestStatus.observeNonNull(this) {
|
||||||
|
when (it) {
|
||||||
|
RequestStatus.Ready -> {
|
||||||
|
fragment_userlist_error_overlay.gone()
|
||||||
|
fragment_userlist_loading_overlay.gone()
|
||||||
|
}
|
||||||
|
RequestStatus.Loading -> {
|
||||||
|
fragment_userlist_error_overlay.gone()
|
||||||
|
fragment_userlist_loading_overlay.visible()
|
||||||
|
}
|
||||||
|
RequestStatus.Errored -> {
|
||||||
|
fragment_userlist_error_overlay.visible()
|
||||||
|
fragment_userlist_loading_overlay.gone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleFetchError(throwable: Throwable) {
|
||||||
|
Timber.e(throwable)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleFetchSuccess(users: List<UserViewable>) {
|
||||||
|
chatAdapter.addAll(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setRecycler() {
|
||||||
|
fragment_userlist_recycler_container.apply {
|
||||||
|
layoutManager = LinearLayoutManager(context)
|
||||||
|
adapter = chatAdapter.apply {
|
||||||
|
onItemClick = {
|
||||||
|
context.toast(it.realName)
|
||||||
|
}
|
||||||
|
|
||||||
|
onFavoriteClick = {
|
||||||
|
context.toast(it.userName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.hako.userlist.model
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
import com.hako.base.domain.database.entities.UserEntity
|
||||||
|
import kotlinx.android.parcel.Parcelize
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class User(
|
||||||
|
@SerializedName("id") val id: Int,
|
||||||
|
@SerializedName("name") val realName: String,
|
||||||
|
@SerializedName("username") val userName: String,
|
||||||
|
@SerializedName("email") val email: String,
|
||||||
|
@SerializedName("phone") val phone: String,
|
||||||
|
@SerializedName("website") val website: String
|
||||||
|
) : Parcelable
|
||||||
|
|
||||||
|
data class UserViewable(
|
||||||
|
val id: Int,
|
||||||
|
val realName: String,
|
||||||
|
val userName: String,
|
||||||
|
var isFavorite: Boolean = false
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.hako.userlist.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.userlist.domain.usecase.GetUsers
|
||||||
|
import com.hako.userlist.model.UserViewable
|
||||||
|
import org.koin.core.KoinComponent
|
||||||
|
import org.koin.core.get
|
||||||
|
|
||||||
|
class UserlistViewmodel : ViewModel(), KoinComponent {
|
||||||
|
|
||||||
|
val data = MutableLiveData<Either<Throwable, List<UserViewable>>>()
|
||||||
|
val requestStatus = MutableLiveData<RequestStatus>()
|
||||||
|
|
||||||
|
private val getUsers: GetUsers = get()
|
||||||
|
|
||||||
|
fun fetchUsers() {
|
||||||
|
getUsers.execute(
|
||||||
|
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,62 @@
|
|||||||
|
package com.hako.userlist.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.userlist.model.UserViewable
|
||||||
|
import com.hako.friendlist_userlist.R
|
||||||
|
import kotlinx.android.synthetic.main.item_user_card.view.*
|
||||||
|
import kotlin.properties.Delegates
|
||||||
|
|
||||||
|
class UserlistAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||||
|
|
||||||
|
private var items by Delegates.observable(emptyList<UserViewable>()) { _, oldList, newList ->
|
||||||
|
autoNotify(oldList, newList) { old, new -> old.id == new.id }
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
var onItemClick: (UserViewable) -> Unit = { }
|
||||||
|
var onFavoriteClick: (UserViewable) -> Unit = { }
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder =
|
||||||
|
UserViewHolder(
|
||||||
|
LayoutInflater
|
||||||
|
.from(parent.context)
|
||||||
|
.inflate(R.layout.item_user_card, parent, false),
|
||||||
|
onItemClick,
|
||||||
|
onFavoriteClick
|
||||||
|
)
|
||||||
|
|
||||||
|
fun getItem(position: Int) = items[position]
|
||||||
|
|
||||||
|
fun addAll(list: List<UserViewable>) {
|
||||||
|
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: (UserViewable) -> Unit,
|
||||||
|
private val onFavoriteClick: (UserViewable) -> Unit) :
|
||||||
|
RecyclerView.ViewHolder(view) {
|
||||||
|
|
||||||
|
fun bind(user: UserViewable) = with(view) {
|
||||||
|
item_user_card_real_name.text = user.realName
|
||||||
|
item_user_card_user_name.text = user.userName
|
||||||
|
item_user_card_container.setOnClickListener {
|
||||||
|
onItemClick(user)
|
||||||
|
}
|
||||||
|
item_user_card_like_button.setOnClickListener {
|
||||||
|
onFavoriteClick(user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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_userlist_base">
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/fragment_userlist_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_userlist_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_userlist_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,44 @@
|
|||||||
|
<?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_user_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_user_card_real_name"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="24dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:textSize="18sp"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="Real Name" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/item_user_card_user_name"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="12sp"
|
||||||
|
app:layout_constraintStart_toStartOf="@+id/item_user_card_real_name"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/item_user_card_real_name"
|
||||||
|
tools:text="User Name" />
|
||||||
|
|
||||||
|
<com.hako.base.widgets.LikeButton
|
||||||
|
android:id="@+id/item_user_card_like_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="24dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</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/userlist_navigation"
|
||||||
|
app:startDestination="@id/userListFragment">
|
||||||
|
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/userListFragment"
|
||||||
|
android:name="com.hako.userlist.feature.UserlistFragment"
|
||||||
|
tools:layout="@layout/fragment_userlist"
|
||||||
|
android:label="UserListFragment" />
|
||||||
|
|
||||||
|
</navigation>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<resources>
|
||||||
|
<string name="app_name">userlist</string>
|
||||||
|
</resources>
|
||||||
Reference in New Issue
Block a user