From b9febcd8d74b0dce9206fec599c887374675fbc6 Mon Sep 17 00:00:00 2001 From: ganfra Date: Sat, 4 Mar 2023 14:14:47 +0100 Subject: [PATCH] [Architecture] experiments to remove DI graph managements from navigation flow nodes --- .../io/element/android/x/MainActivity.kt | 6 +- .../kotlin/io/element/android/x/MainNode.kt | 83 +++++++++++++++++++ .../io/element/android/x/di/AppBindings.kt | 1 + .../android/x/di/MainDaggerComponentOwner.kt | 43 ++++++++++ .../element/android/x/di/SessionComponent.kt | 2 +- .../android/x/node/LoggedInFlowNode.kt | 21 +++-- .../android/x/node/NodeLifecycleCallback.kt | 21 +++++ .../io/element/android/x/node/RoomFlowNode.kt | 25 +++--- .../io/element/android/x/node/RootFlowNode.kt | 14 ++-- settings.gradle.kts | 1 + 10 files changed, 184 insertions(+), 33 deletions(-) create mode 100644 app/src/main/kotlin/io/element/android/x/MainNode.kt create mode 100644 app/src/main/kotlin/io/element/android/x/di/MainDaggerComponentOwner.kt create mode 100644 app/src/main/kotlin/io/element/android/x/node/NodeLifecycleCallback.kt diff --git a/app/src/main/kotlin/io/element/android/x/MainActivity.kt b/app/src/main/kotlin/io/element/android/x/MainActivity.kt index f8ce334e14..fb682318bb 100644 --- a/app/src/main/kotlin/io/element/android/x/MainActivity.kt +++ b/app/src/main/kotlin/io/element/android/x/MainActivity.kt @@ -25,11 +25,9 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.WindowCompat import com.bumble.appyx.core.integration.NodeHost import com.bumble.appyx.core.integrationpoint.NodeComponentActivity -import io.element.android.x.di.AppBindings import io.element.android.libraries.architecture.bindings -import io.element.android.libraries.architecture.createNode import io.element.android.libraries.designsystem.theme.ElementTheme -import io.element.android.x.node.RootFlowNode +import io.element.android.x.di.AppBindings class MainActivity : NodeComponentActivity() { @@ -45,7 +43,7 @@ class MainActivity : NodeComponentActivity() { modifier = Modifier.fillMaxSize(), ) { NodeHost(integrationPoint = appyxIntegrationPoint) { - createNode(it) + MainNode(it, appBindings.mainDaggerComponentOwner()) } } } diff --git a/app/src/main/kotlin/io/element/android/x/MainNode.kt b/app/src/main/kotlin/io/element/android/x/MainNode.kt new file mode 100644 index 0000000000..0c9428b183 --- /dev/null +++ b/app/src/main/kotlin/io/element/android/x/MainNode.kt @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.x + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.composable.Children +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.node.ParentNode +import io.element.android.libraries.architecture.bindings +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.di.DaggerComponentOwner +import io.element.android.libraries.matrix.MatrixClient +import io.element.android.libraries.matrix.room.MatrixRoom +import io.element.android.x.di.MainDaggerComponentOwner +import io.element.android.x.di.RoomComponent +import io.element.android.x.di.SessionComponent +import io.element.android.x.node.LoggedInFlowNode +import io.element.android.x.node.RoomFlowNode +import io.element.android.x.node.RootFlowNode + +class MainNode( + buildContext: BuildContext, + private val mainDaggerComponentOwner: MainDaggerComponentOwner, +) : + ParentNode( + navModel = PermanentNavModel( + navTargets = setOf(NavTarget), + savedStateMap = buildContext.savedStateMap, + ), + buildContext = buildContext, + ), + DaggerComponentOwner by mainDaggerComponentOwner { + + private val loggedInFlowNodeCallback = object : LoggedInFlowNode.LifecycleCallback { + override fun onFlowCreated(client: MatrixClient) { + val component = bindings().sessionComponentBuilder().client(client).build() + mainDaggerComponentOwner.addComponent(client.sessionId.value, component) + } + + override fun onFlowReleased(client: MatrixClient) { + mainDaggerComponentOwner.removeComponent(client.sessionId.value) + } + } + + private val roomFlowNodeCallback = object : RoomFlowNode.LifecycleCallback { + override fun onFlowCreated(room: MatrixRoom) { + val component = bindings().roomComponentBuilder().room(room).build() + mainDaggerComponentOwner.addComponent(room.roomId.value, component) + } + + override fun onFlowReleased(room: MatrixRoom) { + mainDaggerComponentOwner.removeComponent(room.roomId.value) + } + } + + override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { + return createNode(buildContext, plugins = listOf(loggedInFlowNodeCallback, roomFlowNodeCallback)) + } + + @Composable + override fun View(modifier: Modifier) { + Children(navModel = navModel) + } + + object NavTarget +} diff --git a/app/src/main/kotlin/io/element/android/x/di/AppBindings.kt b/app/src/main/kotlin/io/element/android/x/di/AppBindings.kt index d60ff4a15b..4505b1663d 100644 --- a/app/src/main/kotlin/io/element/android/x/di/AppBindings.kt +++ b/app/src/main/kotlin/io/element/android/x/di/AppBindings.kt @@ -22,4 +22,5 @@ import io.element.android.libraries.di.AppScope @ContributesTo(AppScope::class) interface AppBindings { fun matrixClientsHolder(): MatrixClientsHolder + fun mainDaggerComponentOwner(): MainDaggerComponentOwner } diff --git a/app/src/main/kotlin/io/element/android/x/di/MainDaggerComponentOwner.kt b/app/src/main/kotlin/io/element/android/x/di/MainDaggerComponentOwner.kt new file mode 100644 index 0000000000..4a3017e984 --- /dev/null +++ b/app/src/main/kotlin/io/element/android/x/di/MainDaggerComponentOwner.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.x.di + +import android.content.Context +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.di.DaggerComponentOwner +import io.element.android.libraries.di.SingleIn +import javax.inject.Inject + +@SingleIn(AppScope::class) +class MainDaggerComponentOwner @Inject constructor(@ApplicationContext context: Context) : DaggerComponentOwner { + + private val daggerComponents = LinkedHashMap().apply { + put("app", (context as DaggerComponentOwner).daggerComponent) + } + + fun addComponent(identifier: String, component: Any) { + daggerComponents[identifier] = component + } + + fun removeComponent(identifier: String) { + daggerComponents.remove(identifier) + } + + override val daggerComponent: Any + get() = daggerComponents.values.reversed() +} diff --git a/app/src/main/kotlin/io/element/android/x/di/SessionComponent.kt b/app/src/main/kotlin/io/element/android/x/di/SessionComponent.kt index 33969ddd96..075529e3e8 100644 --- a/app/src/main/kotlin/io/element/android/x/di/SessionComponent.kt +++ b/app/src/main/kotlin/io/element/android/x/di/SessionComponent.kt @@ -28,7 +28,7 @@ import io.element.android.libraries.matrix.MatrixClient @SingleIn(SessionScope::class) @MergeSubcomponent(SessionScope::class) -interface SessionComponent : NodeFactoriesBindings, RoomComponent.ParentBindings { +interface SessionComponent : NodeFactoriesBindings { @Subcomponent.Builder interface Builder { diff --git a/app/src/main/kotlin/io/element/android/x/node/LoggedInFlowNode.kt b/app/src/main/kotlin/io/element/android/x/node/LoggedInFlowNode.kt index e627f0d448..8313aa4e45 100644 --- a/app/src/main/kotlin/io/element/android/x/node/LoggedInFlowNode.kt +++ b/app/src/main/kotlin/io/element/android/x/node/LoggedInFlowNode.kt @@ -46,11 +46,9 @@ import io.element.android.libraries.architecture.nodeInputs import io.element.android.libraries.architecture.nodeInputsProvider import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.DaggerComponentOwner import io.element.android.libraries.matrix.MatrixClient import io.element.android.libraries.matrix.core.RoomId import io.element.android.libraries.matrix.ui.di.MatrixUIBindings -import io.element.android.x.di.SessionComponent import kotlinx.parcelize.Parcelize @ContributesNode(AppScope::class) @@ -66,10 +64,16 @@ class LoggedInFlowNode @AssistedInject constructor( ), buildContext = buildContext, plugins = plugins -), DaggerComponentOwner { +) { interface Callback : Plugin { - fun onOpenBugReport() + fun onOpenBugReport() = Unit + } + + interface LifecycleCallback : NodeLifecycleCallback { + fun onFlowCreated(client: MatrixClient) = Unit + + fun onFlowReleased(client: MatrixClient) = Unit } data class Inputs( @@ -78,19 +82,17 @@ class LoggedInFlowNode @AssistedInject constructor( private val inputs: Inputs by nodeInputs() - override val daggerComponent: Any by lazy { - parent!!.bindings().sessionComponentBuilder().client(inputs.matrixClient).build() - } - override fun onBuilt() { super.onBuilt() lifecycle.subscribe( onCreate = { + plugins().forEach { it.onFlowCreated(inputs.matrixClient) } val imageLoaderFactory = bindings().loggedInImageLoaderFactory() Coil.setImageLoader(imageLoaderFactory) inputs.matrixClient.startSync() }, onDestroy = { + plugins().forEach { it.onFlowReleased(inputs.matrixClient) } val imageLoaderFactory = bindings().notLoggedInImageLoaderFactory() Coil.setImageLoader(imageLoaderFactory) } @@ -132,8 +134,9 @@ class LoggedInFlowNode @AssistedInject constructor( } } } else { + val nodeLifecycleCallbacks = plugins() val inputsProvider = nodeInputsProvider(RoomFlowNode.Inputs(room)) - createNode(buildContext, plugins = listOf(inputsProvider)) + createNode(buildContext, plugins = listOf(inputsProvider) + nodeLifecycleCallbacks) } } NavTarget.Settings -> { diff --git a/app/src/main/kotlin/io/element/android/x/node/NodeLifecycleCallback.kt b/app/src/main/kotlin/io/element/android/x/node/NodeLifecycleCallback.kt new file mode 100644 index 0000000000..d63da9b0cb --- /dev/null +++ b/app/src/main/kotlin/io/element/android/x/node/NodeLifecycleCallback.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.x.node + +import com.bumble.appyx.core.plugin.Plugin + +interface NodeLifecycleCallback : Plugin diff --git a/app/src/main/kotlin/io/element/android/x/node/RoomFlowNode.kt b/app/src/main/kotlin/io/element/android/x/node/RoomFlowNode.kt index 3671661b68..09125382c5 100644 --- a/app/src/main/kotlin/io/element/android/x/node/RoomFlowNode.kt +++ b/app/src/main/kotlin/io/element/android/x/node/RoomFlowNode.kt @@ -24,17 +24,15 @@ import com.bumble.appyx.core.lifecycle.subscribe import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.core.plugin.plugins import com.bumble.appyx.navmodel.backstack.BackStack import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode -import io.element.android.x.di.RoomComponent import io.element.android.features.messages.api.MessagesEntryPoint import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.NodeInputs -import io.element.android.libraries.architecture.bindings import io.element.android.libraries.architecture.nodeInputs -import io.element.android.libraries.di.DaggerComponentOwner import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.room.MatrixRoom import kotlinx.parcelize.Parcelize @@ -52,7 +50,12 @@ class RoomFlowNode @AssistedInject constructor( ), buildContext = buildContext, plugins = plugins, -), DaggerComponentOwner { +) { + + interface LifecycleCallback : NodeLifecycleCallback { + fun onFlowCreated(room: MatrixRoom) = Unit + fun onFlowReleased(room: MatrixRoom) = Unit + } data class Inputs( val room: MatrixRoom, @@ -60,14 +63,16 @@ class RoomFlowNode @AssistedInject constructor( private val inputs: Inputs by nodeInputs() - override val daggerComponent: Any by lazy { - parent!!.bindings().roomComponentBuilder().room(inputs.room).build() - } - init { lifecycle.subscribe( - onCreate = { Timber.v("OnCreate") }, - onDestroy = { Timber.v("OnDestroy") } + onCreate = { + Timber.v("OnCreate") + plugins().forEach { it.onFlowCreated(inputs.room) } + }, + onDestroy = { + Timber.v("OnDestroy") + plugins().forEach { it.onFlowReleased(inputs.room) } + } ) } diff --git a/app/src/main/kotlin/io/element/android/x/node/RootFlowNode.kt b/app/src/main/kotlin/io/element/android/x/node/RootFlowNode.kt index a4ad92c80d..66f24be1fe 100644 --- a/app/src/main/kotlin/io/element/android/x/node/RootFlowNode.kt +++ b/app/src/main/kotlin/io/element/android/x/node/RootFlowNode.kt @@ -16,7 +16,6 @@ package io.element.android.x.node -import android.content.Context import android.os.Parcelable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize @@ -29,6 +28,7 @@ import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.node import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.core.plugin.plugins import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.operation.newRoot import com.bumble.appyx.navmodel.backstack.operation.pop @@ -36,7 +36,6 @@ import com.bumble.appyx.navmodel.backstack.operation.push import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode -import io.element.android.x.root.MatrixClientsHolder import io.element.android.features.rageshake.bugreport.BugReportEntryPoint import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler @@ -44,10 +43,9 @@ import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.nodeInputsProvider import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.ApplicationContext -import io.element.android.libraries.di.DaggerComponentOwner import io.element.android.libraries.matrix.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.core.SessionId +import io.element.android.x.root.MatrixClientsHolder import io.element.android.x.root.RootPresenter import io.element.android.x.root.RootView import kotlinx.coroutines.flow.distinctUntilChanged @@ -60,7 +58,6 @@ import timber.log.Timber class RootFlowNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, - @ApplicationContext context: Context, private val authenticationService: MatrixAuthenticationService, private val matrixClientsHolder: MatrixClientsHolder, private val presenter: RootPresenter, @@ -73,9 +70,7 @@ class RootFlowNode @AssistedInject constructor( ), buildContext = buildContext, plugins = plugins - ), - - DaggerComponentOwner by (context as DaggerComponentOwner) { + ) { override fun onBuilt() { super.onBuilt() @@ -178,7 +173,8 @@ class RootFlowNode @AssistedInject constructor( backstack.push(NavTarget.BugReport) } } - createNode(buildContext, plugins = listOf(inputsProvider, callback)) + val nodeLifecycleCallbacks = plugins() + createNode(buildContext, plugins = listOf(inputsProvider, callback) + nodeLifecycleCallbacks) } NavTarget.NotLoggedInFlow -> createNode(buildContext) NavTarget.SplashScreen -> splashNode(buildContext) diff --git a/settings.gradle.kts b/settings.gradle.kts index 37f839c073..85c58d03e7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -65,3 +65,4 @@ include(":libraries:androidutils") include(":samples:minimal") include(":features:messages:api") include(":features:messages:impl") +include(":appnav")