Skip to content
This repository has been archived by the owner on Jan 15, 2025. It is now read-only.

Commit

Permalink
WIP: replace reaktive with pure coroutines
Browse files Browse the repository at this point in the history
  • Loading branch information
julius-b committed Dec 21, 2022
1 parent acb2d20 commit 6d19e54
Show file tree
Hide file tree
Showing 19 changed files with 137 additions and 120 deletions.
1 change: 0 additions & 1 deletion android/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ dependencies {

implementation("com.arkivanov.mvikotlin:mvikotlin:3.0.2")
implementation("com.arkivanov.mvikotlin:mvikotlin-extensions-coroutines:3.0.2")
implementation("com.arkivanov.mvikotlin:mvikotlin-extensions-reaktive:3.0.2")
implementation("com.arkivanov.mvikotlin:rx:3.0.2") // Disposable

// Android
Expand Down
4 changes: 0 additions & 4 deletions common/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,8 @@ kotlin {

implementation("com.arkivanov.mvikotlin:mvikotlin:3.0.2")
implementation("com.arkivanov.mvikotlin:mvikotlin-extensions-coroutines:3.0.2")
implementation("com.arkivanov.mvikotlin:mvikotlin-extensions-reaktive:3.0.2")
implementation("com.arkivanov.mvikotlin:rx:3.0.2") // Disposable

implementation("com.badoo.reaktive:reaktive:1.2.2")
implementation("com.badoo.reaktive:coroutines-interop:1.2.2")

implementation("com.michael-bull.kotlin-result:kotlin-result:1.1.16") // Result type
implementation("com.russhwolf:multiplatform-settings:1.0.0-RC")

Expand Down
8 changes: 8 additions & 0 deletions common/src/androidMain/kotlin/DefaultDispatchers.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers

object DefaultDispatchers : OpiaDispatchers {
override val main: CoroutineDispatcher get() = Dispatchers.Main.immediate
override val io: CoroutineDispatcher get() = Dispatchers.IO
override val unconfined: CoroutineDispatcher get() = Dispatchers.Unconfined
}
6 changes: 0 additions & 6 deletions common/src/androidMain/kotlin/MainDispatcher.kt

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers

// Desktop has no Main dispatcher, need Swing lib
// Android doesn't allow dispatch (UI operation) from non-Main (not Default)
expect fun mainDispatcher(): CoroutineDispatcher
interface OpiaDispatchers {
val main: CoroutineDispatcher
val io: CoroutineDispatcher
val unconfined: CoroutineDispatcher
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import app.opia.common.api.repository.KeyRepo
import app.opia.common.db.DriverFactory
import app.opia.common.db.createDatabase

// TODO use Koin
class ServiceLocator(driverFactory: DriverFactory) {
val database = createDatabase(driverFactory)
val okHttpClient = RetrofitClient.newOkHttpClient(this)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ interface OpiaRoot {

sealed class Child {
data class Splash(val component: OpiaSplash) : Child()
data class Auth(val component: OpiaAuth) : Child()
data class Auth(val component: OpiaAuth, val onError: () -> Unit) : Child()
data class Registration(val component: OpiaRegistration) : Child()
data class Chats(val component: OpiaChats) : Child()
data class Chat(val component: OpiaChat) : Child()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,19 @@ import app.opia.common.ui.chats.chat.ChatComponent
import app.opia.common.ui.chats.chat.OpiaChat
import app.opia.common.ui.splash.OpiaSplash
import app.opia.common.ui.splash.SplashComponent
import app.opia.common.utils.Consumer
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.router.stack.*
import com.arkivanov.decompose.value.Value
import com.arkivanov.essenty.parcelable.Parcelable
import com.arkivanov.essenty.parcelable.Parcelize
import com.arkivanov.mvikotlin.core.store.StoreFactory
import com.badoo.reaktive.base.Consumer
import kotlinx.coroutines.flow.Flow
import java.util.*

class OpiaRootComponent internal constructor(
componentContext: ComponentContext,
private val splash: (ComponentContext, Consumer<OpiaSplash.Output>) -> OpiaSplash,
private val auth: (ComponentContext, Consumer<OpiaAuth.Output>) -> OpiaAuth,
private val auth: (ComponentContext, (OpiaAuth.Output) -> Unit) -> OpiaAuth,
private val registration: (ComponentContext, Consumer<OpiaRegistration.Output>) -> OpiaRegistration,
private val chats: (ComponentContext, selfId: UUID, Consumer<OpiaChats.Output>) -> OpiaChats,
private val chat: (ComponentContext, selfId: UUID, peerId: UUID, Consumer<OpiaChat.Output>) -> OpiaChat
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ fun OpiaRootContent(component: OpiaRoot) {
) {
when (val child = it.instance) {
is Child.Splash -> SplashContent(child.component)
is Child.Auth -> AuthContent(child.component)
is Child.Auth -> AuthContent(child.component, child.onError)
is Child.Registration -> RegistrationContent(child.component)
is Child.Chats -> ChatsContent(child.component)
is Child.Chat -> ChatContent(child.component)
Expand Down
48 changes: 0 additions & 48 deletions common/src/commonMain/kotlin/app/opia/common/ui/auth/Auth.kt

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,60 +1,95 @@
package app.opia.common.ui.auth

import OpiaDispatchers
import app.opia.common.di.ServiceLocator
import app.opia.common.ui.auth.OpiaAuth.*
import app.opia.common.ui.auth.AuthView.*
import app.opia.common.ui.auth.store.AuthStore.*
import app.opia.common.ui.auth.store.AuthStoreProvider
import app.opia.common.ui.auth.store.IdentityProvider
import app.opia.common.utils.asValue
import app.opia.common.utils.getStore
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.value.Value
import com.arkivanov.decompose.value.operator.map
import com.arkivanov.essenty.lifecycle.Lifecycle
import com.arkivanov.essenty.lifecycle.doOnDestroy
import com.arkivanov.mvikotlin.core.binder.BinderLifecycleMode
import com.arkivanov.mvikotlin.core.store.StoreFactory
import com.arkivanov.mvikotlin.extensions.coroutines.bind
import com.arkivanov.mvikotlin.extensions.coroutines.events
import com.arkivanov.mvikotlin.extensions.coroutines.labels
import com.badoo.reaktive.base.Consumer
import com.badoo.reaktive.base.invoke
import com.arkivanov.mvikotlin.extensions.coroutines.states
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import java.util.*

class AuthComponent(
componentContext: ComponentContext,
storeFactory: StoreFactory,
di: ServiceLocator,
private val output: Consumer<Output>
) : OpiaAuth, ComponentContext by componentContext {
private val dispatchers: OpiaDispatchers,
private val output: (Output) -> Unit
) : ComponentContext by componentContext {
private val store = instanceKeeper.getStore {
AuthStoreProvider(
storeFactory = storeFactory, di = di
storeFactory = storeFactory, di = di, dispatchers = dispatchers
).provide()
}

override val models: Value<Model> = store.asValue().map(stateToModel)
//val errors: Flow<Any>

override val events: Flow<Event> = store.labels.map(transform = labelToEvent)
init {
lifecycle.doOnDestroy(store::dispose)

override fun onUniqueChanged(unique: String) {
bind(lifecycle, BinderLifecycleMode.CREATE_DESTROY, dispatchers.unconfined) {
store.labels.bindTo { label ->
when (label) {
is Label.Authenticated -> output(Output.Authenticated(label.selfId))
is Label.NetworkError -> onError()
is Label.UnknownError -> onError()
}
}
}
}

fun onViewCreated(view: AuthView, viewLifecycle: Lifecycle) {
bind(viewLifecycle, BinderLifecycleMode.CREATE_DESTROY, dispatchers.unconfined) {
view.events.mapNotNull(viewEventToIntent) bindTo store
}

bind(viewLifecycle, BinderLifecycleMode.START_STOP, dispatchers.unconfined) {
store.states.map(stateToModel) bindTo view
view.events bindTo ::onEvent
}
}

// viewEvent
private fun onEvent(ev: Event) {
when (ev) {
is Event.ContinueWithProviderClicked -> output(Output.Register)
else -> {} // irrelevant
}
}

fun onUniqueChanged(unique: String) {
store.accept(Intent.SetUnique(unique))
}

override fun onSecretChanged(secret: String) {
fun onSecretChanged(secret: String) {
store.accept(Intent.SetSecret(secret))
}

override fun onLoginClicked() {
fun onLoginClicked() {
store.accept(Intent.Login)
}

override fun onRegisterClicked() {
fun onRegisterClicked() {
output(Output.Register)
}

override fun onContinueWithProviderClicked(provider: IdentityProvider) {
fun onContinueWithProviderClicked(provider: IdentityProvider) {
output(Output.ContinueWithProvider(provider))
}

override fun onAuthenticated(selfId: UUID) {
fun onAuthenticated(selfId: UUID) {
output(Output.Authenticated(selfId))
}
}
Expand All @@ -71,8 +106,18 @@ internal val stateToModel: (State) -> Model = {

internal val labelToEvent: (Label) -> Event = {
when (it) {
is Label.Authenticated -> Event.Authenticated(it.selfId)
is Label.Authenticated -> Intent.Authenticated(it.selfId)
is Label.NetworkError -> Event.NetworkError
is Label.UnknownError -> Event.UnknownError
}
}

internal val viewEventToIntent: (AuthView.Event) -> Intent? = {
when (it) {
is AuthView.Event.UniqueChanged -> Intent.SetUnique(it.unique)
is AuthView.Event.SecretChanged -> Intent.SetSecret(it.secret)
is AuthView.Event.LoginClicked -> Intent.Login
is AuthView.Event.RegisterClicked -> null
is AuthView.Event.ContinueWithProviderClicked -> null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import kotlinx.coroutines.launch

@ExperimentalComposeUiApi
@Composable
fun AuthContent(component: OpiaAuth) {
fun AuthContent(component: AuthComponent) {
val model by component.models.subscribeAsState()

val keyboardController = LocalSoftwareKeyboardController.current
Expand Down
33 changes: 33 additions & 0 deletions common/src/commonMain/kotlin/app/opia/common/ui/auth/AuthView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package app.opia.common.ui.auth

import app.opia.common.ui.auth.AuthView.Event
import app.opia.common.ui.auth.AuthView.Model
import app.opia.common.ui.auth.store.IdentityProvider
import com.arkivanov.mvikotlin.core.view.MviView
import java.util.*

interface AuthView : MviView<Model, Event> {
//val generalError: String?,
data class Model(
val isLoading: Boolean,
val unique: String,
val uniqueError: String?,
val secret: String,
val secretError: String?
)

// Intent / ViewEvent
sealed class Event {
data class UniqueChanged(val unique: String) : Event()
data class SecretChanged(val secret: String) : Event()
object LoginClicked : Event()
object RegisterClicked : Event()
data class ContinueWithProviderClicked(val provider: IdentityProvider) : Event()
}

sealed class Output {
data class Authenticated(val selfId: UUID) : Output()
data class ContinueWithProvider(val provider: IdentityProvider) : Output()
object Register : Output()
}
}
Original file line number Diff line number Diff line change
@@ -1,30 +1,29 @@
package app.opia.common.ui.auth.store

import OpiaDispatchers
import app.opia.common.api.Code
import app.opia.common.api.NetworkResponse
import app.opia.common.di.ServiceLocator
import app.opia.common.ui.auth.store.AuthStore.*
import com.arkivanov.mvikotlin.core.store.Reducer
import com.arkivanov.mvikotlin.core.store.SimpleBootstrapper
import com.arkivanov.mvikotlin.core.store.Store
import com.arkivanov.mvikotlin.core.store.StoreFactory
import com.arkivanov.mvikotlin.extensions.coroutines.CoroutineExecutor
import com.squareup.sqldelight.runtime.coroutines.asFlow
import com.squareup.sqldelight.runtime.coroutines.mapToOne
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mainDispatcher

internal class AuthStoreProvider(
private val storeFactory: StoreFactory, private val di: ServiceLocator
private val storeFactory: StoreFactory,
private val di: ServiceLocator,
private val dispatchers: OpiaDispatchers
) {
fun provide(): AuthStore =
object : AuthStore, Store<Intent, State, Label> by storeFactory.create(
name = "AuthStore",
initialState = State(),
bootstrapper = SimpleBootstrapper(Unit),
executorFactory = ::ExecutorImpl,
reducer = ReducerImpl
) {}
Expand All @@ -39,8 +38,7 @@ internal class AuthStoreProvider(
}

private inner class ExecutorImpl :
CoroutineExecutor<Intent, Unit, State, Msg, Label>(mainDispatcher()) {
override fun executeAction(action: Unit, getState: () -> State) {}
CoroutineExecutor<Intent, Unit, State, Msg, Label>(dispatchers.main) {

override fun executeIntent(intent: Intent, getState: () -> State) = when (intent) {
is Intent.SetUnique -> dispatch(Msg.UniqueChanged(intent.unique))
Expand All @@ -54,17 +52,19 @@ internal class AuthStoreProvider(

scope.launch {
// withContext: fix stuttering
val installation =
withContext(Dispatchers.IO) { di.installationRepo.upsertInstallation() }
val installation = withContext(dispatchers.io) {
di.installationRepo.upsertInstallation()
}
println("[*] login > installation: $installation")
if (installation == null) {
publish(Label.NetworkError)
return@launch
}

// save session & actor
val authRes =
withContext(Dispatchers.IO) { di.actorRepo.login(state.unique, state.secret) }
val authRes = withContext(dispatchers.io) {
di.actorRepo.login(state.unique, state.secret)
}
when (authRes) {
is NetworkResponse.ApiSuccess -> {
val actorId = authRes.body.data.actor_id
Expand Down
Loading

0 comments on commit 6d19e54

Please sign in to comment.