Giter Club home page Giter Club logo

radiography's Introduction

Radiography logo Radiography

Maven Central GitHub license Android CI

Text-ray goggles for your Android UI.

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 }

Usage

Add the radiography dependency to your app's build.gradle file:

dependencies {
  implementation 'com.squareup.radiography:radiography:2.5'
}

Radiography.scan() returns a pretty string rendering of the view hierarchy of all windows managed by the current process.

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

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

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

You can print a subset of the view hierarchies by specifying a ScanScope. By default, Radiography will scan all the windows owned by your app.

// 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)

// 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()
)

Result example

screenshot

com.squareup.radiography.sample/com.squareup.radiography.sample.MainActivity:
window-focus:false
 DecorView { 1080×2160px }
 ├─LinearLayout { 1080×2028px }
 │ ├─ViewStub { id:action_mode_bar_stub, GONE, 0×0px }
 │ ╰─FrameLayout { id:content, 1080×1962px }
 │   ╰─LinearLayout { id:main, 1080×1962px }
 │     ├─ImageView { id:logo, 1080×352px }
 │     ├─EditText { id:username, 580×124px, text-length:0 }
 │     ├─EditText { id:password, 580×124px, text-length:0 }
 │     ├─CheckBox { id:remember_me, 343×88px, text-length:11 }
 │     ├─LinearLayout { 635×154px }
 │     │ ├─Button { id:signin, 205×132px, text-length:7 }
 │     │ ╰─Button { id:forgot_password, 430×132px, text-length:15 }
 │     ├─View { 1080×812px }
 │     ╰─Button { id:show_dialog, 601×132px, text-length:23 }
 ├─View { id:navigationBarBackground, 1080×132px }
 ╰─View { id:statusBarBackground, 1080×66px }

This sample app lives in this repo in the sample directory.

Custom Hierarchy Exploration

The Radiography.scan function provides a String representation of the view hierarchy, but you can also work with the raw hierarchy data directly if you want to programmatically explore it and integrate it into your own custom tools.

Use the ScanScopes object to identify the root of a hierarchy you want to explore. You can use the resulting ScannableView objects, and their ScannableView.children, to explore the hierarchy.

For example, to get the hierarchy starting with a specific Compose test tag in a given activity, you can do this:

@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")
}

The information provided by the ScannableView.ComposeView can be powerful. In particular, the semantic information can be used to validate certain expectations and programmatically test your application. For example:

val myComposableScreen = findComposableRootWithTestTag("My screen tag")

myComposableScreen.allDescendentsDepthFirst
  .filterIsInstance<ScannableView.ComposeView>()
  .onEach { composeView ->
    
    // Validate that all images have a content description set
    if (composeView.displayName == "Image") {
      check(composeView.semanticsConfigurations.any { it.contains(SemanticsProperties.ContentDescription) })
    }
    
    // Perform a click on all clickable elements to ensure none of them cause a crash
    composeView.semanticsConfigurations
      .map { it[SemanticsActions.OnClick] }
      .onEach { clickAction -> clickAction.action?.invoke() }
}

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

Jetpack Compose support

Jetpack Compose is Google's new declarative UI toolkit. It is a completely new implementation, and does not use Android views itself (although it interoperates with them seamlessly).

Radiography will automatically render composables found in your view tree if the Compose Tooling library is on the classpath. If you are using Compose, you're probably already using this library (the @Preview annotation lives in the Tooling library). On the other hand, if you're not using Compose, Radiography won't bloat your app with transitive dependencies on any Compose artifacts.

Compose occasionally has internal implementation changes that affect Radiography. If you are using Radiography with an unsupported version of Compose, or you don't depend on the Tooling library, then Radiography will still try to detect compositions, but instead of rendering the actual hierarchy, it will just include a message asking you to upgrade Radiography or add the Tooling library.

Compose usage

The only thing required for Radiography to render composables is to include the tooling library as a dependency:

dependencies {
  implementation("androidx.compose.ui:ui-tooling:1.0.0-betaXY")
}

When the tooling library is present, Radiography will automatically render composables. However, Radiography's Compose support is experimental. To use any of the compose-specific APIs, you will need to opt-in using the @OptIn(ExperimentalRadiographyComposeApi::class) annotation.

Rendering composables

The DefaultsNoPii and DefaultsIncludingPii renderers include default Compose renderers – you don't need to do anything special. Additional Compose-specific renderers can be found in the ComposableRenderers object.

To create a custom renderer for Compose, implement a ViewStateRenderer to handle values of type ComposeView. However, since Radiography gets most of its information about composables from their semantics properties, in most cases you shouldn't need to define any custom rendering logic. ComposeView has a list of all the Modifiers that have been applied to the composable, including its semantics.

// Custom modifier that tells its parent if it's a favorite child or not.
data class IsFavoriteChildModifier(val isFavorite: Boolean) : ParentDataModifier {
  override fun Density.modifyParentData(parentData: Any?): Any? = this@IsFavoriteChildModifier
}

// Renderer for the above modifier.
@OptIn(ExperimentalRadiographyComposeApi::class)
val IsFavoriteChildRenderer = ViewStateRenderer { view ->
  val modifier = (view as? ComposeView)
      ?.modifiers
      ?.filterIsInstance<IsFavoriteChildModifier>()
      ?.singleOrNull()
      ?: return@ViewStateRenderer
  append(if (modifier.isFavorite) "FAVORITE" else "NOT FAVORITE")
}

Selecting which composables to render

Radiography lets you start scanning from a particular view by using the singleViewScope ScanScope or the View.scan() extension function. Compose doesn't have the concept a "view" as something that can be stored in a variable and passed around, but you can explicitly tag composables with strings using the testTag modifier, and then tell Radiography to only scan certain tags by passing a composeTestTagScope.

For example, say you have an app with some navigation controls at the top and bottom of the screen, but you only want to scan the main body of the screen in between them:

@Composable fun App() {
  Column {
    ActionBar()
    Body(Modifier.testTag("app-body"))
    BottomBar()
  }
}

To start scanning from Body:

@OptIn(ExperimentalRadiographyComposeApi::class)
val prettyHierarchy = Radiography.scan(scanScope = composeTestTagScope("app-body"))

You can also filter composables out using test tags. For example, say you have a screen that has a debug drawer:

@Composable fun App() {
  ModalDrawerLayout(drawerContent = {
    DebugDrawer(Modifier.testTag("debug-drawer"))
  }) {
    Scaffold(…) {
      …
    }
  }
}

To exclude the debug drawer and its children from the output, use skipComposeTestTagsFilter:

@OptIn(ExperimentalRadiographyComposeApi::class)
val prettyHierarchy = Radiography.scan(viewFilter = skipComposeTestTagsFilter("debug-drawer"))

To write a custom filter for Compose, implement a ViewFilter to handle values of type ComposeView. For example, a filter that excludes composables with a particular Modifier.layoutId might look something like this:

fun skipLayoutIdsFilter(skipLayoutId: (Any) -> Boolean) = ViewFilter { view ->
  (view as? ComposeView)
      ?.modifiers
      ?.asSequence()
      ?.filterIsInstance<LayoutIdParentData>()
      ?.none { layoutId -> skipLayoutId(layoutId.id) }
      // Include all views and composables that don't have any layoutId.
      ?: true
}

Subcomposition scanning

In Compose, the term "composition" is often used to refer to a single tree of composables that all share some core state (e.g. ambients) and use the same underlying slot table. A subcomposition is a distinct composition that has a reference to a particular point in another composition (its parent). Subcompositions share the parent's ambients, but can be created at any time and disposed independently of the parent composition.

Subcomposition is used for a number of things:

  1. Compose children which have a data dependency on a property of the parent composition that is only available after the composition pass, e.g. WithConstraints (which can only compose its children during layout).
  2. Lazy composition, where the "current" actual children of a composable depend on some runtime state, and old/uncreated children should be not be composed when not needed, to save resources. LazyColumn does this.
  3. Linking compositions that need to be hosted in entirely separate windows together. Dialog uses this to make the dialog children act as children of the composable that invokes them, even though they're hosted in a separate window, with a Android view host.

Rendering subcomposition is tricky, because there's no explicit reference from the parent CompositionReference to where the subcompositions' composables actually appear. Fortunately, SubcomposeLayout is a helper composable which provides a convenient wrapper around subcomposition for common use cases such as 1 and 2 above – basically any time the subcomposition is actually a visual child of the parent composable, but can only be created during the layout pass. SubcomposeLayout shows up as a pattern in the slot tables' groups which Radiography detects and renders in a way that makes the subcomposition look like regular children of the parent composable.

Non-SubcomposeLayout subcompositions, like the one from Dialog, are rendered a bit more awkwardly. The subcomposition is shown as a child of the parent layout node. In the case of Dialog, this is fine, since there's no actual layout node in the parent composition which acts as a parent for the subcomposition. More complex use cases may be rendered differently, e.g. if there's a layout node which "hosts" the subcomposition, it will appear after the actual CompositionReference in the slot table, and thus the subcomposition and its subtree will appear before the layout node in the rendering.

Subcompositions have their own slot table that is not shared with their parent. For this reason, Radiography needs to do some extra work to scan subcompositions, since they won't be processed simply by reading the parent's slot table. Subcompositions are detected by looking for instances of CompositionReference in the parent slot table. CompositionReference is an abstract class, but the only concrete implementation currently in Compose contains references to all actual compositions that use it as a parent. Reflection is used to pull the actual subcompositions out of the parent reference, and then those compositions' slot tables are analyzed in turn, and its root composables are rendered as childrens of the node that owns the CompositionReference.

Call group collapsing

To simplify the hierarchy output of the Compose tree, only nodes that are "emitted" to the layout are included. This means that intermediate "call" nodes are collapsed together with the emitted node that they wrap. Each emitted node inherits the display name of the top level call node wrapping it.

For example, with this Compose code:

@Composable
fun Call3() {
  BasicText(text = "hello")
}

@Composable
fun Call2() {
  Call3()
}

@Composable
fun Call1() {
  Call2()
}

Column {
  Call1()
}

The hierarchy output will simply look like this: Column -> Call1.

The BasicText composable and its text are represented by the name Call1, because that is the composable function that wraps it. The Call3 and Call2 nodes are not shown because they don't emit a layout, and they are wrapped by Call1.

This approach helps to manage the many levels of nesting that can occur in a Compose tree. However, sometimes you may want to see the granular view of all the calls in the tree. To do this, you can use the ComposeView.callChain property to see the full call chain that led to the emitted node. In this case, the values of the property would look like this:

"Call1", "Call2", "Call3", "BasicText", "Layout", "ReusableComposeNode"

You can access this property if you implement a custom ViewStateRenderer or use Custom Hierarchy Exploration.

For more details on how Radiography works with Compose, see [How are compositions rendered?](#How are compositions rendered?)

Compose example output

screenshot

com.squareup.radiography.sample.compose/com.squareup.radiography.sample.compose.MainActivity:
window-focus:false
 DecorView { 1080×2160px }
 ├─LinearLayout { 1080×2028px }
 │ ├─ViewStub { id:action_mode_bar_stub, GONE, 0×0px }
 │ ╰─FrameLayout { 1080×1962px }
 │   ╰─FitWindowsLinearLayout { id:action_bar_root, 1080×1962px }
 │     ├─ViewStubCompat { id:action_mode_bar_stub, GONE, 0×0px }
 │     ╰─ContentFrameLayout { id:content, 1080×1962px }
 │       ╰─AndroidComposeView { 1080×1962px, focused }
 │         ╰─Providers { 1080×1962px }
 │           ╰─ComposeSampleApp { 992×1874px }
 │             ├─Image { 240×352px }
 │             ├─TextField { 770×154px, test-tag:"text-field" }
 │             │ ├─Box { 200×59px, layout-id:"Label" }
 │             │ │ ╰─ProvideTextStyle { 200×59px, text-length:8 }
 │             │ ╰─ProvideTextStyle { 682×59px, layout-id:"TextField" }
 │             │   ╰─BaseTextField { 682×59px, text-length:0 }
 │             │     ╰─Layout { 682×59px }
 │             ├─TextField { 770×154px }
 │             │ ├─Box { 196×59px, layout-id:"Label" }
 │             │ │ ╰─ProvideTextStyle { 196×59px, text-length:8 }
 │             │ ╰─ProvideTextStyle { 682×59px, layout-id:"TextField" }
 │             │   ╰─BaseTextField { 682×59px, text-length:0 }
 │             │     ╰─Layout { 682×59px }
 │             ├─Row { 387×67px }
 │             │ ├─Checkbox { 55×55px, value:"Unchecked" }
 │             │ ├─Spacer { 22×0px }
 │             │ ╰─Text { 298×59px, text-length:11 }
 │             ├─Row { 685×99px }
 │             │ ├─TextButton { 199×99px }
 │             │ │ ╰─Providers { 155×55px }
 │             │ │   ╰─Text { 155×52px, text-length:7 }
 │             │ ╰─TextButton { 442×99px }
 │             │   ╰─Providers { 398×55px }
 │             │     ╰─Text { 398×52px, text-length:15 }
 │             ├─AndroidView {  }
 │             │ ╰─ViewBlockHolder { 919×53px }
 │             │   ╰─TextView { 919×53px, text-length:53 }
 │             ├─ScrollableRow { 1324×588px }
 │             │ ╰─ScrollableColumn { 1324×1026px }
 │             │   ╰─Text { 1324×1026px, test-tag:"live-hierarchy", text-length:2525 }
 │             ╰─TextButton { 737×99px }
 │               ╰─Providers { 693×55px }
 │                 ╰─Text { 693×52px, text-length:28 }
 ├─View { id:navigationBarBackground, 1080×132px }
 ╰─View { id:statusBarBackground, 1080×66px }

This sample can be found in the sample-compose directory.

FAQ

What is Radiography useful for?

Radiography is useful whenever you want to look at the view hierarchy and don't have the ability to connect the hierarchy viewer tool. You can add the view hierarchy string as metadata to crash reports, add a debug drawer button that will print it to Logcat, and use it to improve Espresso errors (here's an example).

Is Radiography production ready?

The code that retrieves the root views is based on Espresso's RootsOracle so it's unlikely to break in newer Android versions. We've been using Radiography for crash reports in production since 2015 without any issue.

Why use custom attribute string rendering instead of View.toString() ?

The output of View.toString() is useful but harder to read:

// View.toString():
Button { VFED..C.. ........ 0,135-652,261 #7f010001 app:id/show_dialog }
// Radiography:
Button { id:show_dialog, 652x126px, text-length:28 }

If you'd rather rely on View.toString(), you can provide a custom state renderer.

val prettyHierarchy = Radiography.scan(viewStateRenderers = listOf(androidViewStateRendererFor<View> {
  append(
      it.toString()
          .substringAfter(' ')
          .substringBeforeLast('}')
  )
}))

How are compositions rendered?

Disclaimer: Compose is changing frequently, so many of these details may change without warning, and none of this is required to use Radiography!

The API for configuring how composables are rendered is slightly different than for regular views, since composables simply are not Views. What might define a UI "component" or "widget" logically isn't made up of any single, nicely-encapsulated object. It is likely a few layers of convenience @Composable functions, with some Modifiers applied at various levels, and state is stored in the slot table via remember{}, not in instance fields.

Radiography uses the Compose Tooling library to parse a composition's slot table into a tree of objects which represent "groups" – each group can represent some data stored in the slot table, a function call, or an emitted layout node or Android view. Groups may include source locations in certain cases, and contain information about modifiers and function parameters. This is a really powerful API, but there's a lot of data there, and much of it is not helpful for a description that should be easy to read to get a general sense of the state of your UI.

Radiography filters this detailed tree of groups down to only contain the actual LayoutNodes and Android Views emitted by the composition. It identifies each such node by the name of the function call that is highest in the subtree below the parent emitted node. The node's modifiers are used to extract interesting data about the composable. Most of the interesting data is stored in a single modifier, the SemanticsModifier. This is the modifier used to store "semantics" information which includes accessibility descriptions, actions, and flags, and is also used for writing UI tests.

License

Copyright 2020 Square Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

radiography's People

Contributors

afollestad avatar afzalive avatar androiddevnotes avatar elihart avatar jamesmosley55 avatar mr-thierry avatar pyricau avatar rjrjr avatar samruston avatar vrallev avatar zach-klippenstein avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

radiography's Issues

Suitable to include in non-fatal tracking?

Just a question, no bug report:

As radiography scans the whole view hierarchy and does its magic on every node, would you consider it suitable for logging for non-fatals, that is e.g. when reporting warnings to your favourite crash tracking tool? Or is the performance overhead too big and could cause jank/stutters etc.? As you guys have been using it for years in your apps you likely have quite reliable data about that or at least a solid gut feeling. ;)

Text values not written out in newer Compose (1.5.1)

Radiography is not able to write out the text value of a Text composable anymore. To reproduce, you can upgrade the Compose version in this project and run the tests in the compose-tests module.

Old Compose was writing the text value of the Text composable into the text value in the semantics, but this is no longer the case. The text value is available in a Modifier node called TextStringSimpleElement or TextAnnotatedStringElement, but those classes are internal in Compose. As a hacky proof of concept, I was able to grab the value using reflection:

(view as? ComposeView)
      ?.modifiers
      ?.filterIsInstance<ModifierNodeElement<Node>>()
      ?.forEach {
        val clazz = Class.forName("androidx.compose.foundation.text.modifiers.TextStringSimpleElement")
        if (!clazz.isInstance(it)) return@forEach
        val textField = clazz.getDeclaredField("text")
        textField.isAccessible = true
        val stringValue = textField.get(it) as? String
        appendTextValue(label = "text",  text = stringValue, renderTextValue, textValueMaxLength)
      }

I'm working with Compose 1.5.1 - I see that the Radiography tests use Compose 1.0.1. I didn't bisect to see when this broke, but I could probably do so, if that would be helpful.

If I come up with a better solution, I'll open a PR.

Function names lost with 1.6

When using Compose 1.6.0, the function names are no longer available. This is reproduced in the nestedLayouts test which fails:

expected to contain:
Box:
 Box { test-tag:"root" }
 ├─Box
 ├─Column
 │ ├─Box
 │ ╰─Box
 ╰─Row
   ├─Box
   ╰─Box
but was:
:
  { test-tag:"root" }
 ├─
 ├─
 │ ├─
 │ ╰─
 ╰─
   ├─
   ╰─

at radiography.test.compose.ComposeUiTest.nestedLayouts(ComposeUiTest.kt:350)

It looks to be related to this change in Compose: https://android-review.googlesource.com/c/platform/frameworks/support/+/2741462. However, it doesn't seem as simple as calling composer.collectParameterInformation().

Snapshot testing

Hello,

I was wondering if this lib could eventually be used for something very similar to Snapshot testing?
Very similar to Shot, but instead of screenshots with a textual output?

Feel free to close if this is completely out of scope.

Thanks in advance

Filtering isn't working in the sample app

Both the README and sample app (XML layouts) says the Filter should use it to match the view:

viewFilter = { it !is LinearLayout }

Or this doesn't work, it being an AndroidView. it.view must be used instead:

viewFilter = { (it as ScannableView.AndroidView).view !is LinearLayout }

Fix threading ambiguity in ScanScopes.findRoots()

View scanning is performed on the main thread by Radiography.scan. However, finding view roots, via ScanScope.findRoots(), is performed on the calling thread. Some view roots need to run on the main thread or a view-specific thread.

findRoots should get documentation calling out that it can be called on any thread, and the current code should be guarded to make sure it's running on the correct thread. One tricky bit about this is that view scanning is done lazily, via sequences, so scopes that perform operations on sequences need to make sure that's running on the appropriate threads. This could get complicated since sequence items don't include any information about which view they're processing or which thread needs to be used to access that view. Is this laziness really necessary? Would be a lot simpler to do things eagerly, so we don't have to plumb all the information about threading through the system.

I think this might be causing test flakes, so I changed the UI tests to run scan() on the main thread in 03f09c9 for #120. However, this is still a potential issue in production, so we should solve it for real.

Support publish no-op libraries

Now we are integrating this lib like

dependencies {
  implementation 'com.squareup.radiography:radiography:2.5'
}
  val graph = Radiography.scan()
  Log.d("CustomActivity", "Radiography: $graph")

Usually, it's unnecessary to print this graph on release builds, if we can provide no-op libs like what Chucker did, that might be awesome.

dependencies {
  debugImplementation "com.squareup.radiography:radiography:2.5"
  releaseImplementation "com.squareup.radiography:radiography-no-op:2.5"
}

Merge compose text renderer into main one

How the contents of text views are rendered (show/hide, max length) are currently exposed in a number of ways: via multiple Defaults properties, and via parameters on text-specific renderers (both for android views and compose). These are awkward to compose: if you want to customize the text defaults, you can't use the defaults properties at all and must manually pass both the Android and the compose text renderers with the same values for their parameters.

There might be other future cases where similar renderers for related Android and compose concepts both need similar configuration, even though they're implemented as completely separate renderers.

Should we create something like a class RenderingConfig that would contain only the shared configuration values for stuff like text rendering?

The text renderer factory functions would take a config object instead, and the Defaults* properties could become functions that accept an optional config object.

Alternatively, the ViewStateRenderer.render method could take a config object, so the config could be changed independently from the initialization of the set of renderers and passed in through a parameter directly on the top-level scan function.

E.g

// Not a data class for binary compatibility reasons
class RenderingConfig internal constructor(
  val includeText: Boolean,
  val maxTextLength: Int
)

// Can be deprecated and replaced to add values in binary compatible way. 
@JvmOverloads
fun RenderingConfig(
  includeText: Boolean = false,
  maxTextLength: Int = Int.MAX_VALUE
): RenderingConfig

fun Radiography.scan(…, config: RenderingConfig = RenderingConfig()) 

interface ViewStateRenderer {
  fun AttributeAppendable.render(
    value: Any?,
    config: RenderingConfig
  )
}

Consider extracting Compose support for multiplatform

People are already starting to play with running Compose on non-android platforms (eg), and this library might be useful there too.

To support this, the non-android parts of this library should be extracted into a core module (ie ViewFilter, ViewStateRenderer, etc), and the compose support should be extracted as well. A good entry point for the Compose module might be either Composer or SlotTable (the latter exists now, but might go away (#58)).

I believe this would require not require a major version bump as long as things are just moving around and the Android module declared an API dependency on the core module.

Drop SlotTable.scan, add composable selection support

Another solution to this problem, which would be much more useful for devs, would be the ability to start the scan at some point further down in the tree, similar to passing a non-root View to scan(). However that presents a few (solvable) issues:

  1. Composables don't have any implicit identity, so we'd probably need to do something like for filtering and use test tags or layout IDs to identify the root.
  2. The visitor infra doesn't currently support filtering nodes above, only below, so that would have to be implemented. This could also make it possible to visit Groups directly instead of using the intermediate LayoutInfo type - I'm not sure we should, since that type lets us decouple a bit from compose APIs that might change a lot, but we'd have the option.

Having thought about this more, that API makes much more sense than this one. I'll take a stab at implementing it, but would like to not block this PR on it since this API is marked as unstable anyway.

Originally posted by @zach-klippenstein in #33 (comment)

Ktlint 0.38.0 breaks indentation checking

Ktlint 0.38.0 removed support for configuring continuation indentation separate from non-continuation indentation via .editorconfig files. Since our code style uses an indent of 2 spaces with a continuation indent of 4 spaces, ktlint no longer accepts our code. Tracked in pinterest/ktlint#816.

Until this is fixed, I've just disabled ktlint's indentation checking entirely so we can start using kotlin 1.4.

Version 2.0.0 is crashing

We've had a lot of crashes coming from this library on Android 11 and vivo devices:

Caused by: java.lang.NoSuchFieldException: No field mKeyedTags in class Landroid/view/View; (declaration of 'android.view.View' appears in /system/framework/framework.jar!classes3.dex)
	at java.lang.Class.getDeclaredField(Class.java)
	at radiography.internal.ComposeViewsKt.<clinit>(ComposeViewsKt.java:14)
	at radiography.internal.ComposeViewsKt.isComposeAvailable(ComposeViewsKt.java:0)
	at radiography.ViewStateRenderers.<clinit>(ViewStateRenderers.java:58)

The offending usage is:

ImmutableList<ViewStateRenderer> renderers =
    ImmutableList.of(
        ViewStateRenderers.ViewRenderer,
        ViewStateRenderers.textViewRenderer(true, 128),
        ViewStateRenderers.CheckableRenderer);

Can you please advise how to handle this?

Two radiography core UI tests seem to be broken on API 30

These are failing on main, as well as on the last two PRs. Doesn't seem to be related to a code change.

radiography.test.RadiographyUiTest > when_showDialog_then_hierarchyHasTwoWindows[test(AVD) - 11] FAILED 
	expected to be true
	at radiography.test.RadiographyUiTest.waitForFocus(RadiographyUiTest.kt:142)

radiography.test.RadiographyUiTest > when_onlyFocusedWindow_then_hierarchyHasOnlyDialog[test(AVD) - 11] FAILED 
	expected to be true
	at radiography.test.RadiographyUiTest.waitForFocus(RadiographyUiTest.kt:142)

Compose 1.4 breaks AndroidView parsing

We upgraded to compose 1.4 and our radiography graphs now no longer pick up AndroidView interop layers within our Composable UIs. As far as I can tell this is because Radiography expects a Ref holding a reference to the android view, and the AndroidView composable was rewritten a bit to not do this.

Instead, LayoutNode within compose now has a interopViewFactoryHolder that seems to play this role instead.

Any plans to update Radiography to support this?

Consider exposing bounds of ComposeView

Hi, I was looking into the code to try and understand(lol) how compose trees were being acquired and to possibly find the bounds of composables as well.

Since the bounds are being successfully acquired in ComposeLayoutInfo, I am hoping you consider exposing this publicly. I suspect it might be useful for others too. Thanks!

Move internal classes to internal package

One of the ways consumers discover the API surface is by looking at types available in the package. E.g. I'll open up the entry point class, has Android Studio sync the project view and see all the files there. Currently, all of our internal classes are there. We should consider moving them to an internal package so that the public API surface is easy to spot.

image

Explore exposing InspectorInfo in the rendering/filtering API.

There is an effort ongoing right now to add InspectorInfo modifiers to all modifiers that describe their values in a human-readable way for some sort of debugger (presumably the LayoutInspector in Android Studio?). This is exactly the sort of information that could be useful for Radiography to render.

There's an example CL that adds this to a modifier here.

Action items:

  • Figure out how this info is stored in the slot table and how to extract it.
  • Is this information actually worth surfacing explicitly somehow in the Radiography API? (E.g. in ComposeView somehow.) Should it replace exposing modifiers entirely? Are more type-safe modifiers still a better API for reflectively selecting and rendering type-safe data?

Note that even if this does provide a better source of debug data going forward, it will only be available on newer versions of Compose for some time, so Radiography will need to support both for at least a few releases.

Polish repo for public release

  • Add license files
  • Add CODEOWNERS

After making public:

  • Setup CLA-assistant (can only be done for public repos)
  • Migrate build system to gradle
  • Move library code into submodule.
  • Convert to kotlin
  • Add ktlint
  • Setup Github Actions CI
  • Setup publishing to maven central
  • Add CoC and Contributing documents
  • Publish initial public release
  • Add binary compatibility validation plugin

Upgrade stdlib to Kotlin 1.4

This was initially done in #45, but then was reverted in #51 to support older consumers for now. We will probably upgrade to 1.4 after the 2.0.0 release.

Cleanup ComposeLayoutRenderers class init workaround

My response too. Doesn't happen normally, only happens when the debugger was connected and about to stop at a breakpoint that involved ViewStateRenderers.

The NoClassDef wasn't for compose, it was for this class, because of the circular ref. VSR starts initializing, asks this class for its defaults, this class starts initializing, which calls functions on VSR which require that class to be loaded, but it's already in the process of initializing.

I'm not sure why this wasn't happening unless the debugger was connected. Anyway a better solution might just be to make the VSR.Defaults* fields initialize lazily.

Originally posted by @zach-klippenstein in #33 (comment)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.