In the previous article, I introduced the Layout Transpiler that Booster was building – a transpiler that translates XML layout files into class files. During implementation, we uncovered all kinds of design pitfalls in the Android system – massive pitfalls that are nearly impossible to work around. Then Android officially released JetPack Compose, and it was exactly the effect I had been trying to achieve, just in a different form.
The Layout Transpiler
The previous article only sketched the high-level idea of XML-to-class conversion. Many readers said they did not fully understand it, so here I will go deeper into the implementation details.
Why is building AttributeSet the core of Booster’s approach?
At runtime, the AttributeSet passed to View constructors is actually a subclass of XmlPullParser, such as XmlBlock.Parser. So Android parses XML and instantiates Views simultaneously at runtime.
final Map<String, Object> resourcesValueMap777448400 = newHashMap<>(); finalAttributeSetattributeSet777448400=newAttrtibuteSetImpl(7, nameList777448400, valueList777448400, nameResourceList777448400, namespaceList777448400, namespaceValueMap777448400, resourcesValueMap777448400); final android.widget.TextViewview777448400=newandroid.widget.TextView(context, attributeSet777448400); view777448400.setVisibility(0);
final android.view.ViewGroup.LayoutParamslayoutParams944291586=newandroid.view.ViewGroup.LayoutParams(-2, -2); view1833697623.addView(view777448400, layoutParams944291586); return view1833697623; } }
Compile the generated Java code into class files.
Use a Transformer to replace all instructions that access R.layout.${resId} in class files.
The Android Framework Pitfall
The implementation above is nearly perfect. However, we underestimated the Android system’s design. In ResourcesImpl, there are two places that forcefully cast AttributeSet to XmlBlock.Parser. Why would you force-cast an interface to a package-private final class? Your guess is as good as mine.
TypedArray obtainStyledAttributes(@NonNull Resources.Theme wrapper, AttributeSet set, @StyleableResint[] attrs, @AttrResint defStyleAttr, @StyleResint defStyleRes) { synchronized (mKey) { finalintlen= attrs.length; finalTypedArrayarray= TypedArray.obtain(wrapper.getResources(), len); // XXX note that for now we only work with compiled XML files. // To support generic XML files we will need to manually parse // out the attributes from the XML file (applying type information // contained in the resources and such). final XmlBlock.Parserparser= (XmlBlock.Parser) set; // <<<=== look here mAssets.applyStyle(mTheme, defStyleAttr, defStyleRes, parser, attrs, array.mDataAddress, array.mIndicesAddress); array.mTheme = wrapper; array.mXml = parser; return array; } }
staticintgetAttributeSetSourceResId(@Nullable AttributeSet set) { if (set == null || !(set instanceof XmlBlock.Parser)) { // <<<=== look here return ID_NULL; } return ((XmlBlock.Parser) set).getSourceResId(); // <<<=== look here }
A simplified version of how Android Framework natively constructs a View:
1 2 3 4
XmlBlock.Parserparser=newXmlBlock.Parser(); parser.parse(R.layout.main); ... TextViewtxt=newTextView(context, parser); // Note: parser is passed as AttributeSet
After Booster’s optimization, the View construction looks like this:
1 2 3
... AttributeSetImplattrs=newAttributeSetImpl(...); TextViewtxt=newTextView(context, attrs); // Note the difference from native
So when Android Framework tries to force-cast AttributeSetImpl to XmlBlock.Parser, it fails. AttributeSetImpl is not a subclass of XmlPullParser, and it certainly cannot extend a package-private final class like XmlBlock.Parser.
JetPack Compose
Stuck in the Android Framework pitfall with no way out, I was feeling pretty disheartened – until JetPack Compose caught my eye. Here is the official sample:
@Composable funGreeting(name: String) { Text (text = "Hello $name!") }
JetPack Compose is similar to Anko and iOS SwiftUI. Its excellent developer experience comes from two things:
Kotlin’s powerful syntax
Built-in DSL capabilities
Extension functions and properties
Strong IDE support, enabling WYSIWYG editing
As for how Android Studio achieves WYSIWYG, my educated guess is that it works similarly to the Layout Lib described in Layout: XML vs Code, except instead of parsing XML it parses Kotlin code or bytecode. The actual implementation would require a closer look at the Android Studio source code.
Summary
With JetPack Compose, the era of describing layouts in XML is coming to an end. Describing layouts directly in code is the future, and with it, the performance problems caused by XML layouts will cease to exist.