I have created an Empty Compose Activity template using Android Studio Canara 2020.03. Here is the code of the file " MainActivity. kt":
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ButtonPage()
}
}
}
#Composable
fun ButtonPage(){
Button(onClick = {}){Text("Click to go next")}
}
#Composable
fun TextPage(){
Text("Second Page")
}
How do I modify the code so that when you click on this button, it draws only text? (That is, you need that when you click the button, the program draws other content by deleting this one first).
Jetpack Compose version 1.0.0-alpha09, jdk version 15, android version 11
Thank you in advance
A simple solution would be to have a variable that dictates the current screen.
var showSecondScreen by remember { mutableStateOf(false) }
if (!showSecondScreen) {
Button(onClick = {showSecondScreen = true}){Text("Click to go next")}
} else {
Text("Second screen")
}
This doesn't have to be a boolean, you could declare var currentScreen by remember { mutableStateOf("homeScreen") } and use a when block for which screen to show.
#Composable fun MyApp(currentScreen: String) {
when (currentScreen) {
"homeScreen" -> HomeScreen()
"secondScreen" -> SecondScreen()
}
}
So you can think of it less as a transaction and more as a stateful navigation.
But you'll soon realize that this doesn't handle back navigation, so you'll need a navigation library like jetpack compose navigation, decompose, compose-router, etc. Here's more information on jetpack compose navigation.
Related
This is a question about general navigation design in Jetpack compose which I find a bit confusing.
As I understand it, having multiple screens with each own Scaffold causes flickers when navigating (I definitely noticed this issue). Now in the app, I have a network observer that is tied to Scaffold (e.g. to show Snackbar when there is no internet connection) so that's another reason I'm going for a single Scaffold design.
I have a MainViewModel that holds the Scaffold state (e.g. top bar, bottom bar, fab, title) that each screen underneath can turn on and off.
#Composable
fun AppScaffold(
networkMgr: NetworkManager,
mainViewModel: MainViewModel,
navAction: NavigationAction = NavigationAction(mainViewModel.navHostController),
content: #Composable (PaddingValues) -> Unit
) {
LaunchedEffect(Unit) {
mainViewModel.navHostController.currentBackStackEntryFlow.collect { backStackEntry ->
Timber.d("Current screen " + backStackEntry.destination.route)
val route = requireNotNull(backStackEntry.destination.route)
var show = true
// if top level screen, do not show
topLevelScreens().forEach {
if (it.route.contains(route)) {
show = false
return#forEach
}
}
mainViewModel.showBackButton = show
mainViewModel.showFindButton = route == DrawerScreens.Home.route
}
}
Scaffold(
scaffoldState = mainViewModel.scaffoldState,
floatingActionButton = {
if (mainViewModel.showFloatingButton) {
FloatingActionButton(onClick = { }) {
Icon(Icons.Filled.Add, contentDescription = "Add")
}
}
},
floatingActionButtonPosition = FabPosition.End,
topBar = {
if (mainViewModel.showBackButton) {
BackTopBar(mainViewModel, navAction)
} else {
AppTopBar(mainViewModel, navAction)
}
},
bottomBar = {
if (mainViewModel.showBottomBar) {
// TODO
}
},
MainActivity looks like this
setContent {
AppCompatTheme {
var mainViewModel: MainViewModel = viewModel()
mainViewModel.coroutineScope = rememberCoroutineScope()
mainViewModel.navHostController = rememberNavController()
mainViewModel.scaffoldState = rememberScaffoldState()
AppScaffold(networkMgr, mainViewModel) {
NavigationGraph(mainViewModel)
}
}
}
Question 1) How do I make this design scalable? As one screen's FAB may have different actions from another screen's FAB. The bottom bar may be different between screens. The main problem is I need good a way for screens to talk to the parent Scaffold.
Question 2) Where is the best place to put the code under "LaunchedEffect" block whether it's ok here?
I found this StackOverflow answer that covers your question pretty well.
The key answers to your questions according to this answer are:
You define a data class that holds variables for each element that might change between the different screens that will be displayed inside the scaffold. This most probably will be at least the title:
data class ScaffoldViewState(
#StringRes val topAppBarTitle: Int? = null
)
Then, you store this data class using remember, so that a recomposition will be triggered whenever one value within the data class changes:
var scaffoldViewState by remember {
mutableStateOf(ScaffoldViewState())
}
Finally, you can assign the field within the data class to the title slot of the Scaffold.
Changing the variables of the data class should happen from the NavHost, as seen in the linked post.
The following code is from the project.
I find that fun MainScreen() add #Composable, and fun launchDetailsActivity doesn't add #Composable.
It make me confused. I think all function which apply to Compose should to add #Composable, why doesn't fun launchDetailsActivity add #Composable?
#AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
...
setContent {
ProvideWindowInsets {
ProvideImageLoader {
CraneTheme {
MainScreen(
onExploreItemClicked = { launchDetailsActivity(context = this, item = it) }
)
}
}
}
}
}
}
#Composable
fun MainScreen(onExploreItemClicked: OnExploreItemClicked) {
...
}
fun launchDetailsActivity(context: Context, item: ExploreModel) {
context.startActivity(createDetailsActivityIntent(context, item))
}
Function with #Composable is not just a function it tells the compose compiler that this is a UI element. Take this data and build a Widget with it.
So you have to determine when you will add #Composable based on whether this function draws something in the UI or not. In the non compose world you can think of this function like a View.
For example, this function takes a parameter name and builds a Text widget with the text "Hello $name" which you can see in the UI.
#Composable
fun Greeting(name: String) {
Text("Hello $name")
}
But
fun getName(firstName: String, lastName: String): String {
return "$firstName $lastName"
}
This function is not a composable function. It is not annotated with #Composable because it is not a widget, it shouldn't render anything in the UI. It just takes two parameters, Concatenates them, and returns the String.
In your case, MainScreen is the function that is rendering the Main screen of your app so it is a UI element. But function launchDetailsActivity doesn't draw anything in the UI. It just navigates from one activity to another activity.
Few things to remember:
Function with #Composable doesn't return anything.
You can't call a composable function from a non-composable function.
Unlike non-composable function composable function start with an Uppercase letter.
You can read this doc for details https://developer.android.com/jetpack/compose/mental-model
You need to mark view builder functions with #Composable, to be directly called from an other #Composable.
If you have a side effect function, it shouldn't be called directly from composable. It can be called from touch handlers, like click in your example, or using a side effect, like LaunchedEffect. Check out more about what's side effect in documentation.
#Composable
fun SomeView() {
// this is directly called from view builder and should be marked with #Composable
OtherView()
LaunchedEffect(Unit) {
// LaunchedEffect a side effect function, and as it's called
// from LaunchedEffect it can be a suspend fun (optionally)
handleLaunchedEffect()
}
Button(onClick = { handleButtonClick() }) { // or onClick = ::handleButtonClick
}
}
#Composable
fun OtherView() {
}
suspend fun handleLaunchedEffect() {
}
fun handleButtonClick() {
}
You should use #Composable if you are using calling another function annotated with #Composable that's it pretty simple.
Generally all #Composable functions starts with uppercase letter but some also start with lowercase like everything that starts with remember
So when you are using these functions inside another function you need to use #Composable else even android studio will yell at you because composable function can be invoked from another composable.
In the Android developer docs at the following web address: https://developer.android.com/jetpack/compose/mental-model#recomposition
There is a composable function which is given as the following:
#Composable
fun ClickCounter(clicks: Int, onClick: () -> Unit) {
Button(onClick = onClick) {
Text("I've been clicked $clicks times")
}
}
It's said in the text that this produces an element which updates the number of times its been clicked every time it is clicked. However, looking at it, it seems to need a lambda function to do that.
When I try and put it into the SetContent function I get the following:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
WorkTimerTheme {
Conversation(SampleData.conversationSample)
ClickCounter(clicks = 0) {
//Insert My Function Here
}
}
}
}
}
The comment //Insert My Function Here has been added by me. I presume that within this I have to put a Lambda which updates the clicks value of the composable, but I have no idea what to put. Does anyone know an acceptable way of writing this?
You need a MutableState to trigger recomposition and remember{} to keep previous value when recomposition occurred.
I asked a question about it and my question contains answer to your question.
#Composable
fun MyScreenContent(names: List<String> = listOf("Android", "there")) {
val counterState = remember { mutableStateOf(0) }
Column(modifier = Modifier.fillMaxHeight()) {
Counter(
count = counterState.value,
updateCount = { newCount ->
counterState.value = newCount
}
)
}
}
#Composable
fun Counter(count: Int, updateCount: (Int) -> Unit) {
Button(
onClick = { updateCount(count + 1) },
) {
Text("I've been clicked $count times")
}
}
Thanks very much to #Thracian for linking a similar question. As the answer to mine is related yet slightly different I thought I would post my own.
The correct code is as follows:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val counterState = remember { mutableStateOf(0) }
WorkTimerTheme {
Conversation(SampleData.conversationSample)
ClickCounter(clicks = counterState.value) {
counterState.value++
}
}
}
}
}
As suggested I added a mutableState value which appears to remember the value of something at the last "recomposition", unless it is explicitly updated. If a mutablestate is explicitly updated that will trigger a recomposition (as noted in the answer to #Thracian's question).
A recomposition will redraw the Element.
In order to update the value at recomposition the number of times the button has been clicked must be stored in the mutablestate and passed to the Composable Function at each recomposition.
Using the Composable functions Lambda argument to affect the mutable state completes the loop, updating the mutablestate which then recomposes the button with the updated value.
That is the purpose of counterState.value++.
As suggested above for more information on this, try reading this documentation: https://developer.android.com/jetpack/compose/state#viewmodel-state
The video is related to what we're discussing here.
I am having some difficulty using lazycolumn in Jetpack Compose to display a list of users contained in a viewModel.
When the app loads, the data for the first few users is displayed correctly, however, as the user scrolls down, and more users are made visable, the data does not always display in the UI.
Furthermore, is the user scrolls back up, previously visable data is no longer displayed.
I assume this has something to do with the data being lost during recomposition, however I do not know how to solve this issue
MainActivity
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ListPracticeTheme {
val viewModel : MainViewModel by viewModels()
Surface(color = MaterialTheme.colors.background) {
UsersList(viewModel = viewModel)
}
}
}
}
}
UsersList
#Composable
fun UsersList(viewModel: MainViewModel) {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
items(viewModel.users.value) { user ->
Card(
modifier = Modifier.fillMaxWidth()
) {
Column(Modifier.fillMaxWidth()) {
Text(text = "USER ID = ${user.userID}")
Text(text = "USERNAME = ${user.userName}")
Text(text = "DESCRIPTION = ${user.description}")
}
}
}
}
}
MainViewModel
class MainViewModel : ViewModel() {
var users : MutableState<List<User>> = mutableStateOf(ArrayList())
init{
for(i in 1..20){
users.value += randomPost()
}
}
}
As the user scrolls, the UI is not updated correctly, as seen in this video:
https://youtu.be/ETEuMGStIIk
Instead, as the user scrolls, more users data should be displayed, and this data should be made visible again when the user scrolls back up.
You can try to only pass the value of viewModel.users into your UsersList.
This was apparently a confirmed bug, which is fixed in Compose beta-08. Just upgrade the Compose version to that and the Kotlin Version to 1.5.10 and it should be fine
Came across a curious situation with AndroidView this morning.
I have a ProductCard interface that looks like this
interface ProductCard {
val view: View
fun setup(
productState: ProductState,
interactionListener: ProductCardView.InteractionListener
)
}
This interface can be implemented by a number of views.
A composable that renders a list of AndroidView uses ProductCard to get a view and pass in state updates when recomposition happens.
#Composable
fun BasketItemsList(
modifier: Modifier,
basketItems: List<ProductState>,
provider: ProductCard,
interactionListener: ProductCardView.InteractionListener
) {
LazyColumn(modifier = modifier) {
items(basketItems) { product ->
AndroidView(factory = { provider.view }) {
Timber.tag("BasketItemsList").v(product.toString())
provider.setup(product, interactionListener)
}
}
}
}
With this sample, any interaction with the ProductCard view’s (calling ProductCard.setup()) doesn’t update the screen. Logging shows that the state gets updated but the catch is that it’s only updated once per button. For example, I have a favourites button. Clicking it once pushes a state update only once, any subsequent clicks doesn’t propagate. Also the view itself doesn’t update. It’s as if it was never clicked.
Now changing the block of AndroidView.update to use it and casting it as a concrete view type works as expected. All clicks propagate correctly and the card view gets updated to reflect the state.
#Composable
fun BasketItemsList(
modifier: Modifier,
basketItems: List<ProductState>,
provider: ProductCard,
interactionListener: ProductCardView.InteractionListener
) {
LazyColumn(modifier = modifier) {
items(basketItems) { product ->
AndroidView(factory = { provider.view }) {
Timber.tag("BasketItemsList").v(product.toString())
// provider.setup(product, interactionListener)
(it as ProductCardView).setup(product, interactionListener)
}
}
}
}
What am I missing here? why does using ProductCard not work while casting the view to its type works as expected?
Update 1
Seems like casting to ProductCard also works
#Composable
fun BasketItemsList(
modifier: Modifier,
basketItems: List<ProductState>,
provider: ProductCard,
interactionListener: ProductCardView.InteractionListener
) {
LazyColumn(modifier = modifier) {
items(basketItems) { product ->
AndroidView(factory = { provider.view }) {
Timber.tag("BasketItemsList").v(product.toString())
// provider.setup(product, interactionListener)
(it as ProductCard).setup(product, interactionListener)
}
}
}
}
So the question is why do we have to use it inside AndroidView.update instead of any other references to the view?
The answer here is I was missing the key value which is needed for LazyColumn items in order for compose to know which item has changed and call update on it.