The code below is basically changing the state of a Button Widget:
enum class State { unable, enable }
fun configureState(currentState:State, button:Button ,colorInt :Int = Color.BLACK) = when (currentState)
{
State.unable -> {
button.isClickable = false
button.setBackgroundColor(Color.LTGRAY)
button.setTextColor(Color.WHITE)
}
State.enable -> {
button.isClickable = true
button.setBackgroundColor(colorInt)
button.setTextColor(Color.WHITE)
}
}
The goal is to extend the Button Widget and avoid code repetition inside all my activities.
Yes it is easy to just extend via function, if I didn't have the enum State, by doing something like this:
fun Button.configureState(state:Int) = when(state) {
0 -> { //do unable stuff }
1 -> { // do enable stuff }
else -> { // do nada }
}
My question would be what's the proper way to extend with the enum state where I could access it via Extension Function for example:
fun Button.configureWith(state: this.State) = when (state) {
this.State.unable -> { }
this.State.enable -> { }
}
P.S again: I'm new to Kotlin :), any ideas .. all welcome :)
Your code treat the State as a parameter for changing the button state only. It does not necessary to be declared inside the subclass of Button. You can declare it outside of it with an extension function.
enum class ButtonState { unable, enable }
fun Button.configureWith(state: ButtonState, colorInt: Int = Color.BLACK) = when (state) {
ButtonState.unable -> {
clickable = false
setBackgroundColor(Color.LTGRAY)
}
ButtonState.enable -> {
clickable = true
setBackgroundColor(colorInt)
}
}
Related
I want to avoid multiple function call when LaunchEffect key triggers.
LaunchedEffect(key1 = isEnableState, key2 = viewModel.uiState) {
viewModel.scanState(bluetoothAdapter)
}
when first composition isEnableState and viewModel.uiState both will trigger twice and call viewModel.scanState(bluetoothAdapter).
isEnableState is a Boolean type and viewModel.uiState is sealed class of UI types.
var uiState by mutableStateOf<UIState>(UIState.Initial)
private set
var isEnableState by mutableStateOf(false)
private set
So how can we handle idiomatic way to avoid duplicate calls?
Thanks
UPDATE
ContentStateful
#Composable
fun ContentStateful(
context: Context = LocalContext.current,
viewModel: ContentViewModel = koinViewModel(),
) {
LaunchedEffect(key1 = viewModel.isEnableState, key2 = viewModel.uiState) {
viewModel.scanState(bluetoothAdapter)
}
LaunchedEffect(viewModel.previous) {
viewModel.changeDeviceSate()
}
ContentStateLess{
viewModel.isEnableState = false
}
}
ContentStateLess
#Composable
fun ContentStateLess(changeAction: () -> Unit) {
Button(onClick = { changeAction() }) {
Text(text = "Click On me")
}
}
ContentViewModel
class ContentViewModel : BaseViewModel() {
var uiState by mutableStateOf<UIState>(UIState.Initial)
var isEnableState by mutableStateOf(false)
fun scanState(bluetoothAdapter: BluetoothAdapter) {
if (isEnableState && isInitialOrScanningUiState()) {
// start scanning
} else {
// stop scanning
}
}
private fun isInitialOrScanningUiState(): Boolean {
return (uiState == UIState.Initial || uiState == UIState.ScanningDevice)
}
fun changeDeviceSate() {
if (previous == BOND_NONE && newState == BONDING) {
uiState = UIState.LoadingState
} else if (previous == BONDING && newState == BONDED) {
uiState = UIState.ConnectedState(it)
} else {
uiState = UIState.ConnectionFailedState
}
}
}
scanState function is start and stop scanning of devices.
I guess the answer below would work or might require some modification to work but logic for preventing double clicks can be used only if you wish to prevent actions happen initially within time frame of small interval. To prevent double clicks you you set current time and check again if the time is above threshold to invoke click callback. In your situation also adding states with delay might solve the issue.
IDLE, BUSY, READY
var launchState by remember {mutableStateOf(IDLE)}
LaunchedEffect(key1 = isEnableState, key2 = viewModel.uiState) {
if(launchState != BUSY){
viewModel.scanState(bluetoothAdapter)
if(launchState == IDLE){ launchState = BUSY)
}
}
LaunchedEffect(launchState) {
if(launchState == BUSY){
delay(50)
launchState = READY
}
}
When I modify the properties of the objects in the List, the UI does not update
my code:
#OptIn(ExperimentalFoundationApi::class)
#Composable
fun ContactCard(
) {
var stateList = remember {
mutableStateListOf<ListViewData>()
}
viewModel!!.recordRespListLiveData!!.observe(this) { it ->
it.forEach {
stateList.add(ListViewData(false, it))
}
}
LazyColumn() {
stateList.forEachIndexed { index, bean ->
stickyHeader() {
Box(Modifier.clickable {
stateList[index].visible = true
}) {
ContactNameCard(bean.data.contact, index)
}
}
items(bean.data.records) { data ->
if (bean.visible) {
RecordItemCard(record = data)
}
}
}
}
}
When I click on the Box, visible is set to true, but the RecordItemCard doesn't show,why?
For SnapshotList to trigger you need to add, delete or update existing item with new instance. Currently you are updating visible property of existing item.
If ListViewData is instance from data class you can do it as
stateList[index] = stateList[index].copy(visible = true)
i am very beginner. sorry if its too easy or im stupid. i want to make bottom navigation menu with material design 3 kotlin. but this error comes. i want to change visibility of scroll view. what is problem?
again, im using this: m3.material.io
NavigationBarView.OnItemSelectedListener { item ->
when(item.itemId) {
R.id.main -> {
likes.visibility = View.GONE
mainmenu.visibility = View.VISIBLE
}
R.id.starred -> {
mainmenu.visibility = View.GONE
likes.visibility = View.VISIBLE
}
else -> false
}
}
Going off memory, but I think you missed the word set, as in setOnItemSelectedListener, and the listener needs to return a Boolean for all cases. You only returned it for the else case. Very unlikely you need to worry about returning false (for fall-through behavior), so I'd just return treu after the when statement.
NavigationBarView.setOnItemSelectedListener { item ->
when(item.itemId) {
R.id.main -> {
likes.visibility = View.GONE
mainmenu.visibility = View.VISIBLE
}
R.id.starred -> {
mainmenu.visibility = View.GONE
likes.visibility = View.VISIBLE
}
else -> { }
}
true
}
If you're possibly going to be adding more tabs, I suggest doing your logic like this so it's easier to maintain all the possible cases without having to repeat similar lines of code. The way you're doing it now, you have to write a line of code per tab for each view, and make sure you get the visibility right for each case. You can use the isVisible extension property to hide/show views with Boolean logic.
NavigationBarView.setOnItemSelectedListener { item ->
mainmenu.isVisible = item == R.id.main
likes.isVisible = item == R.id.starred
true
}
I think instead of the onItemSelected, It should be setNavigationItemSelectedListener. I had the same issue with the material top nav bar so, maybe try this out too. Also, try adding log statements and verify if the click statements are working. here's a sample code for reference -
binding.navigationViewMainScreen.setNavigationItemSelectedListener {
Log.d(TAG, "navigationViewMainScreen working")
when (it.itemId) {
R.id.log_in -> {
loginPrompt()
true
}
R.id.log_out -> {
Toast.makeText(requireContext(), "Logged Out", Toast.LENGTH_SHORT)
.show()
CoroutineScope(Dispatchers.IO).launch {
accountDataStore.logOut(requireContext())
}
true
}
R.id.sign_up -> {
findNavController()
.navigate(
MainScreenFragmentDirections.actionMainScreenFragmentToSignUpFragment()
)
true
}
R.id.remove_account -> {
Toast.makeText(requireContext(), "Account Removed", Toast.LENGTH_SHORT)
.show()
CoroutineScope(Dispatchers.IO).launch {
accountDataStore.resetAccounts(requireContext())
}
true
}
R.id.switch_account -> {
switchAccountPrompt()
true
}
R.id.contract_interface -> {
if (userLoggedIn) {
findNavController()
.navigate(
MainScreenFragmentDirections
.actionMainScreenFragmentToContractScreenFragment()
)
} else {
Toast.makeText(
requireContext(), "Log In Necessary To Continue", Toast.LENGTH_SHORT
).show()
loginPrompt()
}
true
}
R.id.about -> {
findNavController()
.navigate(
MainScreenFragmentDirections
.actionMainScreenFragmentToAboutFragment()
)
Log.d(TAG, "about working")
true
}
R.id.exit -> {
Log.d(TAG, "exit working")
logOutAndExit()
true
}
else -> {
throw IllegalArgumentException("side menu item not registered.")
}
}
}
just focus on the content inside. like this one -
R.id.log_in -> {
loginPrompt()
true
}
here we can see an action is performed and it returns true to verify that. Just don't forget the true at the ends. that is used to verify an action has been performed after the button was clicked. Hope it helps.
I have implemented a column of buttons in jetpack compose. We realized it is possible to click multiple items at once (with multiple fingers for example), and we would like to disable this feature.
Is there an out of the box way to disable multiple simultaneous clicks on children composables by using a parent column modifier?
Here is an example of the current state of my ui, notice there are two selected items and two unselected items.
Here is some code of how it is implemented (stripped down)
Column(
modifier = modifier
.fillMaxSize()
.verticalScroll(nestedScrollParams.childScrollState),
) {
viewDataList.forEachIndexed { index, viewData ->
Row(modifier = modifier.fillMaxWidth()
.height(dimensionResource(id = 48.dp)
.background(colorResource(id = R.color.large_button_background))
.clickable { onClick(viewData) },
verticalAlignment = Alignment.CenterVertically
) {
//Internal composables, etc
}
}
Check this solution. It has similar behavior to splitMotionEvents="false" flag. Use this extension with your Column modifier
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.pointerInput
import kotlinx.coroutines.coroutineScope
fun Modifier.disableSplitMotionEvents() =
pointerInput(Unit) {
coroutineScope {
var currentId: Long = -1L
awaitPointerEventScope {
while (true) {
awaitPointerEvent(PointerEventPass.Initial).changes.forEach { pointerInfo ->
when {
pointerInfo.pressed && currentId == -1L -> currentId = pointerInfo.id.value
pointerInfo.pressed.not() && currentId == pointerInfo.id.value -> currentId = -1
pointerInfo.id.value != currentId && currentId != -1L -> pointerInfo.consume()
else -> Unit
}
}
}
}
}
}
Here are four solutions:
Click Debounce (ViewModel)r
For this, you need to use a viewmodel. The viewmodel handles the click event. You should pass in some id (or data) that identifies the item being clicked. In your example, you could pass an id that you assign to each item (such as a button id):
// IMPORTANT: Make sure to import kotlinx.coroutines.flow.collect
class MyViewModel : ViewModel() {
val debounceState = MutableStateFlow<String?>(null)
init {
viewModelScope.launch {
debounceState
.debounce(300)
.collect { buttonId ->
if (buttonId != null) {
when (buttonId) {
ButtonIds.Support -> displaySupport()
ButtonIds.About -> displayAbout()
ButtonIds.TermsAndService -> displayTermsAndService()
ButtonIds.Privacy -> displayPrivacy()
}
}
}
}
}
fun onItemClick(buttonId: String) {
debounceState.value = buttonId
}
}
object ButtonIds {
const val Support = "support"
const val About = "about"
const val TermsAndService = "termsAndService"
const val Privacy = "privacy"
}
The debouncer ignores any clicks that come in within 500 milliseconds of the last one received. I've tested this and it works. You'll never be able to click more than one item at a time. Although you can touch two at a time and both will be highlighted, only the first one you touch will generate the click handler.
Click Debouncer (Modifier)
This is another take on the click debouncer but is designed to be used as a Modifier. This is probably the one you will want to use the most. Most apps will make the use of scrolling lists that let you tap on a list item. If you quickly tap on an item multiple times, the code in the clickable modifier will execute multiple times. This can be a nuisance. While users normally won't tap multiple times, I've seen even accidental double clicks trigger the clickable twice. Since you want to avoid this throughout your app on not just lists but buttons as well, you probably should use a custom modifier that lets you fix this issue without having to resort to the viewmodel approach shown above.
Create a custom modifier. I've named it onClick:
fun Modifier.onClick(
enabled: Boolean = true,
onClickLabel: String? = null,
role: Role? = null,
onClick: () -> Unit
) = composed(
inspectorInfo = debugInspectorInfo {
name = "clickable"
properties["enabled"] = enabled
properties["onClickLabel"] = onClickLabel
properties["role"] = role
properties["onClick"] = onClick
}
) {
Modifier.clickable(
enabled = enabled,
onClickLabel = onClickLabel,
onClick = {
App.debounceClicks {
onClick.invoke()
}
},
role = role,
indication = LocalIndication.current,
interactionSource = remember { MutableInteractionSource() }
)
}
You'll notice that in the code above, I'm using App.debounceClicks. This of course doesn't exist in your app. You need to create this function somewhere in your app where it is globally accessible. This could be a singleton object. In my code, I use a class that inherits from Application, as this is what gets instantiated when the app starts:
class App : Application() {
override fun onCreate() {
super.onCreate()
}
companion object {
private val debounceState = MutableStateFlow { }
init {
GlobalScope.launch(Dispatchers.Main) {
// IMPORTANT: Make sure to import kotlinx.coroutines.flow.collect
debounceState
.debounce(300)
.collect { onClick ->
onClick.invoke()
}
}
}
fun debounceClicks(onClick: () -> Unit) {
debounceState.value = onClick
}
}
}
Don't forget to include the name of your class in your AndroidManifest:
<application
android:name=".App"
Now instead of using clickable, use onClick instead:
Text("Do Something", modifier = Modifier.onClick { })
Globally disable multi-touch
In your main activity, override dispatchTouchEvent:
class MainActivity : AppCompatActivity() {
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
return ev?.getPointerCount() == 1 && super.dispatchTouchEvent(ev)
}
}
This disables multi-touch globally. If your app has a Google Maps, you will want to add some code to to dispatchTouchEvent to make sure it remains enabled when the screen showing the map is visible. Users will use two fingers to zoom on a map and that requires multi-touch enabled.
State Managed Click Handler
Use a single click event handler that stores the state of which item is clicked. When the first item calls the click, it sets the state to indicate that the click handler is "in-use". If a second item attempts to call the click handler and "in-use" is set to true, it just returns without performing the handler's code. This is essentially the equivalent of a synchronous handler but instead of blocking, any further calls just get ignored.
The most simple approach that I found for this issue is to save the click state for each Item on the list, and update the state to 'true' if an item is clicked.
NOTE: Using this approach works properly only in a use-case where the list will be re-composed after the click handling; for example navigating to another Screen when the item click is performed.
Otherwise if you stay in the same Composable and try to click another item, the second click will be ignored and so on.
for example:
#Composable
fun MyList() {
// Save the click state in a MutableState
val isClicked = remember {
mutableStateOf(false)
}
LazyColumn {
items(10) {
ListItem(index = "$it", state = isClicked) {
// Handle the click
}
}
}
}
ListItem Composable:
#Composable
fun ListItem(
index: String,
state: MutableState<Boolean>,
onClick: () -> Unit
) {
Text(
text = "Item $index",
modifier = Modifier
.clickable {
// If the state is true, escape the function
if (state.value)
return#clickable
// else, call onClick block
onClick()
state.value = true
}
)
}
Trying to turn off multi-touch, or adding single click to the modifier, is not flexible enough. I borrowed the idea from #Johann‘s code. Instead of disabling at the app level, I can call it only when I need to disable it.
Here is an Alternative solution:
class ClickHelper private constructor() {
private val now: Long
get() = System.currentTimeMillis()
private var lastEventTimeMs: Long = 0
fun clickOnce(event: () -> Unit) {
if (now - lastEventTimeMs >= 300L) {
event.invoke()
}
lastEventTimeMs = now
}
companion object {
#Volatile
private var instance: ClickHelper? = null
fun getInstance() =
instance ?: synchronized(this) {
instance ?: ClickHelper().also { instance = it }
}
}
}
then you can use it anywhere you want:
Button(onClick = { ClickHelper.getInstance().clickOnce {
// Handle the click
} } ) { }
or:
Text(modifier = Modifier.clickable { ClickHelper.getInstance().clickOnce {
// Handle the click
} } ) { }
fun singleClick(onClick: () -> Unit): () -> Unit {
var latest: Long = 0
return {
val now = System.currentTimeMillis()
if (now - latest >= 300) {
onClick()
latest = now
}
}
}
Then you can use
Button(onClick = singleClick {
// TODO
})
Here is my solution.
It's based on https://stackoverflow.com/a/69914674/7011814
by I don't use GlobalScope (here is an explanation why) and I don't use MutableStateFlow as well (because its combination with GlobalScope may cause a potential memory leak).
Here is a head stone of the solution:
#OptIn(FlowPreview::class)
#Composable
fun <T>multipleEventsCutter(
content: #Composable (MultipleEventsCutterManager) -> T
) : T {
val debounceState = remember {
MutableSharedFlow<() -> Unit>(
replay = 0,
extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
}
val result = content(
object : MultipleEventsCutterManager {
override fun processEvent(event: () -> Unit) {
debounceState.tryEmit(event)
}
}
)
LaunchedEffect(true) {
debounceState
.debounce(CLICK_COLLAPSING_INTERVAL)
.collect { onClick ->
onClick.invoke()
}
}
return result
}
#OptIn(FlowPreview::class)
#Composable
fun MultipleEventsCutter(
content: #Composable (MultipleEventsCutterManager) -> Unit
) {
multipleEventsCutter(content)
}
The first function can be used as a wrapper around your code like this:
MultipleEventsCutter { multipleEventsCutterManager ->
Button(
onClick = { multipleClicksCutter.processEvent(onClick) },
...
) {
...
}
}
And you can use the second one to create your own modifier, like next one:
fun Modifier.clickableSingle(
enabled: Boolean = true,
onClickLabel: String? = null,
role: Role? = null,
onClick: () -> Unit
) = composed(
inspectorInfo = debugInspectorInfo {
name = "clickable"
properties["enabled"] = enabled
properties["onClickLabel"] = onClickLabel
properties["role"] = role
properties["onClick"] = onClick
}
) {
multipleEventsCutter { manager ->
Modifier.clickable(
enabled = enabled,
onClickLabel = onClickLabel,
onClick = { manager.processEvent { onClick() } },
role = role,
indication = LocalIndication.current,
interactionSource = remember { MutableInteractionSource() }
)
}
}
Just add two lines in your styles. This will disable multitouch in whole application:
<style name="AppTheme" parent="...">
...
<item name="android:windowEnableSplitTouch">false</item>
<item name="android:splitMotionEvents">false</item>
</style>
I am trying to validate a form by calling the same functions multiple times. My goal is that the isFormValid function should "wait" till all functions are called and then return the boolean.
My current solution works but it looks really od. Ain't there a better way?
FormValidator Class
class FormValidator(private val context: Context) {
// some strings
private fun String.isValidEmail() = android.util.Patterns.EMAIL_ADDRESS.matcher(this).matches()
fun validateNormalET(editText: MutableLiveData<String>, editTextEM: MutableLiveData<String>): Boolean {
if (editText.value.isNullOrEmpty()) {
editTextEM.value = emptyFieldError
return false
}
return true
}
fun validateMinLengthET(editText: MutableLiveData<String>, editTextEM: MutableLiveData<String>, minLength: Int): Boolean {
val errorMessage = when {
minLength < 5 -> postCodeTooFewChar
minLength < 7 -> btNumberTooFewChar
else -> "Error"
}
if (editText.value.isNullOrEmpty()) {
editTextEM.value = emptyFieldError
return false
} else if (editText.value.toString().length < minLength) {
editTextEM.value = errorMessage
return false
}
return true
}
fun validateEmail(editText: MutableLiveData<String>, editTextEM: MutableLiveData<String>): Boolean {
if (editText.value.isNullOrEmpty()) {
editTextEM.value = emptyFieldError
return false
} else if (!editText.value.toString().isValidEmail()) {
editTextEM.value = emailNotValidError
return false
}
return true
}
Current isFormValid Function
fun isFormValid(): Boolean =
formValidator.validateMinLengthET(btNumber, btNumberEM, 7) and
formValidator.validateNormalET(etFirstName, etFirstNameEM) and
formValidator.validateNormalET(etLastName, etLastNameEM) and
formValidator.validateEmail(etEmail, etEmailEM) and
formValidator.validateMinLengthET(etPostCode, etPostCodeEM, 5) and
formValidator.validateNormalET(etCity, etCityEM) and
formValidator.validateNormalET(etStreet, etStreetEM) and
formValidator.validateNormalET(etHouseNumber, etHouseNumberEM)
I appreciate every help, thank you. If there was already a question like this, then I am sorry that I opened another one..
You could use a list with its .all implementation:
fun isFormValid(): Boolean = listOf(
formValidator.validateMinLengthET(btNumber, btNumberEM, 7),
formValidator.validateNormalET(etFirstName, etFirstNameEM),
formValidator.validateNormalET(etLastName, etLastNameEM),
formValidator.validateEmail(etEmail, etEmailEM),
formValidator.validateMinLengthET(etPostCode, etPostCodeEM, 5),
formValidator.validateNormalET(etCity, etCityEM),
formValidator.validateNormalET(etStreet, etStreetEM),
formValidator.validateNormalET(etHouseNumber, etHouseNumberEM)
)
.all { it }
That way you're following the Open/Closed Principle.
If you want to make it slightly shorter, use with(formValidator) {}-scope like #iknow posted in the comment.
EDIT:
If you want it to use as little resources as possible, you could convert the list type to a boolean producer: () -> Boolean
fun isFormValid(): Boolean = listOf(
{ formValidator.validateMinLengthET(btNumber, btNumberEM, 7) },
{ formValidator.validateNormalET(etFirstName, etFirstNameEM) },
...
)
.all { it() }