How can you create a reusable modifier in Compose? - kotlin

Most of my screens(views) start out with a Box composable to contain all the child composables(components).
// background
Box(
modifier = Modifier
.fillMaxSize()
.background(customColor)
)
{
// foreground
Column()
}
Instead of having to create the modifier properties on every screen, how would you create a reusable modifier?

In such a simple case, you can do the following (no need to use then):
fun Modifier.myModifier(customColor: Color) =
this.fillMaxSize()
.background(customColor)
In case you need to save any state in your modifier using remember or other state builders annotated with #Composable, you can do it with Modifier.composed. For example here's how you can add animation to your modifier:
fun Modifier.myModifier(customColor: Color): Modifier = composed {
val animatedColor by animateColorAsState(customColor)
this.fillMaxSize()
.background(animatedColor)
}
Note, that inside composed you should only use state annotated composables, not views. More details can be found here.

You can create it like this
fun Modifier.myModifier(customColor: Color) = this.then(
Modifier.fillMaxSize().background(customColor)
)

Related

Compose: Proper way of extending the RowScope Modifier? (Or any Scope Modifier for that matter)

I'm creating a Row of Text Composables, imagine it like a table row.
Image: https://i.stack.imgur.com/jR3oB.png
If you care for the backstory:
The amount of "cells" of my custom Row Composable is dynamic and I use Box Composables to wrap the Text in the "cells". I need to apply different Modifier parameters to each Box, which I first tried to achieve via creating a Modifier extension.
Code of my Modifier extension:
#SuppressLint("ComposableModifierFactory", "ModifierFactoryExtensionFunction") // I wish I could. The RowScope requirement makes doing it the easy way impossible
#Composable
fun RowScope.rowOfTextCellsBoxModifier(
index: Int,
columnWidths: ArrayList<Dp?>,
columnWeights: ArrayList<Float?>
): Modifier {
#Suppress("RedundantExplicitType") // bull**** (compose kotlinCompilerExtensionVersion 1.3.2)
var returnModifier: Modifier = Modifier
if (index != columnWidths.size - 1)
returnModifier = returnModifier.then(Modifier.verticalEndLine())
returnModifier = if (columnWidths[index] != null)
returnModifier.then(Modifier.width(columnWidths[index]!!))
else {
val weight = try {
columnWeights[index]
} catch (ignored: Throwable) {
1f
}
returnModifier.then(Modifier.weight(weight!!))
}
return returnModifier
}
I moved on to just generating the entire Box in a #Composable function, but that's beside the point ;)
As you can see, I want to add a Modifier.weight() at one point.
However, .weight() is a Modifier function that is only applicable in certain Scopes, like in RowScope. Which means that my "Modifier extension" needs to apply to RowScope, which turned out impossible to find instructions on.
So, imagine I hadn't found a better solution:
What would be the proper way of extending the RowScope Interface's Modifier?
Usually, when you need to create a Composable that is applicable to certain scopes, you write something like:
RowScope.YourComposable() {...}
And when you extend Modifier, you write:
Modifier.yourExtension() {...}
But that does not work for Scoped Modifiers:
RowScope.Modifier.YourModifierExtension() {...}
is invalid code.

Jetpack Compose correct accessing of data from ViewModel

today I'm trying about Jetpack compose and the ViewModel class.
Everything seems to be logic but I got some problems understanding the encapsulation.
// ViewModel
class MyViewModel: ViewModel() {
var uiStateList = mutableStateListOf(1,2,3)
privat set
private var _uiStateList2 = mutableStateListOf(11,22,33)
val uiState2 List<Int>
get() = _uiStateList2
fun addToUiStateList2() {
_uiStateList2.add(_uiStateList2.last()+11)
}
}
The first var is like in the Compose tutorials.
The construction of the second var looks like using MutableLiveData.
// Activity
#Composable
fun MyComposable(
modifier: Modifier = Modifier,
myViewModel: MyViewModel= viewModel()
) {
val uiStateList = myViewModel.uiStateList
val uiStateList2 = myViewModel.uiStateList2
Column {
uiStateList.forEach {
Text(it.toString())
}
Button(
onclick = {uiStateList.add(uiStateList.last()+1)},
content = {text(+1)}
)
uiStateList2.forEach {
Text(it.toString())
}
Button(
onclick = {
// uiStateList2.add(uiStateList2.last()+11) // not possible because of type List<Int>
myViewModel.addToUiStateList2() // possible, changing indirectly the value of uiStateList2
},
content = {text(+11)}
)
}
}
In the first case it's possible to modify the mutableStateList inside of the ViewModel by running just a simple function in the composable. Consequently it is possible to change the value from outside of the class directly.
In the second case I got no chance to change the data in the viewmodel. The var uiStateList2 is a imutable list which reflects the data from the private val from the viewmodel. If the function addToUiStateList2() is triggered, the original list changes and the composable will be recompositioned and everything is fine.
Why can I change the data of the var uiStateList inside the Composable, although the setter is set to private inside the ViewModel class?
In the documentation I read, that private setters could just be used inside the owning class. Do I think too complicated, or is this the usually aproach how everything is build?
Thanks for help Guys!
Private setter doesn't allow to set the value i.e myViewModel.uiStateList= //something. But you can still modify the list because it is mutable. If you want to restrict changing the state from outside viewModel second approach is preferred.

How can I remember a string which is from stringArrayResource( ) when I use Jetpack Compose?

I hope to remember a string which is from stringArrayResource in Code A , but I get the the Error A. How can I fix it?
And more, I find more variables can't be wrapped with remember, such as val context = LocalContext.current , why?
Error A
Composable calls are not allowed inside the calculation parameter of inline fun remember(calculation: () -> TypeVariable(T)): TypeVariable(T)
Code A
#Composable
fun DialogForDBWarningValue(
preferenceState:PreferenceState
) {
val context = LocalContext.current //I can't wrap with remember
val itemName =remember{ stringArrayResource(R.array.dBWarning_Name) } //I can't wrap with remember
}
#Composable
inline fun <T> remember(calculation: #DisallowComposableCalls () -> T): T =
currentComposer.cache(false, calculation)
The reason for that error is #DisallowComposableCalls annotation
This will prevent composable calls from happening inside of the
function that it applies to. This is usually applied to lambda
parameters of inline composable functions that ought to be inlined but
cannot safely have composable calls in them.
I don't know if accessing resources and getting strings would have any impact on performance but as an alternative this can be done using nullable properties, i don't think it's good practice to have nullable objects while you don't have to, by only getting resources once your String is null or an object that holds Strings and sets them on Composition or configuration changes if you wish to change new ones.
class StringHolder() {
var str: String = ""
}
val stringHolder = remember(LocalConfiguration.current) {
StringHolder()
}.apply {
this.str = getString(R.string.dBWarning_Name)
}

How to use Jetpack Compose Material theme color inside Kotlin enum class? [duplicate]

I wish to provide a parameter to a #Composable function that accepts a color, but only one from a discrete list.
Something along the lines of this:
enum class SpecialColor(val color: Color) {
ALPHA(MaterialTheme.colors.onSurface),
BETA(MaterialTheme.colors.onSecondary)
}
#Composable
fun ColorSample(specialColor: SpecialColor) {
Box(
modifier = Modifier
.width(100.dp)
.height(100.dp)
.background(specialColor.color)
)
}
#Preview
#Composable
fun PreviewSample() {
CustomTheme {
ColorSample(specialColor = SpecialColor.ALPHA)
}
}
As the above is referencing MaterialTheme.colors outside a composable context, the following error occurs:
#Composable invocations can only happen from the context of a
#Composable function
If a color is referenced directly, instead of via MaterialTheme, the color won't properly update for things like light/dark mode.
One tactic might be to map enumerated values to MaterialTheme colors within the #Composable function itself. But this is cumbersome, bulky, and doesn't scale well - imagine a much longer list of colors and the same SpecialColor list desired to be reused for many function.
Another tactic might be to modify the theme colors directly with LocalContentColor or similar, but this is too broad of a brush and will change colors for more than just the targeted function.
Returning a color from a utility #Composable function is also not an option, as #Composable functions don't have return values.
So...
Any thoughts on how to provide one of an enumerated list of Compose Material colors as a parameter?
In a clean and extendable way that scales well?
This is actually not true:
#Composable functions don't have return values.
You can annotate both function with return types and even getter only properties with #Composable. For example this is part of Material theme source code.
Here's how you can define your color:
enum class SpecialColor {
ALPHA,
BETA,
;
val color: Color
#Composable
#ReadOnlyComposable
get() = when(this) {
ALPHA -> MaterialTheme.colors.onSurface
BETA -> MaterialTheme.colors.onSecondary
}
}

Observe and respond to changes in elements of an array

I'm experimenting with Jetpack Compose and am trying to make a Canvas with a number of rectangles, each of which is filled or not depending on the value of a Bool at a corresponding index of an array. When an element of that array changes, the Canvas should redraw.
I've discovered I can't simply make a LiveData array of Booleans, or a list, since for that to work the entire object needs to be recreated each time for setValue to trigger and be observed. So I've made an array of LiveData booleans in a view model;
class StripeModel : ViewModel() {
private val _values = Array<MutableLiveData<Boolean>>(50) { MutableLiveData(false) }
val values = Array<LiveData<Boolean>>(50) {i->_values[i]}
fun onValueChanged(index: Int, newVal: Boolean)
{
_values[index].value = newVal;
}
}
If I pass that view model to my test function, I can look at a particular member of it in a way that causes the canvas to recompose on change using something like
val state by model.values[5].observeAsState();
This would be fine if I had a different canvas for each element, but I don't. So I want my single canvas to be looking at all of them, and refresh if any change. The sensible way to do this without explicitly declaring a state variable for each member seemed to be to create an array of states, and the way I came up with to do that was
val states = Array<State<Boolean?>>(20){ i->model.values[i].observeAsState()}
However, this flags an error because observeAsState needs to be in a function marked #Composable. The outer function itself is, but it seems that's not inherited by the lambda. And if I try and mark the lambda as #Composable then it makes Android Studio very unhappy and tells me to report it as a bug. Doesn't crash the environment but I can't compile it.
The reason I have a strong desire to do this in a single canvas is because I want to be able to click a single item to change its value, or drag across a number of items to change a number of them all at once. That seems like it should be a lot more efficient by handling all the coordinates within one widget rather than having 50 separate widgets and trying to figure out which is at the present location during the drag.
So, how can I make my composable function observe n array elements without explicitly writing n lines that create n variables?
Following a few days away I've worked through some of the suggestions people have given.
#cactustictacs suggested the simple approach of making the LiveData array of Booleans. I hadn't actually tried this. Something I'd read made me think it wouldn't work so I tried going more complicated. However, I can't get it to work.
I've simplified the code so it's postable, minus imports.
class StripeModel : ViewModel() {
private val _values = MutableLiveData<Array<Boolean>>(Array<Boolean>(20) {false});
val values: LiveData<Array<Boolean>> = _values;
fun onValueChanged(index: Int, newVal: Boolean)
{
_values.value?.set(index, newVal);
_values.value=_values.value;
}
}
class MainActivity : ComponentActivity() {
private val stripeModel by viewModels<StripeModel>();
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Surface{
MaterialTheme{
TestCanvas(stripeModel);
}
}
}
}
}
#Composable
fun TestCanvas(model : StripeModel)
{
val state by model.values.observeAsState();
Canvas(
Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectTapGestures(
onPress = { it -> model.onValueChanged(5, !state?.get(5)!!) }
)
}
.background(Color.LightGray),
onDraw={
val offset = Offset(100f,100f);
val size = Size(200f,200f);
if (state?.get(5) == true) {
drawRect(
brush = SolidColor(Color.Blue),
size = size,
topLeft = offset
)
}
drawRect(
brush = SolidColor(Color.White),
size = size,
topLeft = offset,
style = Stroke(width = 10f)
)
}
)
}
So there's an arbitrary 20 elements of which I'm just looking at index 5. By changing the initialiser I can see that the value is being read on draw. In the debugger I can see that a tap on the screen fires onValueChanged which changes the stored value. However that doesn't cause TestCanvas to recompose.
#chuckj suggested using a mutableStateListOf<MutableState>. If I change my view model to
class StripeModel : ViewModel() {
val values = mutableStateListOf<MutableState<Boolean>>()
init {
for (i in 0..20)
{
var s = mutableStateOf<Boolean>(false);
values.add(s);
}
}
fun onValueChanged(index: Int, newVal: Boolean)
{
values[index] = mutableStateOf<Boolean>(newVal);
}
}
and I look at it using
val state = model.values;
the behaviour is the same- no display update on tap.
#Robert Nagy suggested a LiveData<List>. So I created the ViewModel as
class StripeModel : ViewModel() {
private val _values : MutableList<Boolean> = Array<Boolean>(20) {false}.toMutableList();
val values = mutableStateOf(_values);
fun onValueChanged(index: Int, newVal: Boolean)
{
_values.set(index,newVal);
values = values;
}
}
and look at it using
val state by model.values;
Here, it won't build if I include the line values = values. Otherwise, though it builds and runs, it still doesn't cause a recompose.
I've not pasted the whole of the code each time, but it's my understanding that by setting that 'state' value at the start of the composable, any change will trigger a re-run of that function from the start, so only that line is relevant?
So, thanks to those who've commented. Is there something I'm doing wrong that this edit's made apparent?
LiveData<List<T>> Should definitely work.
An example, Screens with lists are commonly modified with:
fun removeLastItem(){
_items.value = _items.value.dropLast(1)
}
I suspect something is off at the subscriber/observer.