How to use Viewmodel with Jetpack compose - android

I am trying to use ViewModel with Jetpack Compose,
By doing a number increment.
But it's not working. Maybe I'm not using the view model in right way.
Heres my Main Activity code
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Greeting()
}
}
}
#Composable
fun Greeting(
helloViewModel: ViewModel = viewModel()
) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxSize()
) {
Text(
text = helloViewModel.number.toString(),
fontSize = 60.sp,
fontWeight = FontWeight.Bold
)
Button(onClick = { helloViewModel.addNumber() }) {
Text(text = "Increment Number ${helloViewModel.number}")
}
}
}
#Preview(showBackground = true)
#Composable
fun DefaultPreview() {
JetpackcomposepracticeTheme {
Greeting()
}
}
And here is my Viewmodel class.
It works fine with xml.
How do i create the object of view model:
class ViewModel: ViewModel() {
var number : Int = 0
fun addNumber(){
number++
}
}

Compose can recompose when some with mutable state value container changes. You can create it manually with mutableStateOf(), mutableStateListOf(), etc, or by wrapping Flow/LiveData.
class ViewModel: ViewModel() {
var number : Int by mutableStateOf(0)
private set
fun addNumber(){
number++
}
}
I suggest you start with state in compose documentation, including this youtube video which explains the basic principles.

I use Hilt along with view-Model. Here is another way of using it
#Composable
fun PokemonListScreen(
navController: NavController
) {
val viewModel = hiltViewModel<PokemonListVm>()
val lazyPokemonItems: LazyPagingItems<PokedexListEntry> = viewModel.pokemonList.collectAsLazyPagingItems()
}
I use this composable instead of a fragment and I keep one reference to the composable in the parent

Related

Android Compose - How to handle ViewModel clear focus event in JetPackCompose?

How to handle ViewModel clear focus event in JetPackCompose?
I have a coroutines channel that sometimes notify my screen to clear the TextField focus
How is the best way to notify my composable to clear focus?
I tried to create a mutableStateFlow, but is there a better way to do it?
#Composable
fun HomeScreen(
viewModel: MainViewModel = hiltViewModel()
) {
val clearFocus by viewModel.clearFocus.collectAsStateWithLifecycle()
AppTheme {
HomeScreenContent(
clearFocus
)
}
}
#HiltViewModel
class MainViewModel #Inject constructor() : ViewModel() {
val clearFocus = MutableStateFlow(false)
init {
viewModelScope.launch {
delay(3000)
clearFocus.value = true
}
}
}
#OptIn(ExperimentalMaterial3Api::class)
#Composable
fun HomeScreenContent(
clearFocus: Boolean
) {
val focusRequester = remember { FocusRequester() }
val focusManager = LocalFocusManager.current
var value by rememberSaveable { mutableStateOf("initial value") }
TextField(
value = value,
onValueChange = {
value = it
}
)
if(clearFocus) {
focusManager.clearFocus()
}
}
When a coroutine channel notifies the ViewModel, I want to clear the TextField focus, how is the best way to achieve that?
Instead of delegating to the HomeScreenContent the duty of clearing the focus you could do it in HomeScreen.
You should not use a stateFlow if you want to do an action that does not affect the compose tree. Instead of using StateFlow use a SharedFlow when you want to trigger an Event.
Using a SharedFlow
#HiltViewModel
class MainViewModel #Inject constructor() : ViewModel() {
val clearFocusEvent = MutableSharedFlow<Unit>()
init {
viewModelScope.launch {
delay(3000)
clearFocusEvent.emit(Unit)
}
}
}
#Composable
fun HomeScreen(
viewModel: MainViewModel = hiltViewModel()
) {
val focusManager = LocalFocusManager.current
LaunchedEffect(Unit) {
viewModel.clearFocusEvent.collectLatest {
focusManager.clearFocus()
}
}
AppTheme {
HomeScreenContent()
}
}
Using a sealed interface as event
If you want to have more events between your VM and Composable or just a cleaner code, you can make a sealed interface that will represent the events
#HiltViewModel
class MainViewModel #Inject constructor() : ViewModel() {
val homeScreenEvent = MutableSharedFlow<HomeScreenEvent>()
init {
viewModelScope.launch {
delay(3000)
homeScreenEvent.emit(HomeScreenEvent.ClearFocus)
}
}
}
sealed interface HomeScreenEvent {
object ClearFocus: HomeScreenEvent
}
#Composable
fun HomeScreen(
viewModel: MainViewModel = hiltViewModel()
) {
val focusManager = LocalFocusManager.current
LaunchedEffect(Unit) {
viewModel.homeScreenEvent.collectLatest {
when(it) {
HomeScreenEvent.ClearFocus -> focusManager.clearFocus()
}
}
}
AppTheme {
HomeScreenContent()
}
}
Now when you'll add an event you just have to handle the new case in the when

How to setup android navigation on jetpack compose using hilt with view models responsible for navigation?

I am getting this error when trying to navigate to another screen from the view model,
kotlin.UninitializedPropertyAccessException: lateinit property _navController has not been initialized
This is my activity code,
#AndroidEntryPoint
class MainActivity : ComponentActivity() {
#Inject
lateinit var navigator: Navigator
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AssessmentAppTheme {
// A surface container using the 'background' color from the theme
Column(modifier = Modifier
.fillMaxWidth(1f)
.padding(vertical = 10.dp, horizontal = 10.dp), horizontalAlignment = Alignment.CenterHorizontally) {
AssessmentApp(modifier = Modifier.padding(bottom = 40.dp))
NavigationGraph(navigator)
}
}
}
}
}
This is my navigation module,
#Module
#InstallIn(ActivityRetainedComponent::class)
class AppModule {
#Provides
fun providesNavigation() = Navigator()
}
This is my navigation class,
#ActivityRetainedScoped
class Navigator {
private lateinit var _navController: NavHostController
fun navigate(destination: NavigationDestination) {
_navController.navigate(destination.route)
}
fun setController(controller: NavHostController) {
_navController = controller
}
}
this is the navigation graph where I am remembering the navController,
#Composable
fun NavigationGraph(
navigator: Navigator
) {
val navController = rememberNavController()
navigator.setController(navController)
NavHost(navController = navController, startDestination = Routes.CLIENTS_ROUTE ) {
composable(Routes.CLIENTS_ROUTE) {
val viewModel = hiltViewModel<ClientViewModel>()
ClientScreen(viewModel = viewModel)
}
composable(Routes.ASSESSMENT_OPTIONS_ROUTE, arguments = listOf(navArgument(RouteArgs.CLIENT_ID) {type = NavType.StringType})) { backStackEntry ->
val viewModel = hiltViewModel<ClientViewModel>()
ClientAssessmentOptionScreen(viewModel = viewModel)
}
}
finally, this is one of view models trying to navigate to different screen,
#HiltViewModel
class ClientViewModel #Inject constructor(
private val repository: IClientRepository,
private val navigator: Navigator,
private val savedStateHandle: SavedStateHandle,
) : ViewModel() {
// Some code here //
fun onEvent(event: ClientEvent) {
viewModelScope.launch {
when(event) {
is ClientEvent.OnClientClicked -> {
event.client.clientName?.let {
navigator.navigate(
NavigationDestination(Routes.generateAssessmentOptionsRoute(clientId = it))
)
}
}
}
}
}
}
What am I doing wrong here? and is the approach to make view models handle navigation the right one for jetpack compose applications?
Just answering it here in case someone else also stumbles upon this. I have modified my navigator class and added a shared flow. Which would be used sort of as an event emitter. Whenever we would want to navigate to another screen we can use the navigate method which would emit the route destination.
#Singleton
class Navigator {
private val _sharedFlow =
MutableSharedFlow<NavigationDestination>(extraBufferCapacity = 1)
val sharedFlow = _sharedFlow.asSharedFlow()
fun navigate(destination: NavigationDestination) {
_sharedFlow.tryEmit(destination)
}
}
Now in the NavigationGraph, I have remembered the NavController and have also added a launchedEffect coroutine, which would be listening to the navigate events from the flow. For each flow event, we will trigger the NavController to navigate to that emitted destination.
#Composable
fun NavigationGraph(
navController: NavHostController = rememberNavController(),
navigator: Navigator
) {
LaunchedEffect("navigation") {
navigator.sharedFlow.onEach {
navController.navigate(it.route)
}.launchIn(this)
}
NavHost(navController = navController, startDestination = Routes.CLIENTS_ROUTE ) {
// some code here... //
}
}

How can I share viewmodel from one screen to another screen in jetpack compose?

I am try to learning android jetpack compose, and I have simple app. In ScreenA I have a text field and when I click the button, I am save this data to firestore, and when I come in ScreenB, I want to save city name also in firebase, but I am using one viewmodel, so how can save both text field data in the same place in firestore, I did not find any solution.
ScreenA:
class ScreenAActivity : ComponentActivity() {
private lateinit var viewModel: MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this)[MyViewModel::class.java]
setContent {
ScreenA(viewModel)
}
}
}
#Composable
fun ScreenA(
viewModel : MyViewModel
) {
val name = remember { mutableStateOf(TextFieldValue()) }
OutlinedTextField(
value = name.value,
onValueChange = { name.value = it },
label = { Text(text = "name") },
)
Button(
modifier = Modifier
.width(40.dp)
.height(20.dp),
onClick = {
focus.clearFocus(force = true)
viewModel.onSignUp(
name.value.text,
)
context.startActivity(
Intent(
context,
ScreenB::class.java
)
)
},
colors = ButtonDefaults.buttonColors(
backgroundColor = Color.Red
),
shape = RoundedCornerShape(60)
) {
Text(
text = "OK"
)
)
}
}
ScreenB:
class ScreenBActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ScreenB()
}
}
}
#Composable
fun ScreenB(
) {
val city = remember { mutableStateOf(TextFieldValue()) }
OutlinedTextField(
value = city.value,
onValueChange = { city.value = it },
label = { Text(text = "city") },
)
Button(
modifier = Modifier
.width(40.dp)
.height(20.dp),
onClick = {
focus.clearFocus(force = true)
viewModel.onSignUp(
city.value.text,
)
},
colors = ButtonDefaults.buttonColors(
backgroundColor = Color.Red
),
shape = RoundedCornerShape(60)
) {
Text(
text = "OK"
)
)
}
}
The recommendation is use a single activity and navigate through screens using Navigation library.
After you've refactored your code to use the Navigation library, you can pass the previous back stack entry in order to get the same instance of the View Model.
val navController = rememberNavController()
NavHost(
navController = navController,
...
) {
composable("ScreenA") { backStackEntry ->
val viewModel: MyViewModel = viewModel(backStackEntry)
ScreenA(viewModel)
}
composable("ScreenB") { backStackEntry ->
val viewModel: MyViewModel = viewModel(navController.previousBackStackEntry!!)
ScreenB(viewModel)
}
}
But if you really need to do this using activity, my suggestion is define a shared object between the view models. Something like:
object SharedSignInObject {
fun signUp(name: String) {
// do something
}
// other things you need to share...
}
and in your viewmodels you can use this object...
class MyViewModel: ViewModel() {
fun signUp(name: String) {
SharedSignInObject.signUp(name)
}
}

ViewModel with LiveData and MutableList not Updating in Jetpack Compose

Am new to Android Development in general and especially with Jetpack Compose and its ways of updating the Composables. I have a iOS background with lots of SwiftUI though.
Anyways, I have the following app
The Composables look like this:
#Composable
fun Greeting() {
Column(
modifier = Modifier
.fillMaxHeight()2
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceEvenly
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
IncrementButton()
DecrementButton()
}
PressedText()
SizeText()
}
}
#Composable
fun PressedText() {
val myViewModel: MyViewModel = viewModel()
val myNumber by myViewModel.number.observeAsState()
Text(text = "Pressed: $myNumber")
}
#Composable
fun SizeText() {
val myViewModel: MyViewModel = viewModel()
val myList by myViewModel.list.observeAsState()
Text(text = "Size: ${myList?.size}")
}
#Composable
fun IncrementButton() {
val myViewModel: MyViewModel = viewModel()
Button(onClick = myViewModel::add) {
Text("Add")
}
}
#Composable
fun DecrementButton() {
val myViewModel: MyViewModel = viewModel()
Button(onClick = myViewModel::remove) {
Text("Remove")
}
}
The view model I am using looks like this:
class MyViewModel : ViewModel() {
private val _number = MutableLiveData<Int>()
val number: LiveData<Int> = _number
private val _list = MutableLiveData<MutableList<Int>>()
val list: LiveData<MutableList<Int>> = _list
init {
_number.value = 0
_list.value = mutableListOf()
}
fun add() {
_number.value = _number.value?.plus(1)
_number.value?.let {
_list.value?.add(it)
_list.value = _list.value
}
}
fun remove() {
_number.value = _number.value?.minus(1)
if (_list.value?.isNotEmpty() == true) {
_list.value?.removeAt(0)
_list.value = _list.value
}
}
}
When I press the "Add"-button the number after "Pressed" gets updated but not the number after "Size".
Am really not sure about those lines with _list.value = _list.value that I have from some other SO post that said to update the reference of the list.
What am I missing? Any hints highly appreciated.
Feel free to leave any comments regarding code design.
Thank you!
This _list.value = _list.value is a really bad idea. Depending on underlying implementation, it may work or may not. In this case it's probably compared by pointer, that's why it doesn't trigger recomposition.
Check out Why is immutability important in functional programming.
The safe way is using non mutable list:
private val _list = MutableLiveData<List<Int>>()
And mutate it like this:
_list.value = _list.value?.toMutableList()?.apply {
add(value)
}
By doing this, you're creating a new list each time, and this will trigger recomposition without problems.
Also, using LiveData is not required at all: if you don't have some dependencies, which makes you using it, you can go for Compose mutable state: it's much cleaner:
var number by mutableStateOf(0)
private set
private val _list = mutableStateListOf<Int>()
val list: List<Int> = _list
fun add() {
number++
_list.add(number)
}
fun remove() {
number--
_list.removeAt(0)
}

swipe to refresh using accompanist

I'm using accompanist library for swipe to refresh.
And I adopt it sample code for testing, however, it didn't work.
I search for adopt it, but I couldn't find.
Is there anything wrong in my code?
I want to swipe when user needs to refresh
class MyViewModel : ViewModel() {
private val _isRefreshing = MutableStateFlow(false)
val isRefreshing: StateFlow<Boolean>
get() = _isRefreshing.asStateFlow()
fun refresh() {
// This doesn't handle multiple 'refreshing' tasks, don't use this
viewModelScope.launch {
// A fake 2 second 'refresh'
_isRefreshing.emit(true)
delay(2000)
_isRefreshing.emit(false)
}
}
}
#Composable
fun SwipeRefreshSample() {
val viewModel: MyViewModel = viewModel()
val isRefreshing by viewModel.isRefreshing.collectAsState()
SwipeRefresh(
state = rememberSwipeRefreshState(isRefreshing),
onRefresh = { viewModel.refresh() },
) {
LazyColumn {
items(30) { index ->
// TODO: list items
}
}
}
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
setContent {
TestTheme {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
}
}
}
}
}
Your list doesn't take up the full screen width and you should include the state parameter:
SwipeRefresh(
state = rememberSwipeRefreshState(isRefreshing),
onRefresh = { viewModel.refresh() },
) {
LazyColumn(state = rememberLazyListState(), modifier = Modifier.fillMaxSize()) {
items(100) { index ->
Text(index.toString())
}
}
}
or with Column:
Column(modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())) {
repeat(100) { index ->
Text(index.toString())
}
}

Categories

Resources