Jetpack compose scrollable table - kotlin

I need to have a component that scrolls vertically and horizontally.
Here is what I did so far:
#Composable
fun Screen() {
val scope = rememberCoroutineScope()
val scrollOffset = remember {
mutableStateOf(value = 0F)
}
LazyColumn(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(space = 16.dp)
) {
items(10) {
SimpleRow(
onScrollOffsetChange = {
scrollOffset.value = it
}
)
}
}
}
#Composable
private fun SimpleRow(
onScrollOffsetChange: (Float) -> Unit,
) {
val lazyRowState = rememberLazyListState()
LazyRow(
modifier = Modifier.fillMaxWidth(),
state = lazyRowState
) {
item {
Text(
text = "firstText"
)
}
for (i in 1..30) {
item {
Text(text = "qwerty")
}
}
}
}
When anyone of these SimpleRow is scrolled I want to scroll all of the SimpleRows together.
And I do not know how many SimpleRows I have because they came from our server.
Is there any kind of composable or trick that can do it for me?

You can use multiple rows inside a column that scrolls horizontally and vertically, like this:
#Composable
fun MainContent() {
val scrollStateHorizontal = rememberScrollState()
val scrollStateVertical = rememberScrollState()
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(state = scrollStateVertical)
.horizontalScroll(state = scrollStateHorizontal)
) {
repeat(40){ c ->
Row {
repeat(40){ r ->
Item("col: $c | row: $r")
}
}
}
}
}
#Composable
fun Item(text: String) {
Text(
modifier = Modifier
.padding(5.dp)
.background(Color.Gray),
text = text,
color = Color.White
)
}

Related

How to implement Compose Navigation Arguments

I'm practicing Jetpack compose navigation, currently I'm stuck at passing arguments, so the correct information can be displayed when clicked.
I'm trying to navigate from this Destination, MenuScreen;
#Composable
fun HomeScreen(onHomeCardClick: () -> Unit) {
HomeContentScreen(onHomeCardClick = onHomeCardClick)
}
#Composable
fun HomeContentScreen(
modifier: Modifier = Modifier,
onHomeCardClick: () -> Unit
) {
Column(
modifier
.verticalScroll(rememberScrollState())
.padding(vertical = 16.dp)) {
HomeQuote()
....
}
}
To this destination, MenuInfoScreen;
#Composable
fun HomeInfoScreen(){
WelcomeText()
HomeInfoDetails()
}
#Composable
fun WelcomeText() {
Text(
text = "Welcome, to Home Information",
style = MaterialTheme.typography.h3,
modifier = Modifier.padding(horizontal = 12.dp, vertical = 18.dp)
)
}
#Composable
fun HomeInfoDetails(
homeInfo: HomeInfo
) {
Card(
modifier = Modifier
.padding(10.dp)
.clip(CircleShape),
elevation = 10.dp,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(10.dp)
) {
Image(
painter = painterResource(id = homeInfo.homeInfoImageId),
contentDescription = null,
modifier = Modifier
.fillMaxWidth()
.clip(shape = RoundedCornerShape(topEnd = 4.dp, bottomEnd = 4.dp)),
contentScale = ContentScale.Fit
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = homeInfo.title,
style = MaterialTheme.typography.h3
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = homeInfo.description,
style = MaterialTheme.typography.h5
)
}
}
}
Here's the model I'm trying to follow, HomeInfoModel;
object HomeInfoModel {
val homeInfoModelList = listOf(
HomeInfo(
id = 1,
title = "Monty",
sex = "Male",
age = 14,
description = "Monty enjoys chicken treats and cuddling while watching Seinfeld.",
homeInfoImageId = R.drawable.ab1_inversions
),
.....
)
}
Here's the my NavHost;
NavHost(
navController = navController,
startDestination = Home.route,
modifier = modifier
) {
// builder parameter will be defined here as the graph
composable(route = Home.route) {
HomeScreen(
onHomeCardClick = {}
)
}
....
composable(route = HomeInfoDestination.route) {
HomeInfoScreen()
}
}
}
And my Destinations file;
object Home : KetoDestination {
override val route = "home"
}
....
object HomeInfoDestination : KetoDestination{
override val route = "homeInfo"
}
I know this is a lot and I'm a bit off, but please I've been stuck here for more than a week now. Any little information willl surely come handy.
And Of Course very much appreciated.
Thanks for your help in advance.
You should extract the arguments from the NavBackStackEntry that is available in the lambda of the composable() function.
You can read more in the official android docs https://developer.android.com/jetpack/compose/navigation#nav-with-args

Recompose a Canvas while maintaining the previous drawing

This function draw figures on canvas but when I clicked on button its shows only latest clicked figure but I want to show all figures which was clicked previous.
I think drawScope state can be remembered as a mutable state but how to achieve it.
#Composable
fun PaintBrushLayout(
modifier: Modifier = Modifier,
) {
var clicked by remember {
mutableStateOf(false)
}
var figName by remember {
mutableStateOf("")
}
Column(
verticalArrangement = Arrangement.Bottom, modifier = modifier.fillMaxSize()
) {
Canvas(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(.9f)
) {
Log.d("in Paint", "PaintBrushLayout: $this")
if (clicked) {
Log.d("TAG", "PaintBrushLayout: $figName")
when (figName) {
"Circle" -> {
drawCircleFig(color = Color.Blue)
}
"Rectangle" -> {
drawRectangleFig()
}
"Oval" -> {
drawOvalFig(color = Color.Gray)
}
}
}
}
Row(
modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween
) {
BottomLayout("Circle") { click, name ->
clicked = click
figName = name
}
BottomLayout("Rectangle") { click, name ->
clicked = click
figName = name
}
BottomLayout("Oval") { click, name ->
clicked = click
figName = name
}
BottomLayout("Choose Color") { click, name ->
clicked = click
// figName = name
}
}
}
}
Create Bottom Layout of Buttons
#Composable
fun BottomLayout(
btnName: String,
click: (Boolean, String) -> Unit
) {
Button(onClick = {
click(true, btnName)
}, modifier = Modifier.padding(4.dp)) {
Text(text = btnName)
}
}
Draw Circle Figure on canvas
fun DrawScope.drawCircleFig(color: Color = Color.Red) {
drawCircle(
color = color
)
}
Draw Rectangle Figure on canvas
fun DrawScope.drawRectangleFig(color: Color = Color.Red) {
drawRect(
color = color
)
}
Draw Oval Figure on canvas
fun DrawScope.drawOvalFig(color: Color = Color.Red) {
drawOval(
color = color
)
}
I don't know if it's possible to recompose a Canvas and maintain its state prior to what it was previously, allowing you to drawn on top of what is already there. One solution is to keep track of a queue of the operations that you want to perform and then each time the Canvas is recomposed, you iterate through the queue and draw each item. If this is a drawingn app, it has the benefit of allowing the user to undo the previous steps. I also noticed that the code you provided doesn't recompose when you click on a button. I've noticed this issue before. The solution is to assign the remembered value to another value. Here is the sample on using a queue:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val queue = mutableListOf<DrawingObject>()
PaintBrushLayout(queue = queue)
}
}
}
#Composable
fun PaintBrushLayout(
modifier: Modifier = Modifier,
queue: MutableList<DrawingObject>
) {
var currentDrawingObject by remember { mutableStateOf(DrawingObject.None) }
var c = currentDrawingObject // This fixes a bug in recomposition.
fun addToQueue(drawingObject: DrawingObject) {
queue.add(drawingObject)
currentDrawingObject = drawingObject
}
Column(
verticalArrangement = Arrangement.Bottom, modifier = modifier.fillMaxSize()
) {
Canvas(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(.9f)
) {
queue.forEach { drawingObject ->
when (drawingObject) {
DrawingObject.Circle -> {
drawCircleFig(color = Color.Blue)
}
DrawingObject.Rectangle -> {
drawRectangleFig()
}
DrawingObject.Oval -> {
drawOvalFig(color = Color.Gray)
}
}
}
}
Row(
modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween
) {
BottomLayout("Circle") { click, name ->
addToQueue(DrawingObject.Circle)
}
BottomLayout("Rectangle") { click, name ->
addToQueue(DrawingObject.Rectangle)
}
BottomLayout("Oval") { click, name ->
addToQueue(DrawingObject.Oval)
}
BottomLayout("Choose Color") { click, name ->
}
}
}
}
#Composable
fun BottomLayout(
btnName: String,
click: (Boolean, String) -> Unit
) {
Button(onClick = {
click(true, btnName)
}, modifier = Modifier.padding(4.dp)) {
Text(text = btnName)
}
}
fun DrawScope.drawCircleFig(color: Color = Color.Red) {
drawCircle(
radius = 300f,
color = color
)
}
fun DrawScope.drawRectangleFig(color: Color = Color.Red) {
drawRect(
size = Size(200f, 200f),
color = color
)
}
fun DrawScope.drawOvalFig(color: Color = Color.Red) {
drawOval(
size = Size(100f, 100f),
color = color
)
}
enum class DrawingObject {
None,
Circle,
Rectangle,
Oval
}

Jetpack Compose Row - Draw over child element siblings

I have a LazyColumn with Rows as children
I'm trying to draw a circle on just one of the row elements that overlaps the other children but the circle is being drawn below the siblings to the left and bottom
I have tried playing around with Modifier.zIndex with no luck.
Here's what I have:
#Composable
fun HorizontalGrid() {
val days = (1..300).toList()
val weekChunks: List<List<Int>> = days.chunked(7)
LazyColumn {
items(weekChunks) { days: List<Int> ->
Week(days)
}
}
}
#Composable
fun Week(days: List<Int>) {
Row(horizontalArrangement = Arrangement.SpaceEvenly, modifier = Modifier.fillMaxSize()) {
days.forEach {
Day(it)
}
}
}
#Composable
fun Day(dayOfWeek: Int) {
Box(
modifier = Modifier
.size(48.dp)
.padding(4.dp)
.background(Color.LightGray)
) {
Text(
modifier = Modifier
.drawWithContent {
if (dayOfWeek == 17) {
drawContent()
drawCircle(Color.Gray, radius = 150F, center = Offset(50f, 50f))
}
},
text = dayOfWeek.toString()
)
}
}
Here's what I get:
What I need is the circle to be over all of the squares.
There are two places where need to add zIndex
Code
#Composable
fun HorizontalGrid() {
val days = (1..300).toList()
val weekChunks: List<List<Int>> = days.chunked(7)
LazyColumn {
items(items = weekChunks) { days: List<Int> ->
Week(days)
}
}
}
#Composable
fun Week(days: List<Int>) {
Row(
horizontalArrangement = Arrangement.SpaceEvenly,
modifier = Modifier
.fillMaxSize()
.zIndex(if (days.contains(17)) 2f else 1f)
) {
days.forEach {
Day(it)
}
}
}
#Composable
fun Day(dayOfWeek: Int) {
Box(
modifier = Modifier
.zIndex(if (dayOfWeek == 17) 2f else 1f)
.size(48.dp)
.padding(4.dp)
.background(Color.LightGray)
) {
Text(
modifier = Modifier
.drawWithContent {
if (dayOfWeek == 17) {
drawContent()
drawCircle(Color.Gray, radius = 150F, center = Offset(50f, 50f))
}
},
text = dayOfWeek.toString()
)
}
}
Result

Compose navigation title is not updating

I am trying to update the title of the TopAppBar based on a live data in the ViewModel, which I update on different screens. It looks like the live data is getting updated properly, but the update is not getting reflected on the title of the TopAppBar. Here is the code:
class MainActivity : ComponentActivity() {
#ExperimentalFoundationApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
LazyVerticalGridActivityScreen()
}
}
}
#ExperimentalFoundationApi
#Composable
fun LazyVerticalGridActivityScreen(destinationViewModel: DestinationViewModel = viewModel()) {
val navController = rememberNavController()
var canPop by remember { mutableStateOf(false) }
// getting the latest title value from the view model
val title: String by destinationViewModel.title.observeAsState("")
Log.d("MainActivity_title", title) // not getting called
navController.addOnDestinationChangedListener { controller, _, _ ->
canPop = controller.previousBackStackEntry != null
}
val navigationIcon: (#Composable () -> Unit)? =
if (canPop) {
{
IconButton(onClick = { navController.popBackStack() }) {
Icon(imageVector = Icons.Filled.ArrowBack, contentDescription = null)
}
}
} else {
null
}
Scaffold(
topBar = {
TopAppBar(title = { Text(title) }, navigationIcon = navigationIcon) // updating the title
},
content = {
NavHost(navController = navController, startDestination = "home") {
composable("home") { HomeScreen(navController) }
composable("details/{listId}") { backStackEntry ->
backStackEntry.arguments?.getString("listId")?.let { DetailsScreen(it, navController) }
}
}
}
)
}
#ExperimentalFoundationApi
#Composable
fun HomeScreen(navController: NavHostController, destinationViewModel: DestinationViewModel = viewModel()) {
val destinations = DestinationDataSource().loadData()
// updating the title in the view model
destinationViewModel.setTitle("Lazy Grid Layout")
LazyVerticalGrid(
cells = GridCells.Adaptive(minSize = 140.dp),
contentPadding = PaddingValues(8.dp)
) {
itemsIndexed(destinations) { index, destination ->
Row(Modifier.padding(8.dp)) {
ItemLayout(destination, index, navController)
}
}
}
}
#Composable
fun ItemLayout(
destination: Destination,
index: Int,
navController: NavHostController
) {
Column(
verticalArrangement = Arrangement.Center,
modifier = Modifier
.background(MaterialTheme.colors.primaryVariant)
.fillMaxWidth()
.clickable {
navController.navigate("details/$index")
}
) {
Image(
painter = painterResource(destination.photoId),
contentDescription = stringResource(destination.nameId),
modifier = Modifier.fillMaxWidth(),
contentScale = ContentScale.Crop
)
Text(
text = stringResource(destination.nameId),
color = Color.White,
fontSize = 14.sp,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp)
)
}
}
#Composable
fun DetailsScreen(
index: String,
navController: NavController,
destinationViewModel: DestinationViewModel = viewModel()
) {
val dataSource = DestinationDataSource().loadData()
val destination = dataSource[index.toInt()]
val destinationName = stringResource(destination.nameId)
val destinationDesc = stringResource(destination.descriptionId)
val destinationImage = painterResource(destination.photoId)
// updating the title in the view model
destinationViewModel.setTitle("Destination Details")
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
.verticalScroll(rememberScrollState())
) {
Image(
painter = destinationImage,
contentDescription = destinationName,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxWidth()
)
Column(modifier = Modifier.padding(horizontal = 16.dp)) {
Text(
text = destinationName,
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(vertical = 16.dp)
)
Text(text = destinationDesc, lineHeight = 24.sp)
Row(horizontalArrangement = Arrangement.End, modifier = Modifier.fillMaxWidth()) {
OutlinedButton(
onClick = {
navController.navigate("home") {
popUpTo("home") { inclusive = true }
}
},
modifier = Modifier.padding(top = 24.dp)
) {
Image(
imageVector = Icons.Filled.ArrowBack,
contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colors.primaryVariant),
modifier = Modifier.size(20.dp)
)
Text("Back to Destinations", modifier = Modifier.padding(start = 16.dp))
}
}
}
}
}
EDIT: The ViewModel
class DestinationViewModel : ViewModel() {
private var _title = MutableLiveData("")
val title: LiveData<String>
get() = _title
fun setTitle(newTitle: String) {
_title.value = newTitle
Log.d("ViewModel_title", _title.value.toString())
Log.d("ViewModelTitle", title.value.toString())
}
}
Can anyone please help to find the bug? Thanks!
Edit:
Here is the GitHub link of the project: https://github.com/rawhasan/compose-exercise-lazy-vertical-grid
The reason it's not working is because those are different objects created in different scopes.
When you're using a navController, each destination will have it's own scope for viewModel() creation. By the design you may have a view model for each destination, like HomeViewModel, DestinationViewModel, etc
You can't access an other destination view model from current destination scope, as well as you can't access view model from the outer scope(which you're trying to do)
What you can do, is instead of trying to retrieve it with viewModel(), you can pass outer scope view model to your composable:
composable("details/{listId}") { backStackEntry ->
backStackEntry.arguments?.getString("listId")?.let { DetailsScreen(it, navController, destinationViewModel) }
}
Check out more details about viewModel() in the documentation
Another problem with your code is that you're calling destinationViewModel.setTitle("Lazy Grid Layout") inside composable function. So this code will be called many times, which may lead to recomposition.
To call any actions inside composable, you need to use side-effects. LaunchedEffect in this case:
LaunchedEffect(Unit) {
destinationViewModel.setTitle("Destination Details")
}
This will be called only once after view appear. If you need to call it more frequently, you need to specify key instead of Unit, so it'll be recalled each time when the key changes
You might wanna have a look here https://stackoverflow.com/a/68671477/15880865
Also, you do not need to paste all the code in the question. Just the necessary bit. We shall ask for it if something is missing in the question. Just change your LiveData to mutableStateOf and then let me know if that fixed your problem. Also, you do not need to call observeAsState after modifying the type. Just refer to the link it contains all the info.
Thanks

Integrating Braintree dropin UI into Kotlin Jetpack Compose

I'm trying to convert the android/java dropin UI code from here https://developer.paypal.com/braintree/docs/guides/drop-in/setup-and-integration#starting-drop-in into a jetpack compose app. So far I have
#Composable
fun Account(user: FinUser) {
val context = LocalContext.current
val customerToken = user.userData["customerToken"] as String
val dropInRequest = DropInRequest()
.clientToken(customerToken)
val dropInHintLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartIntentSenderForResult()
) {
print("pause here")
}
val dropInIntent = dropInRequest.getIntent(context)
val dropInPendingIntent = PendingIntent.getBroadcast(
context, 200, dropInIntent,
PendingIntent.FLAG_UPDATE_CURRENT
)
Column(){
Column(
Modifier
.padding(top = 0.dp)
.clickable { launchDropInUi(
dropInHintLauncher=dropInHintLauncher,
dropInPendingIntent=dropInPendingIntent) }) {
Divider(color = Color.LightGray, thickness = 1.dp)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(20.dp, 10.dp)
) {
Column() {
Text("Payment", color = Color.Gray)
Text("*********9999", color = Color.Black)
}
Spacer(modifier = Modifier.fillMaxWidth())
}
Divider(color = Color.LightGray, thickness = 1.dp)
}
}
}
fun launchDropInUi(dropInHintLauncher: ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult>, dropInPendingIntent: PendingIntent){
dropInHintLauncher.launch(
IntentSenderRequest.Builder(dropInPendingIntent).build()
)
}
When I click on my row there's no dropin UI popup but it does register the click and runs over the launchDropInUi function.
I found the issue. I needed to use
val dropInHintLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
)
Giving
#Composable
fun Account(user: FinUser) {
val context = LocalContext.current
val customerToken = user.userData["customerToken"] as String
val dropInRequest = DropInRequest()
.clientToken(customerToken)
val dropInHintLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
){ result: ActivityResult ->
if (result.resultCode == Activity.RESULT_OK) {
// you will get result here in result.data
val data: DropInResult? = result.data?.getParcelableExtra(DropInResult.EXTRA_DROP_IN_RESULT)
}else{
print("throw error popup")
}
}
val dropInIntent = dropInRequest.getIntent(context)
Column(){
Column(
Modifier
.padding(top = 0.dp)
.clickable { launchDropInUi(
dropInHintLauncher =dropInHintLauncher,
dropInIntent =dropInIntent) }) {
Divider(color = Color.LightGray, thickness = 1.dp)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(20.dp, 10.dp)
) {
Column() {
Text("Payment", color = Color.Gray)
Text("*********9999", color = Color.Black)
}
Spacer(modifier = Modifier.fillMaxWidth())
}
Divider(color = Color.LightGray, thickness = 1.dp)
}
}
}
fun launchDropInUi(dropInHintLauncher: ManagedActivityResultLauncher<Intent, ActivityResult>, dropInIntent: Intent){
dropInHintLauncher.launch(dropInIntent)
}