Is there any problem (or is it considered bad form...
# compose
b
Is there any problem (or is it considered bad form) to define a
@Composable
in a ViewModel? I have a block of code that produces a
State
(by calling some methods on my ViewModel) … I was thinking of extracting the block into a method in the ViewModel itself, but that would mean I have a composable in the ViewModel.
a
State is just a data class, which @Composable do you need?
m
I recommend to not put View related stuffs on ViewModels, the ViewModel should deliver data/states and the View should reacts to theses states and render them.....
2
b
Well, for example the JetNews sample app does this in its ArticleScreen:
Copy code
val (post) = produceUiState(postsRepository, postId) {
        getPost(postId)
    }
I’m doing something similar, though with a viewmodel (which has a Repo injected), and a few more calls …
Copy code
val (post) = produceUiState(viewModel, postId) {
        
        // ... call some other viewModel methods as well
        getPost(postId)
    }
I was thinking of extracting it to the viewModel itself so my “Screen” Composable would be:
Copy code
val (post) = viewModel.loadPost(postId)
and My ViewModel would contain:
Copy code
@Composable
fun loadPost(postId: Int): UiState<Post> {
    return produceUiState(postId) {
        // ... call some other viewModel methods as well
        getPost(postId)
    }
}
Not exact, but gives an idea of what I’m thinking …
a
Ohhhh, interesting find though. I believe they made that function so they can manipulate state without needing a ViewModel.
With ViewModel you don’t need any of that stuff, just have a LiveData or StateFlow in ViewModel and let the Screen observe from the ViewModel
then observe with
val state by viewModel.state.observeAsState()
m
well @Bradleycorn the Compose samples are not so well architecture, I mean they don't aim to be a sample of good architecture, they serve better as a tech showcase.
b
oh yeah, @allan.conda I guess that’s right. Ok, so that makes sense But, the theoretical question still stands. Given a composable function that doesn’t do any view rendering, but just produces state for some other composable to consume, is it safe to put that state producer composable in a ViewModel?
I guess 99% of the time if it “just produces State”, it probably doesn’t need to be @Composable
a
Theoretically, you could put your Kotlin code in any kt file. Patterns like MVVM is just a way to organize your code.
b
@allan.conda yes. Putting code into a Android ViewModel deserves a bit of special consideration because it’s tied to a Lifecycle, but your point is taken.
ok, to put a bow on this … example ViewModel method that produces state:
Copy code
fun loadPost(postId: Long, composableScope: CoroutineScope): State<UiState<BlogPost>> {
        val state = mutableStateOf(UiState<BlogPost>(loading = true))
        
        //Use a passed in Coroutine Scope. 
        //We don't want to use the viewModelScope here because the ViewMoel
        //is scoped to the Activity, and we want this coroutine to be scoped
        //to the composable that called this method.
        composableScope.launch {
            try {
                // get a Flow that emits whenever the post changes in the data store.
                ksrRepo.getPost(postId).distinctUntilChanged().collect {post ->
                    Log.d("STATE_TEST", "Post Emitted: $postId")
                    state.value = state.value.copy(loading = false, data = post)
                }
            } catch (ex: Exception) {
                state.value = state.value.copy(loading = false, exception = ex)
            }
        }
        return state
    }
And then, a “Screen” composable that uses it:
Copy code
@Composable
fun PostScreen(viewModel: MainViewModel, postId: Long) {
    // coroutine scope that will cancel when this composable is removed 
    val postScreenScope = rememberCoroutineScope() 
    val post by remember(postId)  { viewModel.loadPost(postId, postScreenScope) }
    

    when {
        post.loading -> Text("Loading Post .... ")
        post.hasError -> Text("Uh Oh!")
        post.data != null -> PostContent(post.data as BlogPost)
    }
}
a
seems roughly analogous to the
produceState
compose api in intent.
produceState
will be better behaved around launching and cancellation than the formulation above though
the example above will not cancel old jobs when
postId
changes, but
produceState
will
b
@Adam Powell Yep … and that’s sort of where I was going with my original question.
produceState
seems to be A.) a bit more robust, and B.) already written, so I don’t have to write code like the above all the time. But
produceState
is
@Composable
, so if you want to use it IN the ViewModel, then the ViewModel method that uses it needs to be
@Composable
. Is that acceptable? Cause it feels like code smell…
a
you can do something like:
Copy code
val myState by produceState(initial, viewModel) {
  value = viewModel.someSuspendingMethod()
}
from inside your composable as the bridge
b
yeah sure.
a
That keeps
@Composable
things a layer away from the VM