How can I make a shape like this?
I see there two ways how I would've done this:
You can create #Composable function with Canvases (official guideline, article on Medium), if you need to use tab of this folder-like shape
// Usage:
#Composable
fun Somewhere() {
FolderLikeCard(
topTailContent = {
// for example, your tab will just be, without content, only for shape
Box(modifier = Modifier.size(64.dp, 12.dp))
},
mainContent = {
// main content
}
)
}
// Implementation:
#Composable
fun FolderLikeCard(
topTailContent: #Composable () -> Unit,
mainContent: #Composable () -> Unit
) {
val cornerSize = 4.dp // for example
Column {
Row {
Spacer(modifier = Modifier.weight(1f))
Box {
Canvas {
TODO("draw with help of links above tab's outline (using drawArc maybe)")
}
Box(
modifier = Modifier.padding(left = cornerSize, top = cornerSize, right = cornerSize),
content = topTailContent
)
}
}
Box {
Canvas {
TODO("draw main part outline")
}
Box(
modifier = Modifier.padding(left = cornerSize, bottom = cornerSize, right = cornerSize),
content = mainContent
)
}
}
}
+ I feel it can be refactored with help of Modifier.drawBehind method
Create an actual Shape for using it in Modifier.background(color = yourColor, shape = FolderLikeShape(tabHeight, tabWidth, ...)) method, link to the article that I read some time ago, + linked question helps, but I don’t see how you can then put the content there so far, if you need it, then I hope the following commentators will help with this.
Related
I am relatively new to kotlin with jetpack compose and I am having a hard time figuring out how to share the same layout of a page among different pages. The thing is that the layout is conditional to whether the user has logged in or not (among other things).
The basic implementation I have looks something like the following code.
Now, when I click in a LoginButton in the LoginPage, it changes a isLoggedIn variable in the ViewModels, which in turns triggers recomposition and the proper layout is displayed is wrapping the actual page contents. Another example is if the "create account" button is clicked, then the SignUpLayout is wrapping the page contents.
Now, another way I can think (and like better than triggering the recomposition in all the application), is for the LoginButton to call the navigate method in the NavHostController. However I am unsure how to implement the "wrapper layouts" for this other way of doing things.
So my questions are:
Is the current way of navigating around layouts fine? Even though it triggers recomposition from the top most composable (aka ClientApp)?
If I wanted for the LoginButton to make navigation by calling the NavHostController.navigate method instead of changing a variable that triggers recomposition, how will the different layouts be handled?
Is there anything else I am missing? Maybe there is another way of accomplishing this that I am not aware of and I am just over engineering things?
enum class Page(
#StringRes val title: Int,
val layout: String,
val route: String
) {
Profile(
title = R.string.profile_page,
layout = "main",
route = "/profile"),
Home(
title = R.string.home_page,
layout = "main",
route = "/home"),
Login(
title = R.string.login_page,
layout = "none",
route = "/login"),
SignUpUserDetails(
title = R.string.user_details_page,
layout = "sign-up",
route = "/sign-up/user-details"),
SignUpAccountDetails(
title = R.string.account_details_page,
layout = "sign-up",
route = "/sign-up/account-details"),
}
#Composable
fun PageNavHost(
navHostController: NavHostController,
modifier: Modifier = Modifier
) {
NavHost(
navController = navHostController,
startDestination = Page.Home.route,
modifier = modifier
) {
composable(route = Page.Profile.route) {
ProfilePage()
}
composable(route = Page.Home.route) {
HomePage()
}
composable(route = Page.Login.route) {
val viewModel = hiltViewModel<LoginViewModel>()
LoginPage(viewModel, navHostController)
}
composable(route = Page.SignUpUser.route) {
SignUpUserPage()
}
}
}
#Composable
fun ClientApp() {
val navHostController: NavHostController = rememberNavController()
val mainViewModel = hiltViewModel<MainViewModel>()
val signUpViewModel = hiltViewModel<SignUpViewModel>()
val loginViewModel = hiltViewModel<LoginViewModel>()
val isLoggedIn by mainViewModel.sessionHolder.isLoggedIn.collectAsState()
if(!isLoggedIn) {
LoginPage(loginViewModel, navHostController)
} else {
val currentPage = navHostController.currentPage()
when (currentPage?.layout) {
"main" -> MainLayout(mainViewModel, navHostController) { content() }
"sign-up" -> SignUpLayout(navHostController, signUpViewModel) { content() }
else -> content()
}
}
}
I have a simple Jetpack Compose application that uses the Navigation Component.
My UI is comprised of the following composables (redacted UI to be concise):
NavHost:
#Composable
fun NavHost(
navController: NavHostController = rememberNavController(),
startDestination: String = "main"
) {
NavHost(
navController = navController,
startDestination = startDestination) {
composable("main") {
SomeList(onNavigateToItemView = {
navController.navigate("listItem")
})
}
composable("listItem") {
ItemView()
}
}
}
SomeList:
#Composable
fun SomeList(onNavigateToItemView: () -> Unit) {
Column {
Row ( Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
Text(text = Constants.APP_TITLE, fontSize = 30.sp, fontWeight = FontWeight.Bold)
}
Box(modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
LazyColumn(
horizontalAlignment = Alignment.CenterHorizontally
) {
items(items) { item->
ItemCard(item, onNavigateToItemView)
}
}
}
}
}
ItemCard:
#Composable
fun ItemCard(item: ItemModel, onNavigateToItemView: () -> Unit) {
Card(
border = BorderStroke(2.dp, Color.Cyan),
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 5.dp)
.clickable {
onNavigateToItemView()
}
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
)
{
....
}
}
}
Now, whenever the user clicks on an ItemCard, he/she transitions to an ItemView.
What I am seeing is that the ItemView is being recomposed several times when navigating to it and also when navigating back from it.
According to the guide linked above,
The NavController's navigate function modifies the NavController's
internal state. To comply with the single source of truth principle as
much as possible, only the composable function or state holder that
hoists the NavController instance and those composable functions that
take the NavController as a parameter should make navigation calls.
Navigation events triggered from other composable functions lower in
the UI hierarchy need to expose those events to the caller
appropriately using functions.
And as you can see above, I am following that practice.
So, am I doing something wrong in my implementation or is this just how navigation and Jetpack Compose work together?
The multiple recomposition calls are affecting my UI unnecessarily.
Short Answer
Yes, navigating using compose navigation will recompose your composable each time you are navigating to it.
Workaround
When you are using navController.navigate() it will automatically recompose the targeted route. However, you can use an option to save the current state of the screen that you leave and restore the state of the screen that you target.
For example :
A->B->A->B
The first time you will load A & B you will recompose because you have no state saved yet. The moment you go from B to A (B->A) the state will be restored so you'll not recompose. Same things occurs the second time you go from A to B (A->B)
Use
Accord to documentation, you can make an extension function like this
fun NavController.popUpTo(destination: String) = navigate(destination) {
popUpTo(graph.findStartDestination().id) {
saveState = true
}
// Restore state when reselecting a previously selected item
restoreState = true
}
And use it like this
SomeList(onNavigateToItemView = {
navController.popUpTo("listItem")
})
I have this composable that is supposed to allow drawing with the pointer to a canvas but when I move the pointer nothing happens
#ExperimentalComposeUiApi
#Composable
fun CanvasDraw() {
val path = remember { Path() }
Canvas(modifier = Modifier
.fillMaxSize()
.pointerInteropFilter {
when (it.action) {
MotionEvent.ACTION_DOWN -> {
path.moveTo(it.x, it.y)
}
MotionEvent.ACTION_MOVE -> {
path.lineTo(it.x, it.y)
}
else -> false
}
true
}) {
drawPath(
path = path,
color = Color.Blue,
style = Stroke(10f)
)
}
}
remember { Path() } is caching lambda content value for next recompositions, but it cannot trigger recomposition when content on this object is changed. To make some state, which will trigger recomposition on changes in Compose, you need to use a mutable state of some kind - it's a new thing made for Compose.
You can find more info about state in Compose in documentation, including this youtube video which explains the basic principles.
Storing points inside Path will not be clean, as it's not a basic type. Instead I'm using a mutable state list of points, like this:
val points = remember { mutableStateListOf<Offset>() }
Canvas(modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectDragGestures { change, _ ->
points.add(change.position)
}
}
) {
drawPath(
path = Path().apply {
points.forEachIndexed { i, point ->
if (i == 0) {
moveTo(point.x, point.y)
} else {
lineTo(point.x, point.y)
}
}
},
color = Color.Blue,
style = Stroke(10f)
)
}
The Code A is from the official sample code here.
I know that in order to preserve state across recompositions, remember the mutable state using remember.
I think that the code val extraPadding by animateDpAsState(...) should be val extraPadding by remember { animateDpAsState(...) }, is it right?
BTW, val extraPadding by remember { animateDpAsState(...) } will cause the error
Composable calls are not allowed inside the calculation parameter of inline fun remember(calculation: () -> TypeVariable(T)): TypeVariable(T)
Code A
#Composable
private fun Greeting(name: String) {
var expanded by remember { mutableStateOf(false) }
val extraPadding by animateDpAsState( //Should I add remember
if (expanded) 48.dp else 0.dp
)
Surface(
color = MaterialTheme.colors.primary,
modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
) {
Row(modifier = Modifier.padding(24.dp)) {
Column(modifier = Modifier
.weight(1f)
.padding(bottom = extraPadding)
) {
Text(text = "Hello, ")
Text(text = name)
}
OutlinedButton(
onClick = { expanded = !expanded }
) {
Text(if (expanded) "Show less" else "Show more")
}
}
}
}
No, you shouldn't. This function is marked with #Composable so it should be used directly in the view builder.
animateDpAsState will calculate its value depending on targetValue on each recomposition.
If you check it source code you'll see, that it uses remember inside, that's why it's marked with #Composable, and that's why you shouldn't bother about remembering some values manually.
For anyone that comes across this in the future, instead of attempting to remember the animateDpAsState it would be better to remember the expansion state. Because in this case we're trying to remember the state in an item of a list we use rememberSaveable:
var expanded by rememberSaveable { mutableStateOf(false) }
A very similar case of storing indexes across list items is shown in the official documentation here
I have some doubts how to realize Search Bar in my Compose application. I call my SeachView function from ContactContent function where I pass state value.
#Composable
fun ContactContent(navigateToProfile: (Contact) -> Unit) {
val contacts = remember { DataProvider.contactList }
val textState = remember { mutableStateOf(TextFieldValue("")) }
Column(){
SearchView(textState)
LazyColumn() {
items(
items = contacts,
itemContent = {
ContactListItem(contact = it, navigateToProfile)
}
...
}
In SearchView I not sure how should I call onImeActionPerformed search state as mine search state is not be recognized.
#Composable
fun SearchView(state: MutableState<TextFieldValue>) {
Surface(){
TextField(
value = state.value,
onValueChange = { value -> state.value = value},
leadingIcon = {...},
keyBoardOptions = KeyboardOptions(
keyboardType = KeyboardType.Text,
imeAction = ImeAction.Search
),
onImeActionPerformed = {action, softKeyboardController -> if (action == ImeAction.Search){
>HERE IS WHERE I AM NOT SURE WHAT TO DO<
DataProvider.newSearch(textState)
}
...
}
newSearch function snippet
fun newSearch (textState: MutableState<String>){
val result = repository.search(
token = token,
page = 1,
query = "chicken"
)
DataProvider.value = result
}
Maybe you have a different solution how to realize the search bar from the list with with Kotlin Compose.
Do not use it directly in the parenthesis of the onImeActionPerformed, extract it in the parent Composable right before calling the TextField. Store it in a val, then use that val inside your onImeActionPerformed. Alogside, I assume you are creating that parameter with something like () -> Unit, ok so I'm not sure of this, but I think changing that to #Composable () -> Unit, you can access it directly without extracting it in a val first. Try it out