Jetpack compose LazyColumn+Coil-Compose doesn't work - android-jetpack-compose

Jetpack compose project
I use Coil-Compose rememberImagePainter
dependencies {
implementation 'io.coil-kt:coil-compose:1.4.0'
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ApplicationTheme {
Surface(color = MaterialTheme.colors.background) {
Image(
painter = rememberImagePainter("https://api.dujin.org/bing/1366.php"),
contentDescription = null
)
}
}
}
}
}
enter image description here
add LazyColumn doesn't work
…………
import coil.compose.rememberImagePainter
…………
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ApplicationTheme {
Surface(color = MaterialTheme.colors.background) {
+ LazyColumn {
+ items(10) {
Text(text = "test")
// doesn't work
Image(
painter = rememberImagePainter("https://api.dujin.org/bing/1366.php"),
contentDescription = null
)
+ }
+ }
}
}
}
}
}
enter image description here
Please help me

In LazyColumn or LazyRow is no problem with local resources, but network pictures you need to specify the width and height
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ApplicationTheme {
Surface(color = MaterialTheme.colors.background) {
LazyColumn {
items(10) {
Text(text = "test")
// doesn't work
Image(
painter = rememberImagePainter("https://api.dujin.org/bing/1366.php"),
contentDescription = null,
Modifier.fillMaxSize().height(300.dp)
)
}
}
}
}
}
}
}

if useful for you too, I've solved this problem with recovering images with Coil from remote resources putting in lazycolumn/lazyrow
modifier = Modifier.fillMaxHeight(0.5f),
with different percentage.

Related

Jetpack Compose - Detect when LazyColumn's scroll position is at the first index

I want to have a Scroll back to top button for my LazyColumn. I successfully made the button work. But I want it to not be visible if I'm already at the top of the LazyColumn. How could I achieve this?
LazyColumn has state property, and if you pass your custom value instead of the default one, you can react on the state changes.
To prevent redundant recompositions, in such cases derivedStateOf should be used: it'll trigger recomposition only when the produced result, based on other state variables, is changed:
Box {
val state = rememberLazyListState()
LazyColumn(state = state) {
// ...
}
val firstItemVisible by remember {
derivedStateOf {
state.firstVisibleItemIndex == 0
}
}
if (!firstItemVisible) {
Button(onClick = { /*TODO*/ }) {
}
}
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startActivity(intent)
setContent {
val scrollState = rememberLazyListState()
val coroutineScope = rememberCoroutineScope()
Column(modifier = Modifier.fillMaxSize()) {
if (scrollState.firstVisibleItemIndex > 0) {
Button(onClick = {
coroutineScope.launch {
scrollState.scrollToItem(0)
}
}, enabled = scrollState.firstVisibleItemIndex > 0) {
Text("Scroll to top")
}
}
LazyColumn(modifier = Modifier.fillMaxSize(), state = scrollState) {
items(MutableList(100) { it }) { i ->
Text(i.toString())
}
}
}
}
}
}

Getting the error message: #Composable invocations can only happen from the context of a #Composable function

I'm getting the following error message from AS's Build Output:
#Composable invocations can only happen from the context of a
#Composable function
It occurs when I try to call the IconButton compose function in of navigationIcon and actions parameters.
How could I fix that error in my code? I've been stuck in that one for few hours I don't know what to do anymore.
By the way I'm using Compose version 1.0.3.
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.RowScope
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent() {
MainTheme()
}
}
}
#Preview(showBackground = true, name = "Text preview")
#Composable
fun DefaultPreview() {
MainTheme()
}
#Composable
fun Greeting(name: String) {
Text(text = "Hello $name!")
}
#Composable
fun MainTheme() {
TopAppBar(
title = { "Coinwatcher" },
navigationIcon = {
IconButton(onClick = { /* doSomething() */ }) {
Icon(Icons.Filled.Menu, contentDescription = null)
}
},
actions = {
IconButton(onClick = {/* doSomething() */}) {
Icon(Icons.Filled.Refresh, contentDescription = null)
}
}
)
Surface(color = MaterialTheme.colors.background) {
Greeting("Android")
}
}
#Composable
fun TopAppBar(
title: () -> Unit,
modifier: Modifier = Modifier,
navigationIcon: () -> Unit,
actions: RowScope.() -> Unit = {},
backgroundColor: Color = MaterialTheme.colors.primarySurface,
contentColor: Color = contentColorFor(backgroundColor),
elevation: Dp = AppBarDefaults.TopAppBarElevation
): #Composable Unit { }
#Composable
fun IconButton(
onClick: () -> Unit,
modifier: Modifier,
enabled: Boolean,
interactionSource: MutableInteractionSource,
content: () -> Unit
) { }
#Composable
fun Icon(
painter: Painter,
contentDescription: String?,
modifier: Modifier,
tint: Color
) { }

multi-dimensional state in jetpack compose

How can I make multidimensional state in compose that also effects the source the state came from?
I tried to ask this question without code previously and it wasn't clear. This is a little more code than necessary, but I will be able to communicate my problem
data class Record(
var duration: Int,
val timeStamp: Instant = Clock.System.now(),
var description: String? = null
)
data class Task(
var name: String,
var defaultAmount: Int,
val completions: MutableList<Record> = mutableListOf()
) {
fun createRecord(description: String? = null, duration: Int = defaultAmount): Record {
val record = Record(duration, description = description)
completions.add(record)
return record
}
}
class SourceOfTruth(private val thisSavesMeTyping: MutableList<Task> = mutableListOf()) :
MutableCollection<Task> by thisSavesMeTyping {
fun print() = thisSavesMeTyping.forEach { Log.d("ugh", it.toString()) }
}
class MyModel : ViewModel() {
val source: SourceOfTruth =
SourceOfTruth().apply {
add(
Task("exercise", 30).apply {
createRecord("push ups")
createRecord("sit ups")
})
add(Task("dishes", 10))
}
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val myModel: MyModel by viewModels()
val tasks = myModel.source.toMutableStateList()
Column {
tasks.forEach { task ->
Row {
Text(task.name)
task.completions.forEach { Text(it.timeStamp.toString().dropLast(15)) }
Button(
onClick = {
task.createRecord("dummy")
myModel.source.print()
}) { Text("now") }
}
}
Button(
onClick = {
tasks.add(Task("dummy task", 90))
myModel.source.print()
}) { Text("new Task") }
}
}
}
}
Pushing the now button updates the source in the viewmodel, but doesn't trigger recomposition. Pushing the add task button triggers recomposition, but doesn't add a task to the source of truth.
Any suggestions on how I can fix those?
If you read all that, Thank you very much. I wish I could buy you a coffee or beer.

Remember list item states while navigation in Jetpack Compose

If we create state for list item like val state = remember(it) { mutableStateOf(ItemState()) } then we loose expanded state while scrolling.
If we lift up states generating higher before LazyColumn then expand state is saving properly
val states = items.map { remember(it) { mutableStateOf(ItemState()) } }
LazyColumn(modifier = Modifier.fillMaxSize()) {....
But when we expand an item, click the button, go to details screen and then go back to items we loose expanded state.
What is the best way to save items state?
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
RememberStateTheme {
val navController = rememberNavController()
NavHost(navController, startDestination = "items") {
composable("items") {
Greeting(
onItemClick = { navController.navigate("details/$it") }
)
}
composable(
"details/{index}",
arguments = listOf(navArgument("index") { type = NavType.IntType })
) { backStackEntry ->
DetailsScreen(backStackEntry.arguments?.getInt("index") ?: -1)
}
}
}
}
}
}
#Composable
fun Greeting(
onItemClick: (Int) -> Unit
) {
val items = remember { (0..100).toList() }
Surface(color = MaterialTheme.colors.background) {
val states = items.map { remember(it) { mutableStateOf(ItemState()) } }
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(items) { item ->
// If we create state here like val state = remember(it) { mutableStateOf(ItemState()) }
// then we loose expanded state while scrolling
// If we lift up states generating higher before LazyColumn then expand state
// is saving properly
//
// But when we expand an item, click the button and then go back to items we loose
// expanded state
val state = states[item]
key(item) {
Item(index = item,
state = state.value,
onClick = { onItemClick(item) },
modifier = Modifier
.fillMaxSize()
.clickable {
state.value.changeState()
}
)
}
Divider()
}
}
}
}
#Composable
fun Item(
index: Int,
state: ItemState,
modifier: Modifier = Modifier,
onClick: () -> Unit
) {
Box(modifier = modifier) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.align(Alignment.Center)
) {
Text(
text = index.toString(),
modifier = Modifier.padding(16.dp)
)
if (state.expanded) {
Button(
onClick = onClick,
modifier = Modifier.padding(8.dp)
) {
Text(text = "Click me")
}
}
}
}
}
class ItemState {
val expanded: Boolean
get() = _expanded.value
private val _expanded = mutableStateOf(false)
fun changeState() {
_expanded.value = !_expanded.value
}
}
#Composable
fun DetailsScreen(
index: Int,
modifier: Modifier = Modifier
) {
Box(
modifier = modifier
.fillMaxSize()
.background(Color.Gray.copy(alpha = 0.3f))
) {
Text(
text = index.toString(),
modifier = Modifier.align(Alignment.Center)
)
}
}
Using rememberSaveable solves the problem.
Thanks to https://stackoverflow.com/users/1424349/leland-richardson
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(items) { item ->
val state = rememberSaveable(item) { mutableStateOf(ItemState()) }
key(item) {
Item(index = item,
state = state.value,
onClick = { onItemClick(item) },
modifier = Modifier
.fillMaxSize()
.clickable {
state.value.changeState()
}
)
}
Divider()
}
}
But when we expand an item, click the button, go to details screen and then go back to items we loose expanded state.
Correct. remember() remembers for the scope of a specific composition. This means it remembers across recompositions, but not when the composition is replaced by a separate composition. In your case, navigation replaces your Greeting() composition with a DetailsScreen() composition.
What is the best way to save items state?
Hoist the state further, to a composition that does not get replaced by navigation. In this case, that would be your root composition, where you have your rememberNavController() call.
Or, have the state be stored in a viewmodel that is scoped to your activity, or at least to that root composition.
If you want to have this state persist beyond the life of your process, I think that the vision is that we should use effects to save the state via a repository to some persistent store (e.g., JSON file) and restore the state from that store. However, I have not experimented with this approach yet.

How can I take reference of Keyboard View in Jetpack Compose?

Dialog moves up when keyboard appears. I want a Composable do the same , but i cant find how to do this.
Ok, I've found the answer.
First, in manifest:
<activity
android:windowSoftInputMode="adjustResize">
After that use "Insets", that are explained in this page:
https://google.github.io/accompanist/insets/
in the following example I use a fab that makes a textfield appear that is positioned above the keyboard
PD: Notice in FloatingActionButton I use Modifier.systemBarsPadding(), it is made so that it is not hidden
class MainActivity : ComponentActivity() {
#ExperimentalAnimatedInsets
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {
MyApplicationTheme {
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
val focusRequester = FocusRequester()
Greeting(focusRequester)
}
}
}
}
}
#ExperimentalAnimatedInsets
#Composable
fun Greeting(focusRequester: FocusRequester) {
var creatorVisibility by remember{ mutableStateOf(false)}
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
Scaffold(floatingActionButton = {
Fab(onClick = { creatorVisibility = true }, creatorVisibility = creatorVisibility)}
) {
ConstraintLayout(constraintSet = constraints, modifier = Modifier.fillMaxSize(1f)) {
TextField(
value = TextFieldValue(),
onValueChange = { /*TODO*/ },
modifier = Modifier.layoutId("mainTf")
)
if (creatorVisibility){
Box(
modifier = Modifier
.fillMaxSize()
.clickable {
creatorVisibility = false
}
.background(color = Color(0f, 0f, 0f, 0.5f))
,contentAlignment = Alignment.BottomCenter
) {
SideEffect(effect = { focusRequester.requestFocus() })
TextField(colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.White),
value = TextFieldValue(),
onValueChange = { /*TODO*/ },
modifier = Modifier
.layoutId("textField")
.imePadding()
.focusRequester(focusRequester = focusRequester)
)
}
}
}
}
}
}
#Composable
fun Fab(onClick : () -> Unit, creatorVisibility:Boolean){
if (!creatorVisibility){
FloatingActionButton(
onClick = { onClick() },
modifier = Modifier.layoutId("fab").systemBarsPadding()
) {
Text(text = "Crear tarea", modifier = Modifier.padding(start = 10.dp, end = 10.dp))
}
}
}
val constraints = ConstraintSet {
val textField = createRefFor("textField")
val mainTf = createRefFor("mainTf")
val fab = createRefFor("fab")
constrain(textField){
start.linkTo(parent.start)
end.linkTo(parent.end)
bottom.linkTo(parent.bottom)
}
constrain(mainTf){
start.linkTo(parent.start)
end.linkTo(parent.end)
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
}
constrain(fab){
end.linkTo(parent.end)
bottom.linkTo(parent.bottom)
}
}

Resources