This commit is contained in:
coco
2026-07-15 16:57:36 +08:00
parent 723ce1af5c
commit 95aa018b8b
314 changed files with 8910 additions and 0 deletions
@@ -0,0 +1,7 @@
# Codelabs
* [Jetpack Compose basics](https://codelabs.developers.google.com/codelabs/jetpack-compose-basics/index.html?index=..%2F..index#0)
* [Migrating to Jetpack Compose](https://codelabs.developers.google.com/codelabs/jetpack-compose-migration)
* [Jetpack Compose Theming](https://codelabs.developers.google.com/codelabs/jetpack-compose-theming/index.html?index=..%2F..index#0)
* [Layouts in Jetpack Compose](https://codelabs.developers.google.com/codelabs/jetpack-compose-layouts/index.html?index=..%2F..index#0)
* [Using State in Jetpack Compose](https://codelabs.developers.google.com/codelabs/jetpack-compose-state/index.html?index=..%2F..index#0)
* [Advanced State Side Effects](https://developer.android.com/codelabs/jetpack-compose-advanced-state-side-effects#0)
@@ -0,0 +1,66 @@
# Compiler Plugin
Compose works by transforming all Kotlin functions that are annotated with **@Composable** and adding code for the Compose Runtime.
To do that it uses a Kotlin Compiler Plugin.
For instance, this Composable:
```kotlin
@Composable
fun Hello(name: String) {
Text(name)
}
```
will be transformed and compiled to Jvm ByteCode. Below you can see the decompiled code as Java code
```java
@Metadata(
mv = {1, 5, 1},
k = 2,
xi = 48,
d1 = {"\u0000\u0010\n\u0000\n\u0002\u0010\u0002\n\u0000\n\u0002\u0010\u000e\n\u0002\b\u0002\u001a\u0015\u0010\u0000\u001a\u00020\u00012\u0006\u0010\u0002\u001a\u00020\u0003H\u0007¢\u0006\u0002\u0010\u0004¨\u0006\u0005"},
d2 = {"Hello", "", "name", "", "(Ljava/lang/String;Landroidx/compose/runtime/Composer;I)V", "app_debug"}
)
public final class GreetingKt {
@Composable
public static final void Hello(@NotNull final String name, @Nullable Composer $composer, final int $changed) {
Intrinsics.checkNotNullParameter(name, "name");
$composer = $composer.startRestartGroup(274849561);
ComposerKt.sourceInformation($composer, "C(Hello)7@166L10:Greeting.kt#tlkiwl");
int $dirty = $changed;
if (($changed & 14) == 0) {
$dirty = $changed | ($composer.changed(name) ? 4 : 2);
}
if (($dirty & 11 ^ 2) == 0 && $composer.getSkipping()) {
$composer.skipToGroupEnd();
} else {
TextKt.Text-fLXpl1I(name, (Modifier)null, 0L, 0L, (FontStyle)null, (FontWeight)null, (FontFamily)null, 0L, (TextDecoration)null, (TextAlign)null, 0L, 0, false, 0, (Function1)null, (TextStyle)null, $composer, 14 & $dirty, 0, 65534);
}
ScopeUpdateScope var4 = $composer.endRestartGroup();
if (var4 != null) {
var4.updateScope((Function2)(new Function2() {
public final void invoke(@Nullable Composer $composer, int $force) {
GreetingKt.Hello(name, $composer, $changed | 1);
}
}));
}
}
}
```
## Where can i find the source code
https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-master-dev/compose/compiler/compiler-hosted/
## See also:
* Leland Richardson(@intelligibabble) started a series about the Compose Compiler https://www.youtube.com/watch?v=bg0R9-AUXQM
* [Under the hood of Jetpack Compose — part 2 of 2](https://medium.com/androiddevelopers/under-the-hood-of-jetpack-compose-part-2-of-2-37b2c20c6cdd)
* [A Hitchhiker's Guide to Compose Compiler: Composers, Compiler Plugins, and Snapshots](https://www.droidcon.com/2022/06/28/ha-hitchhikers-guide-to-compose-compiler-composers-compiler-plugins-and-snapshots/)
* [Explained: Compose Compiler and Runtime](https://www.droidcon.com/2022/08/02/explained-compose-compiler-and-runtime/)
* [Using Compose Runtime to create a client library](https://www.droidcon.com/2022/08/02/using-compose-runtime-to-create-a-client-library/)
@@ -0,0 +1,53 @@
<!---
This is the API of version 1.2.0
-->
# Lifecycle
Compose has some "effects"-functions that can be used in Composables to track the lifecycle of a function.
* LaunchedEffect {}
will be called the first time a compose function is applied.
* DisposableEffect { }
Has a onDispose() which will be called when the compose function isn't part of the composition anymore.
The example below has a Button that will count up everytime it gets clicked.
When the count value gets 3, the Text() function will not be added anymore.
The first time the LifecycleDemo will be executed, the SideEffect in the if-clause will be executed.
When the count value gets 3 +onDispose{} inside the if-clause will be called.
```kotlin
@Composable
fun LifecycleDemo() {
val count = remember { mutableStateOf(0) }
Column {
Button(onClick = {
count.value++
}) {
Text("Click me")
}
if (count.value < 3) {
LaunchedEffect(Unit)
Log.d("Compose", "onactive with value: " + count.value)
}
DisposableEffect(Unit) {
onDispose {
Log.d("Compose", "onDispose because value=" + count.value)
}
}
Text(text = "You have clicked the button: " + count.value.toString())
}
}
}
```
## See also:
* [Full Example Code]({{ site.samplefolder }}/general/LifecycleDemo.kt)
@@ -0,0 +1,36 @@
# Compose Confusion
Because of similar naming of the Compose libraries, there is a some confusion between the different "variants" of Compose. Here is a quick overview that tries to give clarification:
## Compose Runtime
The [Compose Runtime](https://developer.android.com/jetpack/androidx/releases/compose-runtime) is a library for manipulating and managing trees of data.
This library can be used to manage UI tree nodes, but it's a general purpose library, so it can be used with any kind of data.
It is developed as a Kotlin Multiplatform Library, which means that it could be ported to every target that Kotlin Multiplatform supports.
Openly developed targets are Android, JVM and Web.
[Square](https://www.reddit.com/r/androiddev/comments/r77o7d/comment/hmyqbta/?utm_source=share&utm_medium=web2x&context=3
) is running an internal version of it on iOS.
This library contains basic things like @Composable, remember, mutableState, effects.
## Compose Compiler Plugin
The Compose Compiler Plugin is a Kotlin Compiler Plugin that transforms all the **@Composable** functions and adds the needed calls to the **Compose Runtime**
## Compose UI
[Compose UI](https://developer.android.com/jetpack/androidx/releases/compose-ui) is a set of UI libraries for Android using **Compose Runtime/Compose Compiler Plugin**
# Jetpack Compose
Jetpack Compose is a UI Toolkit for Android developed by Google.
It is using **Compose Runtime/Compose Compiler Plugin** and **Compose UI**
# Compose HTML
Compose HTML is a UI Toolkit for Web developed by JetBrains written in Kotlin/JS.
It is using the **Compose Runtime** and the **Compose Compiler Plugin**. It does not use Compose UI, because it uses Compose Wrappers for the HTML DOM UI Elements.
# Compose for Desktop
**Compose for Desktop** is a UI Toolkit developed by JetBrains. It runs on the JVM and it is using **Compose Runtime**, **Compose Compiler Plugin** and a Compose UI version for desktop which is using Skia to target Windows, macOS, Linux. Additional it provides components for desktop specific apps like scrollbars or mouse support and Swing interop.
# Compose Multiplatform
[Compose Multiplatform](https://www.jetbrains.com/lp/compose-mpp/) is a Kotlin multiplatform project which is developed by JetBrains.
It uses Jetpack Compose, Compose for Web and Compose for Desktop
@@ -0,0 +1,85 @@
<!---
This is the API of version 1.2.0
-->
# CompositionLocal
CompositionLocal is useful when you want to create a dependency in a higher node of the layout tree and use it on lower node without having to
pass it down the tree through every child Composable.
## How to create an CompositionLocal?
```kotlin
data class User(val name: String, val age: Int)
val LocalActiveUser = compositionLocalOf<User> { error("No user found!") }
```
Let's say you want to create an CompositionLocal with an User. You can use **compositionLocalOf**. Inside the function you can return an initial user object
or you can throw an exception when the user is missing.
## How to provide a value for an CompositionLocal?
```kotlin
@Composable
private fun MyUserScreen() {
val user = User("Jens", 31)
CompositionLocalProvider(LocalActiveUser provides user) {
UserInfo()
}
}
```
Somewhere above in your hierarchy you have to use CompositionLocalProvider to provide a value for your CompositionLocal.
The syntax is: "**CompositionLocal`<T>`** provides **T**".
All child @Composable of CompositionLocalProvider will implicitly be able to get the value of the CompositionLocals.
## How to use a value of an CompositionLocal?
```kotlin
@Preview
@Composable
fun UserInfo() {
Column {
Text("Name: " + LocalActiveUser.current.name)
Text("Age: " + LocalActiveUser.current.age)
}
}
```
Now you can use your CompositionLocal in your @Composable. Every CompositionLocal has a **current** property that contains the current value.
## Predefined CompositionLocals
The Compose libraries already contain some useful CompositionLocals. You can directly use them without needing add a Providers.
### LocalContext
Provides a [Context] that can be used by Android applications.
### LocalConfiguration
The [Configuration] is useful for determining how to organize the UI.
#### Device orientation
One of the things you can get from the LocalConfiguration is the orientation of your device. This can be used to give the user a different ui when the device is rotated.
```kotlin
val configuration = LocalConfiguration.current
when (configuration.orientation) {
Configuration.ORIENTATION_LANDSCAPE -> {
Text("Landscape")
}
else -> {
Text("Portrait")
}
}
```
### LocalLifecycleOwner
The CompositionLocal containing the current [LifecycleOwner].
### LocalView
The CompositionLocal containing the current Compose [View].
### LocalViewModelStoreOwner
The CompositionLocal containing the current [ViewModelStoreOwner].
## See also:
* [Official Docs](https://developer.android.com/reference/kotlin/androidx/compose/runtime/CompositionLocal)
* [Full Example Code]({{ site.samplefolder }}/general/CompositionLocalExample.kt)
@@ -0,0 +1,17 @@
# CompositionLocalProvider
CompositionLocalProvider are used to provide a value for an [CompositionLocal](../compositionlocal)
## How to provide a value for an CompositionLocal?
```kotlin
@Composable
private fun MyUserScreen() {
val user = User("Jens", 31)
CompositionLocalProvider(LocalActiveUser provides user) {
UserInfo()
}
}
```
Somewhere above in your hierarchy you have to use CompositionLocalProviders to provide a value for your CompositionLocal. You can provide the values of multiple CompositionLocals inside Providers.
The syntax is: "**CompositionLocal`<T>`** provides **T**".
All child @Composable of CompositionLocalProvider will implicitly be able to get the value of the CompositionLocals.
@@ -0,0 +1,49 @@
# Project Setup
## Gradle Dependencies
Add this inside in the **android{}** block your build.gradle
```groovy
android{
//YOUR OTHER CODE
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "{{compose.release}}"
}
}
```
Below are some Compose dependencies that are online available, can find the others [here](https://maven.google.com/web/index.html?q=compose#androidx.compose.ui)
```kotlin
dependencies {
val compose_version = "{{compose.release}}"
implementation("androidx.compose.animation:animation-core:$compose_version")
implementation("androidx.compose.animation:animation:$compose_version")
implementation("androidx.compose.ui:ui:$compose_version")
implementation("androidx.compose.foundation:foundation:$compose_version")
implementation("androidx.compose.ui:ui-geometry:$compose_version")
implementation("androidx.compose.ui:ui-graphics:$compose_version")
implementation("androidx.compose.foundation:foundation-layout:$compose_version")
implementation("androidx.compose.runtime:runtime-livedata:$compose_version")
implementation("androidx.compose.material:material:$compose_version")
implementation("androidx.compose.material:material-icons-core:$compose_version")
implementation("androidx.compose.material:material-icons-extended:$compose_version")
implementation("androidx.compose.runtime:runtime-rxjava2:$compose_version")
implementation("androidx.compose.ui:ui-text:$compose_version")
implementation("androidx.compose.ui:ui-util:$compose_version")
implementation("androidx.compose.ui:ui-viewbinding:$compose_version")
implementation("androidx.compose.ui:ui-tooling:$compose_version")
implementation("androidx.activity:activity-compose:1.3.1")
//Compose Constraintlayout
implementation("androidx.constraintlayout:constraintlayout-compose:1.0.0")
}
```
@@ -0,0 +1,36 @@
# Hello World Compose
### Setup the project
First setup your [Project Setup](getting_started.md)
### Write a simple Compose function
A basic Compose View is using a normal Kotlin function which is annotated with @Composable
```kotlin
@Composable
fun HelloWorld() {
Text("Hello World!")
}
```
### Use a Compose function as a view in your android app
To use the HelloWorld() function in your App you have to use the setContent() extension function inside a onCreate() in an Activity.
```kotlin
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
HelloWorld()
}
}
}
```
@@ -0,0 +1,93 @@
<!---
This is the API of version 1.2.0
-->
# Modifier
Modifiers can be used modify certain aspects of a Composable.
To set them, a Composable needs to accept a modifier as a parameter.
## Combine modifiers
```kotlin
Column(modifier = Modifier.height(500.dp).padding(100.dp)) {
Text("Hello")
}
```
You can chain multiple modifiers.
The order is important modifier elements to the left are applied before modifier elements to the right.
## Some Modifiers
The Compose libraries already contain some useful modifiers.
### LayoutModifier
#### Modifier.width()
You can use this to set the width of a Composable.
#### Modifier.height()
You can use this to set the height of a Composable.
#### Modifier.size()
You can use this to set the width and height of a Composable.
#### Modifier.fillMaxHeight()
This will set the height of the Composable to the maximum available height. This is similar to **MATCH_PARENT** from the classic View system.
#### Modifier.fillMaxWidth()
This will set the width of the Composable to the maximum available width. This is similar to **MATCH_PARENT** from the classic View system.
#### Modifier.fillMaxSize()
This will set the height/width of the Composable to the maximum available height/width
#### Modifier.padding()
You can use **Modifier.padding** to set padding to Composables that take a modifier as an argument.
<p align="left">
<img src ="../../images/general/modifier/PaddingExample.png" height=100 width=300 />
</p>
```kotlin
@Composable
fun PaddingDemo() {
Column {
Text("TextWithoutPadding")
Column(modifier = Modifier.padding(start = 80.dp)){
Text("TextWith80dpOnlyLeftPadding")
}
Column(Modifier.padding(all = 80.dp)){
Text("TextWith80dpPadding")
}
}
}
```
### DrawModifier
#### Modifier.background()
With this modifier you can set a background color/shape for the Composable
#### Modifier.clip()
This modifier can clip the Composable to rectangle, rounded, or circle
### GestureModifier
#### Modifier.clickable
Configure component to receive clicks via input or accessibility "click" event.
#### Modifier.scrollable
You can use this to make a Composable scrollable
#### Modifier.draggable
You can use this to make a Composable draggable
#### Modifier.swipeable
You drag elements which, when released, animate towards typically two or more anchor points defined in an orientation
#### Multitouch: Panning, zooming, rotating
To detect multitouch gestures used for panning, zooming and rotating, you can use the ``transformable`` modifier. This modifier does not transform elements by itself, it only detects the gestures.
## See also:
* [Official Docs](https://developer.android.com/reference/kotlin/androidx/compose/ui/Modifier)
@@ -0,0 +1,12 @@
# Navigation
## On Android
In Android Projects you can use the [Jetpack Compose Navigation](https://developer.android.com/jetpack/compose/navigation)
[Codelab Jetpack Compose Navigation](https://developer.android.com/codelabs/jetpack-compose-navigation#0)
## Multiplatform
When you need a multiplaform solution for Routing, you can use [Decompose](https://github.com/arkivanov/Decompose)
[Decompose Navigation Overview](https://arkivanov.github.io/Decompose/navigation/overview/)
@@ -0,0 +1,48 @@
# Preview
## Preview
You can use the **@Preview** annotation to preview compose functions inside Android Studio.
Preview can not be used on Composables that have parameters without a default parameter.
```kotlin
@Preview
@Composable
fun TextDemo(){
Text("Hello")
}
@Preview(name = "MyPreviewName")
@Composable
fun TextDemo2(){
Text("Hello")
}
```
Android Studio Preview
<p align="center">
<img src ="../../../images/general/preview/ComposePreview.png" height=100 width=300 />
</p>
## Group Previews
```kotlin
@Preview(group = "TestGroup1")
```
The Preview annotation has a group parameter. You can use it to set a group name to your previews.
<p align="center">
<img src ="../../../images/general/preview/grouppreviews.png" />
</p>
The layout preview will now have an option to filter all the previews by the group name.
## Interactive Previews
<p align="center">
<img src ="../../../images/general/preview/interactivepreview.png" />
</p>
Above a generated preview in Android Studio you will find an "Interactive" Button. It will open your Composable in an interactive preview mode where can try your Composable directly inside
Android Studio.
## See also:
* [Official Docs](https://developer.android.com/jetpack/compose/tooling#preview-features)
@@ -0,0 +1,71 @@
# PreviewParameter
You can use **@PreviewParameter** to provide sample data for your Composables.
Let's say you have the following Composable and you want to generate a preview.
```kotlin
data class User(val name :String,val age:Int)
@Composable
fun UserInfo(user:User) {
Text(user.name+ " "+user.age)
}
```
Because UserInfo needs a User you can't directly use @Preview.
One way is to wrap your Composable in a Composable that provides a User
```kotlin
@Preview
@Composable
fun UserPreview() {
UserInfo(user = User("Jens", 31))
}
```
An other way is to use @PreviewParameter on the parameter. With PreviewParameter you can set a class
which will provide values for the needed User
```kotlin
@Preview
@Composable
fun UserInfo(@PreviewParameter(SampleUserProvider::class) user:User) {
Text(user.name+ " "+user.age)
}
```
### Create PreviewParameterProvider
**SampleUserProvider::class** will be the class which provides a User.
To create a PreviewParameterProvider you need to implement the interface PreviewParameterProvider.
The interface has two properties.
**values** is a sequence of your sample data.
```kotlin
class SampleUserProvider: PreviewParameterProvider<User> {
override val values = sequenceOf(User("Jens",31),User("Jim",44))
}
```
### Use PreviewParameterProvider
You can annotate your parameter with @PreviewParameter and the class which provides the sample data.
```kotlin
@Preview
@Composable
fun UserInfo(@PreviewParameter(SampleUserProvider::class) user:User) {
Text(user.name+ " "+user.age)
}
```
Now Android Studio will generate a preview of the Composable for every value that your provider provides.
<p align="center">
<img src ="../../../images/general/preview/previewParam1.png" height=100 width=300 />
</p>
You can limit the amount of previews by settings a limit to PreviewParameter.
```kotlin
@PreviewParameter(SampleUserProvider::class,1)
```
@@ -0,0 +1,3 @@
# Roadmap
You can find the road map here: https://developer.android.com/jetpack/androidx/compose-roadmap
@@ -0,0 +1,62 @@
<!---
This is the API of version 1.2.0
-->
# State
## Define a state
```kotlin
val textState = mutableStateOf("Hello")
```
You can use the mutableStateOf function to create a mutable state.
## Example
In this example we will create a composable with a Text and a Button. On a click on the button, the count state will go up and the text of Text will be updated.
<p align="center">
Initial state:<br>
<img src ="{{ site.images }}/general/state/state1.png" />
</p>
<p align="center">
After Button click:<br>
<img src ="{{ site.images }}/general/state/state2.png" />
</p>
```kotlin
@Composable
fun StateDemo(){
val countState = remember { mutableStateOf(0) }
Column {
Button(colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colors.secondary), onClick = { countState.value++ }) {
Text("count up")
}
Text("You have clicked the Button " + countState.value.toString() + " times")
}
}
```
```kotlin
val countState = remember { mutableStateOf(0) }
```
Here we define the state for the click counter with **mutableStateOf(0)**. 0 will be the initial value. Because 0 is an Int, the counterState will only allow values which are Int.
**remember** is used to remember the countstate. Without remember, every time the value of countstate would change, the StateDemo Composable will be recomposed and your state will also
get recreated with the initial value. When you use remember it will remember the last value and not be recreated.
```kotlin
countState.value
```
With the value property you can get/set the value of the counterstate. In the onClick() function of the Button, the value will be incremented. The Text shows the value of the counterstate. When counterstate is changed,
"Text" will also change.
## See also:
* [Codelab: Using state in Jetpack Compose](https://developer.android.com/codelabs/jetpack-compose-state#0)
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/PMMY23F0CFg" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
* [Invest in Real State: A workshop for State in Compose](https://www.droidcon.com/2022/08/02/invest-in-real-state-a-workshop-for-state-in-compose/)
@@ -0,0 +1,134 @@
# UI Testing
## Setup
Add the Compose testing library to your dependencies in build.gradle
```kotlin
androidTestImplementation("androidx.compose.ui:ui-test:$compose_version")
debugImplementation("androidx.compose.ui:ui-test-manifest:$compose_version")
```
## Example
Let's say you have the following example code, it's a button with a text that says **Hello** and when you click on it, it turns to **Bye**
```kotlin
@Composable
fun TestingExample() {
val state = remember { mutableStateOf("Hello") }
Button(onClick = { state.value = "Bye" }) {
Text(state.value)
}
}
```
## TestRule
To run this Composable in your test Code you have 2 options:
### 1) createComposeRule()
```kotlin
@Rule
@JvmField
var composeTestRule: ComposeContentTestRule = createComposeRule()
```
You can use this when you want to run your Composable without a specific Activity. You can then use the **setContent()** from the TestRule to host your Composable.
```kotlin
composeTestRule.setContent {
TestingExample()
}
```
### 2) createAndroidComposeRule()
```kotlin
@Rule
@JvmField
var composeTestRule: ComposeContentTestRule = createAndroidComposeRule<UiTestingDemoActivity>()
```
You can use this when you want to start your test with a specific Activity.
## Interaction with Composables
Now we want to test how the Composable reacts when we interact with it.
Usually in Tests we would use Espresso for that, but you can use that only for classic Android Views and not for Composables.
The ComposeTestRule will offer a similar API
```kotlin
class ExampleUiTestWithAndroidComposeRule {
@Rule
@JvmField
var composeTestRule: ComposeContentTestRule = createAndroidComposeRule<UiTestingDemoActivity>()
@Test
fun whenIClickOnButton_TheTextShouldChange() {
composeTestRule.onNodeWithText("Hello").assertExists()
composeTestRule.onNodeWithText("Hello").performClick()
composeTestRule.onNodeWithText("Hello").assertDoesNotExist()
composeTestRule.onNodeWithText("Bye").assertExists()
}
}
```
As you can see above we used the TestRule methods to click on the node with the text **Hello** and asserted that the text changed to **Bye**
The TestRule offers a lot of different methods. You can find a cheatSheet [here](https://developer.android.com/jetpack/compose/testing-cheatsheet)
### TestTags
The test above is only half correct, because it looks for the text inside the button and not the button itself.
Composables have no resource ids so we cant just use **onView(withId(R.id.my_view))** to find a Composable in a test, also we can't find Composables of a specific "type"
like a button, because everything is just a Composable function.
When you can't find a Composable by a text and you want to make it detectable in your test. Jetpack Compose offers the concept of a **TestTag**.
testTag is a modifier that needs to be set to a Composable. It expects a string which will be used as a reference in your test.
```kotlin
@Composable
fun TestingExample() {
val state = remember { mutableStateOf("Hello") }
Button(onClick = { state.value = "Bye" }, modifier = Modifier.testTag("MyTestTag")) {
Text(state.value)
}
}
```
Now you can use **onNodeWithTag("MyTestTag")** to find the button Composable.
```kotlin
@Test
fun whenIClickOnButton_TheTextShouldChange() {
composeTestRule.onNodeWithTag("MyTestTag").assertTextEquals("Hello")
composeTestRule.onNodeWithTag("MyTestTag").performClick()
composeTestRule.onNodeWithText("Hello").assertDoesNotExist()
composeTestRule.onNodeWithTag("MyTestTag").assertTextEquals("Bye")
}
```
### PrintToLog
When you want to get more information about how the node tree of a Composable looks like,
you can use **printToLog()** on a node.
```kotlin
composeTestRule.onNodeWithTag("MyTestTag").printToLog("XXX")
```
will print to logcat:
```kotlin
2022-01-07 22:58:55.048 9567-9587/de.jensklingenberg.jetpackcomposeplayground D/XXX: printToLog:
Printing with useUnmergedTree = 'false'
Node #2 at (l=0.0, t=325.0, r=195.0, b=424.0)px, Tag: 'MyTestTag'
Role = 'Button'
Text = '[Hello]'
Actions = [OnClick, GetTextLayoutResult]
MergeDescendants = 'true'
```
## See also:
* [Full Example Code](https://github.com/Foso/Jetpack-Compose-Playground/blob/master/app/src/androidTest/java/de/jensklingenberg/jetpackcomposeplayground/ExampleUiTestWithAndroidComposeRule.kt)
* [Full Example Code](https://github.com/Foso/Jetpack-Compose-Playground/blob/master/app/src/androidTest/java/de/jensklingenberg/jetpackcomposeplayground/ExampleUiTestWithComposeRule.kt)