How to Get Preview in Composable Functions That Depend on a View Model

How to get preview in composable functions that depend on a view model?

This is exactly one of the reasons why the view model is passed with a default value. In the preview, you can pass a test object:

@Preview
@Composable
private fun HomeScreenPreview() {
val viewModel = HomeViewModel()
// setup viewModel as you need it to be in the preview
HomeScreen(viewModel = viewModel, navigateToDetailsAction = {}, openCardDetailsAction = {})
}

Since you have a repository, you can do the same thing you would do to test the view model.

  1. Create interface for CityRepository
interface CityRepositoryI {
val allCities: List<City>

suspend fun addCity(city: City)
suspend fun updateCity(city: City)
suspend fun deleteCity(city: City)
suspend fun getCityById(id: Int)
}

  1. Implement it for CityRepository:
@ViewModelScoped
class CityRepository @Inject constructor(appDatabase: AppDatabase) : CityRepositoryI {
private val dao by lazy { appDatabase.getCityDao() }

override val allCities by lazy { dao.getAllCities() }

override suspend fun addCity(city: City) = dao.insert(city)

override suspend fun updateCity(city: City) = dao.update(city)

override suspend fun deleteCity(city: City) = dao.delete(city)

override suspend fun getCityById(id: Int) = dao.getCityById(id)
}

  1. Create FakeCityRepository for testing purposes:
class FakeCityRepository : CityRepositoryI {
// predefined cities for testing
val cities = listOf(
City(1)
).toMutableStateList()

override val allCities by lazy { cities }

override suspend fun addCity(city: City) {
cities.add(city)
}

override suspend fun updateCity(city: City){
val index = cities.indexOfFirst { it.id == city.id }
cities[index] = city
}

override suspend fun deleteCity(city: City) {
cities.removeAll { it.id == city.id }
}

override suspend fun getCityById(id: Int) = cities.first { it.id == id }
}

So you can pass it into your view model: HomeViewModel(FakeCityRepository())

You can do the same with AppDatabase instead of a repository, it all depends on your needs. Check out more about Hilt testing

p.s. I'm not sure if this will build, since I don't have some of your classes, but you should have caught the idea.

How to make a @Preview in JetpackCompose, when the component depends of some data provide by ViewModel

I'm not sure of the depth of this application, but a potential idea would be to code to an interface and not an implementation.

That is, create an interface with all of the functions you need (that may already exist in your ViewModel), have your PokemonListViewModel implement it, and create another mock class that implements it as well. Pass the mock into your preview and leave the real implementation with PokemonListViewModel

interface PokeListViewModel {
...
// your other val's
val isLoading: Boolean
fun searchPokemonList(pokemon: String)
fun loadPokemonPaginated()
// your other functions
...
}

Once you create your interface you can simply update your composables to be expecting an object that "is a" PokeListViewModel, for example.

Hopefully this helps

How to save data in a composable function with view model constructor

It seems like I need to save the view model as a state or something?

You don't have to. ViewModels are already preserved as part of their owner scope. The same ViewModel instance will be returned to you if you retrieve the ViewModels correctly.

I seem to be missing something.

You are initializing a new instance of your NFTViewModel every time the navigation composable recomposes (gets called) instead of retrieving the NFTViewModel from its ViewModelStoreOwner.

You should retrieve ViewModels by calling viewModel() or if you are using Hilt and @HiltViewModel then call hiltViewModel() instead.

No Hilt

val vm: NFTViewModel = viewModel()

Returns an existing ViewModel or creates a new one in the given owner (usually, a fragment or an activity), defaulting to the owner provided by LocalViewModelStoreOwner.
The created ViewModel is associated with the given viewModelStoreOwner and will be retained as long as the owner is alive (e.g. if it is an activity, until it is finished or process is killed).

If using Hilt (i.e. your ViewModel has the @HiltViewModel annotation)

val vm: NFTViewModel = hiltViewModel()

Returns an existing @HiltViewModel-annotated ViewModel or creates a new one scoped to the current navigation graph present on the NavController back stack.
If no navigation graph is currently present then the current scope will be used, usually, a fragment or an activity.

The above will preserve your view model state, however you are still resetting the state inside your composable if your composable exits the composition and then re-enters it at a later time, which happens every time you navigate to a different screen (to a different "screen" composable, if it is just a dialog then the previous composable won't leave the composition, because it will be still displayed in the background).

Due to this part of the code

@Composable
fun HomeScreen(viewModel: NFTViewModel) {
val feedState by viewModel.nftResponse.observeAsState()

// this resets to false when HomeScreen leaves and later
// re-enters the composition
val fetched = remember { mutableStateOf(false) }

if (!fetched.value) {
fetched.value = true
viewModel.getFeed()
}

fetched will always be false when you navigate to (and back to) HomeScreen and thus getFeed() will be called.
If you don't want to call getFeed() when you navigate back to HomeScreen you have to store the fetched value somewhere else, probably inside your NFTViewModel and only reset it to false when you want that getFeed() is called again.

Jetpack compose preview crashes with hiltViewModel()

After some searching and reviewing of google sample codes here is the way you must follow to prevent the preview crashes:

Have a root composable for your screen and initial view model there, collect your data from viewModel as state (in case your data is flow) then pass data to content composable

@Composable
fun HomeScreen(
navController: NavController
){
val viewModel = hiltViewModel<HomeViewModel>()
val someData by viewModel.someData.collectAsState()
}
@Composable
fun HomeScreenContent(
someData: Type = defaultvalue
){
//your UI codes
}

in preview use, HomeScreenContent instead of HomeScreen, and your problem will be solved!

thanks to @PhilipDukhov that helped in the comments

Trying to get ViewModel in @Composable

The problem is you do not use State value in your view model. State is needed in order to register your value for Composable recomposition (in case the value is assigned/changed)

So You need to have View Model like this (I guess your fetchData is a suspend function):

   class DataViewModel: ViewModel() {

//the list of live data
val listDataState = MutableState<List<Data>> = mutableStateOf(emptyList<Data>())

//initialize the viewmodel
init {

viewModelScope.launch {
val data = fetchData()
listDataState.value = data

}
}

suspend fun fetchData() : List<Data>{
//something like:
return dataRepository.getData()
}

}

Then in your @Composable function you get the data like this:

val viewModel: DataViewModel = viewModel()
val data = viewModel.listDataState.value

Jetpack compose check if a function has been run in preview mode

I usually do this by calling LocalInspectionMode.current to know if I'm in preview mode. It returns true if in preview mode.

Here's what the documentation says about LocalInspectionMode:

Inspectable mode CompositionLocal. True if the composition is composed inside an Inspectable component.

Jetpack compose live preview

If your Preview never show after add @Preview annotation in JetnewsApp class. In my case after adding @Preview i close project and then open project again it's working fine for me in Macbook pro.



Related Topics



Leave a reply



Submit