I am currently trying to write an App for my thesis and currently, I am looking into different approaches. Since I really like Flutter and the Thesis requires me to use Java/Kotlin I would like to use Jetpack compose.
Currently, I am stuck trying to update ListElements.
I want to have a List that shows Experiments and their state/result. Once I hit the Button I want the experiments to run and after they are done update their state. Currently, the run Method does nothing besides setting the state to success.
The problem is I don't know how to trigger a recompose from the viewModel of the ExperimentRow once an experiment updates its state.
ExperimentsActivity:
class ExperimentsActivity : AppCompatActivity() {
val exViewModel by viewModels<ExperimentViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//For now this is just Dummy Data and will be replaced
exViewModel.experiments += listOf(
Experiment("Test1", exViewModel::experimentStateChanged),
Experiment("Strongbox", exViewModel::experimentStateChanged)
)
setContent {
TpmTheme {
// A surface container using the 'background' color from the theme
Surface {
ExperimentScreen(
exViewModel.experiments,
exViewModel::startTests
)
}
}
}
}
}
ExperimentViewModel:
class ExperimentViewModel : ViewModel() {
var experiments by mutableStateOf(listOf<Experiment>())
fun startTests() {
for (exp in experiments) {
exp.run()
}
}
fun experimentStateChanged(experiment: Experiment) {
Log.i("ViewModel", "Changed expState of ${experiment.name} to ${experiment.state}")
// HOW DO I TRIGGER A RECOMPOSE OF THE EXPERIMENTROW FOR THE experiment????
//experiments = experiments.toMutableList().also { it.plus(experiment) }
Log.i("Vi", "Size of Expirments: ${experiments.size}")
}
}
ExperimentScreen:
#Composable
fun ExperimentScreen(
experiments: List<Experiment>,
onStartExperiments: () -> Unit
) {
Column {
LazyColumnFor(
items = experiments,
modifier = Modifier.weight(1f),
contentPadding = PaddingValues(top = 8.dp),
) { ep ->
ExperimentRow(
experiment = ep,
modifier = Modifier.fillParentMaxWidth(),
)
}
Button(
onClick = { onStartExperiments() },
modifier = Modifier.padding(16.dp).fillMaxWidth(),
) {
Text("Run Tests")
}
}
}
#Composable
fun ExperimentRow(experiment: Experiment, modifier: Modifier = Modifier) {
Row(
modifier = modifier
.padding(horizontal = 16.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(experiment.name)
Icon(
asset = experiment.state.vAsset,
)
}
Experiment:
class Experiment(val name: String, val onStateChanged: (Experiment) -> Unit) {
var state: ExperimentState = ExperimentState.DEFAULT
set(value) {
field = value
onStateChanged(this)
}
fun run() {
state = ExperimentState.SUCCESS;
}
}
enum class ExperimentState(val vAsset: VectorAsset) {
DEFAULT(Icons.Default.Info),
RUNNING(Icons.Default.Refresh),
SUCCESS(Icons.Default.Done),
FAILED(Icons.Default.Warning),
}
There's a few ways to address this but key thing is that you need to add a copy of element (with state changed) to experiments to trigger the recomposition.
One possible example would be
data class Experiment(val name: String, val state: ExperimentState, val onStateChanged: (Experiment) -> Unit) {
fun run() {
onStateChanged(this.copy(state = ExperimentState.SUCCESS))
}
}
and then
fun experimentStateChanged(experiment: Experiment) {
val index = experiments.toMutableList().indexOfFirst { it.name == experiment.name }
experiments = experiments.toMutableList().also {
it[index] = experiment
}
}
though I suspect there's probably cleaner way of doing this.
Related
Im trying to createa a #composable function that is able to keep track of all its children.
The first Parent TextExecutionOrder should be able to tell that it has 3 Children of the same composable function TestExecutionOrder("2"), TestExecutionOrder("3") and TestExecutionOrder("10").
#Preview
#Composable
fun test() {
TestExecutionOrder("1") {
TestExecutionOrder("2") {
TestExecutionOrder("15") {}
TestExecutionOrder("5") {
TestExecutionOrder("6") {}
TestExecutionOrder("7") {}
}
}
TestExecutionOrder("3") {}
TestExecutionOrder("10") {}
}
}
For Example the above Code could have a datastructure like a Stack, CircularArray or anything else where it stores
the following.
Parent{1} -> Childs{2,3,10}
Parent{2} -> Childs{15,5}
Parent{15} -> Childs{}
Parent{5} -> Childs{6,7}
Parent{6} -> Childs{}
Parent{7} -> Childs{}
Parent{3} -> Childs{}
Parent{10} -> Childs{}
v
data class Faaa(val name: String)//val size : IntSize,val pos: Offset)
#Composable
fun TestExecutionOrder(
text: String,
arr: CircularArray<Faaa>,
stack: Stack<CircularArray<Faaa>>,
content: #Composable () -> Unit,
) {
//TODO
content()
}
In QML I would be able to iterate through the children elements of a parent and then be able to add
all Items that are an instance of TestExecutionOrder inside my desired data structure.
I tried to use State-hoisting where my Stack data structure is at top of my test() function and then passed through
all children. Where each children will only get the stack().peek() reference of the current circular array but Kotlin
is pass by value so this also doesn't work.
Pass By Reference Solution that obv doesnt work :D
#Composable
fun TestExecutionOrder(
text: String,
arr: CircularArray<Faaa>,
stack: Stack<CircularArray<Faaa>>,
content: #Composable () -> Unit,
) {
arr.addLast(Faaa(text)) // Same reference for all children
stack.push(CircularArray<Faaa>()) // create new circularArray for new children
content()
}
data class Faaa(val name: String)//val size : IntSize,val pos: Offset)
#Preview
#Composable
fun test() {
val stack = Stack<CircularArray<Faaa>>()
stack.push(CircularArray<Faaa>())
TestExecutionOrder("1",stack.peek(),stack) {
var referenceCir = stack.peek()
TestExecutionOrder("2",referenceCir,stack) {
var referenceCir2 = stack.peek()
TestExecutionOrder("15",referenceCir2,stack) {}
TestExecutionOrder("5",referenceCir2,stack) {
var referenceCir3 = stack.peek()
TestExecutionOrder("6",referenceCir3,stack) {}
TestExecutionOrder("7",referenceCir3,stack) {}
}
}
TestExecutionOrder("3",referenceCir,stack) {}
TestExecutionOrder("10",referenceCir,stack) {}
}
}
I am assuming I am overthinking this stuff because I came from a QML/C++ Environment. How can one achieve this kind of stuff?
The Goal is to make this thing self managing I wrap my composable function around other functions and it automatically knows how many children of the same type it has without me explicitly passing it as a parameter.
EDIT1: Im aware that compose function can execute in any order
I am not sure that I understood your question correctly, but:
I would recommend you to think of a composable function as of a way to describe the UI, not an object.
So you should describe your UI in a way, that is not very tied up to execution order, since indeed it is a bit hard to predict.
Assuming your goal, I recommend you to create a single composable function that will draw all "children" and will also manage the movement of the box.
It is unlikely that parent composable will execute after children composables, since composable functions are being called. Therefore to call the child function the system needs to call the parent first.
I created a custom Tree DataStructure and used Kotlins "pass by reference" and it works. It is able to keep track of its children and also of its size (only the global/absolute position is not working(any suggestions are appreciated).
The basic Idea is to create a child at the beginning of the composable then continue with the passed Lambda-Scope(content) and after that go back to the parent.
Track Item Composable
class ToggleViewModel : ViewModel() { //View Model acts as a pass by reference #see todo add stackoverflow
var tree: TreeNode<Data> = TreeNode(Data("root"))
}
data class Data(
val name: String,
var size: IntSize = IntSize(0, 0),
var pos: Offset = Offset(0f, 0f)
)
#Composable
fun TrackItem(
text: String,
toggleViewModel: ToggleViewModel = viewModel(),
content: #Composable () -> Unit,
) {
//empty the list after a recomposition
if(toggleViewModel.tree.value.name == "root"){
toggleViewModel.tree.children = mutableListOf()
}
toggleViewModel.tree.addChild(Data(text))
//step 1 new tree is the child
val lastChildrenIndex = toggleViewModel.tree.children.size - 1
toggleViewModel.tree = toggleViewModel.tree.children[lastChildrenIndex]
var relativeToComposeRootPosition by remember { mutableStateOf(Offset(0f, 0f)) }
var size by remember { mutableStateOf(IntSize(0, 0)) }
//assign new children pos and size after recomposition
toggleViewModel.tree.value = Data(text, size, relativeToComposeRootPosition)
Box(modifier = Modifier
.onGloballyPositioned { coordinates ->
relativeToComposeRootPosition = coordinates.positionInRoot()
}
.onSizeChanged {
size = it
}) {
content()
}
//reverse step1
if (toggleViewModel.tree.parent != null) {
toggleViewModel.tree = toggleViewModel.tree.parent!!
}
}
Tree DataStructure
class TreeNode<T>(var value: T){
var parent:TreeNode<T>? = null
var children:MutableList<TreeNode<T>> = mutableListOf()
var currentChildren: TreeNode<T>? = null;
private var currentIndex = 0
fun addChild(nodeValue: T){
val node = TreeNode(nodeValue)
children.add(node)
node.parent = this
if(children.size == 1){ //change this once adding removeChild fun()
currentChildren = children[0]
}
}
fun nextChildren(){
currentIndex++
if(children.size == currentIndex +1){
currentIndex = 0
}
currentChildren = children[currentIndex]
}
fun previousChildren(){
currentIndex--
if(0 > currentIndex){
currentIndex = children.size - 1
}
currentChildren = children[currentIndex]
}
override fun toString(): String {
var s = "$value"
if (children.isNotEmpty()) {
s += " {" + children.map { it.toString() } + " }"
}
return s
}
}
Example Usage
#Preview
#Composable
fun ToggleFunctionality() {
TrackItem("0") {
Box(
modifier = Modifier
.width(PixelToDp(pixelSize = 200))
.offset(x = PixelToDp(pixelSize = 100), y = PixelToDp(pixelSize = 50))
.height(PixelToDp(pixelSize = 200))
.background(Color.Red)
) {
TrackItem("1") {
Column(
) {
TrackItem("2") {
Box(
Modifier
.size(PixelToDp(pixelSize = 20))
.background(Color.Green)
)
}
TrackItem("3") {
Box(
Modifier
.size(PixelToDp(pixelSize = 20))
.background(Color.Blue)
)
}
}
}
}
}
val toggleViewModel: ToggleViewModel = viewModel()
Text(text= "test", modifier = Modifier
.clickable {
log("TEST")
for (item in toggleViewModel.tree.children) {
log(item.toString())
}
})
}
which prints the following
Data(name=0, size=200 x 200, pos=Offset(0.0, 0.0)) {[Data(name=1, size=20 x 40, pos=Offset(100.0, 50.0)) {[Data(name=2, size=20 x 20, pos=Offset(100.0, 50.0)), Data(name=3, size=20 x 20, pos=Offset(100.0, 70.0))] }] }
I am using Hilt dependency injection to retrieve data from a Firestore Database. I have a getResources coroutine which is called when by the init in my viewModel. In my view, I attempt to get the first x elements of the populated data. However, the app crashes with this error.
java.lang.IndexOutOfBoundsException: Empty list doesn't contain element at index 0.
at kotlin.collections.EmptyList.get(Collections.kt:36)
at kotlin.collections.EmptyList.get(Collections.kt:24)
I am guessing that the data load from Firestore has not completed, so when I try and get resources[i], it's empty and it crashes. This happens even when I do a null check. I cannot init{} without a coroutine and if I try and use a suspend function it doesn't like it either. How do I make the view wait until the viewmodel has the loaded data?
ViewModel.kt
#HiltViewModel
class ResourcesViewModel #Inject constructor(
private val repository: ResourcesRepository
) : ViewModel() {
val data: MutableState<DataOrException<List<Resource>, Exception>> = mutableStateOf(
DataOrException(
listOf(),
Exception("")
)
)
init {
getResources()
}
private fun getResources() {
viewModelScope.launch {
Log.d("getting resources", "currently getting resources")
data.value = repository.getResourcesFromFireStore()
Log.d("complete","complete")
}
}
}
ResourcesRepository.kt
#Singleton
class ResourcesRepository #Inject constructor(
private val db: FirebaseFirestore
) {
val resources = ArrayList<Resource>()
suspend fun getResourcesFromFireStore(): DataOrException<List<Resource>, Exception> {
val resourcesRef: CollectionReference = db.collection("updated-resources-new")
val dataOrException = DataOrException<List<Resource>, Exception>()
try {
dataOrException.data = resourcesRef.get().await().map { document ->
document.toObject(Resource::class.java)
}
Log.d(TAG, "${dataOrException.data}")
} catch (e: FirebaseFirestoreException) {
dataOrException.e = e
}
View.kt
#Composable
fun ResourcesScreenContent(viewModel: ResourcesViewModel) {
LazyColumn (
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 20.dp)
) {
val resources : List<Resource>? = viewModel.data.value.data
items(30) {
for (i in 0 ..30) {
ExpandableCard(resource = resources!![i])
Divider(thickness = 20.dp, color = Color.White)
}
}
}
}
How do I fix this?
you're doing one minor mistake, in forloop you are using 0...30 so it will run 30 times irrespective of resources list size. so in for loop you have to pass resources.count
#Composable
fun ResourcesScreenContent(viewModel: ResourcesViewModel) {
LazyColumn (
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 20.dp)
) {
val resources : List<Resource>? = viewModel.data.value.data
items(resources.count) { i ->
ExpandableCard(resource = resources!![i])
Divider(thickness = 20.dp, color = Color.White)
}
}
}
Hope it is clear my brother
In the following viewModel code I am generating a list of items from graphQl server
private val _balloonsStatus =
MutableStateFlow<Status<List<BalloonsQuery.Edge>?>>(Status.Loading())
val balloonsStatus get() = _balloonsStatus
private val _endCursor = MutableStateFlow<String?>(null)
val endCursor get() = _endCursor
init {
loadBalloons(null)
}
fun loadBalloons(cursor: String?) {
viewModelScope.launch {
val data = repo.getBalloonsFromServer(cursor)
if (data.errors == null) {
_balloonsStatus.value = Status.Success(data.data?.balloons?.edges)
_endCursor.value = data.data?.balloons?.pageInfo?.endCursor
} else {
_balloonsStatus.value = Status.Error(data.errors!![0].message)
_endCursor.value = null
}
}
}
and in the composable function I am getting this data by following this code:
#Composable
fun BalloonsScreen(
navHostController: NavHostController? = null,
viewModel: SharedBalloonViewModel
) {
val endCursor by viewModel.endCursor.collectAsState()
val balloons by viewModel.balloonsStatus.collectAsState()
AssignmentTheme {
Column(modifier = Modifier.fillMaxSize()) {
when (balloons) {
is Status.Error -> {
Log.i("Reyjohn", balloons.message!!)
}
is Status.Loading -> {
Log.i("Reyjohn", "loading..")
}
is Status.Success -> {
BalloonList(edgeList = balloons.data!!, navHostController = navHostController)
}
}
Spacer(modifier = Modifier.weight(1f))
Button(onClick = { viewModel.loadBalloons(endCursor) }) {
Text(text = "Load More")
}
}
}
}
#Composable
fun BalloonList(
edgeList: List<BalloonsQuery.Edge>,
navHostController: NavHostController? = null,
) {
LazyColumn {
items(items = edgeList) { edge ->
UserRow(edge.node, navHostController)
}
}
}
but the problem is every time I click on Load More button it regenerates the view and displays a new set of list, but I want to append the list at the end of the previous list. As far I understand that the list is regenerated as the flow I am listening to is doing the work behind this, but I am stuck here to get a workaround about how to achieve my target here, a kind hearted help would be much appreciated!
You can create a private list in ViewModel that adds List<BalloonsQuery.Edge>?>
and instead of
_balloonsStatus.value = Status.Success(data.data?.balloons?.edges)
you can do something like
_balloonsStatus.value = Status.Success(myLiast.addAll(
data.data?.balloons?.edges))
should update Compose with the latest data appended to existing one
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 completely new to Jetpack Compose AND Kotlin, but not to Android development in Java. Wanting to make first contact with both technologies, I wanted to make a really simple app which populates a LazyColumn with images from Dog API.
All the Retrofit connection part works OK, as I've managed to populate one card with a random puppy, but when the time comes to populate the list, it's just impossible. This is what happens:
The interface is created and a white screen is shown.
The API is called.
Wait about 20 seconds (there's about 400 images!).
dogImages gets updated automatically.
The LazyColumn never gets recomposed again so the white screen stays like that.
Do you have any ideas? I can't find any tutorial on this matter, just vague explanations about state for scroll listening.
Here's my code:
class MainActivity : ComponentActivity() {
private val dogImages = mutableStateListOf<String>()
#ExperimentalCoilApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
PuppyWallpapersTheme {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
DogList(dogImages)
searchByName("poodle")
}
}
}
}
private fun getRetrofit():Retrofit {
return Retrofit.Builder()
.baseUrl("https://dog.ceo/api/breed/")
.addConverterFactory(GsonConverterFactory.create())
.build()
}
private fun searchByName(query: String) {
CoroutineScope(Dispatchers.IO).launch {
val call = getRetrofit().create(APIService::class.java).getDogsByBreed("$query/images")
val puppies = call.body()
runOnUiThread {
if (call.isSuccessful) {
val images = puppies?.images ?: emptyList()
dogImages.clear()
dogImages.addAll(images)
}
}
}
}
#ExperimentalCoilApi
#Composable
fun DogList(dogs: SnapshotStateList<String>) {
LazyColumn() {
items(dogs) { dog ->
DogCard(dog)
}
}
}
#ExperimentalCoilApi
#Composable
fun DogCard(dog: String) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(15.dp),
elevation = 10.dp
) {
Image(
painter = rememberImagePainter(dog),
contentDescription = null
)
}
}
}
Thank you in advance! :)
Your view of the image cannot determine the aspect ratio before it loads, and it does not start loading because the calculated height is zero. See this reply for more information.
Also a couple of tips about your code.
Storing state inside MainActivity is bad practice, you can use view models. Inside a view model you can use viewModelScope, which will be bound to your screen: all tasks will be cancelled, and the object will be destroyed when the screen is closed.
You should not make state-modifying calls directly from the view constructor, as you do with searchByName. This code can be called many times during recomposition, so your call will be repetitive. You should do this with side effects. In this case you can use LaunchedEffect, but you can also do it in the init view model, because it will be created when your screen appears.
It's very convenient to pass Modifier as the last argument, in this case you don't need to add a comma at the end and you can easily add/remove modifiers.
You may have many composables, storing them all inside MainActivity is not very convenient. A good practice is to store them simply in a file, and separate them logically by files.
Your code can be updated to the following:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
PuppyWallpapersTheme {
DogsListScreen()
}
}
}
}
#Composable
fun DogsListScreen(
// pass the view model in this form for convenient testing
viewModel: DogsModel = viewModel()
) {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
DogList(viewModel.dogImages)
}
}
#Composable
fun DogList(dogs: SnapshotStateList<String>) {
LazyColumn {
items(dogs) { dog ->
DogCard(dog)
}
}
}
#Composable
fun DogCard(dog: String) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(15.dp),
elevation = 10.dp
) {
Image(
painter = rememberImagePainter(
data = dog,
builder = {
// don't use it blindly, it can be tricky.
// check out https://stackoverflow.com/a/68908392/3585796
size(OriginalSize)
},
),
contentDescription = null,
)
}
}
class DogsModel : ViewModel() {
val dogImages = mutableStateListOf<String>()
init {
searchByName("poodle")
}
private fun getRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl("https://dog.ceo/api/breed/")
.addConverterFactory(GsonConverterFactory.create())
.build()
}
private fun searchByName(query: String) {
viewModelScope
.launch {
val call = getRetrofit()
.create(APIService::class.java)
.getDogsByBreed("$query/images")
val puppies = call.body()
if (call.isSuccessful) {
val images = puppies?.images ?: emptyList()
dogImages.clear()
dogImages.addAll(images)
}
}
}
}