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 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.feeds.FeedViewModel
import com.readrops.app.compose.repositories.BaseRepository import com.readrops.app.compose.repositories.BaseRepository
import com.readrops.app.compose.repositories.LocalRSSRepository import com.readrops.app.compose.repositories.LocalRSSRepository
import com.readrops.app.compose.timelime.TimelineViewModel import com.readrops.app.compose.timelime.TimelineViewModel
import com.readrops.db.entities.account.Account import com.readrops.db.entities.account.Account
import com.readrops.db.entities.account.AccountType
import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module import org.koin.dsl.module
val composeAppModule = 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 // 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.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.compose.animation.ExperimentalAnimationApi 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.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.Navigator
import cafe.adriel.voyager.navigator.tab.TabNavigator
import cafe.adriel.voyager.transitions.FadeTransition import cafe.adriel.voyager.transitions.FadeTransition
import cafe.adriel.voyager.transitions.SlideTransition import com.readrops.app.compose.account.selection.AccountSelectionScreen
import com.readrops.app.compose.account.AccountSelectionScreen import com.readrops.app.compose.account.selection.AccountSelectionViewModel
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.home.HomeScreen 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 import org.koin.androidx.viewmodel.ext.android.getViewModel
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@ -32,7 +18,7 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val viewModel = getViewModel<AccountViewModel>() val viewModel = getViewModel<AccountSelectionViewModel>()
val accountExists = viewModel.accountExists() val accountExists = viewModel.accountExists()
setContent { 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.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.readrops.app.compose.base.TabViewModel
import com.readrops.app.compose.repositories.BaseRepository import com.readrops.app.compose.repositories.BaseRepository
import com.readrops.db.Database import com.readrops.db.Database
import com.readrops.db.entities.Feed import com.readrops.db.entities.Feed
@ -13,8 +14,7 @@ import kotlinx.coroutines.launch
class FeedViewModel( class FeedViewModel(
private val database: Database, private val database: Database,
private val repository: BaseRepository, ) : TabViewModel(database) {
) : ViewModel() {
private val _feedsState = MutableStateFlow<FeedsState>(FeedsState.InitialState) private val _feedsState = MutableStateFlow<FeedsState>(FeedsState.InitialState)
val feedsState = _feedsState.asStateFlow() val feedsState = _feedsState.asStateFlow()
@ -29,9 +29,13 @@ class FeedViewModel(
fun insertFeed(url: String) { fun insertFeed(url: String) {
viewModelScope.launch(context = Dispatchers.IO) { viewModelScope.launch(context = Dispatchers.IO) {
repository.insertNewFeeds(listOf(url)) repository?.insertNewFeeds(listOf(url))
} }
} }
override fun invalidate() {
}
} }
sealed class FeedsState { sealed class FeedsState {

View File

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

View File

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