Add account creation screens with global account state management

This commit is contained in:
Shinokuni 2023-08-07 13:45:28 +02:00
parent c9db21e881
commit dac54a307f
11 changed files with 274 additions and 75 deletions

View File

@ -1,24 +1,26 @@
package com.readrops.app.compose
import com.readrops.app.compose.account.AccountViewModel
import com.readrops.app.compose.account.selection.AccountSelectionViewModel
import com.readrops.app.compose.feeds.FeedViewModel
import com.readrops.app.compose.repositories.BaseRepository
import com.readrops.app.compose.repositories.LocalRSSRepository
import com.readrops.app.compose.timelime.TimelineViewModel
import com.readrops.db.entities.account.Account
import com.readrops.db.entities.account.AccountType
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
val composeAppModule = module {
viewModel { TimelineViewModel(get(), get()) }
viewModel { TimelineViewModel(get()) }
viewModel { FeedViewModel(get(), get()) }
viewModel { FeedViewModel(get()) }
viewModel { AccountViewModel(get()) }
viewModel { AccountSelectionViewModel(get()) }
// repositories
single<BaseRepository> { LocalRSSRepository(get(), get(), Account(id = 1, isCurrentAccount = true, accountType = AccountType.LOCAL)) }
factory<BaseRepository> { (account: Account) ->
LocalRSSRepository(get(), get(), account)
}
}

View File

@ -4,26 +4,12 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AccountBox
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import cafe.adriel.voyager.navigator.CurrentScreen
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.tab.TabNavigator
import cafe.adriel.voyager.transitions.FadeTransition
import cafe.adriel.voyager.transitions.SlideTransition
import com.readrops.app.compose.account.AccountSelectionScreen
import com.readrops.app.compose.account.AccountTab
import com.readrops.app.compose.account.AccountViewModel
import com.readrops.app.compose.feeds.FeedTab
import com.readrops.app.compose.account.selection.AccountSelectionScreen
import com.readrops.app.compose.account.selection.AccountSelectionViewModel
import com.readrops.app.compose.home.HomeScreen
import com.readrops.app.compose.more.MoreTab
import com.readrops.app.compose.timelime.TimelineTab
import org.koin.androidx.viewmodel.ext.android.getViewModel
class MainActivity : ComponentActivity() {
@ -32,7 +18,7 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val viewModel = getViewModel<AccountViewModel>()
val viewModel = getViewModel<AccountSelectionViewModel>()
val accountExists = viewModel.accountExists()
setContent {

View File

@ -1,17 +0,0 @@
package com.readrops.app.compose.account
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import cafe.adriel.voyager.androidx.AndroidScreen
class AccountSelectionScreen : AndroidScreen() {
@Composable
override fun Content() {
Column {
Text(text = "account selection")
}
}
}

View File

@ -1,21 +0,0 @@
package com.readrops.app.compose.account
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.readrops.db.Database
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
class AccountViewModel(
private val database: Database,
) : ViewModel() {
fun accountExists(): Boolean {
val accountCount = runBlocking {
database.newAccountDao().selectAccountCount()
}
return accountCount > 0
}
}

View File

@ -0,0 +1,41 @@
package com.readrops.app.compose.account.credentials
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.androidx.AndroidScreen
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import com.readrops.app.compose.home.HomeScreen
import com.readrops.db.entities.account.Account
import com.readrops.db.entities.account.AccountType
class AccountCredentialsScreen(
private val accountType: AccountType,
private val account: Account? = null,
) : AndroidScreen() {
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
Column {
Text(
text = "AccountCredentialsScreen"
)
Spacer(modifier = Modifier.size(16.dp))
Button(onClick = { navigator.replaceAll(HomeScreen()) }) {
Text(
text = "skip"
)
}
}
}
}

View File

@ -0,0 +1,80 @@
package com.readrops.app.compose.account.selection
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.androidx.AndroidScreen
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import com.readrops.app.compose.R
import com.readrops.app.compose.account.credentials.AccountCredentialsScreen
import com.readrops.app.compose.home.HomeScreen
import com.readrops.db.entities.account.AccountType
import org.koin.androidx.compose.getViewModel
class AccountSelectionScreen : AndroidScreen() {
@Composable
override fun Content() {
val viewModel = getViewModel<AccountSelectionViewModel>()
val navState by viewModel.navState.collectAsState()
val navigator = LocalNavigator.currentOrThrow
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxSize()
) {
Text(text = "Choose an account")
Spacer(modifier = Modifier.size(8.dp))
AccountType.values().forEach { accountType ->
Row(
modifier = Modifier.clickable { viewModel.createAccount(accountType) }
) {
Icon(
painter = painterResource(id = R.drawable.ic_freshrss),
contentDescription = accountType.name,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.size(4.dp))
Text(text = accountType.name)
}
Spacer(modifier = Modifier.size(8.dp))
}
}
when (navState) {
is AccountSelectionViewModel.NavState.GoToHomeScreen -> {
// using replace makes the app crash due to a screen key conflict
navigator.replaceAll(HomeScreen())
}
is AccountSelectionViewModel.NavState.GoToAccountCredentialsScreen -> {
val accountType = (navState as AccountSelectionViewModel.NavState.GoToAccountCredentialsScreen).accountType
navigator.push(AccountCredentialsScreen(accountType))
viewModel.resetNavState()
}
else -> {}
}
}
}

View File

@ -0,0 +1,69 @@
package com.readrops.app.compose.account.selection
import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.readrops.db.Database
import com.readrops.db.entities.account.Account
import com.readrops.db.entities.account.AccountType
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
class AccountSelectionViewModel(
private val database: Database,
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
) : ViewModel(), KoinComponent {
private val _navState = MutableStateFlow<NavState>(NavState.Idle)
val navState = _navState.asStateFlow()
fun accountExists(): Boolean {
val accountCount = runBlocking {
database.newAccountDao().selectAccountCount()
}
return accountCount > 0
}
fun createAccount(accountType: AccountType) {
if (accountType == AccountType.LOCAL) {
createLocalAccount()
} else {
_navState.update { NavState.GoToAccountCredentialsScreen(accountType) }
}
}
fun resetNavState() {
_navState.update { NavState.Idle }
}
private fun createLocalAccount() {
val context = get<Context>()
val account = Account(
url = null,
accountName = context.getString(AccountType.LOCAL.typeName),
accountType = AccountType.LOCAL,
isCurrentAccount = true
)
viewModelScope.launch(dispatcher) {
database.newAccountDao().insert(account)
_navState.update { NavState.GoToHomeScreen }
}
}
sealed class NavState {
object Idle : NavState()
object GoToHomeScreen : NavState()
class GoToAccountCredentialsScreen(val accountType: AccountType) : NavState()
}
}

View File

@ -0,0 +1,49 @@
package com.readrops.app.compose.base
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.readrops.app.compose.repositories.BaseRepository
import com.readrops.app.compose.repositories.LocalRSSRepository
import com.readrops.db.Database
import com.readrops.db.entities.account.Account
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import org.koin.core.parameter.parametersOf
/**
* Custom ViewModel for Tab screens handling account change
*/
abstract class TabViewModel(
private val database: Database,
) : ViewModel(), KoinComponent {
/**
* Repository intended to be rebuilt when the current account changes
*/
protected var repository: BaseRepository? = null
protected var currentAccount: Account? = null
/**
* This method is called when the repository has been rebuilt from the new current account
*/
abstract fun invalidate()
init {
viewModelScope.launch {
database.newAccountDao()
.selectCurrentAccount()
.distinctUntilChanged()
.collect { account ->
currentAccount = account
repository = get(parameters = { parametersOf(account) })
invalidate()
}
}
}
}

View File

@ -2,6 +2,7 @@ package com.readrops.app.compose.feeds
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.readrops.app.compose.base.TabViewModel
import com.readrops.app.compose.repositories.BaseRepository
import com.readrops.db.Database
import com.readrops.db.entities.Feed
@ -13,8 +14,7 @@ import kotlinx.coroutines.launch
class FeedViewModel(
private val database: Database,
private val repository: BaseRepository,
) : ViewModel() {
) : TabViewModel(database) {
private val _feedsState = MutableStateFlow<FeedsState>(FeedsState.InitialState)
val feedsState = _feedsState.asStateFlow()
@ -29,9 +29,13 @@ class FeedViewModel(
fun insertFeed(url: String) {
viewModelScope.launch(context = Dispatchers.IO) {
repository.insertNewFeeds(listOf(url))
repository?.insertNewFeeds(listOf(url))
}
}
override fun invalidate() {
}
}
sealed class FeedsState {

View File

@ -1,8 +1,7 @@
package com.readrops.app.compose.timelime
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.readrops.app.compose.repositories.BaseRepository
import com.readrops.app.compose.base.TabViewModel
import com.readrops.db.Database
import com.readrops.db.entities.Item
import kotlinx.coroutines.Dispatchers
@ -12,9 +11,8 @@ import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.launch
class TimelineViewModel(
private val database: Database,
private val repository: BaseRepository,
) : ViewModel() {
private val database: Database,
) : TabViewModel(database) {
private val _timelineState = MutableStateFlow<TimelineState>(TimelineState.InitialState)
val timelineState = _timelineState.asStateFlow()
@ -25,23 +23,27 @@ class TimelineViewModel(
init {
viewModelScope.launch(context = Dispatchers.IO) {
database.newItemDao().selectAll()
.catch { _timelineState.value = TimelineState.ErrorState(Exception(it)) }
.collect {
_timelineState.value = TimelineState.LoadedState(it)
}
.catch { _timelineState.value = TimelineState.ErrorState(Exception(it)) }
.collect {
_timelineState.value = TimelineState.LoadedState(it)
}
}
}
fun refreshTimeline() {
_isRefreshing.value = true
viewModelScope.launch(context = Dispatchers.IO) {
repository.synchronize(null) {
repository?.synchronize(null) {
}
_isRefreshing.value = false
}
}
override fun invalidate() {
refreshTimeline()
}
}
sealed class TimelineState {

View File

@ -2,11 +2,15 @@ package com.readrops.db.dao.newdao
import androidx.room.Dao
import androidx.room.Query
import com.readrops.db.entities.account.Account
import kotlinx.coroutines.flow.Flow
@Dao
interface NewAccountDao {
interface NewAccountDao : NewBaseDao<Account> {
@Query("Select Count(*) From Account")
suspend fun selectAccountCount(): Int
@Query("Select * From Account Where current_account = 1")
fun selectCurrentAccount(): Flow<Account>
}