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

Feature/show user name on desktop screen

This commit is contained in:
Evgenii Kozlov 2019-10-24 15:35:12 +00:00
parent 8aade7b250
commit 142bcbdd11
26 changed files with 323 additions and 11 deletions

View file

@ -71,6 +71,13 @@ class ComponentManager(private val main: MainComponent) {
.build()
}
val desktopComponent = Component {
main
.desktopComponentBuilder()
.desktopModule(DesktopModule())
.build()
}
class Component<T>(private val builder: () -> T) {
private var instance: T? = null

View file

@ -0,0 +1,46 @@
package com.agileburo.anytype.di.feature
import com.agileburo.anytype.core_utils.di.scope.PerScreen
import com.agileburo.anytype.domain.auth.repo.AuthRepository
import com.agileburo.anytype.domain.desktop.interactor.GetAccount
import com.agileburo.anytype.presentation.desktop.DesktopViewModelFactory
import com.agileburo.anytype.ui.desktop.DesktopFragment
import dagger.Module
import dagger.Provides
import dagger.Subcomponent
@Subcomponent(
modules = [DesktopModule::class]
)
@PerScreen
interface DesktopSubComponent {
@Subcomponent.Builder
interface Builder {
fun desktopModule(module: DesktopModule): Builder
fun build(): DesktopSubComponent
}
fun inject(fragment: DesktopFragment)
}
@Module
class DesktopModule {
@Provides
@PerScreen
fun provideDesktopViewModelFactory(
getAccount: GetAccount
): DesktopViewModelFactory = DesktopViewModelFactory(
getAccount = getAccount
)
@Provides
@PerScreen
fun provideGetAccountUseCase(
repository: AuthRepository
): GetAccount = GetAccount(
repository = repository
)
}

View file

@ -1,6 +1,7 @@
package com.agileburo.anytype.di.main
import com.agileburo.anytype.di.feature.AuthSubComponent
import com.agileburo.anytype.di.feature.DesktopSubComponent
import com.agileburo.anytype.di.feature.KeychainPhraseSubComponent
import com.agileburo.anytype.di.feature.ProfileSubComponent
import dagger.Component
@ -17,4 +18,5 @@ interface MainComponent {
fun authComponentBuilder(): AuthSubComponent.Builder
fun profileComponentBuilder(): ProfileSubComponent.Builder
fun keychainPhraseComponentBuilder(): KeychainPhraseSubComponent.Builder
fun desktopComponentBuilder(): DesktopSubComponent.Builder
}

View file

@ -2,21 +2,35 @@ package com.agileburo.anytype.ui.desktop
import android.os.Bundle
import android.view.View
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.GridLayoutManager
import com.agileburo.anytype.R
import com.agileburo.anytype.core_utils.ui.ViewState
import com.agileburo.anytype.di.common.componentManager
import com.agileburo.anytype.presentation.desktop.DesktopView
import com.agileburo.anytype.presentation.desktop.DesktopViewModel
import com.agileburo.anytype.presentation.desktop.DesktopViewModelFactory
import com.agileburo.anytype.presentation.profile.ProfileView
import com.agileburo.anytype.ui.base.ViewStateFragment
import kotlinx.android.synthetic.main.fragment_desktop.*
import javax.inject.Inject
class DesktopFragment : ViewStateFragment<ViewState<List<DesktopView>>>(R.layout.fragment_desktop) {
private val vm by lazy {
ViewModelProviders.of(this).get(DesktopViewModel::class.java)
private val profileObserver = Observer<ProfileView> { profile ->
desktopTitle.text = getString(R.string.greet, profile.name)
}
private val vm by lazy {
ViewModelProviders
.of(this, factory)
.get(DesktopViewModel::class.java)
}
@Inject
lateinit var factory: DesktopViewModelFactory
private val desktopAdapter by lazy {
DesktopAdapter(
data = mutableListOf(),
@ -28,6 +42,7 @@ class DesktopFragment : ViewStateFragment<ViewState<List<DesktopView>>>(R.layout
super.onViewCreated(view, savedInstanceState)
vm.state.observe(this, this)
vm.navigation.observe(this, navObserver)
vm.profile.observe(this, profileObserver)
vm.onViewCreated()
}
@ -48,10 +63,10 @@ class DesktopFragment : ViewStateFragment<ViewState<List<DesktopView>>>(R.layout
}
override fun injectDependencies() {
// TODO
componentManager().desktopComponent.get().inject(this)
}
override fun releaseDependencies() {
// TODO
componentManager().desktopComponent.release()
}
}

View file

@ -46,7 +46,6 @@
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginTop="48dp"
android:text="Hi, Konstantin"
android:textColor="@color/white"
android:textSize="36sp"
android:textStyle="bold"

View file

@ -79,4 +79,6 @@ Do the computation of an expensive paragraph of text on a background thread:
<string name="i_ve_written_it_down">i\'ve written it down</string>
<string name="keychain_mock">witch collapse practice feed shame open despair creek road again ice least lake tree young address brain envelope</string>
<string name="greet">Hi, %1$s!</string>
</resources>

View file

@ -6,5 +6,6 @@ interface AuthCache {
suspend fun saveAccount(account: AccountEntity)
suspend fun saveMnemonic(mnemonic: String)
suspend fun getMnemonic(): String
suspend fun getAccount(): AccountEntity
suspend fun logout()
}

View file

@ -47,4 +47,6 @@ class AuthCacheDataStore(private val cache: AuthCache) : AuthDataStore {
override suspend fun logout() {
cache.logout()
}
override suspend fun getAccount() = cache.getAccount()
}

View file

@ -41,6 +41,8 @@ class AuthDataRepository(
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override suspend fun getAccount() = factory.cache.getAccount().toDomain()
override suspend fun saveMnemonic(
mnemonic: String
) = factory.cache.saveMnemonic(mnemonic)

View file

@ -11,6 +11,8 @@ interface AuthDataStore {
suspend fun saveAccount(account: AccountEntity)
fun observeAccounts(): Flow<AccountEntity>
suspend fun getAccount(): AccountEntity
suspend fun createWallet(path: String): WalletEntity
suspend fun recoverWallet(path: String, mnemonic: String)
suspend fun isSignedIn(): Boolean

View file

@ -48,4 +48,8 @@ class AuthRemoteDataStore(
override suspend fun logout() {
throw UnsupportedOperationException()
}
override suspend fun getAccount(): AccountEntity {
throw UnsupportedOperationException()
}
}

View file

@ -35,7 +35,7 @@ ext {
better_link_method_version = '2.2.0'
// Unit Testing
robolectric_version = '3.8'
robolectric_version = '4.3.1'
junit_version = '4.12'
mockito_version = '1.4.0'
kluent_version = '1.14'

View file

@ -11,6 +11,8 @@ interface AuthRepository {
suspend fun saveAccount(account: Account)
fun observeAccounts(): Flow<Account>
suspend fun getAccount(): Account
suspend fun createWallet(path: String): Wallet
suspend fun recoverWallet(path: String, mnemonic: String)
suspend fun isSignedIn(): Boolean

View file

@ -0,0 +1,22 @@
package com.agileburo.anytype.domain.desktop.interactor
import com.agileburo.anytype.domain.auth.model.Account
import com.agileburo.anytype.domain.auth.repo.AuthRepository
import com.agileburo.anytype.domain.base.BaseUseCase
import com.agileburo.anytype.domain.base.Either
/** Use case for getting currently selected user account.
* @property repository repository containing user account
*/
class GetAccount(
private val repository: AuthRepository
) : BaseUseCase<Account, BaseUseCase.None>() {
override suspend fun run(params: None) = try {
repository.getAccount().let {
Either.Right(it)
}
} catch (t: Throwable) {
Either.Left(t)
}
}

View file

@ -19,3 +19,4 @@ android.useAndroidX=true
android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
android.enableUnitTestBinaryResources=true

View file

@ -44,4 +44,6 @@ dependencies {
testImplementation unitTestDependencies.junit
testImplementation unitTestDependencies.kotlinTest
testImplementation unitTestDependencies.mockito
testImplementation unitTestDependencies.robolectric
testImplementation unitTestDependencies.archCoreTesting
}

View file

@ -4,5 +4,9 @@ object Config {
const val DATABASE_NAME = "AnytypeDatabase"
const val ACCOUNT_TABLE_NAME = "Accounts"
const val CLEAR_ACCOUNT_TABLE = "DELETE FROM $ACCOUNT_TABLE_NAME"
const val CLEAR_ACCOUNT_TABLE =
"DELETE FROM $ACCOUNT_TABLE_NAME"
const val QUERY_LAST_ACCOUNT =
"SELECT * FROM $ACCOUNT_TABLE_NAME ORDER BY timestamp DESC LIMIT 1"
}

View file

@ -11,4 +11,6 @@ abstract class AccountDao : BaseDao<AccountTable> {
@Query(Config.CLEAR_ACCOUNT_TABLE)
abstract suspend fun clear()
@Query(Config.QUERY_LAST_ACCOUNT)
abstract suspend fun lastAccount(): List<AccountTable>
}

View file

@ -0,0 +1,11 @@
package com.agileburo.anytype.mapper
import com.agileburo.anytype.data.auth.model.AccountEntity
import com.agileburo.anytype.model.AccountTable
fun AccountTable.toEntity(): AccountEntity {
return AccountEntity(
id = id,
name = name
)
}

View file

@ -7,5 +7,6 @@ import com.agileburo.anytype.common.Config
@Entity(tableName = Config.ACCOUNT_TABLE_NAME)
data class AccountTable(
@PrimaryKey val id: String,
val name: String
val name: String,
val timestamp: Long
)

View file

@ -4,6 +4,7 @@ import android.content.SharedPreferences
import com.agileburo.anytype.data.auth.model.AccountEntity
import com.agileburo.anytype.data.auth.repo.AuthCache
import com.agileburo.anytype.db.AnytypeDatabase
import com.agileburo.anytype.mapper.toEntity
import com.agileburo.anytype.model.AccountTable
import timber.log.Timber
@ -16,11 +17,19 @@ class DefaultAuthCache(
db.accountDao().insert(
AccountTable(
id = account.id,
name = account.name
name = account.name,
timestamp = System.currentTimeMillis()
)
)
}
override suspend fun getAccount() = db.accountDao().lastAccount().let { list ->
if (list.isEmpty())
throw IllegalStateException("Could not found user account")
else
list.first().toEntity()
}
override suspend fun saveMnemonic(mnemonic: String) {
Timber.d("Saving mnemonic: $mnemonic")
prefs.edit().putString(MNEMONIC_KEY, mnemonic).apply()

View file

@ -0,0 +1,68 @@
package com.agileburo.anytype
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.room.Room
import androidx.test.platform.app.InstrumentationRegistry
import com.agileburo.anytype.db.AnytypeDatabase
import com.agileburo.anytype.model.AccountTable
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import kotlin.test.assertTrue
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE)
class AccountDaoTest {
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
private val database = Room.inMemoryDatabaseBuilder(
InstrumentationRegistry.getInstrumentation().context,
AnytypeDatabase::class.java
).allowMainThreadQueries().build()
@After
fun after() {
database.close()
}
@Test
fun `should return empty list if there are no last account in db`() {
runBlocking {
val accounts = database.accountDao().lastAccount()
assertTrue { accounts.isEmpty() }
}
}
@Test
fun `should return last account`() = runBlocking {
val firstAccount = AccountTable(
id = MockDataFactory.randomString(),
name = MockDataFactory.randomString(),
timestamp = System.currentTimeMillis()
)
delay(1)
val secondAccount = AccountTable(
id = MockDataFactory.randomString(),
name = MockDataFactory.randomString(),
timestamp = System.currentTimeMillis()
)
database.accountDao().insert(firstAccount)
database.accountDao().insert(secondAccount)
val result = database.accountDao().lastAccount()
assertTrue { result.size == 1 }
assertTrue { result.first() == secondAccount }
}
}

View file

@ -0,0 +1,64 @@
package com.agileburo.anytype
import java.util.*
import java.util.concurrent.ThreadLocalRandom
object MockDataFactory {
fun randomUuid(): String {
return UUID.randomUUID().toString()
}
fun randomString(): String {
return randomUuid()
}
fun randomInt(): Int {
return ThreadLocalRandom.current().nextInt(0, 1000 + 1)
}
fun randomInt(max: Int): Int {
return ThreadLocalRandom.current().nextInt(0, max)
}
fun randomLong(): Long {
return randomInt().toLong()
}
fun randomFloat(): Float {
return randomInt().toFloat()
}
fun randomDouble(): Double {
return randomInt().toDouble()
}
fun randomBoolean(): Boolean {
return Math.random() < 0.5
}
fun makeIntList(count: Int): List<Int> {
val items = mutableListOf<Int>()
repeat(count) {
items.add(randomInt())
}
return items
}
fun makeStringList(count: Int): List<String> {
val items = mutableListOf<String>()
repeat(count) {
items.add(randomUuid())
}
return items
}
fun makeDoubleList(count: Int): List<Double> {
val items = mutableListOf<Double>()
repeat(count) {
items.add(randomDouble())
}
return items
}
}

View file

@ -12,6 +12,8 @@ android {
targetSdkVersion config["target_sdk"]
testInstrumentationRunner config["test_runner"]
}
testOptions.unitTests.includeAndroidResources = true
}
dependencies {
@ -38,4 +40,7 @@ dependencies {
testImplementation unitTestDependencies.coroutineTesting
testImplementation unitTestDependencies.liveDataTesting
testImplementation unitTestDependencies.archCoreTesting
androidTestImplementation 'androidx.test:core:1.2.0'
}

View file

@ -1,17 +1,42 @@
package com.agileburo.anytype.presentation.desktop
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.agileburo.anytype.core_utils.common.Event
import com.agileburo.anytype.core_utils.ui.ViewState
import com.agileburo.anytype.core_utils.ui.ViewStateViewModel
import com.agileburo.anytype.domain.base.BaseUseCase
import com.agileburo.anytype.domain.desktop.interactor.GetAccount
import com.agileburo.anytype.presentation.navigation.AppNavigation
import com.agileburo.anytype.presentation.navigation.SupportNavigation
import com.agileburo.anytype.presentation.profile.ProfileView
import timber.log.Timber
class DesktopViewModel : ViewStateViewModel<ViewState<List<DesktopView>>>(),
class DesktopViewModel(
private val getAccount: GetAccount
) : ViewStateViewModel<ViewState<List<DesktopView>>>(),
SupportNavigation<Event<AppNavigation.Command>> {
private val _profile = MutableLiveData<ProfileView>()
val profile: LiveData<ProfileView>
get() = _profile
init {
proceedWithGettingAccount()
}
override val navigation = MutableLiveData<Event<AppNavigation.Command>>()
private fun proceedWithGettingAccount() {
getAccount.invoke(viewModelScope, BaseUseCase.None) { result ->
result.either(
fnL = { e -> Timber.e(e, "Error while getting account") },
fnR = { account -> _profile.postValue(ProfileView(name = account.name)) }
)
}
}
fun onViewCreated() {
stateData.postValue(ViewState.Init)
}

View file

@ -1,3 +1,17 @@
package com.agileburo.anytype.presentation.desktop
class DesktopViewModelFactory
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.agileburo.anytype.domain.desktop.interactor.GetAccount
class DesktopViewModelFactory(
private val getAccount: GetAccount
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return DesktopViewModel(
getAccount = getAccount
) as T
}
}