Skip to content

Commit

Permalink
Replace KStore with multiplatform-settings.
Browse files Browse the repository at this point in the history
KStore had a requirement that values could only be read or written in a
suspending context, which was horribly limiting in some cases.
  • Loading branch information
christiandeange committed Dec 2, 2023
1 parent 1ebcb4d commit 64f8f2b
Show file tree
Hide file tree
Showing 25 changed files with 256 additions and 272 deletions.
7 changes: 0 additions & 7 deletions app/android/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,4 @@ android {
versionCode = 100
versionName = version.toString()
}

packaging {
resources {
excludes += "META-INF/AL2.0"
excludes += "META-INF/LGPL2.1"
}
}
}
8 changes: 1 addition & 7 deletions app/android/src/main/java/sh/christian/ozone/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import androidx.core.view.WindowCompat
import androidx.lifecycle.lifecycleScope
import dev.marcellogalhardo.retained.activity.retain
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import sh.christian.ozone.di.AppComponent
Expand All @@ -32,7 +31,7 @@ class MainActivity : AppCompatActivity() {

appComponent.supervisors.forEach { supervisor ->
with(supervisor) {
lifecycleScope.launch(SupervisorJob()) { onStart() }
lifecycleScope.launch(SupervisorJob()) { start() }
}
}

Expand All @@ -42,16 +41,11 @@ class MainActivity : AppCompatActivity() {
initTypography()
}

val authInfo = runBlocking {
appComponent.loginRepository.auth().first()
}

setContent {
AppTheme {
StatusBarTheme()
WorkflowRendering(
workflow = workflow,
props = authInfo,
onOutput = { finish() },
content = { it.Content() },
)
Expand Down
5 changes: 5 additions & 0 deletions app/common/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ android {
}

kotlin {
@Suppress("OPT_IN_USAGE")
compilerOptions {
freeCompilerArgs.add("-Xexpect-actual-classes")
}

sourceSets {
val commonMain by getting {
dependencies {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.yield
Expand All @@ -31,9 +30,9 @@ class ApiProvider(
private val loginRepository: LoginRepository,
) : Supervisor() {

private val apiHost = MutableStateFlow<String?>(null)
private val auth = MutableStateFlow<AuthInfo?>(null)
private val tokens = MutableStateFlow<Tokens?>(null)
private val apiHost = MutableStateFlow(apiRepository.server.host)
private val auth = MutableStateFlow(loginRepository.auth)
private val tokens = MutableStateFlow(loginRepository.auth?.toTokens())

private val client = HttpClient(engine) {
install(Logging) {
Expand All @@ -46,7 +45,7 @@ class ApiProvider(
}

install(DefaultRequest) {
val hostUrl = Url(apiHost.value!!)
val hostUrl = Url(apiHost.value)
url.protocol = hostUrl.protocol
url.host = hostUrl.host
url.port = hostUrl.port
Expand All @@ -60,15 +59,13 @@ class ApiProvider(
override suspend fun CoroutineScope.onStart() {
coroutineScope {
launch(OzoneDispatchers.IO) {
apiRepository.server().map { it.host }
apiRepository.serverFlow().map { it.host }
.distinctUntilChanged()
.collect(apiHost)
}

launch(OzoneDispatchers.IO) {
loginRepository.auth()
.distinctUntilChanged()
.collect {
loginRepository.authFlow().collect {
tokens.value = it?.toTokens()
yield()
auth.value = it
Expand All @@ -78,9 +75,9 @@ class ApiProvider(
launch(OzoneDispatchers.IO) {
tokens.collect { tokens ->
if (tokens != null) {
loginRepository.setAuth(loginRepository.auth().first()!!.withTokens(tokens))
loginRepository.auth = loginRepository.auth?.withTokens(tokens)
} else {
loginRepository.setAuth(null)
loginRepository.auth = null
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
package sh.christian.ozone.api

import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.launch
import me.tatarka.inject.annotations.Inject
import sh.christian.ozone.app.Supervisor
import sh.christian.ozone.di.SingleInApp
import sh.christian.ozone.login.auth.Server
import sh.christian.ozone.store.PersistentStorage
Expand All @@ -14,14 +11,10 @@ import sh.christian.ozone.store.preference
@SingleInApp
class ServerRepository(
storage: PersistentStorage,
) : Supervisor() {
) {
private val serverPreference = storage.preference<Server>("servers", Server.BlueskySocial)

fun setServer(server: Server) {
requireCoroutineScope().launch {
serverPreference.set(server)
}
}
var server: Server by serverPreference

fun server(): Flow<Server> = serverPreference.updates.filterNotNull()
fun serverFlow(): Flow<Server> = serverPreference.updates
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.squareup.workflow1.runningWorker
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import me.tatarka.inject.annotations.Inject
import sh.christian.ozone.api.ApiProvider
import sh.christian.ozone.app.AppState.ShowingLoggedIn
import sh.christian.ozone.app.AppState.ShowingLogin
import sh.christian.ozone.home.HomeOutput
Expand All @@ -18,7 +19,6 @@ import sh.christian.ozone.login.LoginOutput.CanceledLogin
import sh.christian.ozone.login.LoginOutput.LoggedIn
import sh.christian.ozone.login.LoginRepository
import sh.christian.ozone.login.LoginWorkflow
import sh.christian.ozone.login.auth.AuthInfo
import sh.christian.ozone.notifications.NotificationsRepository

@Inject
Expand All @@ -27,25 +27,27 @@ class AppWorkflow(
private val loginWorkflow: LoginWorkflow,
private val homeWorkflow: HomeWorkflow,
private val notificationsRepository: NotificationsRepository,
) : StatefulWorkflow<AuthInfo?, AppState, Unit, AppScreen>() {
private val apiProvider: ApiProvider,
) : StatefulWorkflow<Unit, AppState, Unit, AppScreen>() {
override fun initialState(
props: AuthInfo?,
props: Unit,
snapshot: Snapshot?,
): AppState {
return if (props == null) {
val authInfo = loginRepository.auth
return if (authInfo == null) {
ShowingLogin
} else {
ShowingLoggedIn(HomeProps(props, 0))
ShowingLoggedIn(HomeProps(authInfo, 0))
}
}

override fun render(
renderProps: AuthInfo?,
renderProps: Unit,
renderState: AppState,
context: RenderContext,
): AppScreen = when (renderState) {
is ShowingLogin -> {
context.runningWorker(loginRepository.auth().filterNotNull().asWorker(), "has-auth") { auth ->
context.runningWorker(apiProvider.auth().filterNotNull().asWorker(), "has-auth") { auth ->
action {
state = ShowingLoggedIn(HomeProps(auth, 0))
}
Expand All @@ -54,7 +56,7 @@ class AppWorkflow(
context.renderChild(loginWorkflow) { output ->
action {
when (output) {
is LoggedIn -> loginRepository.setAuth(output.authInfo)
is LoggedIn -> loginRepository.auth = output.authInfo
is CanceledLogin -> setOutput(Unit)
}
}
Expand All @@ -66,7 +68,7 @@ class AppWorkflow(
state = ShowingLoggedIn(renderState.props.copy(unreadNotificationCount = unread))
}
}
context.runningWorker(loginRepository.auth().filter { it == null }.asWorker(), "no-auth") {
context.runningWorker(apiProvider.auth().filter { it == null }.asWorker(), "no-auth") {
action {
state = ShowingLogin
}
Expand All @@ -76,7 +78,7 @@ class AppWorkflow(
action {
when (output) {
is HomeOutput.CloseApp -> setOutput(Unit)
is HomeOutput.SignOut -> loginRepository.setAuth(null)
is HomeOutput.SignOut -> loginRepository.auth = null
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,8 @@ import me.tatarka.inject.annotations.Component
import me.tatarka.inject.annotations.IntoSet
import me.tatarka.inject.annotations.Provides
import sh.christian.ozone.api.ApiProvider
import sh.christian.ozone.api.ServerRepository
import sh.christian.ozone.app.AppWorkflow
import sh.christian.ozone.app.Supervisor
import sh.christian.ozone.login.LoginRepository
import sh.christian.ozone.notifications.NotificationsRepository
import sh.christian.ozone.store.PersistentStorage
import sh.christian.ozone.timeline.TimelineRepository
Expand All @@ -21,8 +19,6 @@ abstract class AppComponent(
) {
abstract val appWorkflow: AppWorkflow

abstract val loginRepository: LoginRepository

abstract val supervisors: Set<Supervisor>

@Provides
Expand All @@ -31,12 +27,6 @@ abstract class AppComponent(
protected val ApiProvider.bind: Supervisor
@Provides @IntoSet get() = this

protected val LoginRepository.bind: Supervisor
@Provides @IntoSet get() = this

protected val ServerRepository.bind: Supervisor
@Provides @IntoSet get() = this

protected val MyProfileRepository.bind: Supervisor
@Provides @IntoSet get() = this

Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,20 @@
package sh.christian.ozone.login

import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import me.tatarka.inject.annotations.Inject
import sh.christian.ozone.app.Supervisor
import sh.christian.ozone.di.SingleInApp
import sh.christian.ozone.login.auth.AuthInfo
import sh.christian.ozone.store.PersistentStorage
import sh.christian.ozone.store.preference
import sh.christian.ozone.store.nullablePreference

@Inject
@SingleInApp
class LoginRepository(
storage: PersistentStorage,
) : Supervisor() {
private val authPreference = storage.preference<AuthInfo>("auth-info", null)
) {
private val authPreference = storage.nullablePreference<AuthInfo>("auth-info", null)

fun setAuth(authInfo: AuthInfo?) {
requireCoroutineScope().launch {
authPreference.set(authInfo)
}
}
var auth: AuthInfo? by authPreference

fun auth(): Flow<AuthInfo?> = authPreference.updates
fun authFlow(): Flow<AuthInfo?> = authPreference.updates
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,26 @@ package sh.christian.ozone.login

import sh.christian.ozone.error.ErrorProps
import sh.christian.ozone.login.auth.Credentials
import sh.christian.ozone.login.auth.Server
import sh.christian.ozone.login.auth.ServerInfo

sealed interface LoginState {
val mode: LoginScreenMode
val serverInfo: ServerInfo?

data class FetchingServer(
override val mode: LoginScreenMode,
override val serverInfo: ServerInfo?,
) : LoginState

data class ShowingLogin(
override val mode: LoginScreenMode,
override val serverInfo: ServerInfo?,
val server: Server,
) : LoginState

data class SigningIn(
override val mode: LoginScreenMode,
override val serverInfo: ServerInfo?,
val server: Server,
val credentials: Credentials,
) : LoginState

data class ShowingError(
override val mode: LoginScreenMode,
override val serverInfo: ServerInfo?,
val server: Server,
val errorProps: ErrorProps,
val credentials: Credentials,
) : LoginState
Expand Down
Loading

0 comments on commit 64f8f2b

Please sign in to comment.