mirror of
https://github.com/hmalik144/StackExchangeUsers.git
synced 2025-12-10 03:05:25 +00:00
Updates to unit tests
Added more units tests - room database test Amended ui Changed how recycler view loads data
This commit is contained in:
@@ -70,6 +70,11 @@ dependencies {
|
|||||||
implementation 'android.arch.core:core-testing'
|
implementation 'android.arch.core:core-testing'
|
||||||
androidTestImplementation 'androidx.test:rules:1.3.0-rc01'
|
androidTestImplementation 'androidx.test:rules:1.3.0-rc01'
|
||||||
|
|
||||||
|
// Mockk
|
||||||
|
def mockk_ver = "1.10.2"
|
||||||
|
testImplementation "io.mockk:mockk:$mockk_ver"
|
||||||
|
androidTestImplementation "io.mockk:mockk-android:$mockk_ver"
|
||||||
|
|
||||||
//Retrofit and GSON
|
//Retrofit and GSON
|
||||||
implementation 'com.squareup.retrofit2:retrofit:2.8.1'
|
implementation 'com.squareup.retrofit2:retrofit:2.8.1'
|
||||||
implementation 'com.squareup.retrofit2:converter-gson:2.8.1'
|
implementation 'com.squareup.retrofit2:converter-gson:2.8.1'
|
||||||
@@ -96,4 +101,6 @@ dependencies {
|
|||||||
// Circle Image View
|
// Circle Image View
|
||||||
implementation 'com.mikhaellopez:circularimageview:4.2.0'
|
implementation 'com.mikhaellopez:circularimageview:4.2.0'
|
||||||
|
|
||||||
|
// Latest version of JSONObject - needed to pass unit tests
|
||||||
|
testImplementation 'org.json:json:20180813'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
package com.example.h_mal.stackexchangeusers
|
|
||||||
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
|
||||||
import org.junit.Assert.assertEquals
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Instrumented test, which will execute on an Android device.
|
|
||||||
*
|
|
||||||
* See [testing documentation](http://d.android.com/tools/testing).
|
|
||||||
*/
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
|
||||||
class ExampleInstrumentedTest {
|
|
||||||
@Test
|
|
||||||
fun useAppContext() {
|
|
||||||
// Context of the app under test.
|
|
||||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
|
||||||
assertEquals("com.example.h_mal.candyspace", appContext.packageName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package com.example.h_mal.stackexchangeusers.data.room
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||||
|
import androidx.room.Room
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers
|
||||||
|
import com.example.h_mal.stackexchangeusers.data.room.entities.UserEntity
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.hamcrest.CoreMatchers
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Assert.*
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.rules.TestRule
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
|
class AppDatabaseTest{
|
||||||
|
@get:Rule
|
||||||
|
var rule: TestRule = InstantTaskExecutorRule()
|
||||||
|
|
||||||
|
private lateinit var simpleDao: UsersDao
|
||||||
|
private lateinit var db: AppDatabase
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun createDb() {
|
||||||
|
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||||
|
db = Room.inMemoryDatabaseBuilder(
|
||||||
|
context, AppDatabase::class.java)
|
||||||
|
.build()
|
||||||
|
simpleDao = db.getUsersDao()
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun closeDb() {
|
||||||
|
db.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun writeEntryAndReadResponse(){
|
||||||
|
// Given
|
||||||
|
val entity = UserEntity(123)
|
||||||
|
val latch = CountDownLatch(1)
|
||||||
|
var userEntity: UserEntity? = null
|
||||||
|
|
||||||
|
// When
|
||||||
|
simpleDao.saveAllUsers(listOf(entity))
|
||||||
|
|
||||||
|
// Then
|
||||||
|
simpleDao.getUser(123).observeForever {
|
||||||
|
userEntity = it
|
||||||
|
latch.countDown()
|
||||||
|
}
|
||||||
|
|
||||||
|
ViewMatchers.assertThat(userEntity, CoreMatchers.equalTo(entity))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun upsertUsersAndReadResponse() = runBlocking{
|
||||||
|
// Given
|
||||||
|
val userEntity = UserEntity(123)
|
||||||
|
val newUserEntity = UserEntity(456)
|
||||||
|
var result: List<UserEntity>? = null
|
||||||
|
|
||||||
|
// When
|
||||||
|
simpleDao.saveAllUsers(listOf(userEntity))
|
||||||
|
simpleDao.upsertNewUsers(listOf(newUserEntity))
|
||||||
|
|
||||||
|
// Then
|
||||||
|
val latch = CountDownLatch(1)
|
||||||
|
simpleDao.getAllUsers().observeForever {
|
||||||
|
result = it
|
||||||
|
latch.countDown()
|
||||||
|
}
|
||||||
|
|
||||||
|
ViewMatchers.assertThat(newUserEntity, CoreMatchers.equalTo(result?.get(0)))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.example.h_mal.stackexchangeusers.data.model
|
||||||
|
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import com.example.h_mal.stackexchangeusers.data.room.entities.UserEntity
|
||||||
|
|
||||||
|
data class UserItem(
|
||||||
|
val userId: Int?,
|
||||||
|
val displayName: String?,
|
||||||
|
val bronze: Int?,
|
||||||
|
val silver: Int?,
|
||||||
|
val gold: Int?,
|
||||||
|
val reputation: Int?,
|
||||||
|
val creationDate: Int?,
|
||||||
|
val profileImage: String?
|
||||||
|
){
|
||||||
|
|
||||||
|
constructor(userEntity: UserEntity): this(
|
||||||
|
userEntity.userId,
|
||||||
|
userEntity.displayName,
|
||||||
|
userEntity.bronze,
|
||||||
|
userEntity.silver,
|
||||||
|
userEntity.gold,
|
||||||
|
userEntity.reputation,
|
||||||
|
userEntity.creationDate,
|
||||||
|
userEntity.profileImage
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ package com.example.h_mal.stackexchangeusers.data.network.api
|
|||||||
|
|
||||||
import com.example.h_mal.stackexchangeusers.data.network.api.interceptors.NetworkConnectionInterceptor
|
import com.example.h_mal.stackexchangeusers.data.network.api.interceptors.NetworkConnectionInterceptor
|
||||||
import com.example.h_mal.stackexchangeusers.data.network.api.interceptors.QueryParamsInterceptor
|
import com.example.h_mal.stackexchangeusers.data.network.api.interceptors.QueryParamsInterceptor
|
||||||
import com.example.h_mal.stackexchangeusers.data.network.model.ApiResponse
|
import com.example.h_mal.stackexchangeusers.data.network.response.ApiResponse
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import org.json.JSONObject
|
|||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
abstract class ResponseUnwrap {
|
abstract class SafeApiCall {
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
suspend fun <T : Any> responseUnwrap(
|
suspend fun <T : Any> responseUnwrap(
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.example.h_mal.stackexchangeusers.data.network.model
|
package com.example.h_mal.stackexchangeusers.data.network.response
|
||||||
|
|
||||||
data class ApiResponse(
|
data class ApiResponse(
|
||||||
val items : List<User>?,
|
val items : List<User>?,
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.example.h_mal.stackexchangeusers.data.network.model
|
package com.example.h_mal.stackexchangeusers.data.network.response
|
||||||
|
|
||||||
import com.google.gson.annotations.Expose
|
import com.google.gson.annotations.Expose
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.example.h_mal.stackexchangeusers.data.network.model
|
package com.example.h_mal.stackexchangeusers.data.network.response
|
||||||
|
|
||||||
import com.google.gson.annotations.Expose
|
import com.google.gson.annotations.Expose
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
package com.example.h_mal.stackexchangeusers.data.repositories
|
package com.example.h_mal.stackexchangeusers.data.repositories
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import com.example.h_mal.stackexchangeusers.data.network.model.ApiResponse
|
import com.example.h_mal.stackexchangeusers.data.network.response.ApiResponse
|
||||||
import com.example.h_mal.stackexchangeusers.data.network.model.User
|
import com.example.h_mal.stackexchangeusers.data.network.response.User
|
||||||
import com.example.h_mal.stackexchangeusers.data.room.entities.UserItem
|
import com.example.h_mal.stackexchangeusers.data.room.entities.UserEntity
|
||||||
|
|
||||||
interface Repository {
|
interface Repository {
|
||||||
|
|
||||||
fun getUsersFromDatabase(): LiveData<List<UserItem>>
|
fun getUsersFromDatabase(): LiveData<List<UserEntity>>
|
||||||
fun getSingleUserFromDatabase(id: Int): LiveData<UserItem>
|
fun getSingleUserFromDatabase(id: Int): LiveData<UserEntity>
|
||||||
suspend fun saveUsersToDatabase(users: List<User>)
|
suspend fun saveUsersToDatabase(users: List<User>)
|
||||||
suspend fun getUsersFromApi(username: String): ApiResponse?
|
suspend fun getUsersFromApi(username: String): ApiResponse?
|
||||||
fun saveCurrentSearchToPrefs(username: String)
|
fun saveCurrentSearchToPrefs(username: String)
|
||||||
|
|||||||
@@ -1,33 +1,29 @@
|
|||||||
package com.example.h_mal.stackexchangeusers.data.repositories
|
package com.example.h_mal.stackexchangeusers.data.repositories
|
||||||
|
|
||||||
import com.example.h_mal.stackexchangeusers.data.network.api.ApiClass
|
import com.example.h_mal.stackexchangeusers.data.network.api.ApiClass
|
||||||
import com.example.h_mal.stackexchangeusers.data.network.model.ApiResponse
|
import com.example.h_mal.stackexchangeusers.data.network.response.ApiResponse
|
||||||
import com.example.h_mal.stackexchangeusers.data.network.model.User
|
import com.example.h_mal.stackexchangeusers.data.network.response.User
|
||||||
import com.example.h_mal.stackexchangeusers.data.network.networkUtils.ResponseUnwrap
|
import com.example.h_mal.stackexchangeusers.data.network.networkUtils.SafeApiCall
|
||||||
import com.example.h_mal.stackexchangeusers.data.preferences.PreferenceProvider
|
import com.example.h_mal.stackexchangeusers.data.preferences.PreferenceProvider
|
||||||
import com.example.h_mal.stackexchangeusers.data.room.AppDatabase
|
import com.example.h_mal.stackexchangeusers.data.room.AppDatabase
|
||||||
import com.example.h_mal.stackexchangeusers.data.room.entities.UserItem
|
import com.example.h_mal.stackexchangeusers.data.room.entities.UserEntity
|
||||||
|
|
||||||
const val MILLISECONDS_ONE_MIN = 60000
|
const val MILLISECONDS_ONE_MIN = 60000
|
||||||
class RepositoryImpl(
|
class RepositoryImpl(
|
||||||
private val api: ApiClass,
|
private val api: ApiClass,
|
||||||
private val database: AppDatabase,
|
private val database: AppDatabase,
|
||||||
private val preference: PreferenceProvider
|
private val preference: PreferenceProvider
|
||||||
) : ResponseUnwrap(), Repository {
|
) : SafeApiCall(), Repository {
|
||||||
|
|
||||||
// Current list of users in the database
|
|
||||||
override fun getUsersFromDatabase() = database.getUsersDao().getAllUsers()
|
override fun getUsersFromDatabase() = database.getUsersDao().getAllUsers()
|
||||||
|
|
||||||
// retrieving a single user from an ID
|
|
||||||
override fun getSingleUserFromDatabase(id: Int) = database.getUsersDao().getUser(id)
|
override fun getSingleUserFromDatabase(id: Int) = database.getUsersDao().getUser(id)
|
||||||
|
|
||||||
// save a list of users to the room database
|
|
||||||
override suspend fun saveUsersToDatabase(users: List<User>){
|
override suspend fun saveUsersToDatabase(users: List<User>){
|
||||||
val userList= getUserList(users)
|
val userList= users.map { UserEntity(it) }
|
||||||
database.getUsersDao().upsertNewUsers(userList)
|
database.getUsersDao().upsertNewUsers(userList)
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetch users from an api call
|
|
||||||
override suspend fun getUsersFromApi(username: String): ApiResponse? {
|
override suspend fun getUsersFromApi(username: String): ApiResponse? {
|
||||||
return when (isSearchValid(username)) {
|
return when (isSearchValid(username)) {
|
||||||
true -> responseUnwrap { api.getUsersFromApi(username) }
|
true -> responseUnwrap { api.getUsersFromApi(username) }
|
||||||
@@ -35,14 +31,12 @@ class RepositoryImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// save current time and current search input into shared prefs
|
|
||||||
override fun saveCurrentSearchToPrefs(username: String){
|
override fun saveCurrentSearchToPrefs(username: String){
|
||||||
val time = System.currentTimeMillis()
|
val time = System.currentTimeMillis()
|
||||||
preference.saveLastSavedAt(username, time)
|
preference.saveLastSavedAt(username, time)
|
||||||
}
|
}
|
||||||
|
|
||||||
// boolean response of validity of search
|
// Check if search is valid based on lasted saved
|
||||||
// if the same search is taking place again with a minute return false
|
|
||||||
private fun isSearchValid(username: String): Boolean {
|
private fun isSearchValid(username: String): Boolean {
|
||||||
val time = preference.getLastSavedAt(username) ?: return true
|
val time = preference.getLastSavedAt(username) ?: return true
|
||||||
val currentTime = System.currentTimeMillis()
|
val currentTime = System.currentTimeMillis()
|
||||||
@@ -50,24 +44,4 @@ class RepositoryImpl(
|
|||||||
|
|
||||||
return difference > MILLISECONDS_ONE_MIN
|
return difference > MILLISECONDS_ONE_MIN
|
||||||
}
|
}
|
||||||
|
|
||||||
// map user from api response to userItem entity for database
|
|
||||||
private fun getUserList(users: List<User>): List<UserItem> {
|
|
||||||
return users.map {
|
|
||||||
it.mapToUserItem()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun User.mapToUserItem(): UserItem {
|
|
||||||
return UserItem(
|
|
||||||
userId,
|
|
||||||
displayName,
|
|
||||||
badgeCounts?.bronze,
|
|
||||||
badgeCounts?.silver,
|
|
||||||
badgeCounts?.gold,
|
|
||||||
reputation,
|
|
||||||
creationDate,
|
|
||||||
profileImage
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -4,10 +4,10 @@ import android.content.Context
|
|||||||
import androidx.room.Database
|
import androidx.room.Database
|
||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
import com.example.h_mal.stackexchangeusers.data.room.entities.UserItem
|
import com.example.h_mal.stackexchangeusers.data.room.entities.UserEntity
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
entities = [UserItem::class],
|
entities = [UserEntity::class],
|
||||||
version = 1
|
version = 1
|
||||||
)
|
)
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
|
|||||||
@@ -2,27 +2,27 @@ package com.example.h_mal.stackexchangeusers.data.room
|
|||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.room.*
|
import androidx.room.*
|
||||||
import com.example.h_mal.stackexchangeusers.data.room.entities.UserItem
|
import com.example.h_mal.stackexchangeusers.data.room.entities.UserEntity
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface UsersDao {
|
interface UsersDao {
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
fun saveAllUsers(users : List<UserItem>)
|
fun saveAllUsers(users : List<UserEntity>)
|
||||||
|
|
||||||
@Query("SELECT * FROM UserItem")
|
@Query("SELECT * FROM UserEntity")
|
||||||
fun getAllUsers() : LiveData<List<UserItem>>
|
fun getAllUsers() : LiveData<List<UserEntity>>
|
||||||
|
|
||||||
// clear database and add new entries
|
// clear database and add new entries
|
||||||
@Transaction
|
@Transaction
|
||||||
suspend fun upsertNewUsers(users : List<UserItem>){
|
suspend fun upsertNewUsers(users : List<UserEntity>){
|
||||||
deleteEntries()
|
deleteEntries()
|
||||||
saveAllUsers(users)
|
saveAllUsers(users)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Query("DELETE FROM UserItem")
|
@Query("DELETE FROM UserEntity")
|
||||||
suspend fun deleteEntries()
|
suspend fun deleteEntries()
|
||||||
|
|
||||||
@Query("SELECT * FROM UserItem WHERE userId = :id")
|
@Query("SELECT * FROM UserEntity WHERE userId = :id")
|
||||||
fun getUser(id: Int) : LiveData<UserItem>
|
fun getUser(id: Int) : LiveData<UserEntity>
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.example.h_mal.stackexchangeusers.data.room.entities
|
||||||
|
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import com.example.h_mal.stackexchangeusers.data.network.response.User
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
data class UserEntity(
|
||||||
|
@PrimaryKey(autoGenerate = false)
|
||||||
|
val userId: Int,
|
||||||
|
val displayName: String? = null,
|
||||||
|
val bronze: Int? = null,
|
||||||
|
val silver: Int? = null,
|
||||||
|
val gold: Int? = null,
|
||||||
|
val reputation: Int? = null,
|
||||||
|
val creationDate: Int? = null,
|
||||||
|
val profileImage: String? = null
|
||||||
|
){
|
||||||
|
|
||||||
|
constructor(user: User): this(
|
||||||
|
user.userId!!,
|
||||||
|
user.displayName,
|
||||||
|
user.badgeCounts?.bronze,
|
||||||
|
user.badgeCounts?.silver,
|
||||||
|
user.badgeCounts?.gold,
|
||||||
|
user.reputation,
|
||||||
|
user.creationDate,
|
||||||
|
user.profileImage
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package com.example.h_mal.stackexchangeusers.data.room.entities
|
|
||||||
|
|
||||||
import androidx.room.Entity
|
|
||||||
import androidx.room.PrimaryKey
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
data class UserItem(
|
|
||||||
@PrimaryKey(autoGenerate = false)
|
|
||||||
val userId: Int?,
|
|
||||||
val displayName: String?,
|
|
||||||
val bronze: Int?,
|
|
||||||
val silver: Int?,
|
|
||||||
val gold: Int?,
|
|
||||||
val reputation: Int?,
|
|
||||||
val creationDate: Int?,
|
|
||||||
val profileImage: String?
|
|
||||||
)
|
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
package com.example.h_mal.stackexchangeusers.ui
|
package com.example.h_mal.stackexchangeusers.ui
|
||||||
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import com.example.h_mal.stackexchangeusers.application.AppClass.Companion.idlingResources
|
import com.example.h_mal.stackexchangeusers.application.AppClass.Companion.idlingResources
|
||||||
|
import com.example.h_mal.stackexchangeusers.data.model.UserItem
|
||||||
import com.example.h_mal.stackexchangeusers.data.repositories.Repository
|
import com.example.h_mal.stackexchangeusers.data.repositories.Repository
|
||||||
|
import com.example.h_mal.stackexchangeusers.data.room.entities.UserEntity
|
||||||
import com.example.h_mal.stackexchangeusers.utils.Event
|
import com.example.h_mal.stackexchangeusers.utils.Event
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -16,11 +19,19 @@ class MainViewModel(
|
|||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
// livedata for user items in room database
|
// livedata for user items in room database
|
||||||
val usersLiveData = repository.getUsersFromDatabase()
|
val usersLiveData = MutableLiveData<List<UserItem>>()
|
||||||
|
|
||||||
val operationState = MutableLiveData<Event<Boolean>>()
|
val operationState = MutableLiveData<Event<Boolean>>()
|
||||||
val operationError = MutableLiveData<Event<String>>()
|
val operationError = MutableLiveData<Event<String>>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
val observer = Observer<List<UserEntity>> {
|
||||||
|
val list = it.map {entity -> UserItem(entity) }
|
||||||
|
usersLiveData.postValue(list)
|
||||||
|
}
|
||||||
|
repository.getUsersFromDatabase().observeForever (observer)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fun getUsers(username: String?){
|
fun getUsers(username: String?){
|
||||||
// validate that search term is not empty
|
// validate that search term is not empty
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import androidx.fragment.app.activityViewModels
|
|||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.example.h_mal.stackexchangeusers.R
|
import com.example.h_mal.stackexchangeusers.R
|
||||||
import com.example.h_mal.stackexchangeusers.application.AppClass.Companion.idlingResources
|
import com.example.h_mal.stackexchangeusers.data.model.UserItem
|
||||||
import com.example.h_mal.stackexchangeusers.data.room.entities.UserItem
|
import com.example.h_mal.stackexchangeusers.data.room.entities.UserEntity
|
||||||
import com.example.h_mal.stackexchangeusers.ui.MainViewModel
|
import com.example.h_mal.stackexchangeusers.ui.MainViewModel
|
||||||
import com.example.h_mal.stackexchangeusers.utils.onSubmitListener
|
import com.example.h_mal.stackexchangeusers.utils.onSubmitListener
|
||||||
import kotlinx.android.synthetic.main.main_fragment.*
|
import kotlinx.android.synthetic.main.main_fragment.*
|
||||||
@@ -21,11 +21,12 @@ import kotlinx.android.synthetic.main.main_fragment.*
|
|||||||
* Results for users will be displayed here
|
* Results for users will be displayed here
|
||||||
*/
|
*/
|
||||||
class MainFragment : Fragment() {
|
class MainFragment : Fragment() {
|
||||||
/**
|
|
||||||
* get [MainViewModel] from [MainActivity] (the calling activity)
|
// Retrieve view model from main activity
|
||||||
*/
|
|
||||||
val viewModel: MainViewModel by activityViewModels()
|
val viewModel: MainViewModel by activityViewModels()
|
||||||
|
|
||||||
|
lateinit var mAdapter: UserRecyclerViewAdapter
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater, container: ViewGroup?,
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
@@ -42,6 +43,13 @@ class MainFragment : Fragment() {
|
|||||||
viewModel.getUsers(query.toString().trim())
|
viewModel.getUsers(query.toString().trim())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mAdapter = UserRecyclerViewAdapter(requireView())
|
||||||
|
recycler_view.apply {
|
||||||
|
layoutManager = LinearLayoutManager(context)
|
||||||
|
setHasFixedSize(true)
|
||||||
|
adapter = mAdapter
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
@@ -55,18 +63,7 @@ class MainFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val usersObserver = Observer<List<UserItem>> {
|
private val usersObserver = Observer<List<UserItem>> {
|
||||||
// create adapter the recycler view
|
mAdapter.updateList(it)
|
||||||
val mAdapter = UserRecyclerViewAdapter(
|
|
||||||
requireView(),
|
|
||||||
it
|
|
||||||
)
|
|
||||||
|
|
||||||
// setup the recyclerview
|
|
||||||
recycler_view.apply {
|
|
||||||
layoutManager = LinearLayoutManager(context)
|
|
||||||
setHasFixedSize(true)
|
|
||||||
adapter = mAdapter
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,24 +3,33 @@ package com.example.h_mal.stackexchangeusers.ui.main.pages.home
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.os.persistableBundleOf
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.example.h_mal.stackexchangeusers.R
|
import com.example.h_mal.stackexchangeusers.R
|
||||||
import com.example.h_mal.stackexchangeusers.data.room.entities.UserItem
|
import com.example.h_mal.stackexchangeusers.data.model.UserItem
|
||||||
|
import com.example.h_mal.stackexchangeusers.data.room.entities.UserEntity
|
||||||
import com.example.h_mal.stackexchangeusers.utils.loadImage
|
import com.example.h_mal.stackexchangeusers.utils.loadImage
|
||||||
import com.example.h_mal.stackexchangeusers.utils.navigateTo
|
import com.example.h_mal.stackexchangeusers.utils.navigateTo
|
||||||
import kotlinx.android.extensions.LayoutContainer
|
import kotlinx.android.extensions.LayoutContainer
|
||||||
import kotlinx.android.synthetic.main.simple_item.view.*
|
import kotlinx.android.synthetic.main.simple_item.view.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a Recyclerview adapter used to display [UserItem]
|
* Create a Recyclerview adapter used to display [UserEntity]
|
||||||
* @param parentView is used for context and navigation
|
* @param parentView is used for context and navigation
|
||||||
* @param items is the list of users parsed from the database
|
|
||||||
*/
|
*/
|
||||||
class UserRecyclerViewAdapter (
|
class UserRecyclerViewAdapter (
|
||||||
val parentView: View,
|
val parentView: View
|
||||||
val items: List<UserItem>
|
|
||||||
): RecyclerView.Adapter<RecyclerView.ViewHolder>(){
|
): RecyclerView.Adapter<RecyclerView.ViewHolder>(){
|
||||||
|
|
||||||
|
var list = mutableListOf<UserItem>()
|
||||||
|
|
||||||
|
fun updateList(users: List<UserItem>) {
|
||||||
|
list.clear()
|
||||||
|
list.addAll(users)
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// create a viewholder with [R.layout.simple_item]
|
// create a viewholder with [R.layout.simple_item]
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||||
val view = LayoutInflater
|
val view = LayoutInflater
|
||||||
@@ -31,13 +40,13 @@ class UserRecyclerViewAdapter (
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
override fun getItemCount(): Int {
|
||||||
return items.size
|
return list.size
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||||
val view = holder as ItemOne
|
val view = holder as ItemOne
|
||||||
|
|
||||||
items[position].let {
|
list[position].let {
|
||||||
view.bindUser(it)
|
view.bindUser(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,8 +62,7 @@ class UserRecyclerViewAdapter (
|
|||||||
itemView.text2.text = userItem.reputation.toString()
|
itemView.text2.text = userItem.reputation.toString()
|
||||||
|
|
||||||
itemView.setOnClickListener {
|
itemView.setOnClickListener {
|
||||||
val action =
|
val action = MainFragmentDirections.toUserProfileFragment(list[layoutPosition].userId!!)
|
||||||
MainFragmentDirections.toUserProfileFragment(items[position].userId!!)
|
|
||||||
parentView.navigateTo(action)
|
parentView.navigateTo(action)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,16 +8,14 @@ import androidx.fragment.app.Fragment
|
|||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import com.example.h_mal.stackexchangeusers.R
|
import com.example.h_mal.stackexchangeusers.R
|
||||||
import com.example.h_mal.stackexchangeusers.data.room.entities.UserItem
|
import com.example.h_mal.stackexchangeusers.data.room.entities.UserEntity
|
||||||
import com.example.h_mal.stackexchangeusers.ui.MainViewModel
|
import com.example.h_mal.stackexchangeusers.ui.MainViewModel
|
||||||
import com.example.h_mal.stackexchangeusers.utils.epochToData
|
import com.example.h_mal.stackexchangeusers.utils.epochToData
|
||||||
import com.example.h_mal.stackexchangeusers.utils.loadImage
|
import com.example.h_mal.stackexchangeusers.utils.loadImage
|
||||||
import kotlinx.android.synthetic.main.fragment_user_profile.*
|
import kotlinx.android.synthetic.main.fragment_user_profile.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A simple [Fragment] subclass.
|
* Fragment to show the user overview
|
||||||
* Use the [UserProfileFragment.newInstance] factory method to
|
|
||||||
* create an instance of this fragment.
|
|
||||||
*/
|
*/
|
||||||
class UserProfileFragment : Fragment() {
|
class UserProfileFragment : Fragment() {
|
||||||
val viewModel: MainViewModel by activityViewModels()
|
val viewModel: MainViewModel by activityViewModels()
|
||||||
@@ -43,7 +41,7 @@ class UserProfileFragment : Fragment() {
|
|||||||
viewModel.getSingleUser(id).removeObserver(singleUserObserver)
|
viewModel.getSingleUser(id).removeObserver(singleUserObserver)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val singleUserObserver = Observer<UserItem> {
|
private val singleUserObserver = Observer<UserEntity> {
|
||||||
username.text = it.displayName
|
username.text = it.displayName
|
||||||
reputation.text = it.reputation.toString()
|
reputation.text = it.reputation.toString()
|
||||||
gold_score.text = it.gold.toString()
|
gold_score.text = it.gold.toString()
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import java.util.*
|
|||||||
fun epochToData(number: Int?): String {
|
fun epochToData(number: Int?): String {
|
||||||
number ?: return ""
|
number ?: return ""
|
||||||
return try {
|
return try {
|
||||||
val sdf = SimpleDateFormat("dd/MM/yyyy")
|
val sdf = SimpleDateFormat("dd/MM/yyyy", Locale.getDefault())
|
||||||
val netDate = Date(number.toLong() * 1000)
|
val netDate = Date(number.toLong() * 1000)
|
||||||
sdf.format(netDate)
|
sdf.format(netDate)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|||||||
@@ -4,20 +4,21 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:layout_margin="16dp"
|
||||||
tools:context=".ui.main.pages.user.UserProfileFragment">
|
tools:context=".ui.main.pages.user.UserProfileFragment">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/imageView"
|
android:id="@+id/imageView"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
app:layout_constraintHeight_percent="0.25"
|
|
||||||
android:adjustViewBounds="true"
|
android:adjustViewBounds="true"
|
||||||
app:layout_constraintVertical_bias="0.12"
|
|
||||||
android:src="@mipmap/ic_launcher"
|
android:src="@mipmap/ic_launcher"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintHeight_percent="0.25"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintVertical_bias="0.12"
|
||||||
tools:src="@drawable/ic_launcher_foreground" />
|
tools:src="@drawable/ic_launcher_foreground" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
@@ -25,37 +26,38 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="12dp"
|
android:layout_marginTop="12dp"
|
||||||
android:text="Username:"
|
android:text="@string/username"
|
||||||
app:layout_constraintRight_toLeftOf="@id/divider"
|
app:layout_constraintRight_toLeftOf="@id/divider"
|
||||||
app:layout_constraintTop_toBottomOf="@id/imageView" />
|
app:layout_constraintTop_toBottomOf="@id/imageView" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/username"
|
android:id="@+id/username"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="12dp"
|
android:layout_marginTop="12dp"
|
||||||
android:text="@{user.displayName}"
|
android:maxLines="1"
|
||||||
app:layout_constraintLeft_toRightOf="@id/divider"
|
app:layout_constraintLeft_toRightOf="@id/divider"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/imageView"
|
app:layout_constraintTop_toBottomOf="@+id/imageView"
|
||||||
tools:text="Username1234" />
|
tools:text="Username1234" />
|
||||||
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/reputation_label"
|
android:id="@+id/reputation_label"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="12dp"
|
android:layout_marginTop="12dp"
|
||||||
android:text="Reputation:"
|
android:text="@string/reputation"
|
||||||
app:layout_constraintRight_toLeftOf="@id/divider"
|
app:layout_constraintRight_toLeftOf="@id/divider"
|
||||||
app:layout_constraintTop_toBottomOf="@id/username_label" />
|
app:layout_constraintTop_toBottomOf="@id/username_label" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/reputation"
|
android:id="@+id/reputation"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="12dp"
|
android:layout_marginTop="12dp"
|
||||||
android:text="@{user.reputation.toString()}"
|
android:maxLines="1"
|
||||||
app:layout_constraintLeft_toRightOf="@id/divider"
|
app:layout_constraintLeft_toRightOf="@id/divider"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/username"
|
app:layout_constraintTop_toBottomOf="@+id/username"
|
||||||
tools:text="1548252" />
|
tools:text="1548252" />
|
||||||
|
|
||||||
@@ -64,7 +66,7 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="12dp"
|
android:layout_marginTop="12dp"
|
||||||
android:text="Bagdes:"
|
android:text="@string/bagdes"
|
||||||
app:layout_constraintRight_toLeftOf="@id/divider"
|
app:layout_constraintRight_toLeftOf="@id/divider"
|
||||||
app:layout_constraintTop_toBottomOf="@id/reputation_label" />
|
app:layout_constraintTop_toBottomOf="@id/reputation_label" />
|
||||||
|
|
||||||
@@ -73,16 +75,17 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="12dp"
|
android:layout_marginTop="12dp"
|
||||||
android:text="Gold - "
|
android:text="@string/gold"
|
||||||
app:layout_constraintLeft_toRightOf="@id/divider"
|
app:layout_constraintLeft_toRightOf="@id/divider"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/reputation" />
|
app:layout_constraintTop_toBottomOf="@+id/reputation" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/gold_score"
|
android:id="@+id/gold_score"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@{user.badgeCounts.gold.toString()}"
|
android:maxLines="1"
|
||||||
app:layout_constraintLeft_toRightOf="@id/badges_gold"
|
app:layout_constraintLeft_toRightOf="@id/badges_gold"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="@id/badges_gold"
|
app:layout_constraintTop_toTopOf="@id/badges_gold"
|
||||||
tools:text="3554" />
|
tools:text="3554" />
|
||||||
|
|
||||||
@@ -91,16 +94,17 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="6dp"
|
android:layout_marginTop="6dp"
|
||||||
android:text="Silver - "
|
android:text="@string/silver"
|
||||||
app:layout_constraintLeft_toRightOf="@id/divider"
|
app:layout_constraintLeft_toRightOf="@id/divider"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/badges_gold" />
|
app:layout_constraintTop_toBottomOf="@+id/badges_gold" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/silver_score"
|
android:id="@+id/silver_score"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@{user.badgeCounts.silver.toString()}"
|
android:maxLines="1"
|
||||||
app:layout_constraintLeft_toRightOf="@id/badges_silver"
|
app:layout_constraintLeft_toRightOf="@id/badges_silver"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="@id/badges_silver"
|
app:layout_constraintTop_toTopOf="@id/badges_silver"
|
||||||
tools:text="3554" />
|
tools:text="3554" />
|
||||||
|
|
||||||
@@ -109,16 +113,17 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="6dp"
|
android:layout_marginTop="6dp"
|
||||||
android:text="Bronze - "
|
android:text="@string/bronze"
|
||||||
app:layout_constraintLeft_toRightOf="@id/divider"
|
app:layout_constraintLeft_toRightOf="@id/divider"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/badges_silver" />
|
app:layout_constraintTop_toBottomOf="@+id/badges_silver" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/bronze_score"
|
android:id="@+id/bronze_score"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@{user.badgeCounts.bronze.toString()}"
|
android:maxLines="1"
|
||||||
app:layout_constraintLeft_toRightOf="@id/badges_bronze"
|
app:layout_constraintLeft_toRightOf="@id/badges_bronze"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="@id/badges_bronze"
|
app:layout_constraintTop_toTopOf="@id/badges_bronze"
|
||||||
tools:text="3554" />
|
tools:text="3554" />
|
||||||
|
|
||||||
@@ -127,17 +132,18 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="12dp"
|
android:layout_marginTop="12dp"
|
||||||
android:text="Date Joined:"
|
android:text="@string/date_joined"
|
||||||
app:layout_constraintRight_toLeftOf="@id/divider"
|
app:layout_constraintRight_toLeftOf="@id/divider"
|
||||||
app:layout_constraintTop_toBottomOf="@id/badges_bronze" />
|
app:layout_constraintTop_toBottomOf="@id/badges_bronze" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/date_joined"
|
android:id="@+id/date_joined"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="12dp"
|
android:layout_marginTop="12dp"
|
||||||
android:text="@{ConverterUtil.epochToData(user.creationDate)}"
|
android:maxLines="1"
|
||||||
app:layout_constraintLeft_toRightOf="@id/divider"
|
app:layout_constraintLeft_toRightOf="@id/divider"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/badges_bronze"
|
app:layout_constraintTop_toBottomOf="@+id/badges_bronze"
|
||||||
tools:text="31/01/2019" />
|
tools:text="31/01/2019" />
|
||||||
|
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
|
||||||
|
|
||||||
<data>
|
|
||||||
<variable
|
|
||||||
name="user"
|
|
||||||
type="com.example.h_mal.stackexchangeusers.data.network.model.User" />
|
|
||||||
</data>
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" >
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/textView"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_margin="12dp"
|
|
||||||
tools:text="12345"
|
|
||||||
android:text="@{user.userId.toString()}"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/textView2"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_margin="12dp"
|
|
||||||
tools:text="Username"
|
|
||||||
android:text="@{user.displayName}"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:layout_constraintLeft_toRightOf="@id/textView"
|
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent" />
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
</layout>
|
|
||||||
@@ -5,4 +5,11 @@
|
|||||||
<string name="hello_blank_fragment">Hello blank fragment</string>
|
<string name="hello_blank_fragment">Hello blank fragment</string>
|
||||||
|
|
||||||
<string name="invalid_entry">Enter a valid username</string>
|
<string name="invalid_entry">Enter a valid username</string>
|
||||||
|
<string name="username">Username:</string>
|
||||||
|
<string name="reputation">Reputation:</string>
|
||||||
|
<string name="bagdes">Bagdes:</string>
|
||||||
|
<string name="gold">Gold -</string>
|
||||||
|
<string name="silver">Silver -</string>
|
||||||
|
<string name="bronze">Bronze -</string>
|
||||||
|
<string name="date_joined">Date Joined:</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
package com.example.h_mal.stackexchangeusers
|
|
||||||
|
|
||||||
import org.junit.Assert.assertEquals
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Example local unit test, which will execute on the development machine (host).
|
|
||||||
*
|
|
||||||
* See [testing documentation](http://d.android.com/tools/testing).
|
|
||||||
*/
|
|
||||||
class ExampleUnitTest {
|
|
||||||
@Test
|
|
||||||
fun addition_isCorrect() {
|
|
||||||
assertEquals(4, 2 + 2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package com.example.h_mal.stackexchangeusers.data.network.networkUtils
|
||||||
|
|
||||||
|
import com.example.h_mal.stackexchangeusers.data.network.response.ApiResponse
|
||||||
|
import io.mockk.mockk
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
|
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||||
|
import org.junit.Assert.*
|
||||||
|
import org.junit.Test
|
||||||
|
import retrofit2.Response
|
||||||
|
import java.io.IOException
|
||||||
|
import kotlin.test.assertFailsWith
|
||||||
|
|
||||||
|
class SafeApiCallTest: SafeApiCall(){
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun successfulResponse_SuccessfulOutput() = runBlocking{
|
||||||
|
// GIVEN
|
||||||
|
val mockApiResponse = mockk<ApiResponse>()
|
||||||
|
val mockResponse = Response.success(mockApiResponse)
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
val result = responseUnwrap { mockResponse }
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assertNotNull(result)
|
||||||
|
assertEquals(mockApiResponse, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun unsuccessfulResponse_thrownOutput() = runBlocking{
|
||||||
|
// GIVEN
|
||||||
|
val errorMessage = "{\n" +
|
||||||
|
" \"status_code\": 7,\n" +
|
||||||
|
" \"error_message\": \"Invalid API key: You must be granted a valid key.\",\n" +
|
||||||
|
"}"
|
||||||
|
|
||||||
|
val errorResponseBody = errorMessage.toResponseBody("application/json".toMediaTypeOrNull())
|
||||||
|
val mockResponse = Response.error<String>(404, errorResponseBody)
|
||||||
|
|
||||||
|
//THEN - assert exception is not null
|
||||||
|
val ioExceptionReturned = assertFailsWith<IOException> {
|
||||||
|
responseUnwrap { mockResponse }!!
|
||||||
|
}
|
||||||
|
|
||||||
|
assertNotNull(ioExceptionReturned)
|
||||||
|
assertEquals(ioExceptionReturned.message, "Invalid API key: You must be granted a valid key.")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
package com.example.h_mal.stackexchangeusers.data.repositories
|
package com.example.h_mal.stackexchangeusers.data.repositories
|
||||||
|
|
||||||
import com.example.h_mal.stackexchangeusers.data.network.api.ApiClass
|
import com.example.h_mal.stackexchangeusers.data.network.api.ApiClass
|
||||||
import com.example.h_mal.stackexchangeusers.data.network.model.ApiResponse
|
import com.example.h_mal.stackexchangeusers.data.network.response.ApiResponse
|
||||||
import com.example.h_mal.stackexchangeusers.data.preferences.PreferenceProvider
|
import com.example.h_mal.stackexchangeusers.data.preferences.PreferenceProvider
|
||||||
import com.example.h_mal.stackexchangeusers.data.room.AppDatabase
|
import com.example.h_mal.stackexchangeusers.data.room.AppDatabase
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertNotNull
|
import org.junit.Assert.assertNotNull
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
@@ -15,6 +16,7 @@ import org.mockito.Mockito.mock
|
|||||||
import org.mockito.MockitoAnnotations
|
import org.mockito.MockitoAnnotations
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
import okhttp3.ResponseBody
|
import okhttp3.ResponseBody
|
||||||
|
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||||
import okio.IOException
|
import okio.IOException
|
||||||
import kotlin.test.assertFailsWith
|
import kotlin.test.assertFailsWith
|
||||||
|
|
||||||
@@ -49,44 +51,49 @@ class RepositoryTest {
|
|||||||
Mockito.`when`(prefs.getLastSavedAt(input)).thenReturn(null)
|
Mockito.`when`(prefs.getLastSavedAt(input)).thenReturn(null)
|
||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
val getUser = repository.getUsersFromApi(input)
|
val apiResponse = repository.getUsersFromApi(input)
|
||||||
assertNotNull(getUser)
|
assertNotNull(apiResponse)
|
||||||
assertEquals(mockApiResponse, getUser)
|
assertEquals(mockApiResponse, apiResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun fetchUserFromApi_negativeResponse() = runBlocking {
|
fun fetchUserFromApi_negativeResponse() = runBlocking {
|
||||||
//GIVEN
|
//GIVEN
|
||||||
//mock retrofit error response
|
val input = "12345"
|
||||||
val mockBody = mock(ResponseBody::class.java)
|
val errorMessage = "{\n" +
|
||||||
val mockRaw = mock(okhttp3.Response::class.java)
|
" \"status_code\": 7,\n" +
|
||||||
val re = Response.error<String>(mockBody, mockRaw)
|
" \"error_message\": \"Invalid API key: You must be granted a valid key.\",\n" +
|
||||||
|
"}"
|
||||||
|
|
||||||
|
val errorResponseBody = errorMessage.toResponseBody("application/json".toMediaTypeOrNull())
|
||||||
|
val mockResponse = Response.error<String>(404, errorResponseBody)
|
||||||
|
|
||||||
//WHEN
|
//WHEN
|
||||||
Mockito.`when`(api.getUsersFromApi("12345")).then { re }
|
Mockito.`when`(api.getUsersFromApi(input)).then { mockResponse }
|
||||||
|
|
||||||
//THEN - assert exception is not null
|
//THEN - assert exception is not null
|
||||||
val ioExceptionReturned = assertFailsWith<IOException> {
|
val ioExceptionReturned = assertFailsWith<IOException> {
|
||||||
repository.getUsersFromApi("12345")
|
repository.getUsersFromApi(input)
|
||||||
}
|
}
|
||||||
assertNotNull(ioExceptionReturned)
|
assertNotNull(ioExceptionReturned)
|
||||||
assertNotNull(ioExceptionReturned.message)
|
assertEquals(ioExceptionReturned.message, "Invalid API key: You must be granted a valid key.")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun fetchUserFromApi_alreadySearched() = runBlocking {
|
fun fetchUserFromApi_alreadySearched() = runBlocking {
|
||||||
// GIVEN
|
// GIVEN
|
||||||
|
val input = "12345"
|
||||||
val mockApiResponse = mock(ApiResponse::class.java)
|
val mockApiResponse = mock(ApiResponse::class.java)
|
||||||
val mockResponse = Response.success(mockApiResponse)
|
val mockResponse = Response.success(mockApiResponse)
|
||||||
|
|
||||||
//WHEN
|
//WHEN
|
||||||
Mockito.`when`(api.getUsersFromApi("12345")).thenReturn(
|
Mockito.`when`(api.getUsersFromApi(input)).thenReturn(
|
||||||
mockResponse
|
mockResponse
|
||||||
)
|
)
|
||||||
Mockito.`when`(prefs.getLastSavedAt("12345")).thenReturn(System.currentTimeMillis())
|
Mockito.`when`(prefs.getLastSavedAt(input)).thenReturn(System.currentTimeMillis())
|
||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
val getUser = repository.getUsersFromApi("12345")
|
val getUser = repository.getUsersFromApi(input)
|
||||||
assertEquals(getUser, null)
|
assertEquals(getUser, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,14 +2,11 @@ package com.example.h_mal.stackexchangeusers.ui.main
|
|||||||
|
|
||||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import com.example.h_mal.stackexchangeusers.data.network.response.ApiResponse
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import com.example.h_mal.stackexchangeusers.data.network.model.ApiResponse
|
|
||||||
import com.example.h_mal.stackexchangeusers.data.network.model.User
|
|
||||||
import com.example.h_mal.stackexchangeusers.data.repositories.Repository
|
import com.example.h_mal.stackexchangeusers.data.repositories.Repository
|
||||||
import com.example.h_mal.stackexchangeusers.data.repositories.RepositoryImpl
|
import com.example.h_mal.stackexchangeusers.data.room.entities.UserEntity
|
||||||
import com.example.h_mal.stackexchangeusers.data.room.entities.UserItem
|
|
||||||
import com.example.h_mal.stackexchangeusers.ui.MainViewModel
|
import com.example.h_mal.stackexchangeusers.ui.MainViewModel
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
@@ -22,8 +19,6 @@ import org.mockito.Mockito.mock
|
|||||||
import org.mockito.MockitoAnnotations
|
import org.mockito.MockitoAnnotations
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import kotlin.test.assertFalse
|
import kotlin.test.assertFalse
|
||||||
import kotlin.test.assertNotNull
|
|
||||||
import kotlin.test.assertTrue
|
|
||||||
|
|
||||||
class MainViewModelTest {
|
class MainViewModelTest {
|
||||||
|
|
||||||
@@ -38,8 +33,11 @@ class MainViewModelTest {
|
|||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
MockitoAnnotations.initMocks(this)
|
MockitoAnnotations.initMocks(this)
|
||||||
viewModel = MainViewModel(repository)
|
|
||||||
|
|
||||||
|
val mockLiveData = object: LiveData<List<UserEntity>>(){}
|
||||||
|
Mockito.`when`(repository.getUsersFromDatabase()).thenReturn(mockLiveData)
|
||||||
|
|
||||||
|
viewModel = MainViewModel(repository)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -52,11 +50,8 @@ class MainViewModelTest {
|
|||||||
|
|
||||||
//THEN
|
//THEN
|
||||||
viewModel.getUsers("12345")
|
viewModel.getUsers("12345")
|
||||||
viewModel.operationState.observeForever{
|
delay(50)
|
||||||
it.getContentIfNotHandled()?.let {result ->
|
assertFalse { viewModel.operationState.value?.getContentIfNotHandled()!! }
|
||||||
assertFalse { result }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -65,11 +60,8 @@ class MainViewModelTest {
|
|||||||
Mockito.`when`(repository.getUsersFromApi("12345")).thenAnswer{ throw IOException("throwed") }
|
Mockito.`when`(repository.getUsersFromApi("12345")).thenAnswer{ throw IOException("throwed") }
|
||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
viewModel.getUsers("fsdfsdf")
|
viewModel.getUsers("12345")
|
||||||
viewModel.operationError.observeForever{
|
delay(50)
|
||||||
it.getContentIfNotHandled()?.let {result ->
|
assertEquals (viewModel.operationError.value?.getContentIfNotHandled()!!, "throwed")
|
||||||
assertEquals(result, "throwed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user