1
0
Fork 0
mirror of https://github.com/anyproto/anytype-kotlin.git synced 2025-06-08 05:47:05 +09:00

Account list loading. Refactoring. (#6)

* Created use case for loading account images.
* Added new implementation for generic FlowUseCase, also added test for observe-accounts use case. Reverted changes in ProfileView model class.
This commit is contained in:
ubu 2019-11-04 15:34:52 +03:00 committed by GitHub
parent 415b0865ee
commit a749e09ab7
Signed by: github
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 418 additions and 51 deletions

View file

@ -5,6 +5,8 @@ import com.agileburo.anytype.core_utils.di.scope.PerScreen
import com.agileburo.anytype.domain.auth.interactor.*
import com.agileburo.anytype.domain.auth.repo.AuthRepository
import com.agileburo.anytype.domain.auth.repo.PathProvider
import com.agileburo.anytype.domain.image.ImageLoader
import com.agileburo.anytype.domain.image.LoadAccountImages
import com.agileburo.anytype.presentation.auth.account.CreateAccountViewModelFactory
import com.agileburo.anytype.presentation.auth.account.SelectAccountViewModelFactory
import com.agileburo.anytype.presentation.auth.account.SetupNewAccountViewModelFactory
@ -227,11 +229,13 @@ class SelectAccountModule {
@Provides
fun provideSelectAccountViewModelFactory(
startLoadingAccounts: StartLoadingAccounts,
observeAccounts: ObserveAccounts
observeAccounts: ObserveAccounts,
loadAccountImages: LoadAccountImages
): SelectAccountViewModelFactory {
return SelectAccountViewModelFactory(
startLoadingAccounts = startLoadingAccounts,
observeAccounts = observeAccounts
observeAccounts = observeAccounts,
loadAccountImages = loadAccountImages
)
}
@ -252,6 +256,12 @@ class SelectAccountModule {
repository = repository
)
}
@Provides
@PerScreen
fun provideLoadAccountImagesUseCase(
loader: ImageLoader
): LoadAccountImages = LoadAccountImages(loader = loader)
}
@Module

View file

@ -3,6 +3,8 @@ package com.agileburo.anytype.data.auth.other
import com.agileburo.anytype.data.auth.mapper.toEntity
import com.agileburo.anytype.domain.auth.model.Image
import com.agileburo.anytype.domain.image.ImageLoader
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class ImageDataLoader(
private val remote: ImageLoaderRemote
@ -10,8 +12,10 @@ class ImageDataLoader(
override suspend fun load(
id: String, size: Image.Size
): ByteArray = remote.load(
id = id,
size = size.toEntity()
)
): ByteArray = withContext(Dispatchers.IO) {
remote.load(
id = id,
size = size.toEntity()
)
}
}

View file

@ -3,11 +3,22 @@ package com.agileburo.anytype.domain.auth.interactor
import com.agileburo.anytype.domain.auth.model.Account
import com.agileburo.anytype.domain.auth.repo.AuthRepository
import com.agileburo.anytype.domain.base.FlowUseCase
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.scan
class ObserveAccounts(
private val repository: AuthRepository
) : FlowUseCase<Account, Unit>() {
) : FlowUseCase<List<Account>, Unit>() {
override fun stream(params: Unit): Flow<Account> = repository.observeAccounts()
override suspend fun build(
params: Unit?
) = repository
.observeAccounts()
.scan(emptyList<Account>()) { list, value -> list + value }
.drop(1)
override suspend fun stream(receiver: suspend (List<Account>) -> Unit) {
build().collect(receiver)
}
}

View file

@ -8,4 +8,15 @@ data class Image(
val sizes: List<Size>
) {
enum class Size { SMALL, LARGE, THUMB }
val smallest: Size?
get() = if (sizes.isNotEmpty()) {
when {
sizes.contains(Size.SMALL) -> Size.SMALL
sizes.contains(Size.THUMB) -> Size.THUMB
else -> Size.LARGE
}
} else {
null
}
}

View file

@ -3,5 +3,6 @@ package com.agileburo.anytype.domain.base
import kotlinx.coroutines.flow.Flow
abstract class FlowUseCase<out Type, in Params> where Type : Any {
abstract fun stream(params: Params): Flow<Type>
abstract suspend fun build(params: Params? = null): Flow<Type>
abstract suspend fun stream(receiver: suspend (Type) -> Unit)
}

View file

@ -0,0 +1,32 @@
package com.agileburo.anytype.domain.image
import com.agileburo.anytype.domain.auth.model.Account
import com.agileburo.anytype.domain.base.BaseUseCase
import com.agileburo.anytype.domain.base.Either
/**
* Loads images for one or several accounts
*/
class LoadAccountImages(
private val loader: ImageLoader
) : BaseUseCase<Map<Account, ByteArray?>, LoadAccountImages.Params>() {
override suspend fun run(params: Params) = try {
params.accounts.associateWith { account ->
account.avatar?.let { avatar ->
avatar.smallest?.let { size ->
try {
loader.load(avatar.id, size)
} catch (t: Throwable) {
null
}
}
}
}.let { Either.Right(it) }
} catch (t: Throwable) {
Either.Left(t)
}
class Params(val accounts: List<Account>)
}

View file

@ -6,7 +6,7 @@ import com.agileburo.anytype.domain.auth.model.AuthStatus
import com.agileburo.anytype.domain.auth.repo.AuthRepository
import com.agileburo.anytype.domain.base.Either
import com.agileburo.anytype.domain.common.CoroutineTestRule
import com.agileburo.anytype.domain.common.DataFactory
import com.agileburo.anytype.domain.common.MockDataFactory
import com.nhaarman.mockitokotlin2.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
@ -53,8 +53,8 @@ class CheckAuthorizationStatusTest {
fun `should return authorized status if account list is not empty`() = runBlocking {
val account = Account(
name = DataFactory.randomString(),
id = DataFactory.randomString(),
name = MockDataFactory.randomString(),
id = MockDataFactory.randomString(),
avatar = null
)

View file

@ -4,7 +4,7 @@ import com.agileburo.anytype.domain.auth.interactor.CreateAccount
import com.agileburo.anytype.domain.auth.model.Account
import com.agileburo.anytype.domain.auth.repo.AuthRepository
import com.agileburo.anytype.domain.common.CoroutineTestRule
import com.agileburo.anytype.domain.common.DataFactory
import com.agileburo.anytype.domain.common.MockDataFactory
import com.nhaarman.mockitokotlin2.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
@ -34,13 +34,13 @@ class CreateAccountTest {
@Test
fun `should create account and save it and set as current user account`() = runBlocking {
val name = DataFactory.randomString()
val name = MockDataFactory.randomString()
val path = null
val account = Account(
id = DataFactory.randomUuid(),
name = DataFactory.randomString(),
id = MockDataFactory.randomUuid(),
name = MockDataFactory.randomString(),
avatar = null
)

View file

@ -0,0 +1,86 @@
package com.agileburo.anytype.domain.auth
import com.agileburo.anytype.domain.auth.interactor.ObserveAccounts
import com.agileburo.anytype.domain.auth.model.Account
import com.agileburo.anytype.domain.auth.repo.AuthRepository
import com.agileburo.anytype.domain.common.CoroutineTestRule
import com.agileburo.anytype.domain.common.MockDataFactory
import com.nhaarman.mockitokotlin2.doReturn
import com.nhaarman.mockitokotlin2.stub
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.collectIndexed
import kotlinx.coroutines.flow.single
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class ObserveAccountsTest {
@ExperimentalCoroutinesApi
@get:Rule
var rule = CoroutineTestRule()
@Mock
lateinit var repo: AuthRepository
lateinit var observeAccounts: ObserveAccounts
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
observeAccounts = ObserveAccounts(repo)
}
@Test
fun `should collect one account when stream is called`() = runBlocking {
val account = Account(
id = MockDataFactory.randomUuid(),
name = MockDataFactory.randomString(),
avatar = null
)
repo.stub {
onBlocking { observeAccounts() } doReturn listOf(account).asFlow()
}
val result = observeAccounts.build().single()
assertTrue { result == listOf(account) }
}
@Test
fun `should collect one account, then two accounts, emitting accumulated results`() =
runBlocking {
val accounts = listOf(
Account(
id = MockDataFactory.randomUuid(),
name = MockDataFactory.randomString(),
avatar = null
),
Account(
id = MockDataFactory.randomUuid(),
name = MockDataFactory.randomString(),
avatar = null
)
)
repo.stub {
onBlocking { observeAccounts() } doReturn accounts.asFlow()
}
observeAccounts.build().collectIndexed { index, value ->
when (index) {
0 -> assertEquals(listOf(accounts.first()), value)
1 -> assertEquals(accounts, value)
}
}
}
}

View file

@ -5,7 +5,7 @@ import com.agileburo.anytype.domain.auth.model.Account
import com.agileburo.anytype.domain.auth.repo.AuthRepository
import com.agileburo.anytype.domain.base.Either
import com.agileburo.anytype.domain.common.CoroutineTestRule
import com.agileburo.anytype.domain.common.DataFactory
import com.agileburo.anytype.domain.common.MockDataFactory
import com.nhaarman.mockitokotlin2.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
@ -36,8 +36,8 @@ class StartAccountTest {
@Test
fun `should select account, set it as current user account and save it`() = runBlocking {
val id = DataFactory.randomString()
val path = DataFactory.randomString()
val id = MockDataFactory.randomString()
val path = MockDataFactory.randomString()
val params = StartAccount.Params(
id = id,
@ -46,7 +46,7 @@ class StartAccountTest {
val account = Account(
id = id,
name = DataFactory.randomString(),
name = MockDataFactory.randomString(),
avatar = null
)
@ -76,8 +76,8 @@ class StartAccountTest {
@Test
fun `should return unit when use case is successfully completed`() = runBlocking {
val id = DataFactory.randomString()
val path = DataFactory.randomString()
val id = MockDataFactory.randomString()
val path = MockDataFactory.randomString()
val params = StartAccount.Params(
id = id,
@ -86,7 +86,7 @@ class StartAccountTest {
val account = Account(
id = id,
name = DataFactory.randomString(),
name = MockDataFactory.randomString(),
avatar = null
)

View file

@ -3,7 +3,7 @@ package com.agileburo.anytype.domain.common
import java.util.*
import java.util.concurrent.ThreadLocalRandom
object DataFactory {
object MockDataFactory {
fun randomUuid(): String {
return UUID.randomUUID().toString()

View file

@ -0,0 +1,203 @@
package com.agileburo.anytype.domain.image
import com.agileburo.anytype.domain.auth.model.Account
import com.agileburo.anytype.domain.auth.model.Image
import com.agileburo.anytype.domain.base.Either
import com.agileburo.anytype.domain.common.MockDataFactory
import com.nhaarman.mockitokotlin2.*
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import kotlin.test.assertEquals
class LoadAccountImagesTest {
@Mock
lateinit var loader: ImageLoader
lateinit var loadAccountImages: LoadAccountImages
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
loadAccountImages = LoadAccountImages(loader = loader)
}
@Test
fun `should not load any image if an account does not have any avatar image`() = runBlocking {
val account = Account(
id = MockDataFactory.randomUuid(),
name = MockDataFactory.randomUuid(),
avatar = null
)
val blob = ByteArray(0)
val params = LoadAccountImages.Params(
accounts = listOf(account)
)
loader.stub {
onBlocking {
load(
id = any(),
size = any()
)
} doReturn blob
}
val result = loadAccountImages.run(params)
verifyZeroInteractions(loader)
val expected = Either.Right(mapOf(account to null))
assertEquals(expected, result)
}
@Test
fun `should load one image for one account`() = runBlocking {
val account = Account(
id = MockDataFactory.randomUuid(),
name = MockDataFactory.randomUuid(),
avatar = Image(
id = MockDataFactory.randomUuid(),
sizes = listOf(Image.Size.SMALL)
)
)
val blob = ByteArray(0)
val params = LoadAccountImages.Params(
accounts = listOf(account)
)
loader.stub {
onBlocking {
load(
id = account.avatar!!.id,
size = Image.Size.SMALL
)
} doReturn blob
}
val result = loadAccountImages.run(params)
verify(loader, times(1)).load(account.avatar!!.id, Image.Size.SMALL)
val expected = Either.Right(mapOf(account to blob))
assertEquals(expected, result)
}
@Test
fun `should load an image for online one of two accounts`() = runBlocking {
val firstAccount = Account(
id = MockDataFactory.randomUuid(),
name = MockDataFactory.randomUuid(),
avatar = Image(
id = MockDataFactory.randomUuid(),
sizes = listOf(Image.Size.SMALL)
)
)
val secondAccount = Account(
id = MockDataFactory.randomUuid(),
name = MockDataFactory.randomUuid(),
avatar = Image(
id = MockDataFactory.randomUuid(),
sizes = listOf(Image.Size.SMALL)
)
)
val blob = ByteArray(0)
val params = LoadAccountImages.Params(
accounts = listOf(firstAccount, secondAccount)
)
loader.stub {
onBlocking {
load(
id = secondAccount.avatar!!.id,
size = Image.Size.SMALL
)
} doReturn blob
}
val result = loadAccountImages.run(params)
verify(loader, times(1)).load(firstAccount.avatar!!.id, Image.Size.SMALL)
val expected = Either.Right(mapOf(firstAccount to null, secondAccount to blob))
assertEquals(expected, result)
}
@Test
fun `should not load an image for given account if there are no image sizes`() = runBlocking {
val sizes = emptyList<Image.Size>()
val account = Account(
id = MockDataFactory.randomUuid(),
name = MockDataFactory.randomUuid(),
avatar = Image(
id = MockDataFactory.randomUuid(),
sizes = sizes
)
)
val params = LoadAccountImages.Params(
accounts = listOf(account)
)
val result = loadAccountImages.run(params)
verifyZeroInteractions(loader)
val expected = Either.Right(mapOf(account to null))
assertEquals(expected, result)
}
@Test
fun `if loading is failed, we get null instead of an image`() = runBlocking {
val account = Account(
id = MockDataFactory.randomUuid(),
name = MockDataFactory.randomUuid(),
avatar = Image(
id = MockDataFactory.randomUuid(),
sizes = listOf(Image.Size.SMALL)
)
)
val params = LoadAccountImages.Params(
accounts = listOf(account)
)
loader.stub {
onBlocking {
load(
id = account.avatar!!.id,
size = Image.Size.SMALL
)
} doThrow IllegalArgumentException("Error while loading image with id: ${account.avatar!!.id}")
}
val result = loadAccountImages.run(params)
verify(loader, times(1)).load(account.avatar!!.id, Image.Size.SMALL)
val expected = Either.Right(mapOf(account to null))
assertEquals(expected, result)
}
}

View file

@ -6,16 +6,18 @@ import androidx.lifecycle.viewModelScope
import com.agileburo.anytype.core_utils.common.Event
import com.agileburo.anytype.domain.auth.interactor.ObserveAccounts
import com.agileburo.anytype.domain.auth.interactor.StartLoadingAccounts
import com.agileburo.anytype.domain.auth.model.Account
import com.agileburo.anytype.domain.image.LoadAccountImages
import com.agileburo.anytype.presentation.auth.model.ChooseProfileView
import com.agileburo.anytype.presentation.navigation.AppNavigation
import com.agileburo.anytype.presentation.navigation.SupportNavigation
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import timber.log.Timber
class SelectAccountViewModel(
private val startLoadingAccounts: StartLoadingAccounts,
private val observeAccounts: ObserveAccounts
private val observeAccounts: ObserveAccounts,
private val loadAccountImages: LoadAccountImages
) : ViewModel(), SupportNavigation<Event<AppNavigation.Command>> {
override val navigation: MutableLiveData<Event<AppNavigation.Command>> = MutableLiveData()
@ -26,32 +28,40 @@ class SelectAccountViewModel(
init {
startObservingAccounts()
startRecoveringAccounts()
startLoadingAccount()
}
private fun startRecoveringAccounts() {
startLoadingAccounts.invoke(viewModelScope, StartLoadingAccounts.Params()) {
Timber.d(it.toString())
private fun startLoadingAccount() {
startLoadingAccounts.invoke(
viewModelScope, StartLoadingAccounts.Params()
) { result ->
result.either(
fnL = { e -> Timber.e(e, "Error while starting account loading") },
fnR = { Timber.d("Account loading started...") }
)
}
}
private fun startObservingAccounts() {
viewModelScope.launch {
observeAccounts
.stream(Unit)
.collect { account ->
state.postValue(
listOf(
ChooseProfileView.ProfileView(
id = account.id,
name = account.name
)
observeAccounts.stream { accounts ->
state.postValue(
accounts.map { account ->
ChooseProfileView.ProfileView(
id = account.id,
name = account.name
)
)
}
}
)
}
}
}
private fun proceedWithLoadingImagesForAccount(account: Account) {
// TODO
}
fun onProfileClicked(id: String) {
navigation.postValue(Event(AppNavigation.Command.SetupSelectedAccountScreen(id)))
}

View file

@ -4,17 +4,20 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.agileburo.anytype.domain.auth.interactor.ObserveAccounts
import com.agileburo.anytype.domain.auth.interactor.StartLoadingAccounts
import com.agileburo.anytype.domain.image.LoadAccountImages
class SelectAccountViewModelFactory(
private val startLoadingAccounts: StartLoadingAccounts,
private val observeAccounts: ObserveAccounts
private val observeAccounts: ObserveAccounts,
private val loadAccountImages: LoadAccountImages
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return SelectAccountViewModel(
startLoadingAccounts = startLoadingAccounts,
observeAccounts = observeAccounts
observeAccounts = observeAccounts,
loadAccountImages = loadAccountImages
) as T
}
}

View file

@ -11,10 +11,6 @@ sealed class ChooseProfileView : ViewType {
override fun getViewType(): Int = PROFILE
}
object AddNewProfile : ChooseProfileView(), ViewType {
override fun getViewType(): Int = ADD_NEW_PROFILE
}
companion object {
const val PROFILE = 0
const val ADD_NEW_PROFILE = 1

View file

@ -22,7 +22,7 @@ class CreateProfileViewModelTest {
val name = session.name
val input = DataFactory.randomString()
val input = MockDataFactory.randomString()
vm.onCreateProfileClicked(input)

View file

@ -33,7 +33,7 @@ class SetupNewAccountViewModelTest {
@Test
fun `should start creating account when view model is initialized`() {
session.name = DataFactory.randomString()
session.name = MockDataFactory.randomString()
vm = SetupNewAccountViewModel(
session = session,
@ -47,7 +47,7 @@ class SetupNewAccountViewModelTest {
@Test
fun `should navigate to next screen if account has been successfully created`() {
session.name = DataFactory.randomString()
session.name = MockDataFactory.randomString()
createAccount.stub {
on { invoke(any(), any(), any()) } doAnswer { answer ->