How to create swipe to refresh in Jetpack compose using kotlin? Please Share proper reference link
SwipeRefresh is not available
SwipeRefresh(
state = rememberSwipeRefreshState(isRefreshing),
onRefresh = { },
) {
LazyColumn {
}
}
Now you can use the built-in pullRefresh modifier.
Something like:
val refreshScope = rememberCoroutineScope()
var refreshing by remember { mutableStateOf(false) }
fun refresh() = refreshScope.launch {
refreshing = true
//...do something
refreshing = false
}
val state = rememberPullRefreshState(refreshing, ::refresh)
Box(Modifier.pullRefresh(state)) {
LazyColumn(Modifier.fillMaxSize()) {
if (!refreshing) {
items(itemCount) {
//...
}
}
}
PullRefreshIndicator(refreshing, state, Modifier.align(Alignment.TopCenter))
}
To create a swipe-to-refresh layout, we need to add dependency in buld.gradle which will provide swipe to refresh layout just like SwipeRefreshLayout in traditional android.
implementation 'com.google.accompanist:accompanist-swiperefresh:0.24.13-rc'
..
To create this kind of layout we require two APIs one SwipeRefresh for layout and another rememberSwipeRefreshState which will remember the state.
#Composable
fun SwipeRefreshCompose() {
var refreshing by remember { mutableStateOf(false) }
LaunchedEffect(refreshing) {
if (refreshing) {
delay(3000)
refreshing = false
}
}
SwipeRefresh(
state = rememberSwipeRefreshState(isRefreshing = refreshing),
onRefresh = { refreshing = true },
) {
// list view
}
}
Related
I have created a LazyRow in jetpack compose. At a certain point in the viewmodel, I want to get the list of currently visible items from that LazyRow.
I know that I can get the list of visible items in the Composable function using the following code:
val listState = rememberLazyListState()
val visibleItemIds = remember {
derivedStateOf { listState.layoutInfo.visibleItemsInfo.map { it.key.toString() } }
}
The problem is how can I pass this data to the viewmodel during a viewmodel event (not a button click etc)
You can add a side effect to know what are the visible items in any time.
LaunchedEffect(visibleItemIds){
//update your viewModel
}
You can also have a List<T> instead of State<List<String>> as in your code with:
val state = rememberLazyListState()
val visibleItemIds: List<Int> by remember {
derivedStateOf {
val layoutInfo = state.layoutInfo
val visibleItemsInfo = layoutInfo.visibleItemsInfo
if (visibleItemsInfo.isEmpty()) {
emptyList()
} else {
visibleItemsInfo.map { it.index }
}
}
}
Also note that visibleItemsInfo returns also the partially visible items.
I'm using AnimatedContent to animate between two different views but even though I'm specifying I want to use vertical transitions, it's still switching between the view but not applying the actual transition animation. Here's the code:
val isEditState = remember { mutableStateOf(false) }
AnimatedContent(
targetState = isEditState,
transitionSpec = {
(slideInVertically() with slideOutVertically()).using(SizeTransform(clip = false))
}
) { targetState ->
if (targetState.value) {
EditView(...)
} else {
NonEditView(...)
}
}
How can I fix this so the animation works?
If I use AnimatedVisibility it does work but I have to apply it to each view specifically.
AnimatedVisibility(
visible = isEditState.value,
enter = slideInVertically(),
exit = slideOutVertically(),
content = EditView(...)
)
Try using isEditable.value as the target state:
AnimatedContent(
targetState = isEditState.value,
transitionSpec = {
(slideInVertically() with slideOutVertically()).using(SizeTransform(clip = false))
}
) { targetState ->
if (targetState) {
EditView(...)
} else {
NonEditView(...)
}
}
The variable isEditable (which is a State) doesn't change, that's why it doesn't animate. What changes is its value.
I think your error is actually in using
val isEditState = remember { mutableStateOf(false) }
rather than
val isEditState by remember { mutableStateOf(false) }
If you update that to match the standard usage of remember, I think the rest of the solution will fall into place:
val isEditState by remember { mutableStateOf(false) }
AnimatedContent(
targetState = isEditState,
transitionSpec = {
(slideInVertically() with slideOutVertically()).using(SizeTransform(clip = false))
}
) { targetState ->
if (targetState) {
EditView(...)
} else {
NonEditView(...)
}
}
I am using Jetpack Compose, along with Paging 3 library & Jetpack Navigation. The issue I am facing is I have a LazyList which is fetching data from remote source using paging library.
ViewModel
fun getImages(): Flow<PagingData<ObjectImage>> = Pager(
PagingConfig(PAGE_SIZE, enablePlaceholders = false)
) { DataHome(RANDOM) }.flow.cachedIn(viewModelScope)
HomeView
val images = viewModelHome.getImages().collectAsLazyPagingItems()
LazyColumn {
...
}
Now whats happening is when I navigate to another View using navHostController.navigate() and then press back to get to HomeView... the LazyColumn resets itself & start loading items again from network.
So I am stuck with this issue. I tried manually caching in viewModel variable... though it works but it screws up SwipeRefresh (which stops showing refresh state)
data.apply {
when {
// refresh
loadState.refresh is LoadState.Loading -> {
ItemLoading()
}
// reload
loadState.append is LoadState.Loading -> {...}
// refresh error
loadState.refresh is LoadState.Error -> {...}
// reload error
loadState.append is LoadState.Error -> {...}
}
}
implementation("androidx.paging:paging-runtime-ktx:3.1.0")
implementation("androidx.paging:paging-compose:1.0.0-alpha14")
Is this an issue with PagingLibrary which is still in alpha??
Update 1 (I am not sure if this is a good solution, but I am solving
the swipe refresh issue as follows)
// get images
var images: Flow<PagingData<ObjectImage>> = Pager(PagingConfig(PAGE_SIZE)) {
DataHome(RANDOM)
}.flow.cachedIn(viewModelScope)
// reload items
fun reload(){
images = Pager(PagingConfig(PAGE_SIZE)) {
DataHome(RANDOM)
}.flow.cachedIn(viewModelScope)
}
// and rather than calling .refresh() method on lazy items... I am calling viewModel.reload()
The problem is that you are creating new Pager every time you call getImages(), which is every time your composable recomposes, that's not how it's supposed to be done.
You should make it a val items = Pager(... for the caching to work.
For the screwed up SwipeRefresh, how do you implement it? There is a refresh() method on LazyPagingItems, you should use that.
EDIT: Ok, so based on the coments and edits to your question:
In your viewmodel, do as I suggested before:
val items = Pager( // define your pager here
Your composable can then look like this:
#Composable
fun Screen() {
val items = viewModel.items.collectAsLazyPagingItems()
val state = rememberSwipeRefreshState(
isRefreshing = items.loadState.refresh is LoadState.Loading,
)
SwipeRefresh(
modifier = Modifier.fillMaxSize(),
state = state,
// use the provided LazyPagingItems.refresh() method,
// no need for custom solutions
onRefresh = { items.refresh() }
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
) {
// display the items only when loadState.refresh is not loading,
// as you wish
if (items.loadState.refresh is LoadState.NotLoading) {
items(items) {
if (it != null) {
Text(
modifier = Modifier.padding(16.dp),
text = it,
)
}
}
// you can also add item for LoadState.Error, anything you want
if (items.loadState.append is LoadState.Loading) {
item {
Box(modifier = Modifier.fillMaxWidth()) {
CircularProgressIndicator(
modifier = Modifier
.align(Alignment.Center)
.padding(16.dp)
)
}
}
}
}
// if the loadState.refresh is Loading,
// display just single loading item,
// or nothing at all (SwipeRefresh already indicates
// refresh is in progress)
else if (items.loadState.refresh is LoadState.Loading) {
item {
Box(modifier = Modifier.fillParentMaxSize()) {
Text(
text = "Refreshing",
modifier = Modifier.align(Alignment.Center))
}
}
}
}
}
}
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 have a Composable that uses a Handler to slowly update the alpha of an image inside a composable.
However, I'm seeing that the screen turns off before the animation could complete.
In XML layouts, we could keep it alive using
android:keepScreenOn
or
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
Is there a way to do this using compose without using the wake lock permission?
You can use LocalContext to get activity, and it has a window on which you can apply needed flags.
In such cases, when you need to run some code on both view appearance and disappearance, DisposableEffect can be used:
#Composable
fun KeepScreenOn() {
val context = LocalContext.current
DisposableEffect(Unit) {
val window = context.findActivity()?.window
window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
onDispose {
window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
}
}
fun Context.findActivity(): Activity? {
var context = this
while (context is ContextWrapper) {
if (context is Activity) return context
context = context.baseContext
}
return null
}
Usage: when screen appears flag is set to on, and when disappears - it's cleared.
#Composable
fun Screen() {
KeepScreenOn()
}
As #Louis CAD correctly pointed out, you can have problems if you use this "view" in many views: if one view appears that uses it, and then disappears previous views that also used it, it will reset the flag.
I haven't found a way of tracking flags state to update the view, I think #Louis CAD solution is OK until Compose have some system support.
This one should be safe from any interference if you have multiple usages in the same composition:
#Composable
fun KeepScreenOn() = AndroidView({ View(it).apply { keepScreenOn = true } })
Usage is then as simple as that:
if (screenShallBeKeptOn) {
KeepScreenOn()
}
In a more Compose way:
#Composable
fun KeepScreenOn() {
val currentView = LocalView.current
DisposableEffect(Unit) {
currentView.keepScreenOn = true
onDispose {
currentView.keepScreenOn = false
}
}
}
This will be disposed of as soon as views disappear from the composition.
Usage is as simple as:
#Composable
fun Screen() {
KeepScreenOn()
}
This is how I implemented mine
In my Composable function I have a button to activate the FLAG_KEEP_SCREEN_ON or clear FLAG_KEEP_SCREEN_ON
#Composable
fun MyButton() {
var state by rememberSaveable {
mutableStateOf(false)
}
val context = LocalContext.current
Button(
...
modifier = Modifier
.clickable {
state = !state
keepScreen(state, context)
}
...
)
}
fun keepScreen(state: Boolean, context : Context) {
val activity = context as Activity
if(state) {
activity.window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}else {
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
}
The code below works best for me.
#Composable
fun ScreenOnKeeper() {
val activity = LocalContext.current as Activity
DisposableEffect(Unit) {
activity.window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
onDispose {
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
}
}
The code below didn't work when I toggle its presence (conditionally add, remove, add the component).
#Composable
fun ScreenOnKeeper() {
val view = LocalView.current
DisposableEffect(Unit) {
view.keepScreenOn = true
onDispose {
view.keepScreenOn = false
}
}
}