Tag Archives: Android Studio Flamingo

Records in Android Studio Flamingo

Posted by Clément Béra, Senior software engineer

Records are a new Java feature for immutable data carrier classes introduced in Java 16 and Android 14. To use records in Android Studio Flamingo, you need an Android 14 (API level 34) SDK so the java.lang.Record class is in android.jar. This is available from the "Android UpsideDownCake Preview" SDK revision 4. Records are essentially classes with immutable properties and implicit hashCode, equals, and toString methods based on the underlying data fields. In that respect they are very similar to Kotlin data classes. To declare a Person record with the fields String name and int age to be compiled to a Java record, use the following code:

@JvmRecord data class Person(val name: String, val age: Int)

The build.gradle file also needs to be extended to use the correct SDK and Java source and target. Currently the Android UpsideDownCake Preview is required, but when the Android 14 final SDK is released use "compileSdk 34" and "targetSdk 34" in place of the preview version.

android { compileSdkPreview "UpsideDownCake" defaultConfig { targetSdkPreview "UpsideDownCake" } compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { jvmTarget = '17' } }

Records don’t necessarily bring value compared to data classes in pure Kotlin programs, but they let Kotlin programs interact with Java libraries whose APIs include records. For Java programmers this allows Java code to use records. Use the following code to declare the same record in Java:

public record Person(String name, int age) {}

Besides the record flags and attributes, the record Person is roughly equivalent to the following class described using Kotlin source:

class PersonEquivalent(val name: String, val age: Int) { override fun hashCode() : Int { return 31 * (31 * PersonEquivalent::class.hashCode() + name.hashCode()) + Integer.hashCode(age) } override fun equals(other: Any?) : Boolean { if (other == null || other !is PersonEquivalent) { return false } return name == other.name && age == other.age } override fun toString() : String { return String.format( PersonEquivalent::class.java.simpleName + "[name=%s, age=%s]", name, age.toString() ) } } println(Person(“John”, 42).toString()) >>> Person[name=John, age=42]

It is possible in a record class to override the hashCode, equals, and toString methods, effectively replacing the JVM runtime generated methods. In this case, the behavior is user-defined for these methods.

Record desugaring

Since records are not supported on any Android device today, the D8/R8 desugaring engine needs to desugar records: it transforms the record code into code compatible with the Android VMs. Record desugaring involves transforming the record into a roughly equivalent class, without generating or compiling sources. The following Kotlin source shows an approximation of the generated code. For the application code size to remain small, records are desugared so that helper methods are shared in between records.

class PersonDesugared(val name: String, val age: Int) { fun getFieldsAsObjects(): Array<Any> { return arrayOf(name, age) } override fun hashCode(): Int { return SharedRecordHelper.hash( PersonDesugared::class.java, getFieldsAsObjects()) } override fun equals(other: Any?): Boolean { if (other == null || other !is PersonDesugared) { return false } return getFieldsAsObjects().contentEquals(other.getFieldsAsObjects()) } override fun toString(): String { return SharedRecordHelper.toString( getFieldsAsObjects(), PersonDesugared::class.java, "name;age") } // The SharedRecordHelper is present once in each app using records and its // methods are shared in between all records. class SharedRecordHelper { companion object { fun hash(recordClass: Class<*>, fieldValues: Array<Any>): Int { return 31 * recordClass.hashCode() + fieldValues.contentHashCode() } fun toString( fieldValues: Array<Any>, recordClass: Class<*>, fieldNames: String ): String { val fieldNamesSplit: List<String> = if (fieldNames.isEmpty()) emptyList() else fieldNames.split(";") val builder: StringBuilder = StringBuilder() builder.append(recordClass.simpleName).append("[") for (i in fieldNamesSplit.indices) { builder .append(fieldNamesSplit[i]) .append("=") .append(fieldValues[i]) if (i != fieldNamesSplit.size - 1) { builder.append(", ") } } builder.append("]") return builder.toString() } } } }

Record shrinking

R8 assumes that the default hashCode, equals, and toString methods generated by javac effectively represent the internal state of the record. Therefore, if a field is minified, the methods should reflect that; toString should print the minified name. If a field is removed, for example because it has a constant value across all instances, then the methods should reflect that; the field is ignored by the hashCode, equals, and toString methods. When R8 uses the record structure in the methods generated by javac, for example when it looks up fields in the record or inspects the printed record structure, it's using reflection. As is the case for any use of reflection, you must write keep rules to inform the shrinker of the reflective use so that it can preserve the structure.

In our example, assume that age is the constant 42 across the application while name isn’t constant across the application. Then toString returns different results depending on the rules you set:

Person(“John”, 42).toString(); // With D8 or R8 with -dontobfuscate -dontoptimize >>> Person[name=John, age=42] // With R8 and no keep rule. >>> a[a=John] // With R8 and -keep,allowshrinking,allowoptimization class Person >>> Person[b=John] // With R8 and -keepclassmembers,allowshrinking,allowoptimization class Person { <fields>; } >>> a[name=John] // With R8 and -keepclassmembers,allowobfuscation class Person { <fields>; } >>> a[a=John, b=42] // With R8 and -keep class Person { <fields>; } >>> Person[name=John, age=42]
Reflective use cases

Preserve toString behavior

Say you have code that uses the exact printing of the record and expects it to be unchanged. For that you must keep the full content of the record fields with a rule such as:

-keep,allowshrinking class Person -keepclassmembers,allowoptimization class Person { <fields>; }

This ensures that if the Person record is retained in the output, any toString callproduces the exact same string as it would in the original program. For example:

Person("John", 42).toString(); >>> Person[name=John, age=42]

However, if you only want to preserve the printing for the fields that are actually used, you can let the unused fields to be removed or shrunk with allowshrinking:

-keep,allowshrinking class Person -keepclassmembers,allowshrinking,allowoptimization class Person { <fields>; }

With this rule, the compiler drops the age field:

Person("John", 42).toString(); >>> Person[name=John]

Preserve record members for reflective lookup

If you need to reflectively access a record member, you typically need to access its accessor method. For that you must keep the accessor method:

-keep,allowshrinking class Person -keepclassmembers,allowoptimization class Person { java.lang.String name(); }

Now if instances of Person are in the residual program you can safely look up the existence of the accessor reflectively:

Person("John", 42)::class.java.getDeclaredMethod("name").invoke(obj); >>> John

Notice that the previous code accesses the record field using the accessor. For direct field access, you need to keep the field itself:

-keep,allowshrinking class Person -keepclassmembers,allowoptimization class Person { java.lang.String name; }

Build systems and the Record class

If you’re using another build system than AGP, using records may require you to adapt the build system. The java.lang.Record class is not present until Android 14, introduced in the SDK from "Android UpsideDownCake Preview" revision 4. D8/R8 introduces the com.android.tools.r8.RecordTag, an empty class, to indicate that a record subclass is a record. The RecordTag is used so that instructions referencing java.lang.Record can directly be rewritten by desugaring to reference RecordTag and still work (instanceof, method and field signatures, etc.).

This means that each build containing a reference to java.lang.Record generates a synthetic RecordTag class. In a situation where an application is split in shards, each shard being compiled to a dex file, and the dex files put together without merging in the Android application, this could lead to duplicate RecordTag class.

To avoid the issue, any D8 intermediate build generates the RecordTag class as a global synthetic, in a different output than the dex file. The dex merge step is then able to correctly merge global synthetics to avoid unexpected runtime behavior. Each build system using multiple compilation such as sharding or intermediate outputs is required to support global synthetics to work correctly. AGP fully supports records from version 8.1.

Android Studio Flamingo is stable

Posted by Steven Jenkins, Product Manager, Android Studio

Today, we are thrilled to announce the stable release of Android Studio Flamingo🦩: The official IDE for building Android apps!

This release includes improvements to help you build pixel-perfect UI with Live Edit, new features that assist with inspecting your app, IntelliJ updates, and more. Read on or watch the video to learn more about how Android Studio Flamingo🦩 can help supercharge your productivity and download the latest stable version today!

  

UI Tools

Jetpack Compose and Material 3 templates – Jetpack Compose is now recommended for new projects so the templates use Jetpack Compose and Material 3 by default.

Live Edit (Compose) experimental – Iteratively build an app using Compose by pushing code changes directly to an attached device or emulator. Push changes on file save or automatically and watch your UI update in real time. Live Edit is experimental and can be enabled in the Editor Settings. There are known limitations. Please send us your feedback so that we can continue to improve it. Learn more.

Moving image illustrating a live edit
Live edit

Themed app icon Preview support – You can now use the System UI Mode selector on the toolbar to switch wallpapers and see how your themed app icons react to the chosen wallpaper. (Note: required in apps targeting API level 33 and higher.)

Moving image illustrating preview of themed app icons across different wallpapers
Previewing Themed app icons across different wallpapers
Dynamic color Preview

Enable dynamic color in your app and use the new wallpaper attribute in an @Preview composable to switch wallpapers and see how your UI reacts to different wallpapers. (Note: you must use Compose 1.4.0 or higher.)

Moving image illustrating dynamic color wallpaper in Compose Preview
Compose Preview: dynamic color wallpaper

Build

Build Analyzer task categorization – Build Analyzer now groups tasks by categories such as Manifest, Android Resources, Kotlin, Dexing and more. Categories are sorted by duration and can be expanded to display a list of the corresponding tasks for further analysis. This makes it easy to know which categories have the most impact on build time.

Image of Build Analyzer Task Categorization
Build Analyzer Task Categorization

One-click automated profileable build and run – When you are profiling your app, you want to avoid profiling a debuggable build. These are great during development, but the results can be skewed. Instead, you should profile a non-debuggable build because that is what your users will be running. This is now more convenient with one-click automated profileable build and run. Easily configure a profileable app and profile it with one click. You can still choose to profile your debuggable build by selecting Profile app with complete data. Read more on the blog.

Image illustrating One-click Automated Profileable Build and Run
One-click Automated Profileable Build and Run

Lint support for SDK extensions – SDK extensions leverage modular system components to add APIs to the public SDK for previously released API levels. Now, you can scan for and fix SDK extension issues with lint support. Android Studio automatically generates the correct version checks for APIs that are launched using SDK extensions.

Image showing Lint Support for SDK Extensions
Lint Support for SDK Extensions

Android Gradle Plugin 8.0.0 – Android Studio Flamingo ships with a new, major version of the Android Gradle plugin. The plugin brings many improvements, but also introduces a number of behavior changes and the Transform API removal. Please make sure to read about the required changes before you upgrade the AGP version in your projects.

Inspect

Updates to App Quality Insights – Discover, investigate, and reproduce issues reported by Crashlytics with App Quality Insights. You can filter by app version, Crashlytics signals, device type, or operating system version. In the latest update you can now close issues or add useful annotations in the Notes pane.

Image showing how you can annotate and close issues inside the notes pane
Annotate and close issues inside the notes pane

Network Inspector traffic interception – Network Inspector now shows all traffic data for the full timeline by default. Create and manage rules that help test how your app behaves when encountering different responses such as status codes, and response headers and bodies. The rules determine what responses to intercept and how to modify these responses before they reach the app. You can choose which rule to enable or disable by checking the Active box next to each rule. Rules are automatically saved every time you modify them.

Image showing Network Inspector Traffic Interception
Network Inspector Traffic Interception

Auto-connect to foreground process in Layout Inspector – Layout Inspector now automatically connects to the foreground process. You no longer have to click to attach it to your app.

IntelliJ

IntelliJ Platform Update – Android Studio Flamingo (2022.2.1) includes the IntelliJ 2022.2 platform release, which comes with IDE performance improvements, enhanced rendering performance on macOS thanks to the Metal API and more. It also improves the IDE performance when using Kotlin, which positively impacts code highlighting, completion, and find usages. Read the IntelliJ release notes here.

Summary

To recap, Android Studio Flamingo (2022.2.1) includes these new enhancements and features:

UI Tools
  • Live Edit (Compose) - Experimental
  • Themed app icon Preview support
  • Dynamic color Preview
  • Jetpack Compose and Material 3 Templates

Build
  • Build Analyzer Task Categorization
  • One-click Automated Profileable Build and Run
  • Lint Support for SDK Extensions
  • Breaking changes in Android Gradle Plugin 8.0

Inspect
  • Updates to App Quality Insights
  • Network Inspector Traffic Interception
  • Auto-connect to foreground process in Layout Inspector

IntelliJ
  •  IntelliJ Platform 2022.2 Update

Check out the Android Studio release notes, Android Gradle plugin release notes, and the Android Emulator release notes for more details.

Download Studio Today!

Now is the time to download Android Studio Flamingo (2022.2.1) to incorporate the new features into your workflow. As always, we appreciate any feedback on things you like and issues or features you would like to see. If you find a bug or issue, please file an issue and also check out known issues. Remember to also follow us on Twitter, Medium, or YouTube for more Android development updates!