Description:
It's pretty basic problem but I've tried few solutions and nothing is working for me. I simply want to save state of BasicTextField in ViewModel. I tried mutableStateOf("SampleText") and text appears in BasicTextField but is not editable, even keyboard didn't appear.
ViewModel Code:
#HiltViewModel
class NewWorkoutViewModel #Inject constructor(...) : ViewModel() {
var workoutTitle by mutableStateOf("")
...
}
Screen Code:
#Composable
fun NewWorkoutScreen(
navController: NavController,
viewModel: NewWorkoutViewModel = hiltViewModel()
) {
Scaffold(...) { contentPadding ->
LazyColumn(...) {
item {
FillInContentBox(title = "Title:") {
// TextField which is not working
BasicTextField(textStyle = TextStyle(fontSize = 20.sp),
value = viewModel.workoutTitle,
onValueChange ={viewModel.workoutTitle = it})
}
}
}
}
}
#Composable
fun FillInContentBox(title: String = "", content: #Composable () -> Unit = {}) {
Box(...) {
Column(...) {
Text(text = title)
content()
}
}
}
Second attempt:
I tried also using State Flow but still, I can't edit (fill in) text into TextField.
ViewModel Code:
#HiltViewModel
class NewWorkoutViewModel #Inject constructor(...) : ViewModel() {
private val _workoutTitle = MutableStateFlow("")
var workoutTitle = _workoutTitle.asStateFlow()
fun setWorkoutTitle(workoutTitle: String){
_workoutTitle.value = workoutTitle
}
...
}
Screen Code:
#Composable
fun NewWorkoutScreen(
navController: NavController,
viewModel: NewWorkoutViewModel = hiltViewModel()
) {
Scaffold(...) { contentPadding ->
LazyColumn(...) {
item {
FillInContentBox(title = "Title:") {
// TextField which is not working
BasicTextField(textStyle = TextStyle(fontSize = 20.sp),
value = viewModel.workoutTitle.collectAsState().value,
onValueChange = viewModel::setWorkoutTitle)
}
}
}
}
}
#Composable
fun FillInContentBox(title: String = "", content: #Composable () -> Unit = {}) {
Box(...) {
Column(...) {
Text(text = title)
content()
}
}
}
You can do it this way:
Create variable inside your composable
var workoutTitle = remember{mutableStateOf("")}
Pass your variable value in your textfield:
TextField(
value = workoutTitle.value,
onValueChange = {
/* You can store the value of your text view inside your
view model maybe as livedata object or state anything which suits you
*/
}
)
Related
I am migrating my multiple activity app to single activity app for compose.
I have created a composable Home which contains a Top app bar with a title as shown below:
#Composable
fun Home() {
val navController = rememberNavController()
var actionBarTitle by rememberSaveable { mutableStateOf("Home") }
var actionBarSubtitle by rememberSaveable { mutableStateOf("") }
Scaffold(topBar = {
Header(title = actionBarTitle, subTitle = actionBarSubtitle,
onBackPress = { navController.popBackStack() },
showInfo = true, onActionClick = {
navController.navigate(Screen.Info.route)
}, modifier = Modifier.fillMaxWidth())
}) {
AppNavigation(navController = navController, onNavigate = { title, subtitle ->
actionBarTitle = title
actionBarSubtitle = subtitle
})
}
onNavigate is triggered whenever I use navController.navigate for any screen as shown below:
onNavigate("Top up", "Please topm up with minimum of X amount")
navController.navigateTo(Screen.TopUp.route)
My question is when I use backpress I don't know to which screen composable I will be navigated to, so how can I call onNavigate to change the title.
You can observe the navigation changes using the currentBackstackEntryFlow.
#Composable
fun Home() {
val context = LocalContext.current
val navController = rememberNavController()
...
LaunchedEffect(navController) {
navController.currentBackStackEntryFlow.collect { backStackEntry ->
// You can map the title based on the route using:
actionBarTitle = getTitleByRoute(context, backStackEntry.destination.route)
}
}
...
}
Of course, you would need write this getTitleByRoute() to get the correct title in according to the navigation route.
It would be something like:
fun getTitleByRoute(context: Context, route:String): String {
return when (route) {
"Screen1" -> context.getString(R.string.title_screen_1)
// other cases
else -> context.getString(R.string.title_home)
}
}
1. Use LiveData to change the Screen Title while using Composable
implementation "androidx.compose.runtime:runtime-livedata:1.2.0-beta02"
2. Create ViewModel Class
class MainViewModel: ViewModel() {
private var _screenTitle = MutableLiveData("")
val screenTitle: LiveData<String>
get() = _screenTitle
fun setTitle(newTitle: String) {
_screenTitle.value = newTitle
}
}
3. In Your Activity Class
setContent {
Surface(color = MaterialTheme.colors.onPrimary) {
LoadMainScreen()
}
}
// Observe ScreenTitle
#Composable
fun LoadMainScreen(mainViewModel: MainViewModel = viewModel()){
val title: String by mainViewModel.screenTitle.observeAsState("")
Scaffold(
topBar = {
TopAppBar(title = { title?.let { Text(it) } },
navigationIcon = {
Icon(
imageVector = Icons.Default.Menu,
contentDescription = "Menu",
tint = Color.White
)
}
)
}
)
}
4. Change the Title Value from Screen
#Composable
fun ScreenOne(mainViewModel: MainViewModel) {
LaunchedEffect(Unit){
mainViewModel.setTitle("One")
}
}
#Composable
fun ScreenTwo(mainViewModel: MainViewModel) {
LaunchedEffect(Unit){
mainViewModel.setTitle("Two")
}
}
You can get the label of the current destination from navHostcontrollor, just use it as the title
val navController = rememberNavController()
val currentBackStackEntry by navController.currentBackStackEntryAsState()
val title = currentBackStackEntry?.destination?.label
The default composable function is implemented as follows
/**
* Add the [Composable] to the [NavGraphBuilder]
*
* #param route route for the destination
* #param arguments list of arguments to associate with destination
* #param deepLinks list of deep links to associate with the destinations
* #param content composable for the destination
*/
public fun NavGraphBuilder.composable(
route: String,
arguments: List<NamedNavArgument> = emptyList(),
deepLinks: List<NavDeepLink> = emptyList(),
content: #Composable (NavBackStackEntry) -> Unit
) {
addDestination(
ComposeNavigator.Destination(provider[ComposeNavigator::class], content).apply {
this.route = route
arguments.forEach { (argumentName, argument) ->
addArgument(argumentName, argument)
}
deepLinks.forEach { deepLink ->
addDeepLink(deepLink)
}
}
)
}
overload it:
fun NavGraphBuilder.composable(
route: String,
label: String,
arguments: List<NamedNavArgument> = emptyList(),
deepLinks: List<NavDeepLink> = emptyList(),
content: #Composable (NavBackStackEntry) -> Unit
) {
addDestination(
ComposeNavigator.Destination(provider[ComposeNavigator::class], content).apply {
this.route = route
this.label = label
arguments.forEach { (argumentName, argument) ->
addArgument(argumentName, argument)
}
deepLinks.forEach { deepLink ->
addDeepLink(deepLink)
}
}
)
}
You can use it this way:
composable("route", "title") {
...
}
I have a main screen where I have retrieved a list string in a viewmodel. I also have a button that opens a Dialog. In this dialog I have a text field for the user to write the word that he want to filter (potato name field), and buttons to filter and cancel.
How can I apply the filter in the viewmodel when the user clicks on the button to accept that on the main screen the list is already filtered?
MainScreen:
#Composable
fun PotatosScreen(
viewModel: PotatosViewModel,
state: Success<PotatosData>
) {
val expandedItem = viewModel.expandedCardList.collectAsState()
Box(
modifier = Modifier.fillMaxSize()
) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(vertical = 8.dp)
) {
items(state.data.potatos) { potato ->
potatoCard(
potato = potato,
onCardArrowClick = { viewModel.itemArrowClick(potato.id) },
expanded = expandedItem.value.contains(potato.id)
)
}
}
var showCustomDialogWithResult by remember { mutableStateOf(true) }
// Text button for open Dialog
Text(
text = "Filter",
modifier = Modifier
.clickable {
if (showCustomDialogWithResult) {
DialogFilter(
onDismiss = {
showCustomDialogWithResult = !showCustomDialogWithResult
},
onNegativeClick = {
showCustomDialogWithResult = !showCustomDialogWithResult
},
onPositiveClick = {
showCustomDialogWithResult = !showCustomDialogWithResult
}
)
}
}
)
}
}
Dialog:
#Composable
fun DialogFilter(
onDismiss: () -> Unit,
onNegativeClick: () -> Unit,
onPositiveClick: () -> Unit
) {
var text by remember { mutableStateOf("") }
Dialog(onDismissRequest = onDismiss) {
Card(
shape = RoundedCornerShape(10.dp),
modifier = Modifier.padding(10.dp, 5.dp, 10.dp, 10.dp),
elevation = 8.dp
) {
Column(
Modifier.background(Color.White)
) {
Text(
text = stringResource("Filter")
)
TextField(
value = text
)
TextButton(onClick = onNegativeClick) {
Text(
text = "Cancel
)
}
// How apply filter in viewModel here?
TextButton(onClick = onPositiveClick) {
Text(
text = "Filter"
)
}
}
}
}
}
ViewModel:
#HiltViewModel
class PotatosViewModel #Inject constructor(
private val getPotatosDataUseCase: GetPotatosData
) : ViewModel() {
private val _state = mutableStateOf<Response<PotatosData>>(Loading)
val state: State<Response<PotatosData>> = _state
private val _expandedItemList = MutableStateFlow(listOf<Int>())
val expandedCardList: StateFlow<List<Int>> get() = _expandedItemList
private val _isRefreshing = MutableStateFlow(false)
val isRefreshing: StateFlow<Boolean>
get() = _isRefreshing.asStateFlow()
init {
getPotatos()
}
fun refresh() {
viewModelScope.launch {
_isRefreshing.emit(true)
getPotatos()
_isRefreshing.emit(false)
}
}
private fun getPotatos() {
viewModelScope.launch {
getPotatosDataUseCase().collect { response ->
_state.value = response
}
}
}
fun containsItem(potatoId: Int): Boolean {
return _expandedItemList.value.toMutableList().contains(potatoId)
}
fun itemArrowClick(potatoId: Int) {
_expandedItemList.value = _expandedItemList.value.toMutableList().also { list ->
if (list.contains(potatoId)) {
list.remove(potatoId)
} else {
list.add(potatoId)
}
}
}
}
State:
data class PotatosState(
val potatoes: List<Potato>,
)
Potato:
data class Potato(
val id: Int,
val name: String)
You can change the onPositiveClick callback to accept a String and pass it to the ViewModel in order to apply your filter, something like this:
fun DialogFilter(
onDismiss: () -> Unit,
onNegativeClick: () -> Unit,
onPositiveClick: (String) -> Unit
)
And then the callback would call your ViewModel with the text
onPositiveClick = { filter ->
showCustomDialogWithResult = !showCustomDialogWithResult
viewModel.applyFilter(filter)
}
Edit 1
TextButton(onClick = {
onPositiveClick(text)
}) {
Text(
text = "Filter"
)
}
I want to call "onLogin" function and pass user but I can't access "onLogin" in ViewModel , I tried to use mutableLiveData but I couldn't,I don't know should I pass onLogin to viewmodel or this is a bad practice
there is button whose title is "Sign In" , it calls method in ViewModel called "Submit" use apollo (graphql) to get the user
SignInScreen
#Composable
fun SignInScreen(
onNavigateToSignUp:() -> Unit,
onLogin:(User) -> Unit
){
val viewModel:SignInViewModel = viewModel()
Scaffold(
bottomBar = {
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.padding(bottom = 10.dp)
.fillMaxWidth()
) {
Text(text = "Don't have an account?")
Text(
text = "Sign Up.",
modifier = Modifier
.padding(start = 5.dp)
.clickable { onNavigateToSignUp() },
fontWeight = FontWeight.Bold
)
}
}
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(it),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(text = "Instagram")
Spacer(modifier = Modifier.size(30.dp))
Input(viewModel.username,placeholder = "username"){
viewModel.username = it
}
Spacer(modifier = Modifier.size(20.dp))
Input(viewModel.password,placeholder = "Password"){
viewModel.password = it
}
Spacer(modifier = Modifier.size(30.dp))
Button(onClick = {viewModel.submit()},modifier = Modifier.fillMaxWidth()) {
Text(text = "Sign In")
}
}
}
}
ViewModel
class SignInViewModel(application:Application):AndroidViewModel(application) {
var username by mutableStateOf("")
var password by mutableStateOf("")
private val context = application.applicationContext
private val _user = MutableLiveData<User>(null)
val user:LiveData<User> get() = _user
fun submit(){
viewModelScope.launch {
val response = apolloClient.mutate(LoginMutation(username = Input.fromNullable(username),password = Input.fromNullable(password))).await()
_user.value = response.data?.login?.user as User
}
}
}
This is how I did it.
1. First I created this class to communicate from ViewModel to view(s) and to have stateful communication where the UI knows what to show with every update and through one live data.
sealed class UIState<out T>() {
class Idle() : UIState<Nothing>()
class Loading(val progress: Int = 0) : UIState<Nothing>()
class Success<out T>(val data: T?) : UIState<T>()
class Error(
val error: Throwable? = null,
val message: String? = null,
val title: String? = null
) : UIState<Nothing>()
}
2. Then Of course create the live data in ViewModel and also an immutable copy for the view:
private val _loginState by lazy { MutableLiveData<UIState<ResponseUser>>() }
val loginState: LiveData<UIState<ResponseUser>> = _loginState
fun performLogin(username: String, password: String) {
viewModelScope.launch {
_loginState.postValue(loading)
// your login logic here
if ("login was successful") {
_loginState.postValue(UIState.Success("your login response if needed in UI"))
} else {
_loginState.postValue(UIState.Error("some error here"))
}
}
}
3. Now in the UI I need to observe this live data as a state, which is pretty easy we have delegate literally called observeAsState. But here is the catch and that's if you are doing something like navigation, which you only want to happen only once:
#Composable
fun LoginScreen(viewModel: LoginViewModel) {
val loginState by viewModel.loginState.observeAsState(UIState.Idle())
val hasHandledNavigation = remember { mutableStateOf(false)}
if (loginState is UIState.Success && !hasHandledNavigation.value ) {
navigateToWelcomeScreen()
else {
LoginScreenUI(loginState) { username, password ->
viewModel.performLogin(username, password)
}
}
}
4. in the UI you want, among other things, two text fields and a button, and you want to remember the username and password that entered:
#Composable
fun LoginScreenUI(
state: UIState<ResponseUser>, onLoginButtonClicked: (username: String, password: String) -> Unit
) {
Column() {
var username by rememberSaveable { mutableStateOf("") }
OutlinedTextField(
value = username,
onValueChange = { username = it },
)
var password by rememberSaveable { mutableStateOf("") }
OutlinedTextField(
value = password,
onValueChange = { password = it },
)
Button(
onClick = {
onLoginButtonClicked(
username, password
)
}
) {
Text(text = "Login")
}
if (state is UIState.Error) {
AlertDialogComponent(state.title, state.message)
}
}
}
I hope I've covered everything :D
My solution is to use the LaunchedEffect because the Android developer documentation is mentioning showing SnackBar as an example which is a single time event, code example following the same as Amin Keshavarzian Answer
just change the part 3 to use LaunchedEffect instead of the flag state hasHandledNavigation
#Composable
fun LoginScreen(viewModel: LoginViewModel) {
val loginState by viewModel.loginState.observeAsState(UIState.Idle())
LaunchedEffect(key1 = loginState) {
if (loginState is UIState.Success)
navigateToWelcomeScreen()
}
LoginScreenUI(loginState) { username, password ->
viewModel.performLogin(username, password)
}
}
I have this composable function that a button will toggle show the text and hide it
#Composable
fun Greeting() {
Column {
val toggleState = remember {
mutableStateOf(false)
}
AnimatedVisibility(visible = toggleState.value) {
Text(text = "Edit", fontSize = 64.sp)
}
ToggleButton(toggleState = toggleState) {}
}
}
#Composable
fun ToggleButton(modifier: Modifier = Modifier,
toggleState: MutableState<Boolean>,
onToggle: (Boolean) -> Unit) {
TextButton(
modifier = modifier,
onClick = {
toggleState.value = !toggleState.value
onToggle(toggleState.value)
})
{ Text(text = if (toggleState.value) "Stop" else "Start") }
}
One thing I didn't like the code is val toggleState = remember { ... }.
I prefer val toggleState by remember {...}
However, if I do that, as shown below, I cannot pass the toggleState over to ToggleButton, as ToggleButton wanted mutableState<Boolean> and not Boolean. Hence it will error out.
#Composable
fun Greeting() {
Column {
val toggleState by remember {
mutableStateOf(false)
}
AnimatedVisibility(visible = toggleState) {
Text(text = "Edit", fontSize = 64.sp)
}
ToggleButton(toggleState = toggleState) {} // Here will have error
}
}
#Composable
fun ToggleButton(modifier: Modifier = Modifier,
toggleState: MutableState<Boolean>,
onToggle: (Boolean) -> Unit) {
TextButton(
modifier = modifier,
onClick = {
toggleState.value = !toggleState.value
onToggle(toggleState.value)
})
{ Text(text = if (toggleState.value) "Stop" else "Start") }
}
How can I fix the above error while still using val toggleState by remember {...}?
State hoisting in Compose is a pattern of moving state to a composable's caller to make a composable stateless. The general pattern for state hoisting in Jetpack Compose is to replace the state variable with two parameters:
value: T: the current value to display
onValueChange: (T) -> Unit: an event that requests the value to change, where T is the proposed new value
You can do something like
// stateless composable is responsible
#Composable
fun ToggleButton(modifier: Modifier = Modifier,
toggle: Boolean,
onToggleChange: () -> Unit) {
TextButton(
onClick = onToggleChange,
modifier = modifier
)
{ Text(text = if (toggle) "Stop" else "Start") }
}
and
#Composable
fun Greeting() {
var toggleState by remember { mutableStateOf(false) }
AnimatedVisibility(visible = toggleState) {
Text(text = "Edit", fontSize = 64.sp)
}
ToggleButton(toggle = toggleState,
onToggleChange = { toggleState = !toggleState }
)
}
You can also add the same stateful composable which is only responsible for holding internal state:
#Composable
fun ToggleButton(modifier: Modifier = Modifier) {
var toggleState by remember { mutableStateOf(false) }
ToggleButton(modifier,
toggleState,
onToggleChange = {
toggleState = !toggleState
},
)
}
For example it working with state{} within composable function.
#Composable
fun CounterButton(text: String) {
val count = state { 0 }
Button(
modifier = Modifier.padding(8.dp),
onClick = { count.value++ }) {
Text(text = "$text ${count.value}")
}
}
But how to update value outside of composable function?
I found a mutableStateOf() function and MutableState interface which can be created outside composable function, but no effect after value update.
Not working example of my attempts:
private val timerState = mutableStateOf("no value", StructurallyEqual)
#Composable
private fun TimerUi() {
Button(onClick = presenter::onTimerButtonClick) {
Text(text = "Timer Button")
}
Text(text = timerState.value)
}
override fun updateTime(time: String) {
timerState.value = time
}
Compose version is 0.1.0-dev14
You can do it by exposing the state through a parameter of the controlled composable function and instantiate it externally from the controlling composable.
Instead of:
#Composable
fun CounterButton(text: String) {
val count = remember { mutableStateOf(0) }
//....
}
You can do something like:
#Composable
fun screen() {
val counter = remember { mutableStateOf(0) }
Column {
CounterButton(
text = "Click count:",
count = counter.value,
updateCount = { newCount ->
counter.value = newCount
}
)
}
}
#Composable
fun CounterButton(text: String, count: Int, updateCount: (Int) -> Unit) {
Button(onClick = { updateCount(count+1) }) {
Text(text = "$text ${count}")
}
}
In this way you can make internal state controllable by the function that called it.
It is called state hoisting.