diff --git a/app/src/main/java/io/element/android/x/MainActivity.kt b/app/src/main/java/io/element/android/x/MainActivity.kt index 4fdb426392..5bac04cab9 100644 --- a/app/src/main/java/io/element/android/x/MainActivity.kt +++ b/app/src/main/java/io/element/android/x/MainActivity.kt @@ -22,10 +22,7 @@ import io.element.android.x.designsystem.ElementXTheme import io.element.android.x.di.AppBindings import io.element.android.x.node.RootFlowNode -class MainActivity : NodeComponentActivity(), DaggerComponentOwner { - - override val daggerComponent: Any - get() = listOfNotNull((applicationContext as? DaggerComponentOwner)?.daggerComponent) +class MainActivity : NodeComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -41,7 +38,7 @@ class MainActivity : NodeComponentActivity(), DaggerComponentOwner { NodeHost(integrationPoint = appyxIntegrationPoint) { RootFlowNode( buildContext = it, - appComponentOwner = this, + appComponentOwner = applicationContext as DaggerComponentOwner, matrix = appBindings.matrix(), sessionComponentsOwner = appBindings.sessionComponentsOwner() ) diff --git a/app/src/main/java/io/element/android/x/di/AppComponent.kt b/app/src/main/java/io/element/android/x/di/AppComponent.kt index 2c170f202b..e68a58eac3 100644 --- a/app/src/main/java/io/element/android/x/di/AppComponent.kt +++ b/app/src/main/java/io/element/android/x/di/AppComponent.kt @@ -8,10 +8,10 @@ import io.element.android.x.core.di.DaggerMavericksBindings @SingleIn(AppScope::class) @MergeComponent(AppScope::class) -interface AppComponent: DaggerMavericksBindings { +interface AppComponent : DaggerMavericksBindings { @Component.Factory interface Factory { fun create(@ApplicationContext @BindsInstance context: Context): AppComponent } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/element/android/x/di/SessionComponent.kt b/app/src/main/java/io/element/android/x/di/SessionComponent.kt index 5acc18d384..e24dd193dc 100644 --- a/app/src/main/java/io/element/android/x/di/SessionComponent.kt +++ b/app/src/main/java/io/element/android/x/di/SessionComponent.kt @@ -5,11 +5,12 @@ import com.squareup.anvil.annotations.MergeSubcomponent import dagger.BindsInstance import dagger.Subcomponent import io.element.android.x.core.di.DaggerMavericksBindings +import io.element.android.x.core.di.NodeFactoriesBindings import io.element.android.x.matrix.MatrixClient @SingleIn(SessionScope::class) @MergeSubcomponent(SessionScope::class) -interface SessionComponent: DaggerMavericksBindings { +interface SessionComponent: DaggerMavericksBindings, NodeFactoriesBindings { fun matrixClient(): MatrixClient diff --git a/app/src/main/java/io/element/android/x/node/LoggedInFlowNode.kt b/app/src/main/java/io/element/android/x/node/LoggedInFlowNode.kt index 28914b78c0..bf1047ae52 100644 --- a/app/src/main/java/io/element/android/x/node/LoggedInFlowNode.kt +++ b/app/src/main/java/io/element/android/x/node/LoggedInFlowNode.kt @@ -4,27 +4,23 @@ import android.os.Parcelable import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.bumble.appyx.core.composable.Children -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.node.ParentNode import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.operation.pop import com.bumble.appyx.navmodel.backstack.operation.push +import io.element.android.x.core.di.createNode import io.element.android.x.core.di.viewModelSupportNode import io.element.android.x.features.messages.MessagesScreen import io.element.android.x.features.roomlist.RoomListNode -import io.element.android.x.features.roomlist.RoomListPresenter -import io.element.android.x.matrix.MatrixClient import io.element.android.x.matrix.core.RoomId import io.element.android.x.matrix.core.SessionId import kotlinx.parcelize.Parcelize -import timber.log.Timber class LoggedInFlowNode( buildContext: BuildContext, val sessionId: SessionId, - private val matrixClient: MatrixClient, private val backstack: BackStack = BackStack( initialElement = NavTarget.RoomList, savedStateMap = buildContext.savedStateMap, @@ -34,11 +30,10 @@ class LoggedInFlowNode( buildContext = buildContext ) { - init { - lifecycle.subscribe( - onCreate = { Timber.v("OnCreate") }, - onDestroy = { Timber.v("OnDestroy") } - ) + private val roomListCallback = object : RoomListNode.Callback { + override fun onRoomClicked(roomId: RoomId) { + backstack.push(NavTarget.Messages(roomId)) + } } sealed interface NavTarget : Parcelable { @@ -51,12 +46,9 @@ class LoggedInFlowNode( override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { - NavTarget.RoomList -> RoomListNode( - buildContext = buildContext, - presenter = RoomListPresenter(matrixClient), - onRoomClicked = { - backstack.push(NavTarget.Messages(it)) - }) + NavTarget.RoomList -> { + createNode(buildContext, plugins = listOf(roomListCallback)) + } is NavTarget.Messages -> viewModelSupportNode(buildContext) { MessagesScreen( roomId = navTarget.roomId.value, diff --git a/app/src/main/java/io/element/android/x/node/RootFlowNode.kt b/app/src/main/java/io/element/android/x/node/RootFlowNode.kt index 643763e5ad..a19e0f5a53 100644 --- a/app/src/main/java/io/element/android/x/node/RootFlowNode.kt +++ b/app/src/main/java/io/element/android/x/node/RootFlowNode.kt @@ -135,8 +135,7 @@ class RootFlowNode( override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { is NavTarget.LoggedInFlow -> { - val matrixClient = sessionComponentsOwner.activeSessionComponent!!.matrixClient() - LoggedInFlowNode(buildContext, navTarget.sessionId, matrixClient) + LoggedInFlowNode(buildContext, navTarget.sessionId) } NavTarget.NotLoggedInFlow -> NotLoggedInFlowNode(buildContext) NavTarget.SplashScreen -> node(buildContext) { diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/LastMessageFormatter.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/LastMessageFormatter.kt index bc59f0544c..62b08b6fcf 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/LastMessageFormatter.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/LastMessageFormatter.kt @@ -16,10 +16,10 @@ import java.util.Locale import javax.inject.Inject import kotlin.math.absoluteValue -class LastMessageFormatter @Inject constructor( - private val clock: Clock = Clock.System, +class LastMessageFormatter @Inject constructor() { + + private val clock: Clock = Clock.System private val locale: Locale = Locale.getDefault() -) { private val onlyTimeFormatter: DateTimeFormatter by lazy { val pattern = DateFormat.getBestDateTimePattern(locale, "HH:mm") diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListModule.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListModule.kt new file mode 100644 index 0000000000..d4fc2d25bb --- /dev/null +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListModule.kt @@ -0,0 +1,19 @@ +package io.element.android.x.features.roomlist + +import com.squareup.anvil.annotations.ContributesTo +import dagger.Binds +import dagger.Module +import dagger.multibindings.IntoMap +import io.element.android.x.core.di.AssistedNodeFactory +import io.element.android.x.core.di.NodeKey +import io.element.android.x.di.SessionScope + +@Module +@ContributesTo(SessionScope::class) +abstract class RoomListModule { + + @Binds + @IntoMap + @NodeKey(RoomListNode::class) + abstract fun bindRoomListNodeFactory(factory: RoomListNode.Factory): AssistedNodeFactory<*> +} diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt index cf3ab985c5..1e4f0a0c27 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListNode.kt @@ -6,15 +6,30 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier 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 dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import io.element.android.x.core.di.AssistedNodeFactory import io.element.android.x.features.roomlist.model.RoomListEvents import io.element.android.x.matrix.core.RoomId import io.element.android.x.presentation.presenterConnector -class RoomListNode( - buildContext: BuildContext, +class RoomListNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, presenter: RoomListPresenter, - private val onRoomClicked: (RoomId) -> Unit -) : Node(buildContext) { +) : Node(buildContext, plugins = plugins) { + + @AssistedFactory + interface Factory : AssistedNodeFactory { + override fun create(buildContext: BuildContext, plugins: List): RoomListNode + } + + interface Callback : Plugin { + fun onRoomClicked(roomId: RoomId) + } private val connector = presenterConnector(presenter) @@ -30,12 +45,16 @@ class RoomListNode( connector.emitEvent(RoomListEvents.Logout) } + private fun onRoomClicked(roomId: RoomId) { + plugins().forEach { it.onRoomClicked(roomId) } + } + @Composable override fun View(modifier: Modifier) { val state by connector.stateFlow.collectAsState() RoomListView( state = state, - onRoomClicked = onRoomClicked, + onRoomClicked = this::onRoomClicked, onFilterChanged = this::updateFilter, onScrollOver = this::updateVisibleRange, onLogoutClicked = this::logout diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListPresenter.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListPresenter.kt index 1906dced95..3760f2f02c 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListPresenter.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListPresenter.kt @@ -32,7 +32,7 @@ private const val extendedRangeSize = 40 class RoomListPresenter @Inject constructor( private val client: MatrixClient, - private val lastMessageFormatter: LastMessageFormatter = LastMessageFormatter(), + private val lastMessageFormatter: LastMessageFormatter, ) : Presenter { @Composable diff --git a/libraries/core/build.gradle.kts b/libraries/core/build.gradle.kts index e308392a8c..29f4ec2274 100644 --- a/libraries/core/build.gradle.kts +++ b/libraries/core/build.gradle.kts @@ -9,6 +9,5 @@ android { dependencies { api(libs.mavericks.compose) api(libs.dagger) - api(libs.androidx.fragment) api(libs.appyx.core) } diff --git a/libraries/core/src/main/java/io/element/android/x/core/di/AssistedNodeFactory.kt b/libraries/core/src/main/java/io/element/android/x/core/di/AssistedNodeFactory.kt new file mode 100644 index 0000000000..022fe03720 --- /dev/null +++ b/libraries/core/src/main/java/io/element/android/x/core/di/AssistedNodeFactory.kt @@ -0,0 +1,9 @@ +package io.element.android.x.core.di + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin + +interface AssistedNodeFactory { + fun create(buildContext: BuildContext, plugins: List): NODE +} diff --git a/libraries/core/src/main/java/io/element/android/x/core/di/Bindings.kt b/libraries/core/src/main/java/io/element/android/x/core/di/Bindings.kt index b5f1b5c5a4..9fe69bc112 100644 --- a/libraries/core/src/main/java/io/element/android/x/core/di/Bindings.kt +++ b/libraries/core/src/main/java/io/element/android/x/core/di/Bindings.kt @@ -7,12 +7,12 @@ import com.bumble.appyx.core.node.Node /** * Use this to get the Dagger "Bindings" for your module. Bindings are used if you need to directly interact with a dagger component such as: - * * an inject function: `inject(MyFragment frag)` + * * an inject function: `inject(node: MyNode)` * * an explicit getter: `fun myClass(): MyClass` * * Anvil will make your Dagger component implement these bindings so that you can call any of these functions on an instance of your component. * - * [bindings] will walk up the Fragment/Activity hierarchy and check for [DaggerComponentOwner] to see if any of its components implement the + * [bindings] will walk up the Node/Activity hierarchy and check for [DaggerComponentOwner] to see if any of its components implement the * specified bindings. Most of the time this will "just work" and you don't have to think about it. * * For example, if your class has @Inject properties: @@ -24,11 +24,6 @@ import com.bumble.appyx.core.node.Node inline fun Context.bindings() = bindings(T::class.java) -/** - * @see bindings - */ -inline fun Fragment.bindings() = bindings(T::class.java) - inline fun Node.bindings() = bindings(T::class.java) /** Use no-arg extension function instead: [Context.bindings] */ @@ -44,18 +39,6 @@ fun Context.bindings(klass: Class): T { ?: error("Unable to find bindings for ${klass.name}") } -/** Use no-arg extension function instead: [Fragment.bindings] */ -fun Fragment.bindings(klass: Class): T { - // search dagger components in fragment hierarchy, then fallback to activity and application - return generateSequence(this, Fragment::getParentFragment) - .filterIsInstance() - .map { it.daggerComponent } - .flatMap { if (it is Collection<*>) it else listOf(it) } - .filterIsInstance(klass) - .firstOrNull() - ?: requireActivity().bindings(klass) -} - /** Use no-arg extension function instead: [Node.bindings] */ fun Node.bindings(klass: Class): T { // search dagger components in node hierarchy diff --git a/libraries/core/src/main/java/io/element/android/x/core/di/DaggerMavericksViewModelFactory.kt b/libraries/core/src/main/java/io/element/android/x/core/di/DaggerMavericksViewModelFactory.kt index 48793e9823..a67c9b204d 100644 --- a/libraries/core/src/main/java/io/element/android/x/core/di/DaggerMavericksViewModelFactory.kt +++ b/libraries/core/src/main/java/io/element/android/x/core/di/DaggerMavericksViewModelFactory.kt @@ -41,10 +41,7 @@ class DaggerMavericksViewModelFactory, S : MavericksS ) : MavericksViewModelFactory { override fun create(viewModelContext: ViewModelContext, state: S): VM { - val bindings: DaggerMavericksBindings = when (viewModelContext) { - is FragmentViewModelContext -> viewModelContext.fragment.bindings() - else -> viewModelContext.activity.bindings() - } + val bindings: DaggerMavericksBindings = viewModelContext.activity.bindings() val viewModelFactoryMap = bindings.viewModelFactories() val viewModelFactory = viewModelFactoryMap[viewModelClass] ?: error("Cannot find ViewModelFactory for ${viewModelClass.name}.") @@ -57,4 +54,4 @@ class DaggerMavericksViewModelFactory, S : MavericksS interface DaggerMavericksBindings { fun viewModelFactories(): Map>, AssistedViewModelFactory<*, *>> -} \ No newline at end of file +} diff --git a/libraries/core/src/main/java/io/element/android/x/core/di/NodeFactories.kt b/libraries/core/src/main/java/io/element/android/x/core/di/NodeFactories.kt new file mode 100644 index 0000000000..94605d4a6a --- /dev/null +++ b/libraries/core/src/main/java/io/element/android/x/core/di/NodeFactories.kt @@ -0,0 +1,21 @@ +package io.element.android.x.core.di + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin + +inline fun Node.createNode(context: BuildContext, plugins: List = emptyList()): NODE { + val nodeClass = NODE::class.java + val bindings: NodeFactoriesBindings = bindings() + val nodeFactoryMap = bindings.nodeFactories() + val nodeFactory = nodeFactoryMap[nodeClass] ?: error("Cannot find NodeFactory for ${nodeClass.name}.") + + @Suppress("UNCHECKED_CAST") + val castedNodeFactory = nodeFactory as? AssistedNodeFactory + val node = castedNodeFactory?.create(context, plugins) + return node as NODE +} + +interface NodeFactoriesBindings { + fun nodeFactories(): Map, AssistedNodeFactory<*>> +} diff --git a/libraries/core/src/main/java/io/element/android/x/core/di/NodeKey.kt b/libraries/core/src/main/java/io/element/android/x/core/di/NodeKey.kt new file mode 100644 index 0000000000..b6541a4688 --- /dev/null +++ b/libraries/core/src/main/java/io/element/android/x/core/di/NodeKey.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019 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.core.di + +import com.bumble.appyx.core.node.Node +import dagger.MapKey +import kotlin.reflect.KClass + +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) +@MapKey +annotation class NodeKey(val value: KClass)