From zero to 10 million lines of Kotlin
We’re sharing lessons learned from shifting our Android development from Java to Kotlin. Kotlin is a popular language for Android development and offers some key advantages over Java. As of today,…
Topic
24 articles
We’re sharing lessons learned from shifting our Android development from Java to Kotlin. Kotlin is a popular language for Android development and offers some key advantages over Java. As of today,…
Meta has been on a years-long undertaking to translate our entire Android codebase from Java to Kotlin. Today, despite having one of the largest Android codebases in the world, we’re well pas…
Five years ago, we announced that React Native (RN) is the future of mobile at Shopify. Today, we are excited to share the progress we've made, lessons learned, and what the future holds. To recap, we decided to switch to RN for 3 main reasons: Write it once - Stop building the same features twice, once on iOS and once on Android Talent portability - Enable devs to work fluently across iOS, Android, and Web Ship more value - Spend more time delivering value to users instead of chasing feature parity We’re happy to share that our transition has been quite successful: Not having to build the same features twice has given us a step change in productivity Engineers are able to work across web and mobile allowing teams to do more with the same number of people and unlocked new growth opportunities Maintaining feature parity between iOS and Android has become a non-issue, freeing up capacity to ship a lot more value Our apps are blazing fast (<500ms screen loads) and stable (>99.9% crash-free sessions) We continue to leverage native wherever it is the best tool for the job, giving us the best of both worlds Over the past 5 years, we have migrated all our apps to React Native. Instead of using a one-size-fits-all approach to do so, each team chose when and how to migrate their app. This allowed them to continue shipping features while also aligning with our strategy of leveraging RN. What did we learn? React Native apps are fast We care very deeply about performance at Shopify. As our CEO Tobi Lutke says, “not all fast software is great, but all great software is fast”. The biggest question we had while switching to RN and the main reason we didn’t do it sooner was whether we’d be able to achieve our performance goals with it. Before making the decision to switch, we did extensive prototyping which led to promising results. We also saw all the work that Meta
By David Henry & Mel Yahya
It turns out that the first “all Google” phone includes a non-Google bug. Learn about the details of CVE-2022-38181, a vulnerability in the Arm Mali GPU. Join me on my journey through reporting the vulnerability to the Android security team, and the exploit that used this vulnerability to gain arbitrary kernel code execution and root on a Pixel 6 from an Android app.
Starting From the Top Imagine joining a new team at a new company and you finally get access to a codebase. Slack iOS Xcode File Hierarchy 2017 Where do you add a source file for your team’s project? Great question. Your new team has three source directories for you to add files to, and oh,…
In 2020, we announced that React Native is the future of mobile at Shopify. As part of that journey, we’ve been migrating Shopify Mobile (our largest app at 300 screens per platform) from native to React Native. Here’s how it’s going.
Shopify successfully migrated two of its largest apps, Shopify Mobile and Shopify Point of Sale (POS) to React Native's New Architecture while maintaining weekly releases and serving millions of merchants. This migration involved a complex codebase with hundreds of screens and native modules, extensive custom components, and deep integration with first-party libraries like FlashList.
How Airbnb ships features faster across web, iOS, and Android using a server-driven UI system named Ghost Platform 👻.
Accept Google Pay on Android, web, and React Native with automatic wallet authentication and compatibility with subscriptions and recurring payments.
Today we’re excited to announce that we’ll be adding support for Swift packages to GitHub Package Registry. Swift packages make it easy to share your libraries and source code across your projects and with the Swift community.
We are excited to begin rolling out the new version of Messenger on iOS. To make the Messenger iOS app faster, smaller, and simpler, we rebuilt the architecture and rewrote the entire codebase, whi…
Managing app size at Facebook is a unique challenge: Every day, developers check in large volumes of code, and each line of code translates into additional bits in the apps that people ultimately d…
We are proud to announce that Meta has officially joined the Kotlin Foundation as a gold member, marking a significant milestone in our ongoing commitment to Kotlin and the broader Android developm…
Facebook for iOS (FBiOS) is the oldest mobile codebase at Meta. Since the app was rewritten in 2012, it has been worked on by thousands of engineers and shipped to billions of users, and it can sup…
Use our server-side libraries to work with Stripe from Ruby, Python, PHP, Java, Node, Go and .NET or our Web SDKs to work with HTML and JavaScript (including React). We also provide Mobile SDKs for iOS, Android and React Native.
We built a framework that transforms WebViews in the Shopify app to feel native and solve performance and integration challenges without rebuilding for mobile.
We rebuilt our mobile end-to-end testing framework with a strict API and computer vision, raising test stability drastically.
Balancing Engineering Ambition with Product Realism Introduction In July of 2023, Etsy’s App Updates team, responsible for the Updates feed in Etsy’s mobile apps, set off with an ambitious goal: to revamp the Updates tab to become Deals, a home for a shopper’s coupons and sales, in time for Cyber Week 2023. The Updates tab had been around for years, and in the course of its evolution ended up serving multiple purposes. It was a hub for updates about a user’s favorite shops and listings, but it was also increasingly a place to start new shopping journeys. Not all updates were created equal. The most acted-upon updates in the tab were coupons offered for abandoned cart items, which shoppers loved. We spotted an opportunity to clarify intentions for our users: by refactoring favorite-based updates into the Favorites tab, and (more boldly), by recentering Updates and transforming it into a hub for a buyer’s deals. Technical Opportunity While investigating the best way to move forward with the Deals implementation, iOS engineers on the team advocated for developing a new tab from the ground up. Although it meant greater initial design and architecture effort, an entirely new tab built on modern patterns would let us avoid relying on Objective C, as well as internal frameworks like SDL (server-driven layout), which is present in many legacy Etsy app screens and comes with a variety of scalability and performance issues, and is in the process of being phased out. At the same time, we needed a shippable product by October. Black Friday and Cyber Week loomed on the horizon in November, and it would be a missed opportunity, for us and for our users, not to have the Deals tab ready to go. Our ambition to use modern, not yet road-tested technologies would have to balance with realism about the needs of the product, and we were conscious of maintaining that balance throughout the course of development. In comes Swift UI and Tuist! Two new frameworks were front of mind when starting this project: Swift UI and Tuist. Swift UI provides a clear, declarative framework for UI development, and makes it easy for engineers to break down views into small, reusable components. Maybe Swift UI’s biggest benefit is its built-in view previews: in tandem with componentization, it becomes a very straightforward process to build a view out of smaller pieces and preview at every step of the way. Our team had experimented with Swift UI in the past, but with scopes limited to small views, such as headers. Confident as we were about the framework, we expected that building out a whole screen in Swift UI would present us some initial hurdles to overcome. In fact, one hurdle presented itself right away. In a decade-old codebase, not everything is optimized for use with newer technologies. The build times we saw for our Swift UI previews, which were almost long enough to negate the framework’s other benefits, testified to that fact. This is where Tuist comes in. Our App Enablement team, which has been hard at work over the past few years modernizing the Etsy codebase, has adopted Tuist as a way of taming the monolith making it modular. Any engineer at Etsy can declare a Tuist module in their project and start working on it, importing parts of the larger codebase only as they need them. (For more on Etsy’s usage of Tuist, check out this article by Mike Simons from the App Enablement team.) Moving our work for the Deals tab into a Swift-based Tuist module gave us what it took to make a preview-driven development process practical: our previews build nearly instantly, and so long as we’re only making changes in our framework the app recompiles with very little delay. Figure 1. A view of a goal end state of a modular Etsy codebase, with a first layer of core modules (in blue), and a second layer of client-facing modules that combine to build the Etsy app. Our architecture The Deals tab comprises a number of modules for any given Etsy user, including a Deals Just for You module with abandoned cart coupons, and a module that shows a user their favorite listings that are on sale. Since the screen is just a list of modules, the API returns them as an array of typed items with the following structure: { "type": "<ModuleName>", "<ModuleName>": { ... } } Assigning each module a type enables us to parse it correctly on the client, and moves us away from the anonymous component-based API models we had used in the past. Many models are still used across modules, however. These include, but are not limited to, buttons, headers and footers, and listing cards. To parse a new module, we either have to build a new component if it doesn't exist yet, or reuse one that does. Adding a footer to a module, for example, can be as simple as: // Model { "type": "my_module", "my_module": { "target_listing": { <ListingCard> }", "recommended_listings": [ <ListingCard> ], "footer": { <Footer> } // Add footer here } } // View var body: some View { VStack { ListingView(listing: targetListing) ListingCarouselView(listings: recommendedListings) MyFooterView(footer: footer) // Add footer here } } We also used Decodable implementations for our API parsing, leading to faster, clearer code and an easier way to handle optionals. With Etsy’s internal APIv3 framework built on top of Apple’s Decodable protocol, it is very straightforward to define a model and decide what is and isn’t optional, and let the container handle the rest. For example, if the footer was optional, but the target and recommended listings are required, decoding would look like this: init(from decoder: Decoder) throws { let container = try decoder.containerV3(keyedBy: CodingKeys.self) // These will throw if they aren't included in the response self.targetListing = try container.requireV3(forKey: .targetListing) self.recommendedListings = try container.requireV3(forKey: .recommendedListings) // Footer is optional self.footer = container.decodeV3(forKey: .footer) } As for laying out the view on the screen, we used a Swift UI List to make the most of the under-the-hood cell reuse that List uses. Figure 2. On the left-hand side, a diagram of how the DealsUI view is embedded in the Etsy app. On the right-hand side, a diagram of how the DeasUI framework takes the API response and renders a list of module views with individual components. Previews, previews, more previews If we were going to maintain a good development pace, we needed to figure out a clean way to use Swift previews. Previewing a small component, like a header that takes a string, is simple enough: just initialize the header view with the header string. For more complex views, though, it gets cumbersome to build a mock API response every time you need to preview. This complexity is only amplified when previewing an entire Deals module. To streamline the process, we decided to add a Previews enum to our more complex models. A good example of this is in the Deals Just for You coupon cards. These cards display an image or an array of images, a few lines of custom text (depending on the coupon type), and a button. Our previews enum for this API model looks like this: // In an extension to DealsForYouCard enum Previews { static var shopCouponThreeImage: ResponseModels.DealsForYouCard { let titleText = "IrvingtonWoodworksStudio" let images = [...] // Three images let button = ResponseModels.Button( buttonText: "10% off shop", action: .init(...) ) return ResponseModels.DealsForYouCard( button: button, saleBadge: "20% off", titleText: titleText, subtitleText: "Favorited shop", action: .init(...), images: images ) } static var listingCoupon: ResponseModels.DealsForYouCard { ... } } Then, previewing a variety of coupon cards, it’s as straightforward as: #Preview { DealsForYouCardView(coupon: .Previews.listingCoupon) } #Preview { DealsForYouCardView(coupon: .Previews.shopCouponThreeImage) } The other perk of this architecture is that it makes it very easy to nest previews, for example when previewing an entire module. To build preview data for the Deals for You module, we can use our coupon examples this way: // In an extension to DealsForYouModule enum Previews { static var mockModule: ResponseModels.DealsForYouModule { let items: [ResponseModels.DealsForYouCard] = [.Previews.listingCoupon, .Previews.shopCouponThreeImage, .Previews.shopCouponTwoImage] let header = ResponseModels.DealsForYouHeader(title: "Deals just for you") return .init(header: header, items: items) } } These enums are brief, clear, and easy to understand; they allow us to lean into the benefits of modularity. This architecture, along with our Decodable models, also enabled us to clear a roadblock that used to occur when our team had to wait for API support before we could build new modules. For example, both the Similar Items on Sale and Extra Special Deals modules in the Deals tab were built via previews, and were ready approximately two weeks before the corresponding API work was complete, helping us meet deadlines and not have to wait for a new App Store release. By taking full advantage of Swift UI's modularity and previewability, not only were we able to prove out a set of new technologies, we also exceeded product expectations by significantly beating our deadlines even with the initial overhead of adopting the framework. Challenges: UIKit interoperability Particularly when it came to tasks like navigation and favoriting, interfacing between our module and the Etsy codebase could pose challenges. An assumption that we had as engineers going into this project was that the code to open a listing page, for example, would just be readily available to use; this was not the case, however. Most navigation code within the Etsy codebase is handled by an Objective C class called EtsyScreenController. While in the normal target, it’s as straightforward as calling a function to open a listing page, that functionality was not available to us in our Deals module. One option would have been to build our own navigation logic using Swift UI Navigation stacks; we weren’t trying to reinvent the wheel, however. To balance product deadlines and keep things as simple as possible, we decided not to be dogmatic, and to handle navigation outside of our framework. We did this by building a custom @Environment struct, called DealsAction, which passes off responsibility for navigation back to the main target, and uses the new Swift callAsFunction() feature so we can treat this struct like a function in our views. We have a concept of a DealsAction type in our API response, which enables us to match an action with an actionable part of the screen. For example, a button response has an action that will be executed when a user taps the button. The DealsAction handler takes that action, and uses our existing UIKit code to perform it. The Deals tab is wrapped in a UIHostingController in the main Etsy target, so when setting up the Swift UI view, we also set the DealsAction environment object using a custom view modifier: let dealsView = DealsView() .handleDealsAction { [weak self] in self?.handleAction(action: $0) } ... func handleDealsAction(action: DealsAction) { // UIKit code to execute action } Then, when we need to perform an action from a Swift UI view, the action handler is present at any layer within the view hierarchy within the Deals tab. Performing the action is as simple as: @Environment(\.handleDealsAction) var handleDealsAction: DealsAction ... MyButton(title: buttonText, fillWidth: false) { handleDealsAction(model.button?.action) } We reused this pattern for other existing functionality that was only available in the main target. For example, we built an environment object for favoriting listings, or for following a shop, and for logging performance metrics. This pattern allows us to include environment objects as needed, and it simplifies adding action handling to any view. Instead of rebuilding this functionality in our Tuist module in pure Swift, which could have taken multiple sprints, we struck a balance between modernization and the need to meet product deadlines. Challenges: Listing Cards The listing card view is a common component used across multiple screens within the Etsy app. This component was originally written in Objective-C and throughout the years has been enhanced to support multiple configurations and layouts, and to be available for A/B testing. It also has built-in functionality like favoriting, which engineers shouldn't have to reimplement each time they want to present a listing card. Figure 3. A diagram of how listing card views are conventionally built in UIKit, using configuration options and the analytics framework to combine various UIKit subviews. It's been our practice to reuse this same single component and make small modifications to support changes in the UI, as per product or experimentation requirements. This means that many functionalities, such as favoriting, long-press menus, and image manipulation, are heavily coupled with this single component, many parts of which are still written in Objective C. Early in the process of developing the new tab, we decided to scope out a way of supporting conventional listing card designs—ones that matched existing cards elsewhere in the app—without having to rebuild the entire card component in Swift UI. We knew a rebuild would eventually be necessary, since we expected to have to support listing cards that differed significantly from the standard designs, but the scope of such a rebuild was a known unknown. To balance our deadlines, we decided to push this more ambitious goal until we knew we had product bandwidth. Since the listing card view is heavily coupled with old parts of the codebase, however, it wasn’t as simple as just typing import ListingCard and flying along. We faced two challenges: first, the API model for a listing card couldn’t be imported into our module, and second the view couldn’t be imported for simple use in a UIViewRepresentable wrapper. To address these, we deferred responsibility back up to the UIKit view controller. Our models for a listing card component look something like this: struct ListingCard { public let listingCardWrapper: ListingCardWrapper let listingCard: TypedListingCard } The model is parsed in two ways: as a wrapper, where it is parsed as an untyped dictionary that will eventually be used to initialize our legacy listing card model, and as a TypedListingCard, which is used only within the Deals tab module. Figure 4. A diagram of how a UIKit listing card builder is passed from the main target to the Deals framework for rendering listing cards. To build the listing card view, we pass in a view builder to the SwiftUI DealsView initializer in the hosting controller code. Here, we are in the full Etsy app codebase, meaning that we have access to the legacy listing card code. When we need to build a listing card, we use this view builder as follows: var body: some View { LazyVGrid(...) { ForEach(listings) { listing in cardViewBuilder(listing) // Returns a UIViewRepresentable } } } There was some initial overhead involved in getting these cards set up, but it was worth it to guarantee that engineering unknowns in a Swift UI rewrite wouldn’t block us and compromise our deadlines. Once built, the support for legacy cards enabled us to reuse them easily wherever they were needed. In fact, legacy support was one of the things that helped us move faster than we expected, and it became possible to stretch ourselves and build at least some listing cards in the Deals tab entirely in Swift UI. This meant that writing the wrapper ultimately gave us the space we needed to avoid having to rely solely on the wrapper! Conclusion After just three months of engineering work, the Deals tab was built and ready to go, even beating product deadlines. While it took some engineering effort to overcome initial hurdles, as well as the switch in context from working in UIKit in the main target to working in Swift UI in our own framework, once we had solutions to those challenges and could really take advantage of the new architecture, we saw a very substantial increase in our engineering velocity. Instead of taking multiple sprints to build, new modules could take just one sprint or less; front-end work was decoupled from API work using Previews, which meant we no longer had to wait for mock responses or even API support at all; and maybe most important, it was fun to use Swift UI’s clear and straightforward declarative UI building, and see our changes in real time! From a product perspective, the Deals tab was a great success: buyers converted their sessions more frequently, and we saw an increase in visits to the Etsy app. The tab was rolled out to all users in mid October, and has seen significant engagement, particularly during Black Friday and Cyber Monday. By being bold and by diving confidently into new frameworks that we expected to see benefits from, we improved engineer experience and not just met but beat our product deadlines. More teams at Etsy are using Swift UI and Tuist in their product work now, thanks to the success of our undertaking, sometimes using our patterns to work through hurdles, sometimes creating their own. We expect to see more of this kind of modernization start to make its way into the codebase. As we iterate on the Deals tab over the next year, and make it even easier for buyers to find the deals that mean the most to them, we look forward to continuing to work in the same spirit. Special thanks to Vangeli Ontiveros for the diagrams in this article, and a huge shoutout to the whole App Deals team for their hard work on this project!
Easily the most important and complex screen in the Buy on Etsy Android app is the listing screen, where all key information about an item for sale in the Etsy marketplace is displayed to buyers. Far from just a title and description, a price and a few images, over the years the listing screen has come to aggregate ratings and reviews, seller and shipping and stock information, and gained a variety of personalization and recommendation features. As information-rich as it is, as central as it is to the buying experience, for product teams the listing screen is an irresistible place to test out new methods and approaches. In just the last three years, apps teams have run nearly 200 experiments on it, often with multiple teams building and running experiments in parallel. Eventually, with such a high velocity of experiment and code change, the listing screen started showing signs of stress. Its architecture was inconsistent and not meant to support a codebase expanding so much and so rapidly in size and complexity. Given the relative autonomy of Etsy app development teams, there ended up being a lot of reinventing the wheel, lots of incompatible patterns getting layered atop one another; in short the code resembled a giant plate of spaghetti. The main listing Fragment file alone had over 4000 lines of code in it! Code that isn’t built for testability doesn’t test well, and test coverage for the listing screen was low. VERY low. Our legacy architecture made it hard for developers to add tests for business logic, and the tests that did get written were complex and brittle, and often caused continuous integration failures for seemingly unrelated changes. Developers would skip tests when it seemed too costly to write and maintain them, those skipped tests made the codebase harder for new developers to onboard into or work with confidently, and the result was a vicious circle that would lead to even less test coverage. Introducing Macramé We decided that our new architecture for the listing screen, which we’ve named Macramé, would be based on immutable data propagated through a reactive UI. Reactive frameworks are widely deployed and well understood, and we could see a number of ways that reactivity would help us untangle the spaghetti. We chose to emulate architectures like Spotify’s Mobius, molded to fit the shape of Etsy’s codebase and its business requirements. At the core of the architecture is an immutable State object that represents our data model. State for the listing screen is passed to the UI as a single data object via a StateFlow instance; each time a piece of the data model changes the UI re-renders. Updates to State can be made either from a background thread or from the main UI thread, and using StateFlow ensures that all updates reach the main UI thread. When the data model for a screen is large, as it is for the listing screen, updating the UI from a single object makes things much simpler to test and reason about than if multiple separate models are making changes independently. And that simplicity lets us streamline the rest of the architecture. When changes are made to the State, the monolithic data model gets transformed into a list of smaller models that represent what will actually be shown to the user, in vertical order on the screen. The code below shows an example of state held in the Buy Box section of the screen, along with its smaller Title sub-component. data class BuyBox( val title: Title, val price: Price, val saleEndingSoonBadge: SaleEndingSoonBadge, val unitPricing: UnitPricing, val vatTaxDescription: VatTaxDescription, val transparentPricing: TransparentPricing, val firstVariation: Variation, val secondVariation: Variation, val klarnaInfo: KlarnaInfo, val freeShipping: FreeShipping, val estimatedDelivery: EstimatedDelivery, val quantity: Quantity, val personalization: Personalization, val expressCheckout: ExpressCheckout, val cartButton: CartButton, val termsAndConditions: TermsAndConditions, val ineligibleShipping: IneligibleShipping, val lottieNudge: LottieNudge, val listingSignalColumns: ListingSignalColumns, val shopBanner: ShopBanner, ) data class Title( val text: String, val textInAlternateLanguage: String? = null, val isExpanded: Boolean = false, ) : ListingUiModel() In our older architecture, the screen was based on a single scrollable View. All data was bound and rendered during the View's initial layout pass, which created a noticeable pause the first time the screen was loaded. In the new screen, a RecyclerView is backed by a ListAdapter, which allows for asynchronous diffs of the data changes, avoiding the need to rebind portions of the screen that aren't receiving updates. Each of the vertical elements on the screen (title, image gallery, price, etc.) is represented by its own ViewHolder, which binds whichever of the smaller data models the element relies on. In this code, the BuyBox is transformed into a vertical list of ListingUiModels to display in the RecyclerView. fun BuyBox.toUiModels(): List<ListingUiModel> { return listOf( price, title, shopBanner, listingSignalColumns, unitPricing, vatTaxDescription, transparentPricing, klarnaInfo, estimatedDelivery, firstVariation, secondVariation, quantity, personalization, ineligibleShipping, cartButton, expressCheckout, termsAndConditions, lottieNudge, ) } An Event dispatching system handles user actions, which are represented by a sealed Event class. The use of sealed classes for Events, coupled with Kotlin "when" statements mapping Events to Handlers, provides compile-time safety to ensure all of the pieces are in place to handle the Event properly. These Events are fed to a single Dispatcher queue, which is responsible for routing Events to the Handlers that are registered to receive them. Handlers perform a variety of tasks: starting asynchronous network calls, dispatching more Events, dispatching SideEffects, or updating State. We want to make it easy to reason about what Handlers are doing, so our architecture promotes keeping their scope of responsibility as small as possible. Simple Handlers are simple to write tests for, which leads to better test coverage and improved developer confidence. In the example below, a click handler on the listing title sets a State property that tells the UI to display an expanded title: class TitleClickedHandler constructor() { fun handle(state: ListingViewState.Listing): ListingEventResult.StateChange { val buyBox = state.buyBox return ListingEventResult.StateChange( state = state.copy( buyBox = buyBox.copy( title = title.copy(isExpanded = true) ) ) ) } } SideEffects are a special type of Event used to represent, typically, one-time operations that need to interact with the UI but aren’t considered pure business logic: showing dialogs, logging events, performing navigation or showing Snackbar messages. SideEffects end up being routed to the Fragment to be handled. Take the scenario of a user clicking on a listing's Add to Cart button. The Handler for that Event might: dispatch a SideEffect to log the button click start an asynchronous network call to update the user’s cart update the State to show a loading indicator while the cart update finishes While the network call is running on a background thread, the Dispatcher is free to handle other Events that may be in the queue. When the network call completes in the background, a new Event will be dispatched with either a success or failure result. A different Handler is then responsible for handling both the success and failure Events. This diagram illustrates the flow of Events, SideEffects, and State through the architecture: Figure 1. A flow chart illustrating system components (blue boxes) and how events and state changes (yellow boxes) flow between them. Results The rewrite process took five months, with as many as five Android developers working on the project at once. One challenge we faced along the way was keeping the new listing screen up to date with all of the experiments being run on the old listing screen while development was in progress. The team also had to create a suite of tests that could comprehensively cover the diversity of listings available on Etsy, to ensure that we didn’t forget any features or break any. With the rewrite complete, the team ran an A/B experiment against the existing listing screen to test both performance and user behavior between the two versions. Though the new listing screen felt qualitatively quicker than the old listing screen, we wanted to understand how users would react to subtle changes in the new experience. We instrumented both the old and the new listing screens to measure performance changes from the refactor. The new screen performed even better than expected. Time to First Content was decreased by 18%, going from 1585 ms down to 1298 ms. This speedup resulted in the average number of listings viewed by buyers increasing 2.4%, add to carts increasing 0.43%, searches increasing by 2%, and buyer review photo views increasing by 3.3%. On the developer side, unit test coverage increased from single digit percentages to a whopping 76% code coverage of business logic classes. This significantly validates our decision to put nearly all business logic into Handler classes, each responsible for handling just a single Event at a time. We built a robust collection of tools for generating testing States in a variety of common configurations, so writing unit tests for the Handlers is as simple as generating an input event and validating that the correct State and SideEffects are produced. Creating any new architecture involves making tradeoffs, and this project was no exception. Macramé is under active development, and we have a few pieces of feedback on our agenda to be addressed: There is some amount of boilerplate still needed to correctly wire up a new Event and Handler, and we'd like to make that go away. The ability of Handlers to dispatch their own Events sometimes makes debugging complex Handler interactions more difficult than previous formulations of the same business logic. On a relatively simple screen, the architecture can feel like overkill. Adding new features correctly to the listing screen is now the easy thing to do. The dual benefit of increasing business metrics while also increasing developer productivity and satisfaction has resulted in the Android team expanding the usage of Macramé to two more of the key screens in the app (Cart and Shop), both of which completely rewrote their UI using Jetpack Compose: but those are topics for future Code as Craft posts.
FlashList v2 is a complete rewrite, delivering faster load times, improved scrolling performance, and precise rendering without requiring item size estimates. It powers thousands of lists in the Shopify mobile app and is now production-ready.
In the last three years, React Native Skia has established itself as the go-to solution for 2D graphics on React Native. And we are seeing many users pushing the boundaries of what can be done with the library.
One of our Guiding Principles at Etsy is that we “commit to our craft.” This means that we have a culture of learning, in which we’re constantly looking for opportunities to improve and learn, adopt industry best practices, and share our findings with our colleagues and our community. As part of that process, Etsy recently adopted Jetpack Compose – Android's modern toolkit for defining native UIs – as our preferred means of building our Android app. The process of adoption consisted of a gradual expansion in the size and complexity of features built using Compose, eventually culminating in a full rewrite of one of the primary screens in the app. The results of that rewrite gave us the confidence to recommend Compose as the primary tool for our Android engineers to build UIs going forward. Adoption Our engineers are always investigating the latest industry trends and technologies, but in this case a more structured approach was warranted due to the central nature of UI toolkits in the development process. Several engineers on the Android team were assigned to study the existing Compose documentation and examples we had used in prior builds and then create a short curriculum based on what they learned. Over several months, the team held multiple information sessions with the entire Android group, showing how to use Compose to build simple versions of some of our real app screens. Part of our in-house curriculum for learning Jetpack Compose via small modules. Each module built upon the previous module to build more complex versions of various features in our real app. Next, our Design Systems team started creating Compose versions of our internal UI toolkit components, with the goal of having a complete Compose implementation of our design system before major adoption. Compose is designed for interoperability with our existing toolkit, XML Views, providing an uncomplicated migration path that enables us to start using these new toolkit components in our existing XML Views with minimal disruption. This was our first chance to validate that the performance of Compose would be as good as or better than our existing toolkit components. This also gave the wider Android community at Etsy a chance to start using Compose in their day-to-day work and get comfortable with the new patterns Compose introduced. A partial list of the design system components our team was able to make available in Compose. Our Design Systems team also made heavy use of one of Compose’s most powerful features: Previews. Compose Previews allow a developer to visualize Composables in as many configurations as they want using arbitrary test data, all without having to run the app on a device. Every time the team made a change to a Design Systems Composable, they could validate the effect in a wide range of scenarios. After a few months of building and adopting toolkit components in Compose, our team felt it was time for a more significant challenge: rebuilding an entire screen. To prevent inadvertently causing a disruption for buyers or sellers on Etsy, we chose a heavily used screen only available in our backend for development builds. This step exposed us to a much wider scope of concerns: navigation, system UI, data fetching using coroutines from our API, and the orchestration of multiple Compose components interacting with each other. Using Kotlin Flows, we worked out how to structure our business and UI logic around a unidirectional data flow, a key unlock for future integration of Compose with Macramé – our standard architecture for use across all screens in the Etsy app. With a full internal screen under our belts, it was time to put Compose in front of real users. A few complex bottom sheets were the next pieces of our app to get the Compose treatment. For the first time, we exposed a major part of our UI, now fully written in Compose, to buyers and sellers on Etsy. We also paired a simple version of our Macramé architecture with these bottom sheets to prove that the two were compatible. A bottom sheet fully using Compose hosted inside of a screen built using Views. After successfully rolling out bottom sheets using Compose, we saw an opportunity to adopt Compose on a larger scale in the Shop screen. The existing Shop screen code was confusing to follow and very difficult to run experiments on – limiting our ability to help sellers improve their virtual storefronts. Compose and Macramé held the promise of addressing all these concerns. The Shop screen, fully built using Compose. In just around three months, our small team completed the rebuild. Our first order of business was to run an A/B experiment on the Shop screen to compare old vs. new. The results gave Compose even better marks than we had hoped for. Initial screen rendering time improved by 5%, and subjective interactions with the Shop screen, like taps and scrolls, were quicker and more fluid. User analytics showed the new screen improved conversion rate, add to cart actions, checkout starts, shop favoriting, listing views, and more – meaning these changes made a tangible, positive impact for our sellers. For the engineers tasked with coding the Shop screen, the results were just as impressive. An internal survey of engineers who had worked with the Shop screen before the rewrite showed a significant improvement in overall developer satisfaction. Building features required fewer lines of code, our respondents told us, and thanks to the Macramé architecture, testing was much easier and enabled us to greatly increase test coverage of business logic. Similar to what we learned during the development of our Design System components, Compose Previews were called out as a superpower for covering edge cases, and engineers said they were excited to work in a codebase that now featured a modern toolkit. Learnings We've learned quite a lot about Compose on our path to adopting it: Because of the unidirectional data flow of our Macramé architecture and stateless components built with Compose, state is decoupled from the UI and business logic is isolated and testable. The combination of Macramé and Compose has become the standard way we build features for our app. Colocation of layout and display logic allows for much easier manipulation of spacing, margins, and padding when working with complex display logic. Dynamic spacing is extremely difficult to do with XML layouts alone, and requires code in separate files to keep it all in sync. Creating previews of all possible Compose states using mock data has eliminated a large source of rework, bugs, and bad experiences for our buyers. Our team found it easier to build lazy-style lists in Compose compared to managing all the pieces involved with using RecyclerView, especially horizontal lazy lists. Interoperability between Compose and Views in both directions enabled a gradual adoption of Compose. Animation of Composables can be triggered automatically by data changes–no writing extra code to start and stop the animations properly. While no individual tool is perfect, we’re excited about the opportunities and efficiencies Compose has unlocked for our teams. As with any new technology, there's a learning curve, and some bumps along the way. One issue we found was in a 3rd party library we use. While the library has support for Compose, at the time of the Shop screen conversion, that support was still in alpha stage. After extensive testing, we decided to move forward using the alpha version, but an incompatibility could have necessitated us finding an alternative solution. Another learning is that LazyRows and LazyColumns, while similar in some respects to RecyclerView, come with their own specific way of handling keys and item reuse. This new lazy list paradigm has occasionally tripped us up and resulted in some unexpected behavior. Conclusion We’re thrilled with our team’s progress and outcomes in adopting this new toolkit. We’ve now fully rewritten several key UI screens, including Listing, Favorites, Search, and Cart using Compose, with more to come. Compose has given us a set of tools that lets us be more productive when delivering new features to our buyers, and its gradual rollout in our codebase is a tangible example of the Etsy team's commitment to our craft.