RoomList: setup dagger for node (remove fragment bindings)

This commit is contained in:
ganfra 2023-01-04 20:19:01 +01:00
parent fc14973049
commit 3ffbba954e
15 changed files with 122 additions and 60 deletions

View file

@ -22,10 +22,7 @@ import io.element.android.x.designsystem.ElementXTheme
import io.element.android.x.di.AppBindings import io.element.android.x.di.AppBindings
import io.element.android.x.node.RootFlowNode import io.element.android.x.node.RootFlowNode
class MainActivity : NodeComponentActivity(), DaggerComponentOwner { class MainActivity : NodeComponentActivity() {
override val daggerComponent: Any
get() = listOfNotNull((applicationContext as? DaggerComponentOwner)?.daggerComponent)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -41,7 +38,7 @@ class MainActivity : NodeComponentActivity(), DaggerComponentOwner {
NodeHost(integrationPoint = appyxIntegrationPoint) { NodeHost(integrationPoint = appyxIntegrationPoint) {
RootFlowNode( RootFlowNode(
buildContext = it, buildContext = it,
appComponentOwner = this, appComponentOwner = applicationContext as DaggerComponentOwner,
matrix = appBindings.matrix(), matrix = appBindings.matrix(),
sessionComponentsOwner = appBindings.sessionComponentsOwner() sessionComponentsOwner = appBindings.sessionComponentsOwner()
) )

View file

@ -8,7 +8,7 @@ import io.element.android.x.core.di.DaggerMavericksBindings
@SingleIn(AppScope::class) @SingleIn(AppScope::class)
@MergeComponent(AppScope::class) @MergeComponent(AppScope::class)
interface AppComponent: DaggerMavericksBindings { interface AppComponent : DaggerMavericksBindings {
@Component.Factory @Component.Factory
interface Factory { interface Factory {

View file

@ -5,11 +5,12 @@ import com.squareup.anvil.annotations.MergeSubcomponent
import dagger.BindsInstance import dagger.BindsInstance
import dagger.Subcomponent import dagger.Subcomponent
import io.element.android.x.core.di.DaggerMavericksBindings import io.element.android.x.core.di.DaggerMavericksBindings
import io.element.android.x.core.di.NodeFactoriesBindings
import io.element.android.x.matrix.MatrixClient import io.element.android.x.matrix.MatrixClient
@SingleIn(SessionScope::class) @SingleIn(SessionScope::class)
@MergeSubcomponent(SessionScope::class) @MergeSubcomponent(SessionScope::class)
interface SessionComponent: DaggerMavericksBindings { interface SessionComponent: DaggerMavericksBindings, NodeFactoriesBindings {
fun matrixClient(): MatrixClient fun matrixClient(): MatrixClient

View file

@ -4,27 +4,23 @@ import android.os.Parcelable
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import com.bumble.appyx.core.composable.Children 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.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.ParentNode import com.bumble.appyx.core.node.ParentNode
import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.pop import com.bumble.appyx.navmodel.backstack.operation.pop
import com.bumble.appyx.navmodel.backstack.operation.push 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.core.di.viewModelSupportNode
import io.element.android.x.features.messages.MessagesScreen import io.element.android.x.features.messages.MessagesScreen
import io.element.android.x.features.roomlist.RoomListNode 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.RoomId
import io.element.android.x.matrix.core.SessionId import io.element.android.x.matrix.core.SessionId
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import timber.log.Timber
class LoggedInFlowNode( class LoggedInFlowNode(
buildContext: BuildContext, buildContext: BuildContext,
val sessionId: SessionId, val sessionId: SessionId,
private val matrixClient: MatrixClient,
private val backstack: BackStack<NavTarget> = BackStack( private val backstack: BackStack<NavTarget> = BackStack(
initialElement = NavTarget.RoomList, initialElement = NavTarget.RoomList,
savedStateMap = buildContext.savedStateMap, savedStateMap = buildContext.savedStateMap,
@ -34,11 +30,10 @@ class LoggedInFlowNode(
buildContext = buildContext buildContext = buildContext
) { ) {
init { private val roomListCallback = object : RoomListNode.Callback {
lifecycle.subscribe( override fun onRoomClicked(roomId: RoomId) {
onCreate = { Timber.v("OnCreate") }, backstack.push(NavTarget.Messages(roomId))
onDestroy = { Timber.v("OnDestroy") } }
)
} }
sealed interface NavTarget : Parcelable { sealed interface NavTarget : Parcelable {
@ -51,12 +46,9 @@ class LoggedInFlowNode(
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) { return when (navTarget) {
NavTarget.RoomList -> RoomListNode( NavTarget.RoomList -> {
buildContext = buildContext, createNode<RoomListNode>(buildContext, plugins = listOf(roomListCallback))
presenter = RoomListPresenter(matrixClient), }
onRoomClicked = {
backstack.push(NavTarget.Messages(it))
})
is NavTarget.Messages -> viewModelSupportNode(buildContext) { is NavTarget.Messages -> viewModelSupportNode(buildContext) {
MessagesScreen( MessagesScreen(
roomId = navTarget.roomId.value, roomId = navTarget.roomId.value,

View file

@ -135,8 +135,7 @@ class RootFlowNode(
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) { return when (navTarget) {
is NavTarget.LoggedInFlow -> { is NavTarget.LoggedInFlow -> {
val matrixClient = sessionComponentsOwner.activeSessionComponent!!.matrixClient() LoggedInFlowNode(buildContext, navTarget.sessionId)
LoggedInFlowNode(buildContext, navTarget.sessionId, matrixClient)
} }
NavTarget.NotLoggedInFlow -> NotLoggedInFlowNode(buildContext) NavTarget.NotLoggedInFlow -> NotLoggedInFlowNode(buildContext)
NavTarget.SplashScreen -> node(buildContext) { NavTarget.SplashScreen -> node(buildContext) {

View file

@ -16,10 +16,10 @@ import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
class LastMessageFormatter @Inject constructor( class LastMessageFormatter @Inject constructor() {
private val clock: Clock = Clock.System,
private val clock: Clock = Clock.System
private val locale: Locale = Locale.getDefault() private val locale: Locale = Locale.getDefault()
) {
private val onlyTimeFormatter: DateTimeFormatter by lazy { private val onlyTimeFormatter: DateTimeFormatter by lazy {
val pattern = DateFormat.getBestDateTimePattern(locale, "HH:mm") val pattern = DateFormat.getBestDateTimePattern(locale, "HH:mm")

View file

@ -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<*>
}

View file

@ -6,15 +6,30 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext 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 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.features.roomlist.model.RoomListEvents
import io.element.android.x.matrix.core.RoomId import io.element.android.x.matrix.core.RoomId
import io.element.android.x.presentation.presenterConnector import io.element.android.x.presentation.presenterConnector
class RoomListNode( class RoomListNode @AssistedInject constructor(
buildContext: BuildContext, @Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
presenter: RoomListPresenter, presenter: RoomListPresenter,
private val onRoomClicked: (RoomId) -> Unit ) : Node(buildContext, plugins = plugins) {
) : Node(buildContext) {
@AssistedFactory
interface Factory : AssistedNodeFactory<RoomListNode> {
override fun create(buildContext: BuildContext, plugins: List<Plugin>): RoomListNode
}
interface Callback : Plugin {
fun onRoomClicked(roomId: RoomId)
}
private val connector = presenterConnector(presenter) private val connector = presenterConnector(presenter)
@ -30,12 +45,16 @@ class RoomListNode(
connector.emitEvent(RoomListEvents.Logout) connector.emitEvent(RoomListEvents.Logout)
} }
private fun onRoomClicked(roomId: RoomId) {
plugins<Callback>().forEach { it.onRoomClicked(roomId) }
}
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val state by connector.stateFlow.collectAsState() val state by connector.stateFlow.collectAsState()
RoomListView( RoomListView(
state = state, state = state,
onRoomClicked = onRoomClicked, onRoomClicked = this::onRoomClicked,
onFilterChanged = this::updateFilter, onFilterChanged = this::updateFilter,
onScrollOver = this::updateVisibleRange, onScrollOver = this::updateVisibleRange,
onLogoutClicked = this::logout onLogoutClicked = this::logout

View file

@ -32,7 +32,7 @@ private const val extendedRangeSize = 40
class RoomListPresenter @Inject constructor( class RoomListPresenter @Inject constructor(
private val client: MatrixClient, private val client: MatrixClient,
private val lastMessageFormatter: LastMessageFormatter = LastMessageFormatter(), private val lastMessageFormatter: LastMessageFormatter,
) : Presenter<RoomListState, RoomListEvents> { ) : Presenter<RoomListState, RoomListEvents> {
@Composable @Composable

View file

@ -9,6 +9,5 @@ android {
dependencies { dependencies {
api(libs.mavericks.compose) api(libs.mavericks.compose)
api(libs.dagger) api(libs.dagger)
api(libs.androidx.fragment)
api(libs.appyx.core) api(libs.appyx.core)
} }

View file

@ -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<NODE : Node> {
fun create(buildContext: BuildContext, plugins: List<Plugin>): NODE
}

View file

@ -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: * 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` * * 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. * 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. * 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: * For example, if your class has @Inject properties:
@ -24,11 +24,6 @@ import com.bumble.appyx.core.node.Node
inline fun <reified T : Any> Context.bindings() = bindings(T::class.java) inline fun <reified T : Any> Context.bindings() = bindings(T::class.java)
/**
* @see bindings
*/
inline fun <reified T : Any> Fragment.bindings() = bindings(T::class.java)
inline fun <reified T : Any> Node.bindings() = bindings(T::class.java) inline fun <reified T : Any> Node.bindings() = bindings(T::class.java)
/** Use no-arg extension function instead: [Context.bindings] */ /** Use no-arg extension function instead: [Context.bindings] */
@ -44,18 +39,6 @@ fun <T : Any> Context.bindings(klass: Class<T>): T {
?: error("Unable to find bindings for ${klass.name}") ?: error("Unable to find bindings for ${klass.name}")
} }
/** Use no-arg extension function instead: [Fragment.bindings] */
fun <T : Any> Fragment.bindings(klass: Class<T>): T {
// search dagger components in fragment hierarchy, then fallback to activity and application
return generateSequence(this, Fragment::getParentFragment)
.filterIsInstance<DaggerComponentOwner>()
.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] */ /** Use no-arg extension function instead: [Node.bindings] */
fun <T : Any> Node.bindings(klass: Class<T>): T { fun <T : Any> Node.bindings(klass: Class<T>): T {
// search dagger components in node hierarchy // search dagger components in node hierarchy

View file

@ -41,10 +41,7 @@ class DaggerMavericksViewModelFactory<VM : MavericksViewModel<S>, S : MavericksS
) : MavericksViewModelFactory<VM, S> { ) : MavericksViewModelFactory<VM, S> {
override fun create(viewModelContext: ViewModelContext, state: S): VM { override fun create(viewModelContext: ViewModelContext, state: S): VM {
val bindings: DaggerMavericksBindings = when (viewModelContext) { val bindings: DaggerMavericksBindings = viewModelContext.activity.bindings()
is FragmentViewModelContext -> viewModelContext.fragment.bindings()
else -> viewModelContext.activity.bindings()
}
val viewModelFactoryMap = bindings.viewModelFactories() val viewModelFactoryMap = bindings.viewModelFactories()
val viewModelFactory = viewModelFactoryMap[viewModelClass] ?: error("Cannot find ViewModelFactory for ${viewModelClass.name}.") val viewModelFactory = viewModelFactoryMap[viewModelClass] ?: error("Cannot find ViewModelFactory for ${viewModelClass.name}.")

View file

@ -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 <reified NODE : Node> Node.createNode(context: BuildContext, plugins: List<Plugin> = 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<NODE>
val node = castedNodeFactory?.create(context, plugins)
return node as NODE
}
interface NodeFactoriesBindings {
fun nodeFactories(): Map<Class<out Node>, AssistedNodeFactory<*>>
}

View file

@ -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<out Node>)