Skip to content

Commit

Permalink
chore: simplify passing arguments to scoped ViewModels (#2023)
Browse files Browse the repository at this point in the history
Co-authored-by: Jakub Żerko <iot.zerko@gmail.com>
  • Loading branch information
saleniuk and Garzas committed Aug 3, 2023
1 parent 5d8ad2e commit 6b26f04
Show file tree
Hide file tree
Showing 12 changed files with 235 additions and 67 deletions.
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ dependencies {
// smaller view models
implementation(libs.resaca.core)
implementation(libs.resaca.hilt)
implementation(libs.bundlizer.core)

// firebase
implementation(platform(libs.firebase.bom))
Expand Down
71 changes: 71 additions & 0 deletions app/src/main/kotlin/com/wire/android/di/ViewModelScoped.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Wire
* Copyright (C) 2023 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
package com.wire.android.di

import android.os.Bundle
import androidx.compose.runtime.Composable
import androidx.core.os.bundleOf
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import com.sebaslogen.resaca.hilt.hiltViewModelScoped
import dev.ahmedmourad.bundlizer.Bundlizer
import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.serializer
import kotlin.reflect.KClass

/**
* Returns a proper scoped arguments instance from the given [SavedStateHandle] for the scoped [ViewModel].
*/
inline fun <reified R : ScopedArgs> SavedStateHandle.scopedArgs(): R =
scopedArgs(R::class, this)

/**
* Returns a proper scoped arguments instance from the given [SavedStateHandle].
*
* @param argsClass the class of the arguments, must implement [ScopedArgs] and be serializable
* @param argsContainer the [SavedStateHandle] to get the arguments from
*/
@OptIn(InternalSerializationApi::class)
fun <R : ScopedArgs> scopedArgs(argsClass: KClass<R>, argsContainer: SavedStateHandle): R =
Bundlizer.unbundle(argsClass.serializer(), argsContainer.toBundle())

/**
* Custom implementation of [hiltViewModelScoped] that takes proper scoped serializable arguments that implement [ScopedArgs]
* and provides them into scoped [ViewModel] converting it automatically to [Bundle] using [Bundlizer].
* Proper key will be taken from the [ScopedArgs.key] property.
*
* @param arguments The arguments that will be provided to the [ViewModel], must implement [ScopedArgs] and be serializable
*/
@OptIn(InternalSerializationApi::class)
@Composable
inline fun <reified T : ViewModel, reified R : ScopedArgs> hiltViewModelScoped(arguments: R): T =
hiltViewModelScoped(key = arguments.key, defaultArguments = Bundlizer.bundle(R::class.serializer(), arguments))

/**
* Creates a [Bundle] with all key-values from the given [SavedStateHandle].
*/
@Suppress("SpreadOperator")
fun SavedStateHandle.toBundle(): Bundle = bundleOf(*(keys().map { it to get<Any>(it) }.toTypedArray()))

/**
* Interface for arguments for scoped ViewModels.
* It is used to provide a unique key for the scoped ViewModel.
*/
interface ScopedArgs {
val key: Any?
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,13 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.core.os.bundleOf
import com.sebaslogen.resaca.hilt.hiltViewModelScoped
import com.wire.android.R
import com.wire.android.di.hiltViewModelScoped
import com.wire.android.model.ActionableState
import com.wire.android.model.ClickBlockParams
import com.wire.android.navigation.EXTRA_USER_ID
import com.wire.android.navigation.EXTRA_USER_NAME
import com.wire.android.ui.common.button.WireButtonState
import com.wire.android.ui.common.button.WirePrimaryButton
import com.wire.android.ui.common.button.WireSecondaryButton
Expand All @@ -55,21 +51,11 @@ fun ConnectionActionButton(
userName: String,
connectionStatus: ConnectionState,
onConnectionRequestIgnored: (String) -> Unit = {},
onOpenConversation: (ConversationId) -> Unit = {}
onOpenConversation: (ConversationId) -> Unit = {},
viewModel: ConnectionActionButtonViewModel =
hiltViewModelScoped<ConnectionActionButtonViewModelImpl, ConnectionActionButtonArgs>(ConnectionActionButtonArgs(userId, userName))
.also { LocalSnackbarHostState.current.collectAndShowSnackbar(snackbarFlow = it.infoMessage) }
) {
val viewModel: ConnectionActionButtonViewModel = if (LocalInspectionMode.current) {
ConnectionActionButtonPreviewModel(ActionableState())
} else {
hiltViewModelScoped<ConnectionActionButtonViewModelImpl>(
key = "${ConnectionActionButtonViewModelImpl.ARGS_KEY}$userId",
defaultArguments = bundleOf(
EXTRA_USER_ID to userId.toString(),
EXTRA_USER_NAME to userName
)
).also {
LocalSnackbarHostState.current.collectAndShowSnackbar(snackbarFlow = it.infoMessage)
}
}
val unblockUserDialogState = rememberVisibilityState<UnblockUserDialogState>()

UnblockUserDialogContent(
Expand Down Expand Up @@ -185,7 +171,8 @@ fun PreviewOtherUserConnectionActionButtonPending() {
ConnectionActionButton(
userId = UserId("value", "domain"),
userName = "Username",
connectionStatus = ConnectionState.PENDING
connectionStatus = ConnectionState.PENDING,
viewModel = ConnectionActionButtonPreviewModel(ActionableState())
)
}

Expand All @@ -195,7 +182,8 @@ fun PreviewOtherUserConnectionActionButtonNotConnected() {
ConnectionActionButton(
userId = UserId("value", "domain"),
userName = "Username",
connectionStatus = ConnectionState.NOT_CONNECTED
connectionStatus = ConnectionState.NOT_CONNECTED,
viewModel = ConnectionActionButtonPreviewModel(ActionableState())
)
}

Expand All @@ -205,7 +193,8 @@ fun PreviewOtherUserConnectionActionButtonBlocked() {
ConnectionActionButton(
userId = UserId("value", "domain"),
userName = "Username",
connectionStatus = ConnectionState.BLOCKED
connectionStatus = ConnectionState.BLOCKED,
viewModel = ConnectionActionButtonPreviewModel(ActionableState())
)
}

Expand All @@ -215,7 +204,8 @@ fun PreviewOtherUserConnectionActionButtonCanceled() {
ConnectionActionButton(
userId = UserId("value", "domain"),
userName = "Username",
connectionStatus = ConnectionState.CANCELLED
connectionStatus = ConnectionState.CANCELLED,
viewModel = ConnectionActionButtonPreviewModel(ActionableState())
)
}

Expand All @@ -225,7 +215,8 @@ fun PreviewOtherUserConnectionActionButtonAccepted() {
ConnectionActionButton(
userId = UserId("value", "domain"),
userName = "Username",
connectionStatus = ConnectionState.ACCEPTED
connectionStatus = ConnectionState.ACCEPTED,
viewModel = ConnectionActionButtonPreviewModel(ActionableState())
)
}

Expand All @@ -235,6 +226,7 @@ fun PreviewOtherUserConnectionActionButtonSent() {
ConnectionActionButton(
userId = UserId("value", "domain"),
userName = "Username",
connectionStatus = ConnectionState.SENT
connectionStatus = ConnectionState.SENT,
viewModel = ConnectionActionButtonPreviewModel(ActionableState())
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Wire
* Copyright (C) 2023 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
package com.wire.android.ui.connection

import com.wire.android.di.ScopedArgs
import com.wire.kalium.logic.data.user.UserId
import kotlinx.serialization.Serializable

@Serializable
data class ConnectionActionButtonArgs(
val userId: UserId,
val userName: String
) : ScopedArgs {
override val key = "$ARGS_KEY:$userId"

companion object {
const val ARGS_KEY = "ConnectionActionButtonArgsKey"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,14 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wire.android.R
import com.wire.android.appLogger
import com.wire.android.di.scopedArgs
import com.wire.android.model.ActionableState
import com.wire.android.model.finishAction
import com.wire.android.model.performAction
import com.wire.android.navigation.EXTRA_USER_ID
import com.wire.android.navigation.EXTRA_USER_NAME
import com.wire.android.util.dispatchers.DispatcherProvider
import com.wire.android.util.ui.UIText
import com.wire.kalium.logic.data.id.ConversationId
import com.wire.kalium.logic.data.id.QualifiedID
import com.wire.kalium.logic.data.id.QualifiedIdMapper
import com.wire.kalium.logic.data.id.toQualifiedID
import com.wire.kalium.logic.feature.connection.AcceptConnectionRequestUseCase
import com.wire.kalium.logic.feature.connection.AcceptConnectionRequestUseCaseResult
import com.wire.kalium.logic.feature.connection.CancelConnectionRequestUseCase
Expand Down Expand Up @@ -77,12 +74,12 @@ class ConnectionActionButtonViewModelImpl @Inject constructor(
private val ignoreConnectionRequest: IgnoreConnectionRequestUseCase,
private val unblockUser: UnblockUserUseCase,
private val getOrCreateOneToOneConversation: GetOrCreateOneToOneConversationUseCase,
savedStateHandle: SavedStateHandle,
qualifiedIdMapper: QualifiedIdMapper
savedStateHandle: SavedStateHandle
) : ConnectionActionButtonViewModel, ViewModel() {

private val userId: QualifiedID = savedStateHandle.get<String>(EXTRA_USER_ID)!!.toQualifiedID(qualifiedIdMapper)
val userName: String = savedStateHandle.get<String>(EXTRA_USER_NAME)!!
private val args: ConnectionActionButtonArgs = savedStateHandle.scopedArgs()
private val userId: QualifiedID = args.userId
val userName: String = args.userName

private var state: ActionableState by mutableStateOf(ActionableState())

Expand Down Expand Up @@ -194,10 +191,6 @@ class ConnectionActionButtonViewModelImpl @Inject constructor(
}
}
}

companion object {
const val ARGS_KEY = "ConnectionActionButtonViewModelKey"
}
}

@Suppress("EmptyFunctionBlock")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ import androidx.compose.runtime.setValue
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wire.android.navigation.EXTRA_MESSAGE_ID
import com.wire.android.di.scopedArgs
import com.wire.android.ui.home.conversations.model.CompositeMessageArgs
import com.wire.android.ui.navArgs
import com.wire.kalium.logic.data.id.MessageButtonId
import com.wire.kalium.logic.data.id.QualifiedID
Expand All @@ -42,7 +43,8 @@ class CompositeMessageViewModel @Inject constructor(
private val conversationNavArgs: ConversationNavArgs = savedStateHandle.navArgs()
val conversationId: QualifiedID = conversationNavArgs.conversationId

private val messageId: String = savedStateHandle.get<String>(EXTRA_MESSAGE_ID)!!
private val scopedArgs: CompositeMessageArgs = savedStateHandle.scopedArgs()
private val messageId: String = scopedArgs.messageId

var pendingButtonId: MessageButtonId? by mutableStateOf(null)
@VisibleForTesting
Expand All @@ -58,8 +60,4 @@ class CompositeMessageViewModel @Inject constructor(
pendingButtonId = null
}
}

companion object {
const val ARGS_KEY = "CompositeMessageViewModelKey"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Wire
* Copyright (C) 2023 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
package com.wire.android.ui.home.conversations.model

import com.wire.android.di.ScopedArgs
import kotlinx.serialization.Serializable

@Serializable
data class CompositeMessageArgs(
val messageId: String
) : ScopedArgs {
override val key = "$ARGS_KEY:$messageId"

companion object {
const val ARGS_KEY = "CompositeMessageArgsKey"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.core.os.bundleOf
import com.sebaslogen.resaca.hilt.hiltViewModelScoped
import com.wire.android.di.hiltViewModelScoped
import com.wire.android.model.Clickable
import com.wire.android.model.ImageAsset
import com.wire.android.navigation.EXTRA_MESSAGE_ID
import com.wire.android.ui.common.button.WireButtonState
import com.wire.android.ui.common.button.WireSecondaryButton
import com.wire.android.ui.common.dimensions
Expand Down Expand Up @@ -118,13 +116,8 @@ internal fun MessageBody(
fun MessageButtonsContent(
messageId: String,
buttonList: List<MessageButton>,
viewModel: CompositeMessageViewModel = hiltViewModelScoped(CompositeMessageArgs(messageId))
) {
val viewModel = hiltViewModelScoped<CompositeMessageViewModel>(
key = "${CompositeMessageViewModel.ARGS_KEY}$messageId",
defaultArguments = bundleOf(
EXTRA_MESSAGE_ID to messageId,
)
)
Column(
modifier = Modifier
.wrapContentSize()
Expand Down

0 comments on commit 6b26f04

Please sign in to comment.