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
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
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 ""
}
}
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)
}
}
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
what I am using:
#Preview(showBackground = true)
#Composable
fun DefaultPreview() {
MyMVIApp1Theme {
val myViewModel = remember { AppViewModel() }
Greeting(viewModel = myViewModel,"Android")
}
}
Related
Folks I am stuck engineering a proper solution to access a viewModel scoped to a nav graph , from a button that exists in the TopAppBar in a compose application
Scaffold{
TopAppBar-> Contains the Save Button
Body->
BioDataGraph() -> Contains 5 screens to gather biodata information , and a viewmodel scoped to the graph
}
}
My BioDataViewModel looks like this
class BioDataViewModel{
fun gatherPersonalInformation()
fun gatherPhotos()
...
fun onSaveEverything()
}
The issue i came across is as i described above , how should i go about access the BioDataViewModel , such that i can invoke onSaveEverything when save is clicked in the TopAppBar.
What I have tried
private val performSave by mutableStateOf(false)
Scaffold(
topBar = {
TopAppBar(currentDestination){
//save is clicked.
performSave = true
}
})
{
NavHost(
navController = navController,
startDestination = homeNavigationRoute,
modifier = Modifier
.padding(padding)
.consumedWindowInsets(padding),
) {
composable(route = bioDataRoute) {
val viewModel = hiltViewModel<BioDataViewModel>()
if (performSave){
viewModel.onSaveEverything()
}
BioDataScreen(
viewModel
)
}
}
}
The problem with the approach above is that how and when should i reset the state of performSave ? . Because if i do not set it to false; on every recomposition onSaveEverything would get called.
What would be the best way to engineer a solution for this ? . I checked to see if a similar situation was tackled in jetpack samples , but i found nothing there .
I'm not sure if I understand you correctly, but you can define the BioDataViewModel in activity level, and you can access it in the TopAppBar like this
class MyActivity: ComponentActivity() {
// BioDataViewModel definition here
private val viewModel: BioDataViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Scaffold(
topBar = {
TopAppBar(currentDestination) {
//save is clicked.
viewModel.onSaveEverything() // call onSaveEverything here
}
})
{
...
...
}
...
...
Edit:
If you want to have the same instance of ViewModel from activity and NavGraph level, you can consider this, a reference from my other answer.
You can define the ViewModelStoreOwner in the navigation graph level.
NavHost(
navController = navController,
startDestination = homeNavigationRoute,
modifier = Modifier
.padding(padding)
.consumedWindowInsets(padding),
) {
val viewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
"LocalViewModelStoreOwner not available"
}
composable(route = bioDataRoute) {
val viewModel = hiltViewModel<BioDataViewModel>(viewModelStoreOwner)
if (performSave){
viewModel.onSaveEverything()
}
BioDataScreen(
viewModel
)
}
}
Hello developers i have a problem i calling an api from viewmodel but is a coroutine and i do not how to wait to api response to make login .... i have this method in viewmodel:
fun createUser(context: Context){
viewModelScope.launch {
val jsonObject= JSONObject()
jsonObject.put("correo", register.value.correo)
jsonObject.put("password", register.value.password)
jsonObject.put("nombre", register.value.nombre)
jsonObject.put("apellido", register.value.apellido)
jsonObject.put("direccion",register.value.direccion)
jsonObject.put("telefono", register.value.telefono)
var bodyrequest=jsonObject.toString().toRequestBody()
val result: RegisterResponse = registerService.register(bodyrequest)
response.value=result
Log.d("response",result.message)
}
}
and this is onClick event from button in a #Composable function:
onClick = {
if (vm.validateCredentials()=="ok"){
vm.createUser(context)
Log.d("result from compose","${vm.response.value.message}")
navController.navigate(Screen.login.route){
popUpTo(Screen.login.route){
inclusive=true
}
}
}else{
showPopUp = !showPopUp
}
}
The big problem is that when the navigation is triggered, the coroutine has not finished yet and therefore I cannot find out if the api response was successful or not. How could I do this?
You can let createUser return the Job, and then join it in onClick. Like this:
fun createUser(context: Context) = viewModelScope.launch {...}
...
onClick = {
scope.launch {
viewModel.createUser(context).join()
navController.navigate(Screen.login.route){
popUpTo(Screen.login.route){
inclusive=true
}
}
}
}
But this makes the code difficult to maintain. I suggest having the ViewModel layer send an Event when createUser is done and the View layer collects it and navigate to a new screen. Like this:
interface Event
abstract class BaseViewModel<E : Event> : ViewModel() {
private val _event = Channel<E>()
val event = _event.receiveAsFlow().shareIn(viewModelScope, SharingStarted.Lazily)
protected suspend fun sendEvent(event: E) = _event.send(event)
protected fun sendEventSync(event: E) = viewModelScope.launch { _event.send(event) }
}
#Composable
fun <E : Event> OnEvent(event: Flow<E>, onEvent: (E) -> Unit) {
LaunchedEffect(Unit) {
event.collect(onEvent)
}
}
sealed interface LoginEvent : Event {
object LoginSuccess : LoginEvent
data class LoginFailure(val exception: Throwable) : LoginEvent
}
class LoginViewModel() : BaseViewModel<LoginEvent>() {
fun createUser(context: Context){
viewModelScope.launch {
repository.loign()
.onSuccess { sendEvent(LoginEvent.LoginSuccess) }
.onFailure { sendEvent(LoginEvent.LoginFailure(it)) }
}
}
}
#Composable
fun Screen() {
...
OnEvent(viewModel.event) {
when (it) {
LoginEvent.LoginSuccess ->
navController.navToXXXScreen()
is LoginEvent.LoginFailure ->
context.toast(it.exception.localizedMessage ?: "")
}
}
}
I am quite new in Android Flow and JetPack compose,
I am trying to update my UI when mutable state is being changed , but this is not calling our composable , here is my code
#Composable
fun Grid() {
val mainViewModel by viewModels<DashBoardViewModel>()
mainViewModel.getData()
when (val result = mainViewModel.mutableState.value) {
is Resource.Success -> {
LazyVerticalGrid(
cells = GridCells.Adaptive(100.dp)
) {
items(result.device.items.first().devices.count()) {
EachItem(it)
}
}
}
is Resource.Error -> { Text(text = result.message) }
Resource.Loading -> { CircularProgressIndicator() }
Resource.Empty -> {}
else -> { CircularProgressIndicator() }
}
}
ViewModel:
#HiltViewModel
class DashBoardViewModel #Inject constructor(
private val dashBoardRepository: DashBoardRepository
) : ViewModel() {
val mutableState = MutableLiveData<Resource>()
fun getData() = viewModelScope.launch {
flow {
emit(Resource.Loading)
try {
val mResponse = dashBoardRepository.getDevice()
emit(Resource.Success(mResponse))
} catch (e: Exception) {
e.printStackTrace()
emit(Resource.Error("Error"))
}
}.flowOn(Dispatchers.IO).collect {
mutableState.value = it
}
}
}
There are two problems in your code:
mainViewModel.mutableState.value gets only the current value from your mutable state. Your composable will not be notified when this value changes and thus it cannot reflect the change. If you want to use LiveData in your viewmodel, you have to use observeAsState() extension function which converts LivaData to State that can be automatically observed by composable function. Other option is to have (Mutable)State directly in your viewmodel. See this state explanation.
Your mainViewModel.getData() function will be called every time your Grid() function recomposes, which will be every time your mainViewModel.mutableState changes (once you observe it correctly). You definitely don't want that. Better solution would be to call getData() from your viewModel's init block, or, if you really need to call it from your composable function, use LaunchedEffect.
And, as a side note, the way you are creating flow and then collecting it into LiveData is really odd and unnecessary. You can do something like this instead:
fun getData() = viewModelScope.launch {
mutableState.value = Resource.Loading
try {
val mResponse = dashBoardRepository.getDevice()
mutableState.value = Resource.Success(mResponse)
} catch (e: Exception) {
e.printStackTrace()
mutableState.value = Resource.Error("Error")
}
}
I need to pass a compose content parameter in data class. For example a button can render when added into this content.
data class ContentData {
val content: #Composable ()-> Unit
}
This is working but when I get the app background I am getting parcelable exception. How to solve this problem.
One possible explanation I think that will occur related with a parcelable error, happens if you try to pass such object between activities as extras through Intent. Consider not use Composable as parameters in objects. Instead, try to represent the parameters of your Composable with a model which contains the parameters.
// your compose function
#Composable
fun Item(content: String = "Default", padding: Dp){
// ...
}
// Ui Model which contains your data (instead of have a weird composable reference) as a parcelable.
data class ContentData(
val content: String = "Default",
val paddingRaw: Int = 0
) : Parcelable {
constructor(parcel: Parcel) : this(
parcel.readString().orEmpty(),
parcel.readInt()
) {
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(content)
parcel.writeInt(paddingRaw)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<ContentData> {
override fun createFromParcel(parcel: Parcel): ContentData {
return ContentData(parcel)
}
override fun newArray(size: Int): Array<ContentData?> {
return arrayOfNulls(size)
}
}
}
// Example if you need the model between activities through the intent as an extra.
val data = ContentData("your content", 11)
val intent = Intent().apply {
putExtra("keyContentData", data)
}
//The way of get and use your model.
val contentData = intent.extras?.get("keyContentData") as ContentData
#Composable
fun ParentComponent(){
// ...
Item(
contentData?.content.orEmpty(),
contentData?.paddingRaw?.dp ?: 0.dp
)
// ...
}
How to pass parameter to viewmodel constructor using HiltViewModel library with jetpack compose
My code:
#HiltViewModel
class GetPurchaseViewModel #Inject constructor(val id:Long) : ViewModel() {
private val service= RestModule
var state = MutableStateFlow<State>(State.START)
init{
get(id)
}
private fun get(id:Long){
viewModelScope.launch {
state.value = State.LOADING
try {
val users = withContext(Dispatchers.IO) {
service.getIntance().getPurchase(id)
}
state.value = State.SUCCESLISTPURCHASE(users)
} catch (e: Exception) {
state.value = State.FAILURE(message = e.localizedMessage!!)
}
}
}
}
my call in my composable function :
val model:GetPurchaseViewModel=hiltViewModel(idUser)