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
@@ -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()
) )
@@ -8,10 +8,10 @@ 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 {
fun create(@ApplicationContext @BindsInstance context: Context): AppComponent fun create(@ApplicationContext @BindsInstance context: Context): AppComponent
} }
} }
@@ -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
@@ -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,
@@ -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) {
@@ -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")
@@ -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<*>
}
@@ -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
@@ -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
-1
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)
} }
@@ -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
}
@@ -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
@@ -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}.")
@@ -57,4 +54,4 @@ class DaggerMavericksViewModelFactory<VM : MavericksViewModel<S>, S : MavericksS
interface DaggerMavericksBindings { interface DaggerMavericksBindings {
fun viewModelFactories(): Map<Class<out MavericksViewModel<*>>, AssistedViewModelFactory<*, *>> fun viewModelFactories(): Map<Class<out MavericksViewModel<*>>, AssistedViewModelFactory<*, *>>
} }
@@ -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<*>>
}
@@ -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>)