mirror of
https://github.com/imcarlost/Friendlists.git
synced 2026-04-10 02:46:54 -04:00
Merge pull request #4 from hakodeveloper/feature/userlist
Userlist Implementation
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
34
app/src/main/java/com/hako/friendlists/MainApplication.kt
Normal file
34
app/src/main/java/com/hako/friendlists/MainApplication.kt
Normal file
@@ -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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
app/src/main/java/com/hako/friendlists/di/AppModules.kt
Normal file
18
app/src/main/java/com/hako/friendlists/di/AppModules.kt
Normal file
@@ -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) }
|
||||||
|
}
|
||||||
22
app/src/main/java/com/hako/friendlists/view/MainActivity.kt
Normal file
22
app/src/main/java/com/hako/friendlists/view/MainActivity.kt
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
8
app/src/main/res/layout/activity_main.xml
Normal file
8
app/src/main/res/layout/activity_main.xml
Normal file
@@ -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"/>
|
||||||
9
app/src/main/res/navigation/main_navigation.xml
Normal file
9
app/src/main/res/navigation/main_navigation.xml
Normal file
@@ -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
|
||||||
|
|||||||
33
base/src/main/java/com/hako/base/domain/Either.kt
Normal file
33
base/src/main/java/com/hako/base/domain/Either.kt
Normal file
@@ -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))
|
||||||
5
base/src/main/java/com/hako/base/domain/UseCase.kt
Normal file
5
base/src/main/java/com/hako/base/domain/UseCase.kt
Normal file
@@ -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)
|
||||||
46
base/src/main/java/com/hako/base/widgets/LikeButton.kt
Normal file
46
base/src/main/java/com/hako/base/widgets/LikeButton.kt
Normal file
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
18
base/src/main/java/com/hako/base/widgets/LoadingOverlay.kt
Normal file
18
base/src/main/java/com/hako/base/widgets/LoadingOverlay.kt
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
9
base/src/main/res/drawable/bg_card.xml
Normal file
9
base/src/main/res/drawable/bg_card.xml
Normal file
@@ -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>
|
||||||
19
base/src/main/res/layout/like_button.xml
Normal file
19
base/src/main/res/layout/like_button.xml
Normal file
@@ -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>
|
||||||
21
base/src/main/res/layout/loading_overlay.xml
Normal file
21
base/src/main/res/layout/loading_overlay.xml
Normal file
@@ -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>
|
||||||
33
base/src/main/res/layout/network_error_overlay.xml
Normal file
33
base/src/main/res/layout/network_error_overlay.xml
Normal file
@@ -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>
|
||||||
1
base/src/main/res/raw/like_animation.json
Normal file
1
base/src/main/res/raw/like_animation.json
Normal file
File diff suppressed because one or more lines are too long
1
base/src/main/res/raw/loading_animation.json
Normal file
1
base/src/main/res/raw/loading_animation.json
Normal file
File diff suppressed because one or more lines are too long
1
base/src/main/res/raw/network_animation.json
Normal file
1
base/src/main/res/raw/network_animation.json
Normal file
File diff suppressed because one or more lines are too long
8
base/src/main/res/values/colors.xml
Normal file
8
base/src/main/res/values/colors.xml
Normal file
@@ -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
core.gradle
Normal file
29
core.gradle
Normal file
@@ -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,2 +1,2 @@
|
|||||||
include ':app', ':base'
|
include ':app', ':base', ':userlist'
|
||||||
rootProject.name='Friendlists'
|
rootProject.name='Friendlists'
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
1
userlist/.gitignore
vendored
Normal file
1
userlist/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/build
|
||||||
6
userlist/build.gradle
Normal file
6
userlist/build.gradle
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
apply plugin: 'com.android.library'
|
||||||
|
apply from: '../core.gradle'
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation project(':base')
|
||||||
|
}
|
||||||
0
userlist/consumer-rules.pro
Normal file
0
userlist/consumer-rules.pro
Normal file
21
userlist/proguard-rules.pro
vendored
Normal file
21
userlist/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
||||||
2
userlist/src/main/AndroidManifest.xml
Normal file
2
userlist/src/main/AndroidManifest.xml
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
userlist/src/main/java/com/hako/userlist/model/UserModels.kt
Normal file
28
userlist/src/main/java/com/hako/userlist/model/UserModels.kt
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
38
userlist/src/main/res/layout/fragment_userlist.xml
Normal file
38
userlist/src/main/res/layout/fragment_userlist.xml
Normal 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_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>
|
||||||
44
userlist/src/main/res/layout/item_user_card.xml
Normal file
44
userlist/src/main/res/layout/item_user_card.xml
Normal file
@@ -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>
|
||||||
14
userlist/src/main/res/navigation/userlist_navigation.xml
Normal file
14
userlist/src/main/res/navigation/userlist_navigation.xml
Normal 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/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>
|
||||||
3
userlist/src/main/res/values/strings.xml
Normal file
3
userlist/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<resources>
|
||||||
|
<string name="app_name">userlist</string>
|
||||||
|
</resources>
|
||||||
Reference in New Issue
Block a user