TimelineItemPresenterFactories (#1609)

DI infrastructure to allow injection of presenters into the timeline.
Add an `@AssistedFactory` of type `TimelineItemPresenterFactory` to a `Presenter` class and bind this factory into the TimelineItemPresenterFactory map multi binding using:

```
@Binds
@IntoMap
@TimelineItemEventContentKey(MyTimelineItemContent::class)
```

A map multibinding of such factories will be available in the `LocalTimelineItemPresenterFactories` composition local for further use down the UI tree.
This commit is contained in:
Marco Romano
2023-10-19 15:57:34 +02:00
committed by GitHub
parent d7054fb34d
commit cf3039c6f3
5 changed files with 160 additions and 13 deletions

View File

@@ -17,6 +17,7 @@
package io.element.android.features.messages.impl
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.lifecycle.subscribe
import com.bumble.appyx.core.modality.BuildContext
@@ -28,6 +29,8 @@ import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.messages.impl.attachments.Attachment
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories
import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.UserId
@@ -44,6 +47,7 @@ class MessagesNode @AssistedInject constructor(
private val room: MatrixRoom,
private val analyticsService: AnalyticsService,
private val presenterFactory: MessagesPresenter.Factory,
private val timelineItemPresenterFactories: TimelineItemPresenterFactories,
) : Node(buildContext, plugins = plugins), MessagesNavigator {
private val presenter = presenterFactory.create(this)
@@ -106,17 +110,21 @@ class MessagesNode @AssistedInject constructor(
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
MessagesView(
state = state,
onBackPressed = this::navigateUp,
onRoomDetailsClicked = this::onRoomDetailsClicked,
onEventClicked = this::onEventClicked,
onPreviewAttachments = this::onPreviewAttachments,
onUserDataClicked = this::onUserDataClicked,
onSendLocationClicked = this::onSendLocationClicked,
onCreatePollClicked = this::onCreatePollClicked,
modifier = modifier,
)
CompositionLocalProvider(
LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories,
) {
val state = presenter.present()
MessagesView(
state = state,
onBackPressed = this::navigateUp,
onRoomDetailsClicked = this::onRoomDetailsClicked,
onEventClicked = this::onEventClicked,
onPreviewAttachments = this::onPreviewAttachments,
onUserDataClicked = this::onUserDataClicked,
onSendLocationClicked = this::onSendLocationClicked,
onCreatePollClicked = this::onCreatePollClicked,
modifier = modifier,
)
}
}
}

View File

@@ -0,0 +1,29 @@
/*
* 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.features.messages.impl.timeline.di
import dagger.MapKey
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
import kotlin.reflect.KClass
/**
* Annotation to add a factory of type [TimelineItemPresenterFactory] to a
* Dagger map multi binding keyed with a subclass of [TimelineItemEventContent].
*/
@Retention(AnnotationRetention.RUNTIME)
@MapKey
annotation class TimelineItemEventContentKey(val value: KClass<out TimelineItemEventContent>)

View File

@@ -0,0 +1,77 @@
/*
* 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.features.messages.impl.timeline.di
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.staticCompositionLocalOf
import com.squareup.anvil.annotations.ContributesTo
import dagger.Module
import dagger.multibindings.Multibinds
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.di.RoomScope
import javax.inject.Inject
/**
* Dagger module that declares the [TimelineItemPresenterFactory] map multi binding.
*
* Its sole purpose is to support the case of an empty map multibinding.
*/
@Module
@ContributesTo(RoomScope::class)
interface TimelineItemPresenterFactoriesModule {
@Multibinds
fun multiBindTimelineItemPresenterFactories(): @JvmSuppressWildcards Map<Class<out TimelineItemEventContent>, TimelineItemPresenterFactory<*, *>>
}
/**
* Wrapper around the [TimelineItemPresenterFactory] map multi binding.
*
* Its only purpose is to provide a nicer type name than:
* `@JvmSuppressWildcards Map<Class<out TimelineItemEventContent>, TimelineItemPresenterFactory<*, *>>`.
*
* A typealias would have been better but typealiases on Dagger types which use @JvmSuppressWildcards
* currently make Dagger crash.
*
* Request this type from Dagger to access the [TimelineItemPresenterFactory] map multibinding.
*/
data class TimelineItemPresenterFactories @Inject constructor(
val factories: @JvmSuppressWildcards Map<Class<out TimelineItemEventContent>, TimelineItemPresenterFactory<*, *>>,
)
/**
* Provides a [TimelineItemPresenterFactories] to the composition.
*/
val LocalTimelineItemPresenterFactories = staticCompositionLocalOf {
TimelineItemPresenterFactories(emptyMap())
}
/**
* Creates and remembers a presenter for the given content.
*
* Will throw if the presenter is not found in the [TimelineItemPresenterFactory] map multi binding.
*/
@Composable
inline fun <reified C : TimelineItemEventContent, reified S : Any> TimelineItemPresenterFactories.rememberPresenter(
content: C
): Presenter<S> = remember(content) {
factories.getValue(C::class.java).let {
@Suppress("UNCHECKED_CAST")
(it as TimelineItemPresenterFactory<C, S>).create(content)
}
}

View File

@@ -0,0 +1,33 @@
/*
* 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.features.messages.impl.timeline.di
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
import io.element.android.libraries.architecture.Presenter
/**
* A factory for a [Presenter] associated with a timeline item.
*
* Implementations should be annotated with [AssistedFactory] to be created by Dagger.
*
* @param C The timeline item's [TimelineItemEventContent] subtype.
* @param S The [Presenter]'s state class.
* @return A [Presenter] that produces a state of type [S] for the given content of type [C].
*/
fun interface TimelineItemPresenterFactory<C : TimelineItemEventContent, S : Any> {
fun create(content: C): Presenter<S>
}

View File

@@ -219,7 +219,7 @@ Compose:
CompositionLocalAllowlist:
active: true
# You can optionally define a list of CompositionLocals that are allowed here
allowedCompositionLocals: LocalCompoundColors, LocalSnackbarDispatcher, LocalCameraPositionState
allowedCompositionLocals: LocalCompoundColors, LocalSnackbarDispatcher, LocalCameraPositionState, LocalTimelineItemPresenterFactories
CompositionLocalNaming:
active: true
ContentEmitterReturningValues: