# Radiography: printing the Android view hierarchy as a readable tree

> Radiography is a Square library that renders the view hierarchy of an Android app as a compact ASCII tree with sizes, ids, focus state and text lengths, and it can also expose the raw hierarchy as objects for programmatic assertions. It is a debugging tool for developers, published on Maven Central and free of personal data by default.

**block/radiography** — Text-ray goggles for your Android UI.

- Repository: https://github.com/block/radiography
- Stars: 900 · Forks: 56
- Language: Kotlin
- License: Apache-2.0
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/block-radiography

## The problem: a layout you can only inspect by tapping

The Android layout inspector is good at answering a question about one view. It is much weaker at answering the question you actually have at two in the morning, which is what is in this screen at all, which view is oversized, which container is eating the space, and why is this button pushed off screen.

Radiography is a Square library for exactly that, described by the project as text-ray goggles for your Android UI. The output is an ASCII tree, and the shape of it is the whole point. A login screen comes out like this:

```
DecorView { 1080×2160px }
├─LinearLayout { id:main, 1080×1962px }
│ ├─EditText { id:username, 580×124px, focused, text-length:0, ime-target }
│ ├─EditText { id:password, 580×124px, text-length:0 }
│ ╰─LinearLayout { 635×154px }
│   ├─Button { id:signin, 205×132px, text-length:7 }
│   ╰─Button { id:forgot_password, 430×132px, text-length:15 }
├─View { id:navigationBarBackground, 1080×132px }
╰─View { id:statusBarBackground, 1080×66px }
```

Every node carries its pixel size, its resource id where it has one, its state where the state matters, and the length of its text rather than the text itself. The tree structure is the layout, the numbers are the geometry, and the result reads like a diff you can paste into a bug report.

The audience is Android developers working with the view system, and it is a debugging and testing tool rather than a runtime feature. The library is written in Kotlin, published on Maven Central under the com.squareup.radiography group, and licensed under Apache-2.0.

## The scan: one call returns every window in the process

The API is a single entry point. Radiography.scan() returns a pretty string rendering of the view hierarchy of all windows managed by the current process, so it is not tied to the activity you happen to be in.

That default is deliberate and it is also the first thing to think about when using it. A process with several windows, a dialog and a popup will produce a tree containing all of them, which is exactly what you want when diagnosing a focus problem and exactly what you do not want when you only care about one screen. The scoping options exist to solve that.

The output format is designed to be diffable, and the details in each node are chosen for that purpose. An EditText shows focused, text-length:0, ime-target, so you can see at a glance which field has the input method attached and which one has content without exposing the content. Buttons show their text length. Views that are gone or empty still appear with their size, which is often the answer in itself, since a zero sized view is a view that will not be tappable.

The result example in the documentation shows the same tree from a real sample app, with a window focus flag at the top and the full activity name as a header, so you can tell which of several windows a given tree belongs to.

## Adding the dependency and taking your first scan

The dependency is one line in the app's build.gradle file:

```gradle
dependencies {
  implementation 'com.squareup.radiography:radiography:2.10'
}
```

The version here matches the most recent release, v2.10, published on 2026-07-24. Before that there was v2.9 on 2026-02-26 and v2.8 on 2026-02-19, so the library is moving regularly and the changelog is in the repository.

The first scan is a single call:

```kotlin
// Render the view hierarchy for all windows.
val prettyHierarchy = Radiography.scan()
```

There is no setup, no registration and no instrumentation step. You call it from a debug build, print the result, and read the tree.

Two things are worth doing on the first run. The first is to be aware of what the string contains by default, because the renderers are the privacy-sensitive part of the design and are covered in the next section. The second is to decide where the call goes: a debug-only code path, a developer menu entry, or a test.

The repository also ships a sample app in the sample directory, plus a sample-compose/ directory, and separate compose-tests/ and compose-unsupported-tests/ directories. That last one is the interesting one, because it is where the behaviour against an unsupported Compose version gets pinned down.

## Text content and personal data: the renderer switch

The default output deliberately leaves out the text in your views. You get text-length:11, not the actual string, which is what makes a Radiography dump safe to paste into an issue tracker that anyone can read.

When you do need the content, the renderer set is a parameter. The two named defaults are DefaultsNoPii and DefaultsIncludingPii:

```kotlin
// Include the text content from TextView instances.
val prettyHierarchy = Radiography.scan(viewStateRenderers = DefaultsIncludingPii)
```

And because the renderers are a set you can extend, you can add your own attributes on top of a default:

```kotlin
// Append custom attribute rendering
val prettyHierarchy = Radiography.scan(viewStateRenderers = DefaultsNoPii +
    androidViewStateRendererFor<LinearLayout> {
      append(if (it.orientation == LinearLayout.HORIZONTAL) "horizontal" else "vertical")
    })
```

The generic function takes the view type and a builder, so you can render any property of any view type in the same tree format. The example adds the orientation of a LinearLayout, which is the kind of thing you would otherwise have to squint at in the inspector.

The design point is that privacy is a parameter with a safe default rather than something you remember to switch off. That matters more than it might sound, because the normal workflow for this library is to paste its output into a bug report, and the contents of a user's text field is exactly the kind of thing that ends up in a screenshot in a public issue.

## Narrowing the output: scan scopes and view filters

The default scope is every window owned by the app, and there are two mechanisms to narrow it.

A scan scope changes the root of the walk. There is an extension function on View that renders starting from that view, and there is a scope that renders only the focused window if there is one:

```kotlin
// Extension function on View, renders starting from that view.
val prettyHierarchy = someView.scan()

// Render only the view hierarchy from the focused window, if any.
val prettyHierarchy = Radiography.scan(scanScope = FocusedWindowScope)
```

View filters then remove individual views from the output. A filter can skip views by id, and filters compose with an and operator, so you can build a predicate out of several rules:

```kotlin
// Filter out views with specific ids.
val prettyHierarchy = Radiography.scan(viewFilter = skipIdsViewFilter(R.id.debug_drawer))

// Combine view filters.
val prettyHierarchy = Radiography.scan(
  viewFilter = skipIdsViewFilter(R.id.debug_drawer) and MyCustomViewFilter()
)
```

Why would you want to filter? Because a tree that includes your debug drawer, your developer overlay and your analytics view is a tree where the interesting part is buried. Being able to say, in code, that the debug drawer is not part of what you are looking at, keeps the output focused and keeps the output stable between runs, which is what makes it usable in an assertion.

## Beyond the string: ScannableView and Compose semantics

The scan function returns a String, but the documentation also points at the raw hierarchy data, so you can explore it programmatically and integrate it into your own tools. The entry point is the ScanScopes object, which identifies the root of a hierarchy you want to explore, and the ScannableView objects it returns, whose children you can walk.

The example finds the hierarchy rooted at a specific Compose test tag inside a given activity:

```kotlin
@OptIn(ExperimentalRadiographyComposeApi::class)
fun Activity.findComposableRootWithTestTag(tag: String): ScannableView.ComposeView {
   val rootView = window.decorView.findViewById<ViewGroup>(R.id.content)

   return ScanScopes.composeTestTagScope(
      testTag = tag,
      inScope = ScanScopes.singleViewScope(rootView)
   ).findRoots()
      .firstOrNull()
      as? ScannableView.ComposeView
      ?: error("No composable found with test tag $tag")
}
```

That is where the tool earns its keep in a test suite. The documentation gives a concrete example of what you can then assert: walk all descendants depth first, filter to composable views, and check that every image has a content description set, then invoke the on click semantics action of every clickable element to make sure none of them crash. The depth first traversal is a four line extension property:

```kotlin
val ScannableView.allDescendentsDepthFirst: Sequence<ScannableView>
    get() = children.flatMap { sequenceOf(it) + it.allDescendentsDepthFirst }
```

So the library is not only a pretty printer. It is a structured, queryable model of what is on screen, which is a much stronger foundation for a UI regression test than a screenshot comparison, since it can express intent rather than pixels.

## The Compose caveat, the release line and the licence

Jetpack Compose support has a condition attached, and the documentation is clear about it. Radiography will automatically render composables found in your view tree if the Compose Tooling library is on the classpath, and if you use Compose you are probably already depending on that library, since the @Preview annotation lives in it. If you are not using Compose, Radiography will not bloat your app with transitive dependencies on any Compose artifacts.

The failure mode is worth knowing. Compose occasionally has internal implementation changes that affect Radiography. If you use an unsupported Compose version, or you do not depend on the Tooling library, Radiography will still try to detect compositions, but instead of rendering the actual hierarchy it will include a message asking you to upgrade Radiography or add the Tooling library. So a placeholder message in your output is a version problem, not a bug in your UI.

The library is published on Maven Central as com.squareup.radiography, with the group id carrying the Square name even though the repository now lives under the Block organisation, which is the sort of detail that breaks a naive dependency rename script.

On maintenance, the repository is not archived and the last push was on 2026-10-07, with releases v2.10 on 2026-07-24, v2.9 on 2026-02-26 and v2.8 on 2026-02-19, and a CHANGELOG.md and RELEASING.md in the repository root.

The licence is Apache-2.0, which is permissive with an explicit patent grant, so shipping it inside a commercial application carries no obligation to publish anything. The tooling and CI story in the repository is a Gradle build with a wrapper, a settings file in Kotlin DSL, a Code of Conduct, a contributing guide and a dedicated directory for the plugin, so the project is run like a real library rather than a script.

## Conclusion

Adopt Radiography if you debug Android layouts by reading structure rather than by tapping through the inspector, and if you want to assert on view properties in tests instead of only looking at them. Do not adopt it as a general UI snapshot framework, because it captures structure and state rather than pixels. Before you adopt it, check that your Compose version is supported, because an unsupported Compose version or a missing Tooling dependency turns composable output into a placeholder message, and decide early whether you want text content included, since the default renderers deliberately exclude it.

## FAQ

### How do I add Radiography to an Android project?

Add the dependency to your app's build.gradle file with implementation 'com.squareup.radiography:radiography:2.10', then call Radiography.scan() to get a string rendering of the view hierarchy of all windows in the current process.

### Does Radiography include the text from my views?

Not by default. The default renderers show a text length rather than the text itself, and you opt in to including it with Radiography.scan(viewStateRenderers = DefaultsIncludingPii). You can also extend the default set with your own renderers for specific view types.

### How do I scan only part of the app with Radiography?

You can call the extension function on a View to render from that view, use the FocusedWindowScope to render only the focused window, or pass a view filter such as skipIdsViewFilter to exclude particular views. Filters compose, so several rules can be combined with an and operator.

### Why does Radiography not render my composables?

Compose support requires the Compose Tooling library on the classpath and a supported Compose version. If either is missing, Radiography still detects compositions but emits a message asking you to upgrade Radiography or add the Tooling library instead of the real hierarchy.

### What licence is Radiography released under?

Apache-2.0, with the LICENSE file in the repository root and the licence recorded in the README badge. The library is published on Maven Central under the com.squareup.radiography group.

## Sources

- [block/radiography on GitHub](https://github.com/block/radiography)
- [Issues](https://github.com/block/radiography/issues)
- [License: Apache-2.0](https://github.com/block/radiography/blob/main/LICENSE)
- [README](https://github.com/block/radiography/blob/main/README.md)
- [Releases](https://github.com/block/radiography/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/block-radiography
