https://kotlinlang.org logo
#compose
Title
# compose
c

codeslubber

02/28/2020, 7:25 PM
I am doing a CountdownTimer. So I am showing the time left in a Text component. But then I want to have a field that you can edit, and upon editing it, it will save some changes. I have used
TextField
before, but with a
TextFieldInput
that was in a state var, e.g.
state { TextFieldInput("") }
) But I already have a state so I wanted to put the TextFieldInput into that instance, so, something like
state { TimerState() }
where TimerState has the TextFieldInput in it, but not having much success with that. The other possibility (part of why I am posting) is way back during early MVC days there was another pattern called PAC. One of the main distinctions is PAC had hierarchies of components, and each component might have its own Abstraction (model) and controller. Should I do that? Make the text input a component? and if so, how do I bubble the edit result up to the enclosing component?
l

Leland Richardson [G]

02/28/2020, 8:00 PM
But I already have a state so I wanted to put the TextFieldInput into that instance, so, something like
state { TimerState() }
where TimerState has the TextFieldInput in it, but not having much success with that.
What was unsuccessful about this?
i’m not super clear on what you’re asking, but in general if you wanted to create a component that “wrapped” a TextField, you would want to have the
value
/
onValueChange
parameters be passed through to the lower level component
Copy code
@Composable fun FancyTextField(value: String, onValueChange: (String) -> Unit) {
  TextField(value, onValueChange)
}
c

codeslubber

02/28/2020, 8:06 PM
What happens is the editing just does not work if the outer state has a
MutableState<TextFieldInput>
. Not sure why .If I have a composable function, it all works, but then when I click submit, how should I get the value back into the outer view? I saw examples that have you pass the var you want it assigned to into the submethod?
l

Leland Richardson [G]

02/28/2020, 8:09 PM
have a code snippet? sorry i’m having trouble understanding what you mean here
c

codeslubber

02/28/2020, 8:15 PM
So I have this:
Copy code
class TimerState(hours:TimeDigit = TimeDigit(0), minutes:TimeDigit = TimeDigit(0), seconds:TimeDigit = TimeDigit(0)) {
    var currentStatus:String = {
        "$hours:$minutes:$seconds"
    }()

    var amountOfSkinInput = TextFieldValue("")
    var amountOfSkin:Int = 50
}
and this does not work. I think because the input field is not a `MutableState<TextFieldValue>`right? but I can’t nest
state {}
so?
what does work is this:
Copy code
@Composable
fun numberInput(startAt:Int, range: IntRange, units: String){
    val state = state { TextFieldValue(text = "") }
    TextField(
        value = state.value,
        //modifier = Border(Dp.Hairline, Color.Gray),
        onValueChange = {
            state.value = it
        },
        keyboardType = KeyboardType.Number,
        textStyle = TextStyle(fontSize = 64.sp)
    )
    Button(onClick = {
        if (!range.contains(state.value.text.toInt())){
            Log.d("VALIDATOR", "Out of range!")
        } else {

        }
    }){
        Text("Submit")
    }
}
but then on submit, I would have to assign this value back to the field in the outer view state.
Does that make sense?
l

Leland Richardson [G]

02/28/2020, 8:30 PM
yeah
so there’s a few things to think about here
one is that i would suggest you make
numberInput
what we call a “controlled” composable
there’s a lot of literature on this already on the web from react ecosystem if you search for “react controlled components”
same concept
but i would have number input behave more similarly to this:
Copy code
@Composable
fun numberInput(
  value: Int, 
  onValueChange: (Int) -> Unit, 
  startAt:Int, 
  range: IntRange, 
  units: String
) {
  TextField(
    value = value.toString(),
    onValueChange= {
      // TODO: validation here
      onValueChange(it.toInt())
    }
  )
}
but this is kind of skirting around the other part of your question, so i’ll try to ignore the numberInput and explain what to do here more generally
state
will only invalidate the composition when the value itself is set
c

codeslubber

02/28/2020, 8:35 PM
yeah lemme change to that, I have done that approach before…
right so nesting a bunch of things and expecting subfields to trigger rerenders is delusional
l

Leland Richardson [G]

02/28/2020, 8:36 PM
so if you have
state { TimerState() }
and you mutate a property on the
TimerState
instance itself, there is no way compose will recognize that and recompose
👍🏻 1
one option you have is to use
@Model
👍🏻 1
@Model will cause properties on that class to be observable in this way
so you could have
Copy code
@Model
class TimerState(hours:TimeDigit = TimeDigit(0), minutes:TimeDigit = TimeDigit(0), seconds:TimeDigit = TimeDigit(0)) {
    var currentStatus:String = {
        "$hours:$minutes:$seconds"
    }()
    var amountOfSkinInput = TextFieldValue("")
    var amountOfSkin:Int = 50
}
c

codeslubber

02/28/2020, 8:36 PM
I have done that too, sure.
l

Leland Richardson [G]

02/28/2020, 8:37 PM
another way to do this is to not use @Model, and instead use the class as a value type. one way to make this easier is with kotlin’s data class
c

codeslubber

02/28/2020, 8:37 PM
but you prefer the first approach right? (of course will end up with model eventually right)?
sure
yeah I was thinking also of doing something like Redux at some point: just have each thing like editing a field invoke a reducer, which would then spit out a new version of the whole state value
l

Leland Richardson [G]

02/28/2020, 8:38 PM
data class TimerState(...)
and then have
var timerState by state { TimerState(…) }
Then when you want to have an invalidation, you can do
timerState = timerState.copy(…)
👍🏻 1
finally, there is a third option, which is to use
mutableStateOf
c

codeslubber

02/28/2020, 8:39 PM
and that would be a good way to tee up the redux approach…
l

Leland Richardson [G]

02/28/2020, 8:39 PM
Copy code
class TimerState(hours:TimeDigit = TimeDigit(0), minutes:TimeDigit = TimeDigit(0), seconds:TimeDigit = TimeDigit(0)) {
    val currentStatus:String = {
        "$hours:$minutes:$seconds"
    }()
    var amountOfSkinInput by mutableStateOf("...")
    var amountOfSkin:Int = 50
}
so with this, you can modify
amountOfSkinInput
and since it is backed by a
MutableState
instance, it will be observed by compose
c

codeslubber

02/28/2020, 8:39 PM
interesting!
l

Leland Richardson [G]

02/28/2020, 8:40 PM
generally speaking, you could say: 1. “never” mutate a
var
when using compose unless it is a
State
property delegate, or a property on a @Model instance
that is, “never” if you are expecting compose to do anything about it
c

codeslubber

02/28/2020, 8:41 PM
Wow great. Would it be worthwhile to write some of this up on Medium or something? I feel bad that I made you school me on state management, but I was trying to start yet another component and see how far I could get with it. So in my other projects I had proceeded on the model, but was left with the impression that state is still useful in a lot of cases.
l

Leland Richardson [G]

02/28/2020, 8:41 PM
personally, i prefer to push people towards using
state
and immutable value types only
it feels easier to track what’s going on to me
but
@Model
also has its uses, and others might disagree with me
👍🏻 1
the problem with a medium article is that some of these things are still in flux and we are still discussing a lot of it internally
so we want to hold off on anything official feeling until we have a consensus on what we should recommend
but definitely i’ve been pining to write my own blog post on state management in compose for some time now
i am curious about something: why are you using the
TextFieldValue
overload and not the
String
overload of
TextField
?
c

codeslubber

02/28/2020, 8:44 PM
Well, so one of the reasons I was doing this is these 2 guys who do videos in the ios world, they did a stopwatch series. And they introduced the concept of a Lap. Ones they did that they had to restart the timer, etc. They did NOT do Redux, but would make so much sense: the state when it is changed, should really trigger saving the accumulated state (as a lap).
l

Leland Richardson [G]

02/28/2020, 8:44 PM
personally i always try to avoid using this unless there’s a reason i need it
c

codeslubber

02/28/2020, 8:45 PM
I was using that because I had
EditorModel
in there and then found
TextFieldValue
was the replacement
but I agree. Thanks so much for taking the time @Leland Richardson [G] because everything you are saying underlines what I am trying to do: keep things as spare as possible, and not fall into some of the pits we got used to in other frameworks (shared mutable state)…
l

Leland Richardson [G]

02/28/2020, 8:47 PM
👍
if you are interested in redux-like patterns, i wrote these up a while ago playing around with some things. you might be interested: https://gist.github.com/lelandrichardson/be118f65f19e3246e6df66d9f114dc47
1
c

codeslubber

02/28/2020, 8:53 PM
oh yeah I forgot about that!
haha I just went and saw on my twitter feed that one of my cranky agile friends is excoriating the use of documentation, as it’s always out of date.. funny… the one thing I do think would have been amazing for Compose would have been to make it testable as unit tests, as much as I hate to agree with judgment billowing agile scolds… 🙂
l

Leland Richardson [G]

02/28/2020, 8:58 PM
testing compose as unit tests will work. there’s no fundamental mismatch here, we just need to provide some utilities to do this easily
💯 1
c

codeslubber

02/28/2020, 9:33 PM
Love your reduce code @Leland Richardson [G], very nice, and clean… have you used it? or are you aware of anyone who used it in a project?
l

Leland Richardson [G]

02/28/2020, 9:37 PM
i’ve only played around with it or some variant of it as a concept, not used it in a realistic situation
✔️ 1
not sure what the right names for these things ought to be, but some of them are interesting ot think about
the
history
one has some interesting use cases i think
c

codeslubber

02/28/2020, 9:51 PM
yeah, when Redux first broke, that was much discussed but then almost never see it in the wild
so for implementing audit trails, undoes, etc.
I will try and use these classes for what I am doing
@Leland Richardson [G] surprise your pest of the day has another question: do you see any value to the idea of writing some state classes and then some unit tests that use your classes?
l

Leland Richardson [G]

02/28/2020, 10:20 PM
what do you mean by state classes?
c

codeslubber

02/28/2020, 10:37 PM
Well one of the great things about redux is you can make state classes, like my TimerState
and then you define the changes that you allow as reducers
and then write a bunch of unit tests that prove that the reducer logic all works, so when I change the amount of skin, I want it to take the running total (with the old amount of skin) and add that to your History list, and start running a new timer with the new value
when you click stop, I will take those individual sessions and combine them into a CompoundSession… and that’s the whole thing
make sense?
l

Leland Richardson [G]

02/28/2020, 10:41 PM
i mean if you are trying to go for something like redux, i would say that the state is just a piece of data. there’s not really anything to test. the thing you want to test is the reducer
c

codeslubber

02/28/2020, 10:45 PM
well yeah of course
I guess I wasn’t very clear
even that for what I want to do is kind of silly, because all I am doing is basically versioning with values, but if there are ways that values might interact with each other, that logic would exist in the reducers and the proof that the chain of logical implications of an action worked would be proven by the test
I am also just thinking it would be more fun to learn the classes you wrote in some tests, or a sample project..
seriously stupid question: do the reducer classes compile for you
?
l

Leland Richardson [G]

02/28/2020, 11:26 PM
sorry what do you mean?
c

codeslubber

02/28/2020, 11:27 PM
I just put the reducers code you had in the gist into the project
but it does not compile.. there are clearly a few things I have to add of my own code, but there are other things that just look like compile issues
l

Leland Richardson [G]

02/28/2020, 11:28 PM
looks like some parens got removed somewhere along the line
👍🏻 1
is that what you meant?
i’m copy/pasting these from a google doc so sometimes there is some… lost in translation issues
c

codeslubber

02/28/2020, 11:30 PM
there were more fixed a lot of them…
l

Leland Richardson [G]

02/28/2020, 11:31 PM
oh and currentComposer doesn’t exist for you 🙂
c

codeslubber

02/28/2020, 11:31 PM
yeah
and then there’s this:
l

Leland Richardson [G]

02/28/2020, 11:31 PM
currentComposerNonNull is the equivalent
c

codeslubber

02/28/2020, 11:31 PM
l

Leland Richardson [G]

02/28/2020, 11:31 PM
for dev05
c

codeslubber

02/28/2020, 11:31 PM
ok
l

Leland Richardson [G]

02/28/2020, 11:32 PM
currentComposer will be a public composable in dev06
c

codeslubber

02/28/2020, 11:32 PM
nice!
l

Leland Richardson [G]

02/28/2020, 11:32 PM
that was just some a stub i had there to explain to people before .current existed
🙂
half of the code i write is in google docs and not IDEs 😛 sometimes my internal compiler isn’t as good as the real thing
👍🏻 1
c

codeslubber

02/28/2020, 11:33 PM
so am I just being a 👶🏻 ?
doesn’t like
Ref
either, it’s solution to import it from java.sql is of course wrong…
l

Leland Richardson [G]

02/28/2020, 11:35 PM
class Ref<T>(var value: T)
that is Ref’s very complicated implementation
lol
(potentially add a
private
to the front of it)
FYI, this is a “safer” version of
reduce
Copy code
@Composable fun <T> reduce(value: T, reducer: (T, T) -> T): T = {
  var result = value
  var first = false
  val ref = remember { first = true; Ref(value) }
  if (!first) {
    result = reducer(ref.value, value)
  }
  onCommit { ref.value = result }
  return result
}
the first one wouldn’t be safe to use if compose becomes multithreaded
c

codeslubber

02/28/2020, 11:38 PM
arguments were reversed here:
Copy code
ref.value = reducer(ref.value, value)
isn’t it telling you use { or equals but not both?
(scala flashback here?…)
l

Leland Richardson [G]

02/28/2020, 11:40 PM
where does it say that?
c

codeslubber

02/29/2020, 12:01 AM
sorry lemme get
on reduce: the definition has an = {
just removing the = makes it compile 🙂
l

Leland Richardson [G]

02/29/2020, 12:08 AM
oh
yeah, also an error
i should compile these things more
part of the problem is i wrote them so long ago that none of these APIs were the same, and so i initially wrote them in an IDE but i “upgraded” them in a doc 🙂
c

codeslubber

02/29/2020, 12:10 AM
down to 12 errors!
how would this compile?
Copy code
// in the case of the first run, does this return null.... or do we just pass the value that came in???
@Composable
fun prev(value: T): T? {
of course it’s saying it doesn’t know what T is?
and this is not compiling:
Copy code
@Composable val Ambient<T>.current: T
and this is not going:
(meaning the next and update calls…)
l

Leland Richardson [G]

02/29/2020, 12:13 AM
sigh, one sec
c

codeslubber

02/29/2020, 12:14 AM
sorry no rush on this if you wanna just leave me to piece out the solutions.. don’t mean to demand counter service!
l

Leland Richardson [G]

02/29/2020, 12:18 AM
no worries
i updated the gist
should compile now, and only uses public API
c

codeslubber

02/29/2020, 1:11 AM
omg so it compiles
but now it’s showing me that same code generation error that the typography made appear….
😞
with the whole file commented out, it compiles and runs
l

Leland Richardson [G]

02/29/2020, 1:15 AM
can you hover over the IllegalStateException and copy/paste it somewhere else?
it should contain the whole stack trace
c

codeslubber

02/29/2020, 1:25 AM
I tried so many ways?? ugh lemme try again
l

Leland Richardson [G]

02/29/2020, 1:26 AM
i think also if you go up to the top line where it says “Build failed”, that renders the full thing on the right hand side then
c

codeslubber

02/29/2020, 2:22 AM
Copy code
e: java.lang.IllegalStateException: Backend Internal error: Exception during code generation
Element is unknownThe root cause java.util.NoSuchElementException was thrown at: androidx.compose.plugins.kotlin.compiler.lower.ComposableCallTransformer.irComposableExpr(ComposableCallTransformer.kt:1362)
	at org.jetbrains.kotlin.codegen.CompilationErrorHandler.lambda$static$0(CompilationErrorHandler.java:35)
	at org.jetbrains.kotlin.backend.jvm.JvmBackendFacade.doGenerateFilesInternal$backend_jvm(JvmBackendFacade.kt:93)
	at org.jetbrains.kotlin.backend.jvm.JvmBackendFacade.doGenerateFilesInternal$backend_jvm$default(JvmBackendFacade.kt:64)
	at org.jetbrains.kotlin.backend.jvm.JvmBackendFacade.doGenerateFilesInternal$backend_jvm(JvmBackendFacade.kt:52)
	at org.jetbrains.kotlin.backend.jvm.JvmIrCodegenFactory.generateModule(JvmIrCodegenFactory.kt:36)
	at org.jetbrains.kotlin.codegen.KotlinCodegenFacade.doGenerateFiles(KotlinCodegenFacade.java:47)
	at org.jetbrains.kotlin.codegen.KotlinCodegenFacade.compileCorrectFiles(KotlinCodegenFacade.java:39)
	at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.generate(KotlinToJVMBytecodeCompiler.kt:638)
	at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.compileModules$cli(KotlinToJVMBytecodeCompiler.kt:198)
	at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:172)
	at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:56)
	at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:85)
	at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:43)
	at org.jetbrains.kotlin.cli.common.CLITool.exec(CLITool.kt:104)
	at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:349)
	at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:105)
	at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileIncrementally(IncrementalCompilerRunner.kt:237)
	at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:88)
	at org.jetbrains.kotlin.daemon.CompileServiceImplBase.execIncrementalCompiler(CompileServiceImpl.kt:606)
	at org.jetbrains.kotlin.daemon.CompileServiceImplBase.access$execIncrementalCompiler(CompileServiceImpl.kt:99)
	at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1645)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:357)
	at sun.rmi.transport.Transport$1.run(Transport.java:200)
	at sun.rmi.transport.Transport$1.run(Transport.java:197)
	at java.security.AccessController.doPrivileged(Native Method)
	at sun.rmi.transport.Transport.serviceCall(Transport.java:196)
	at sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:573)
	at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:834)
	at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:688)
	at java.security.AccessController.doPrivileged(Native Method)
	at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:687)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
Caused by: java.util.NoSuchElementException: Collection contains no element matching the predicate.
	at androidx.compose.plugins.kotlin.compiler.lower.ComposableCallTransformer.irComposableExpr(ComposableCallTransformer.kt:1362)
	at androidx.compose.plugins.kotlin.compiler.lower.ComposableCallTransformer.visitBlock(ComposableCallTransformer.kt:252)
	at org.jetbrains.kotlin.ir.visitors.IrElementTransformerVoid.visitBlock(IrElementTransformerVoid.kt:128)
	at org.jetbrains.kotlin.ir.visitors.IrElementTransformerVoid.visitBlock(IrElementTransformerVoid.kt:24)
	at org.jetbrains.kotlin.ir.expressions.impl.IrBlockImpl.accept(IrBlockImpl.kt:52)
omg, the split window was collapsed for some reason…
l

Leland Richardson [G]

02/29/2020, 3:01 AM
java.util.NoSuchElementException was thrown at: androidx.compose.plugins.kotlin.compiler.lower.ComposableCallTransformer.irComposableExpr(ComposableCallTransformer.kt:1362)
This exception happens when the compose runtime module can’t be found on the classpath, or the newer compiler has an older version of compose runtime on the classpath (like dev03). I’m not sure if this is the reason for your issue or not, but can you paste your build.gradle file?
c

codeslubber

02/29/2020, 3:26 AM
sure sorry I did not get a notification about this message
I made this project using the Compose template and then updated only the gradle plugin, did see that it was using
dev05
of course…
Copy code
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 29

    defaultConfig {
        applicationId "com.ontometrics.dminder.suntimer"
        minSdkVersion 21
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), '<http://proguard-rules.pro|proguard-rules.pro>'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
    buildFeatures {
        compose true
    }
}

dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar"])
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.2.0'
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.ui:ui-framework:0.1.0-dev05'
    implementation 'androidx.ui:ui-layout:0.1.0-dev05'
    implementation 'androidx.ui:ui-material:0.1.0-dev05'
    implementation 'androidx.ui:ui-tooling:0.1.0-dev05'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'

}
Looks like it does not have the compose options setting?
lemme try to add that
jesus that fixed it.. 👏🏻
thanks @Leland Richardson [G] I thought it was probably a build issue but figured if that was required the template would have it (stupid), I did add it to my other project..
l

Leland Richardson [G]

02/29/2020, 3:54 AM
No worries. This stuff is kind of a mess right now!
👍🏻 1
26 Views