musso
musso

Reputation: 731

ViewModels creation is not supported in Preview

I followed the official guide to create viewModel instance and it works perfectly. However, when there is any viewModel in the @composable, Android Studio isn't able to render the preview and with the error code ViewModels creation is not supported in Preview. Anyone got any solution?

P.S. using compose version 1.0.0-alpha06

Upvotes: 66

Views: 27817

Answers (8)

Praveen Rao V P
Praveen Rao V P

Reputation: 11

Instead of passing the view model, which won't instantiate in previews, pass whatever value you want to display into the composable as parameters. That way it is easy to get the previews. (sry for bad english)

For example, instead of doing

@Composable
fun TestView(
    viewModel: TestViewModel
) {
    Text(text = viewModel.customText)
    Button(onClick = { viewModel.doSomething() }) {
        Text("Something")
    }
}

@Preview
@Composable
fun TestViewPreview() {
    TestView(viewModel = hiltViewModel())
}

you can do

TestView.kt

@Composable
fun TestView(
    customText: String,
    event: (String) -> Unit
) {
    Text(text = customText)
    Button(onClick = { event(TestEvent.SomeEvent) }) {
        Text("Something")
    }
}

TestEvent.kt

sealed class TestEvent {
    object SomeEvent : TestEvent()
}

TestViewModel.kt

@HiltViewModel
class TestViewModel @Inject constructor(
    ... 
) : ViewModel() {
    val customText = // logic to get the text
    
    fun onEvent(event: TestEvent) {
        when(event) {
            TestEvent.SomeEvent -> doSomethng()
        }
    }

    fun doSomething() {
        ...
    }
}

and at the place where you are calling the TestView composable (possibly at the navigation), you could instantiate the view model and pass the values.

@Composable
fun NavGraph(...) {
    ...


    composable(route = "test") {
        val viewModel: TestViewModel = hiltViewModel() 
        TestView(
            customText = viewModel.customText,
            event = viewModel::onEvent
        )
    }

    ...
}

Upvotes: 1

FOOKTHISOFFICIAL
FOOKTHISOFFICIAL

Reputation: 11

It's because some features aren't supported with compose preview. like if you're using Hilt. Maybe it works with Koin i'm not sure. If anyone's got the problem you can refer to official android developer document Compose Preview and ViewModel to see some solution.

Upvotes: 0

heuberg_industries
heuberg_industries

Reputation: 79

Since LiveEdit is now available, you don't need to use Compose Preview for a whole screen with ViewModel anymore. Just debug the app with Live Edit. This is also how it's done in Flutter with "hot reload". In Flutter there isn't even something like Compose Preview.

I would recommend using Compose Preview only for smaller composables that don't have a ViewModel.

So you don't have to write all this boilerplate code with extending the ViewModel or passing primitives from the ViewModel into a sub composable.

Keep in mind that one requirement to use LiveEdit, is to use AndroidStudio Flamingo or later.

Upvotes: 7

L.Grillo
L.Grillo

Reputation: 981

The @Preview annotated composable class should be agnostic from viewmodel. i use this solution adding an extra class with models as input

@Composable
fun TabScreen(
    viewModel: DetailsViewModel = viewModel()
) {
    val details = viewModel.details.observeAsState()

    Tabs(
        details.value
    )
}

@Composable
fun Tabs(
    details: Details?
) {
    val pagerState = rememberPagerState()
    val coroutineScope = rememberCoroutineScope()
    
}


@Composable
@Preview(showBackground = true)
fun TabsPreview(
    @PreviewParameter(DetailsPreview::class)
    details: Details?
) {
    AppTheme {
        Tabs(details)
    }
}

Upvotes: 2

user3439901
user3439901

Reputation: 1

what I am using:

  @Preview(showBackground = true)
    @Composable
    fun DefaultPreview() {
         MyMVIApp1Theme {
           val myViewModel = remember { AppViewModel() }
           Greeting(viewModel = myViewModel,"Android")
         }
    }

Upvotes: -4

blacktiago
blacktiago

Reputation: 393

You could use interfaces and hilt.

interface IMyViewModel {

    fun getTextA() : String

}

@HiltViewModel
class MyViewModel() : ViewModel(), IMyViewModel {
    fun getTextA() : String {
        //do some cool stuff
    }
}

class MyViewModelPreview() : IMyViewModel {
    fun getTextA() : String {
        //do some mock stuff
    }
}

@Composable
fun MyScreen(myVm = hiltViewModel<MyViewModel>()) {
    Text(text = myVm.getTextA())
}


@Preview()
@Composable
fun MyScreenPreview() {
    MyScreen(myVm = MyViewModelPreview())
}

In this point MyViewModel is an implementation of IMyViewModel annotated with @HiltViewModel and hiltViewModel make all the required wired for you, In preview you could use any other simple mock implementation.

If you need to provide some dependency to your view model use injected constructor with dagger injection(already supported by hilt). Obviously this dependency should be paced on your actual viewmodel and your preview implementations need to be just a wrapper class with no other dependency since they function is just satisfy arguments

@HiltViewModel
class MyViewModel @Inject constructor(
    private val myDependencyRepositoryOne: MyDependencyRepositoryOne,
    private val myDependencyRepositoryTwo: MyDependencyRepositoryTwo)
: ViewModel(), IMyViewModel {
    fun getTextA() : String {
        //do some cool stuff
    }
}

Here is another useful resource related to viewmodel injection in compose

Upvotes: 0

Blindsurfer
Blindsurfer

Reputation: 403

I had exactly the same problem. The solution was: Extend the ViewModel with an interface

ComposeView:

@Composable
fun MyScreen(myVm: IMyViewModel = MyViewModel()) {
    Text(text = myVm.getTextA())
}


@Preview()
@Composable
fun MyScreenPreview() {
    MyScreen(myVm = MyViewModelPreview())
}

ViewModel:

abstract class IMyViewModel : ViewModel(){
    abstract val dynamicValue: StateFlow<String>
    abstract fun getTextA() : String
}

class MyViewModel : IMyViewModel() {
    private val _dynamicValue: MutableStateFlow<String> = MutableStateFlow("")
    override val dynamicValue: StateFlow<String> = _dynamicValue

    init {
    }

    override fun getTextA(): String {
        return "Details: ${EntityDb.getAllEntities().lastOrNull()?.details}"
    }
}

class MyViewModelPreview(override val dynamicValue: StateFlow<String> =    MutableStateFlow("no data")) : IMyViewModel() {
    override fun getTextA(): String {
        return ""
    }
}

Upvotes: 15

C. Hellmann
C. Hellmann

Reputation: 644

You could use an approach that looks like this which will show up in the recommended video bellow:

@Composable
fun TestView(
    action: MainActions,
    viewModel: OnboardViewModel = getViewModel()
) {
    TestUI(onClick = viewModel.clickMethod())
}

@Composable
fun TestUI(onClick: () -> Unit) {}

@Preview
@Composable
fun TestUIPreview() {
    MaterialTheme() {
        TestUI(onClick = {})
    }
}

There is a recommendation from google in this video at the selected time: https://youtu.be/0z_dwBGQQWQ?t=573

Upvotes: 19

Related Questions