I created a android compose component and to avoid multiple params, it takes only one parameter : an object
Here is my component :
#Composable
fun ValidationButton(validationButtonModel: ValidationButtonModel)
My object :
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
#Parcelize
data class ValidationButtonModel(
var title: Int,
var contentDescription: Int,
var rfidReadingStarted: Boolean,
var progression: Float,
var read: Int,
var expected: Int,
) : Parcelable
Here is how i define it to compose for being remember between recomposition AND being a state (launch recomposition when change) :
val validationButtonModelState by rememberSaveable() {
mutableStateOf(
ValidationButtonModel(
R.string.common_validate,
R.string.common_contentDescription,
true,
0.1f,
7,
10
)
)
}
But if i try to update it with for example this :
ValidationButton(
validationButtonModelState,
)
Button(onClick =
{
validationButtonModelState.rfidReadingStarted = true
if (validationButtonModelState.progression < 1.0f) {
validationButtonModelState.progression += 0.1f
validationButtonModelState.read += 1
}
}, content = {
Text("INCREMENT")
})
There is no re composition.
I try to add a Saver but doesn't work too :
val validationButtonModelSaver = listSaver<ValidationButtonModel, Any>(
save = { listOf(it.rfidReadingStarted, it.read, it.progression, it.expected, it.title, it.contentDescription) },
restore = { ValidationButtonModel(rfidReadingStarted = it[0] as Boolean, read = it[1] as Int, progression = it[2] as Float, expected = it[4] as Int, title = it[5] as Int, contentDescription = it[6] as Int)}
)
val validationButtonModelState by rememberSaveable(validationButtonModelSaver) {
mutableStateOf(
ValidationButtonModel(
R.string.common_validate,
R.string.common_contentDescription,
true,
0.1f,
7,
10
)
)
}
Am i missing something ? Following this : https://developer.android.com/jetpack/compose/state#parcelize
It should works
If you only change the fields inside your model, mutableStateOf(ModValidationButtonModel) it won't work, as you are still updating your old instance. You need to pass in a new instance of your model effectively.
try
validationButtonModelState = ModValidationButtonModel()
or since it is a data class you can also do
validationButtonModelState = validationButtonModelState.copy(
read = validationButtonModelState.read+1
)
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))] }] }
We are making custom lint for the purpose of applying them to the Android project.
The problematic part is the lint that requires the use of NamedArgument in the Composable function.
#Composable
fun MyComposable(
a: String,
b: Int,
onClick: () -> Unit = {},
) {}
#Composable
fun success1() {
MyComposable(
a = "success",
b = 1,
) {
}
}
I was thinking about how to determine if MyComposable is #Composable when calling MyComposable in the success1 function in the following cases:
class NamedArgumentDetector : Detector(), SourceCodeScanner {
override fun getApplicableUastTypes() = listOf(
UExpression::class.java,
)
override fun createUastHandler(context: JavaContext) = object : UElementHandler() {
override fun visitExpression(node: UExpression) {
if (node !is KotlinUFunctionCallExpression || !node.isInvokedWithinComposable()) return
val firstMethodName = node.methodName?.first() ?: return
if (firstMethodName !in 'A'..'Z') return
val lastArgumentIndex = node.valueArguments.lastIndex
node.valueArguments.fastForEachIndexed { index, argument ->
if (index == lastArgumentIndex && argument is KotlinULambdaExpression) return
val expressionSourcePsi = argument.sourcePsi
val argumentParent = expressionSourcePsi?.node?.treeParent ?: return
val argumentFirstChildNode = argumentParent.firstChildNode
val argumentParentFirstChildNode = argumentParent.treeParent.firstChildNode
if (!(
argumentFirstChildNode.isValueArgumentName() ||
argumentParentFirstChildNode.isValueArgumentName()
)
) {
context.report(
issue = NamedArgumentIssue,
scope = expressionSourcePsi,
location = context.getLocation(expressionSourcePsi),
message = Explanation,
)
return
}
}
}
}
private fun ASTNode.isValueArgumentName() =
this.elementType == VALUE_ARGUMENT_NAME
}
Detector code. In the example above, we were able to check whether success1 invokes Composable, but I can't think of a way to check whether MyComposable called by success1 is a Composable function.
I would appreciate it if you let me know if there is a way to check.
I'm currently trying out Android Compose. I have a Text that shows price of a crypto coin. If a price goes up the color of a text should be green, but if a price goes down it should be red. The function is called when a user clicks a button. The problem is that the function showPrice() is called multiple times (sometimes just once, sometimes 2-4 times). And because of that the user can see the wrong color. What can I do to ensure that it's only called once?
MainActivity:
#Composable
fun MainScreen() {
val priceLiveData by viewModel.trackLiveData.observeAsState()
val price = priceLiveData ?: return
when (price) {
is ViewState.Response -> showPrice(price = price.data)
is ViewState.Error -> showError(price.text)
}
Button(onClick = {viewModel.post()} )
}
#Composable
private fun showPrice(price: Double) {
lastPrice = sharedPref.getFloat("eth", 0f).toDouble()
val color by animateColorAsState(if (price >= (lastPrice)) Color.Green else
Color.Red)
Log.v("TAG", "last=$lastPrice new = $price")
editor.putFloat("eth", price.toFloat()).apply()
Text(
text = price.toString(),
color = color,
fontSize = 28.sp,
fontFamily = fontFamily,
fontWeight = FontWeight.Bold
)
}
ViewModel:
#HiltViewModel
class MyViewModel #Inject constructor(
private val repository: Repository
): ViewModel() {
private val _trackLiveData: MutableLiveData<ViewState<Double>> = MutableLiveData()
val trackLiveData: LiveData<ViewState<Double>>
get() = _trackLiveData
fun post(
) = viewModelScope.launch(Dispatchers.Default) {
try {
val response = repository.post()
_trackLiveData.postValue(ViewState.Response(response.rate.round(7)))
} catch (e: Exception) {
_trackLiveData.postValue(ViewState.Error())
Log.v("TAG: viewmodelPost", e.message.toString())
}
}
}
ViewState:
sealed class ViewState<out T : Any> {
class Response<out T : Any>(val data: T): ViewState<T>()
class Error(val text:String = "Unknown error"): ViewState<Nothing>()
}
So when I press Button to call showPrice(). I can see these lines on Log:
2021-06-10 16:39:18.407 16781-16781/com.myapp.myapp V/TAG: last=2532.375732421875 new = 2532.7403716
2021-06-10 16:39:18.438 16781-16781/com.myapp.myapp V/TAG: last=2532.740478515625 new = 2532.7403716
2021-06-10 16:39:18.520 16781-16781/com.myapp.myapp V/TAG: last=2532.740478515625 new = 2532.7403716
What can I do to ensure that it's only called once?
Nothing, that's how it's meant to work. In the View system you would not ask "Why is my view invalidated 3 times?". The framework invalidates (recomposes) the view as it needs, you should not need to know or care when that happens.
The issue with your code is that your Composable is reading the old value from preferences, that is not how it should work, that value should be provided by the viewmodel as part of the state. Instead of providing just the new price, expose a Data Class that has both the new and old price and then use those 2 values in your composable to determine what color to show, or expose the price and the color to use.
I have a multiple choice quiz with 4 choices per answer. In the ArrayList with the questions and choices, the correct answer is set to the index of the correct option. I want to shuffle the choices but am not sure how to identify the new index of the correct answer. Any thoughts?
Question object
object ConstantsAnalysis {
const val TOTAL_CORRECT: String = "total_correct"
const val TOTAL_OPP: String = "total_opp"
fun getQuestions3(): ArrayList<Questions3> {
val questionList = ArrayList<Questions3>()
val q1 = Questions3(1, null,
"On a graph, the horizontal line along which data are plotted is the _____",
"y axis", "x axis", "origin", "quadrant", 2, R.string.Jones_1995, null)
questionList.addAll(listOf(q1))
questionList.shuffle()
return questionList
}
}
Data class
data class Questions3(
val id: Int, val image: Int?, val question: String, val option1: String, val option2: String,
val option3: String, val option4: String, val correctAnswer: Int, val dialogBox: Int?, val dialogBox2: Int?)
Shuffle choices
val ansorder = arrayOf(question.option1, question.option2, question.option3, question.option4)
ansorder.shuffle()
radio_button1.text = ansorder[0]
radio_button2.text = ansorder[1]
radio_button3.text = ansorder[2]
radio_button4.text = ansorder[3]
Check answer choice
if (questions3!!.correctAnswer != mSelectedOptionPosition) {
//do x
}
Edit (Since correct answer is a string and the index changes after shuffling, answerView(questions3.correctAnswer, R.drawable.correct_option_border.
class QuestionsActivityAnalysis : AppCompatActivity(), View.OnClickListener {
private var mCurrentPosition:Int = 1
private var mQuestionsList:ArrayList<Questions3>? = null
private var mSelectedOptionPosition:Int = 0
private var mCorrectAnswers: Int = 0
private var mSelectedOptionText: String? = null
private fun shuffle() {
val question = mQuestionsList!![mCurrentPosition - 1]
val ansorder = arrayOf(question.option1, question.option2, question.option3, question.option4)
ansorder.shuffle()
radio_button1.text = ansorder[0]
radio_button2.text = ansorder[1]
radio_button3.text = ansorder[2]
radio_button4.text = ansorder[3]
}
override fun onClick(v: View?) {
when(v?.id){
R.id.radio_button1 -> { selectedOptionView(radio_button1, 1)
mSelectedOptionText = radio_button1.text as String?
}
R.id.radio_button2 -> { selectedOptionView(radio_button2, 2)
mSelectedOptionText = radio_button2.text as String?
}
R.id.radio_button3 -> { selectedOptionView(radio_button3, 3)
mSelectedOptionText = radio_button3.text as String?
}
R.id.radio_button4 -> { selectedOptionView(radio_button4, 4)
mSelectedOptionText = radio_button4.text as String?
}
R.id.btn_submit -> {
val questions3 = mQuestionsList?.get(mCurrentPosition - 1)
if (questions3!!.correctAnswer != mSelectedOptionText) {
} else {
mCorrectAnswers++
}
answerView(questions3.correctAnswer, R.drawable.correct_option_border)
private fun answerView(answer: Int, drawableView: Int) {
when(answer){
1 -> {
radio_button1.background = ContextCompat.getDrawable(this, drawableView)
}
2 -> {
radio_button2.background = ContextCompat.getDrawable(this, drawableView)
}
3 -> {
radio_button3.background = ContextCompat.getDrawable(this, drawableView)
}
4 -> {
radio_button4.background = ContextCompat.getDrawable(this, drawableView)
}
}
}
I would really recommend just creating a data class like this:
data class QuestionOption(val question:String, val isCorrect = false)
Afterwards you can shuffle any way you like and just check if the selected QuestionOption has isCorrect set to true. You get a bunch of benefits and the logic gets simpler.
Edit:
To make it easier to declare questions this way:
In general if you add questions in your code you want only as much necessary code as required. For this you can either declare a good constructor or a function that basically maps your values to a constructor. In your case I'd say
data class Questions3(
val id: Int, val question: String, val option1: String, val option2: String,
val option3: String, val correctOption: String, val image: Int?=null,val dialogBox1: Int?=null,val dialogBox2: Int?=null)
(notice how the optional parameters are last, you don't need to specify them as well thanks to them beeing null by default)
Makes sense, in theory you could also (not too clean but easy) just shuffle option 1-3 & correctOption and then just compare if the correctOption String matches the selected String.
Otherwise as I said, you can always create logic for mapping stuff. Here you can either map from Constructor to another Constructor, same with Functions that return a finished Object.
Given the following data class:
data class Coords(val x: Int, val y: Int) {
companion object CoordsOps {
private val shiftTable: Map<Direction, Pair<Int, Int>> = mapOf(
Direction.North to Pair(0, 1),
Direction.East to Pair(1, 0),
Direction.South to Pair(0, -1),
Direction.West to Pair(-1, 0)
)
private operator fun Coords.plus(increment: Pair<Int, Int>): Coords =
Coords(x + increment.first, y + increment.second)
fun Coords.shift(direction: Direction): Coords = this + shiftTable.getValue(direction)
}
}
I'm not able to use the extension function shift() from outside that object. I would like to use Coords.shift(direction).
What am I doing wrong?
You should be able to. I copied your code exactly and was able to call extension function shift from another file.
Perhaps you're missing the import? Or calling it incorrectly? Try this
import <your_package>.Coords.CoordsOps.shift
val y = Coords(3, 4).shift(Direction.East)