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:
parent
8aade7b250
commit
142bcbdd11
26 changed files with 323 additions and 11 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -47,4 +47,6 @@ class AuthCacheDataStore(private val cache: AuthCache) : AuthDataStore {
|
|||
override suspend fun logout() {
|
||||
cache.logout()
|
||||
}
|
||||
|
||||
override suspend fun getAccount() = cache.getAccount()
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -48,4 +48,8 @@ class AuthRemoteDataStore(
|
|||
override suspend fun logout() {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override suspend fun getAccount(): AccountEntity {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -44,4 +44,6 @@ dependencies {
|
|||
testImplementation unitTestDependencies.junit
|
||||
testImplementation unitTestDependencies.kotlinTest
|
||||
testImplementation unitTestDependencies.mockito
|
||||
testImplementation unitTestDependencies.robolectric
|
||||
testImplementation unitTestDependencies.archCoreTesting
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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>
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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()
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue