refactor: hoist state out of UserProfile

Solves the issue of every pop of the backstack causing data to be re-fetched,
but now has the issue of the data being stale for a few frames. Still better
than the current state, so I'll take it.
This commit is contained in:
Harsh Shandilya 2025-05-26 19:02:37 +05:30
parent 5d65d1ea51
commit 0d3c08c10a
5 changed files with 76 additions and 34 deletions

View file

@ -222,7 +222,6 @@ fun LobstersPostsScreen(
) { dest ->
UserProfile(
username = dest.username,
getProfile = viewModel::getUserProfile,
contentPadding = contentPadding,
openUserProfile = { clawBackStack.add(User(it)) },
)

View file

@ -161,17 +161,6 @@ constructor(
suspend fun getLinkMetadata(url: String) =
withContext(ioDispatcher) { linkMetadataRepository.getLinkMetadata(url) }
suspend fun getUserProfile(username: String) =
withContext(ioDispatcher) {
when (val result = api.getUser(username)) {
is Success -> result.value
is Failure.NetworkFailure -> throw result.error
is Failure.UnknownFailure -> throw result.error
is Failure.HttpFailure -> throw result.toError()
is Failure.ApiFailure -> throw IOException("API returned an invalid response")
}
}
suspend fun importPosts(input: InputStream) = dataTransferRepository.importPosts(input)
suspend fun exportPostsAsJson(output: OutputStream) =

View file

@ -25,6 +25,8 @@ android {
namespace = "dev.msfjarvis.claw.common"
}
whetstone.addOns.compose = true
androidComponents { beforeVariants { (it as HasUnitTestBuilder).enableUnitTest = false } }
anvil { generateDaggerFactories.set(true) }
@ -40,6 +42,7 @@ dependencies {
api(libs.androidx.compose.ui)
api(libs.dagger)
api(libs.javax.inject)
api(projects.api)
api(projects.core)
api(projects.database.core)
api(projects.model)

View file

@ -20,17 +20,14 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.unit.dp
import com.github.michaelbull.result.coroutines.runSuspendCatching
import com.github.michaelbull.result.fold
import dev.msfjarvis.claw.common.NetworkState
import com.deliveryhero.whetstone.compose.injectedViewModel
import dev.msfjarvis.claw.common.NetworkState.Error
import dev.msfjarvis.claw.common.NetworkState.Loading
import dev.msfjarvis.claw.common.NetworkState.Success
@ -44,35 +41,25 @@ import dev.msfjarvis.claw.model.User
@Composable
fun UserProfile(
username: String,
getProfile: suspend (username: String) -> User,
openUserProfile: (String) -> Unit,
contentPadding: PaddingValues,
openUserProfile: (String) -> Unit,
modifier: Modifier = Modifier,
viewModel: UserProfileViewModel = injectedViewModel(),
) {
val user by
produceState<NetworkState>(Loading) {
runSuspendCatching { getProfile(username) }
.fold(
success = { profile -> value = Success(profile) },
failure = {
value = Error(error = it, description = "Failed to load profile for $username")
},
)
}
when (user) {
LaunchedEffect(username) { viewModel.loadProfile(username) }
when (val state = viewModel.userProfile) {
is Success<*> -> {
UserProfileInternal(
user = (user as Success<User>).data,
user = (state as Success<User>).data,
openUserProfile = openUserProfile,
modifier = modifier.padding(contentPadding),
)
}
is Error -> {
val error = user as Error
Box(modifier = Modifier.padding(contentPadding).fillMaxSize()) {
NetworkError(
label = error.description,
error = error.error,
label = state.description,
error = state.error,
modifier = Modifier.align(Alignment.Center),
)
}

View file

@ -0,0 +1,64 @@
/*
* Copyright © Harsh Shandilya.
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
package dev.msfjarvis.claw.common.user
import android.app.Application
import android.content.Context
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.AndroidViewModel
import com.deliveryhero.whetstone.app.ApplicationScope
import com.deliveryhero.whetstone.viewmodel.ContributesViewModel
import com.github.michaelbull.result.coroutines.runSuspendCatching
import com.github.michaelbull.result.fold
import com.slack.eithernet.ApiResult
import com.slack.eithernet.ApiResult.Failure
import com.squareup.anvil.annotations.optional.ForScope
import dev.msfjarvis.claw.api.LobstersApi
import dev.msfjarvis.claw.api.toError
import dev.msfjarvis.claw.common.NetworkState
import dev.msfjarvis.claw.common.NetworkState.Error
import dev.msfjarvis.claw.common.NetworkState.Loading
import dev.msfjarvis.claw.common.NetworkState.Success
import dev.msfjarvis.claw.core.injection.IODispatcher
import dev.msfjarvis.claw.model.User
import java.io.IOException
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
@ContributesViewModel
class UserProfileViewModel
@Inject
constructor(
private val api: LobstersApi,
@IODispatcher private val ioDispatcher: CoroutineDispatcher,
@ForScope(ApplicationScope::class) context: Context,
) : AndroidViewModel(context as Application) {
var userProfile by mutableStateOf<NetworkState>(Loading)
suspend fun loadProfile(username: String) {
userProfile =
runSuspendCatching<User> {
withContext(ioDispatcher) {
when (val result = api.getUser(username)) {
is ApiResult.Success -> result.value
is Failure.NetworkFailure -> throw result.error
is Failure.UnknownFailure -> throw result.error
is Failure.HttpFailure -> throw result.toError()
is Failure.ApiFailure -> throw IOException("API returned an invalid response")
}
}
}
.fold(
success = { profile -> Success(profile) },
failure = { Error(error = it, description = "Failed to load profile for $username") },
)
}
}