Merge pull request #5420 from element-hq/feature/bma/metroAssistedInject

Ensure Metro `@AssistedInject` is used.
This commit is contained in:
Benoit Marty
2025-09-30 15:39:23 +02:00
committed by GitHub
14 changed files with 127 additions and 54 deletions

View File

@@ -38,6 +38,7 @@ dependencies {
implementation(projects.libraries.pushproviders.api)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.matrixui)
implementation(projects.libraries.uiCommon)
implementation(projects.libraries.uiStrings)
implementation(projects.features.login.api)

View File

@@ -36,10 +36,8 @@ import com.bumble.appyx.navmodel.backstack.operation.pop
import com.bumble.appyx.navmodel.backstack.operation.push
import com.bumble.appyx.navmodel.backstack.operation.replace
import com.bumble.appyx.navmodel.backstack.operation.singleTop
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import dev.zacsweers.metro.Inject
import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.annotations.ContributesNode
import io.element.android.appnav.loggedin.LoggedInNode
@@ -82,6 +80,7 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.verification.SessionVerificationServiceListener
import io.element.android.libraries.matrix.api.verification.VerificationRequest
import io.element.android.libraries.push.api.notifications.conversations.NotificationConversationService
import io.element.android.libraries.ui.common.nodes.emptyNode
import io.element.android.services.appnavstate.api.AppNavigationStateService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.first
@@ -282,7 +281,7 @@ class LoggedInFlowNode(
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) {
NavTarget.Placeholder -> createNode<PlaceholderNode>(buildContext)
NavTarget.Placeholder -> emptyNode(buildContext)
NavTarget.LoggedInPermanent -> {
val callback = object : LoggedInNode.Callback {
override fun navigateToNotificationTroubleshoot() {
@@ -549,13 +548,6 @@ class LoggedInFlowNode(
}
}
@ContributesNode(AppScope::class)
@Inject
class PlaceholderNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
) : Node(buildContext, plugins = plugins)
@Parcelize
private class AttachRoomOperation(
val roomTarget: LoggedInFlowNode.NavTarget.Room,

View File

@@ -11,15 +11,11 @@ import android.content.Intent
import android.os.Parcelable
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope
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.state.MutableSavedStateMap
import com.bumble.appyx.navmodel.backstack.BackStack
@@ -51,7 +47,6 @@ import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.waitForChildAttached
import io.element.android.libraries.core.uri.ensureProtocol
import io.element.android.libraries.deeplink.api.DeeplinkData
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.core.SessionId
@@ -61,6 +56,7 @@ import io.element.android.libraries.oidc.api.OidcAction
import io.element.android.libraries.oidc.api.OidcActionFlow
import io.element.android.libraries.sessionstorage.api.LoggedInState
import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.ui.common.nodes.emptyNode
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -219,9 +215,10 @@ import timber.log.Timber
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) {
is NavTarget.LoggedInFlow -> {
val matrixClient = matrixSessionCache.getOrNull(navTarget.sessionId) ?: return splashNode(buildContext).also {
Timber.w("Couldn't find any session, go through SplashScreen")
}
val matrixClient = matrixSessionCache.getOrNull(navTarget.sessionId)
?: return emptyNode(buildContext).also {
Timber.w("Couldn't find any session, go through SplashScreen")
}
val inputs = LoggedInAppScopeFlowNode.Inputs(matrixClient)
val callback = object : LoggedInAppScopeFlowNode.Callback {
override fun onOpenBugReport() {
@@ -252,7 +249,7 @@ import timber.log.Timber
)
).build()
}
NavTarget.SplashScreen -> splashNode(buildContext)
NavTarget.SplashScreen -> emptyNode(buildContext)
NavTarget.BugReport -> {
val callback = object : BugReportEntryPoint.Callback {
override fun onDone() {
@@ -289,12 +286,6 @@ import timber.log.Timber
}
}
private fun splashNode(buildContext: BuildContext) = node(buildContext) {
Box(modifier = it.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
}
suspend fun handleIntent(intent: Intent) {
val resolvedIntent = intentResolver.resolve(intent) ?: return
when (resolvedIntent) {

View File

@@ -34,6 +34,7 @@ dependencies {
implementation(projects.libraries.matrixui)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.preferences.api)
implementation(projects.libraries.uiCommon)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.testtags)
implementation(projects.features.analytics.api)

View File

@@ -8,10 +8,7 @@
package io.element.android.features.ftue.impl
import android.os.Parcelable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope
import com.bumble.appyx.core.modality.BuildContext
@@ -20,10 +17,8 @@ import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.newRoot
import com.bumble.appyx.navmodel.backstack.operation.replace
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import dev.zacsweers.metro.Inject
import io.element.android.annotations.ContributesNode
import io.element.android.features.analytics.api.AnalyticsEntryPoint
import io.element.android.features.ftue.impl.notifications.NotificationsOptInNode
@@ -35,8 +30,8 @@ import io.element.android.features.lockscreen.api.LockScreenEntryPoint
import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.ui.common.nodes.emptyNode
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -88,7 +83,7 @@ class FtueFlowNode(
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) {
NavTarget.Placeholder -> {
createNode<PlaceholderNode>(buildContext)
emptyNode(buildContext)
}
is NavTarget.SessionVerification -> {
val callback = object : FtueSessionVerificationFlowNode.Callback {
@@ -147,17 +142,3 @@ class FtueFlowNode(
BackstackView()
}
}
@ContributesNode(AppScope::class)
@Inject
class PlaceholderNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
) : Node(buildContext, plugins = plugins) {
@Composable
override fun View(modifier: Modifier) {
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
}
}

View File

@@ -39,6 +39,7 @@ dependencies {
implementation(projects.libraries.testtags)
implementation(projects.libraries.uiUtils)
implementation(projects.features.logout.api)
implementation(projects.libraries.uiCommon)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.sessionStorage.api)
implementation(projects.services.appnavstate.api)

View File

@@ -14,7 +14,6 @@ import androidx.lifecycle.lifecycleScope
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.node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.newRoot
@@ -30,6 +29,7 @@ import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.ui.common.nodes.emptyNode
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
@@ -42,7 +42,7 @@ class LockScreenSettingsFlowNode(
private val pinCodeManager: PinCodeManager,
) : BaseFlowNode<LockScreenSettingsFlowNode.NavTarget>(
backstack = BackStack(
initialElement = NavTarget.Unknown,
initialElement = NavTarget.Loading,
savedStateMap = buildContext.savedStateMap,
),
buildContext = buildContext,
@@ -50,7 +50,7 @@ class LockScreenSettingsFlowNode(
) {
sealed interface NavTarget : Parcelable {
@Parcelize
data object Unknown : NavTarget
data object Loading : NavTarget
@Parcelize
data object Unlock : NavTarget
@@ -94,6 +94,9 @@ class LockScreenSettingsFlowNode(
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) {
NavTarget.Loading -> {
emptyNode(buildContext)
}
NavTarget.Unlock -> {
val callback = object : PinUnlockNode.Callback {
override fun onUnlock() {
@@ -113,7 +116,6 @@ class LockScreenSettingsFlowNode(
}
createNode<LockScreenSettingsNode>(buildContext, plugins = listOf(callback))
}
NavTarget.Unknown -> node(buildContext) { }
}
}

View File

@@ -19,7 +19,7 @@ import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.push
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.features.space.api.SpaceEntryPoint
import io.element.android.features.space.impl.leave.LeaveSpaceNode
@@ -33,7 +33,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class)
@Inject
@AssistedInject
class SpaceFlowNode(
@Assisted val buildContext: BuildContext,
@Assisted plugins: List<Plugin>,

View File

@@ -17,7 +17,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import io.element.android.features.space.api.SpaceEntryPoint
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
@@ -35,7 +35,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlin.jvm.optionals.getOrNull
@Inject
@AssistedInject
class LeaveSpacePresenter(
@Assisted private val inputs: SpaceEntryPoint.Inputs,
matrixClient: MatrixClient,

View File

@@ -0,0 +1,19 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
plugins {
id("io.element.android-compose-library")
}
android {
namespace = "io.element.android.libraries.ui.common"
}
dependencies {
implementation(libs.appyx.core)
implementation(projects.libraries.designsystem)
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.ui.common.nodes
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.node
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
/**
* Ref: https://www.figma.com/design/0MMNu7cTOzLOlWb7ctTkv3/Element-X?node-id=1518-85323
*/
fun emptyNode(
buildContext: BuildContext,
): Node = node(buildContext) { modifier ->
EmptyView(modifier)
}
@Composable
private fun EmptyView(
modifier: Modifier = Modifier,
) = Box(
modifier = modifier
.fillMaxSize()
.background(ElementTheme.colors.bgCanvasDefault),
)
@PreviewsDayNight
@Composable
internal fun EmptyViewPreview() = ElementPreview {
EmptyView(Modifier)
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.tests.konsist
import com.lemonappdev.konsist.api.Konsist
import com.lemonappdev.konsist.api.ext.list.withAnnotationOf
import com.lemonappdev.konsist.api.ext.list.withParameter
import com.lemonappdev.konsist.api.verify.assertTrue
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.Inject
import org.junit.Test
class KonsistDiTest {
@Test
fun `class annotated with @Inject should not have constructors with @Assisted parameter`() {
Konsist
.scopeFromProject()
.classes()
.withAnnotationOf(Inject::class)
.assertTrue(
additionalMessage = "Class with @Assisted parameter in constructor should be annotated with @AssistedInject and not @Inject"
) { classDeclaration ->
classDeclaration.constructors
.withParameter { parameterDeclaration ->
parameterDeclaration.hasAnnotationOf(Assisted::class)
}
.isEmpty()
}
}
}