CINXE.COM
Our Android framework - Wave Blog
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no"> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous"> <link rel="stylesheet" href="/walsheim.css"/> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap" rel="stylesheet"> <link rel="stylesheet" href="/main.css"> <link rel="icon" type="image/png" href="/img/favicon.png"> <script async defer data-domain="wave.com" src="https://plausible.io/js/plausible.js"></script> <meta property="og:type" content="article" /> <meta name="twitter:site" content="@WaveSenegal" /> <meta property="og:title" content="Our Android framework" /> <meta property="og:description" content="We built our first Android app in 2014. Like most 2014-era Android apps, it was a mess of fat Activities, heavily-nested callbacks, and spaghetti dataflow. Fortunately, since then Google has iterated a lot on their Android best practices, and we’ve iterated a lot on our apps, giving us plenty of chances to make things better. During the most recent time we rebuilt our agent app, we looked at the new androidx libraries and decided that they almost met our needs–but they left a few crucial gaps that still made writing Android apps more painful than it should be." /> <meta property="og:image" content="https://www.wave.com/img/blog/android-screenshot.jpg" /> <meta name="twitter:card" content="summary_large_image" /> <link rel="stylesheet" href="/jetbrains-mono.css"/> <title>Our Android framework - Wave Blog</title> </head> <body> <header> <nav class="navbar navbar-expand-lg navbar-light"> <a class="navbar-brand" href="/en/"><img src="/img/nav-logo.png" class="logo-small" alt="Wave logo"></a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarCollapsible" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarCollapsible"> <ul class="navbar-nav mr-auto"> <li><a href="/en/">Personal </a></li> <li><a href="/en/business/">Business </a></li> </ul> <ul class="navbar-nav ml-auto"> <li><a href="/en/about/">About</a></li> <li><a href="/en/careers/">Careers</a></li> <li><a href="/en/blog/">Blog</a></li> </ul> </div> </nav> </header> <main id="articleWrapper" class="waverow longform-content"> <article> <h1>Our Android framework</h1> <div class="byline"> <img class="authorThumbnail" src="/img/blog-authors/ben-kuhn.jpg" alt="picture of Ben Kuhn"/> <div class="authorDescription"> <div class="name-and-date"> <span class="name">Ben Kuhn </span> <span class="date"><span class="date-on"> on </span>July 29, 2019</span> </div> <div class="tag"><div class="tag-text">engineering</div></div> </div> </div> <hr> <p>We built our first Android app in 2014. Like most 2014-era Android apps, it was a mess of fat Activities, heavily-nested callbacks, and spaghetti dataflow. Fortunately, since then Google has iterated a lot on their Android best practices, and we’ve <a href="/posts/split/">iterated</a> a lot on our apps, giving us plenty of chances to make things better.</p> <p>During the most recent time we rebuilt our agent app, we looked at the new <a href="https://developer.android.com/jetpack/androidx">androidx</a> libraries and decided that they <em>almost</em> met our needs–but they left a few crucial gaps that still made writing Android apps more painful than it should be. So we built a lightweight set of components and patterns that work together to smooth over those final wrinkles and make Android development truly tolerable.</p> <p>The rest of this post is an excerpt from our internal documentation explaining how these components work together.</p> <hr> <p><strong>Goal of our framework:</strong> eliminate the crappiness of default Android development by…</p> <ul> <li>Keeping Activity classes minimal, using <a href="#viewmodels">Viewmodels</a> and <a href="#actions">Actions</a> to decouple the code from the Android UI layer, making it easy to test</li> <li>Using <a href="#livedata">LiveData</a> to eliminate the toil of propagating data from the viewmodel to the UI</li> <li>Using the <a href="#repository">Repository</a> and GraphQL to eliminate all bookkeeping associated with tracking server-originated data</li> <li>Using <a href="#coroutines">Coroutines</a> to eliminate callback hell</li> <li>Using <a href="#typed-intents">Typed Intents</a> to make it type-safe and boilerplate-free to transition between activities</li> </ul> <h2 id="first-understand-the-basics">First, understand the basics</h2> <ul> <li>Learn what an Activity (“a thing that occupies the screen”) is (<a href="https://developer.android.com/guide/components/activities/intro-activities">intro</a>), and its <a href="https://developer.android.com/guide/components/activities/activity-lifecycle">lifecycle</a></li> <li>Learn how Android UIs are specified (<a href="https://developer.android.com/guide/topics/ui/declaring-layout#write">XML</a>)</li> <li>Kotlin <a href="https://kotlinlang.org/docs/tutorials/kotlin-for-py/introduction.html">intro</a></li> <li>GraphQL <a href="https://graphql.org/learn/">intro</a></li> </ul> <h2 id="major-concepts">Major concepts</h2> <h3 id="viewmodels">ViewModels</h3> <p>Relevant Android docs: <a href="https://developer.android.com/topic/libraries/architecture/viewmodel">viewmodels</a>, <a href="https://developer.android.com/topic/libraries/data-binding">databinding</a></p> <p><strong>Historical context:</strong> In the bad old days, all your UI-related code (and unless you were thoughtful, all your code period!) lived in Activity subclasses. Your Activity would start out by inflating some XML layout. It would search through the resulting views for the ones it cared about and add event handlers to them. The event handlers would trigger some updates, and the updates would run hand-written glue code that propagated data back to the UI. This had many downsides:</p> <ul> <li>To test effectively you need to run code that depends on Android libs, which means running code on an actual Android phone to test effectively, and that’s really flakey.</li> <li>Activities take up a lot of memory and it’s easy for the Activity to trigger background work that retains a reference to the Activity, causing the Activity never to get garbage-collected.</li> <li>Android destroys and recreates activities all the time—for instance, when you rotate the screen, switch to a different application, or just because they felt like it. By default this destroys all UI state, but ViewModels can survive these destroy/recreate events.</li> </ul> <p><strong>New hotness:</strong> Instead, the Activity constructs a ViewModel class, and the ViewModel doesn’t have any dependencies on Android libraries. The layout XML gets “bound” to the view model, which means that you write little expressions in the XML, and an Android tool called the “databinding compiler” generates a class named e.g. <code>AgentHomeBindings</code> that glues the UI to the ViewModel. To run business logic, the bindings glue UI events to ViewModel methods.</p> <h3 id="actions">Actions</h3> <p>Wave’s docs: <code>Actions.kt</code></p> <p>If the ViewModel doesn’t depend on Android, what happens when our business logic needs to do something Android-specific (like launch an activity, or present a dialog)? In this case, the viewmodel uses an <code>Actions</code> instance. <code>Actions</code> is an interface that defines functions, like <code>showDialog</code>, that the ViewModel can invoke but which require cooperation from the Activity. When the ViewModel’s Activity is running, its <code>actions</code> instance is an object that talks to the Activity and tells it to do UI stuff; when the Activity is inactive, the corresponding ViewModel’s actions become a “no-op” instance to avoid holding a reference to the Activity. In tests, the actions are faked so that we can test ViewModel behavior without needing an Activity (or anything from the Android framework) at all.</p> <h3 id="livedata">LiveData</h3> <p><a href="https://developer.android.com/topic/libraries/architecture/livedata">Relevant Android docs</a></p> <p>LiveData is the blessed Android abstraction for a variable where you can get notified if it updates. This is how data flows from the ViewModel to the UI—the databinding compiler (mentioned above) generates code that subscribes to each relevant LiveData, takes their updates and sticks them in the appropriate place in the UI. Generally, a ViewModel’s main job is to construct a tree/DAG of LiveData instances, and then receive callbacks that update the LiveDatas (or do other business logic, e.g. hit the server).</p> <p>(Note that Android supports <em>bidirectional</em> data flow too—for instance, a LiveData connected to the text field, where the user edits and the ViewModel both change the livedata value. We prefer not to do this, because bidirectional data flow is confusing. Instead, we have the UI “request” a change to the LiveData through a callback, so that the ViewModel is the sole authority on what the UI state should be. This prevents hard-to-test ViewModel↔UI interaction bugs.)</p> <h3 id="repository">Repository</h3> <p><a href="https://www.apollographql.com/docs/android/essentials/get-started/">Underlying GQL library (Apollo-Android) docs</a></p> <p>Inspired by: <a href="https://relay.dev/">Relay</a></p> <p>The <code>Repository</code> deals with fetching and caching data from the server, and submitting updates. Its main nicety is that you can submit a GraphQL query, and then subscribe to updates to it—even updates that the Repository found out about via a different query or mutation. For instance, the Chooser screen fetches the logged-in user’s balance via the UserQuery operation, and subscribes to the result. If the user later sends a transaction, it will invoke a SendMutation, which will re-fetch the user’s balance, but via a different fragment. The Repository will be smart enough to know that the two balances belong to the same user, and will update the balance on screen.</p> <p>The Repository is a wrapper around the <a href="https://www.apollographql.com/docs/android/essentials/get-started/">apollo-android</a> GraphQL library, which generates (strongly-typed) Java classes for every <code>.graphql</code> query/mutation we write and implements the caching/subscribing infrastructure.</p> <p>The Repository knows which objects have been updated at an appropriately granular level (e.g., the Wallet in the above example) by keying on the ID columns returned by your queries, so you should generally be requesting “id” in most of your queries and mutations. Internally it has a disk-based persistent cache called the “normalized cache”; Apollo-android provides most of the implementation of the cache but we’ve overridden certain aspects of the cache key generation.</p> <h3 id="coroutines">Coroutines</h3> <p><a href="https://kotlinlang.org/docs/reference/coroutines-overview.html">Relevant Kotlin docs</a></p> <p><strong>Historical context:</strong> Like most UI toolkits, Android requires some computation (that interacts with views) to happen on the main thread. But no <em>long-running</em> computation can happen on the main thread, because the main thread is also responsible for handling user input. So if you block, the UI will lock up.</p> <p>So what happens if you want to run a computation that takes a long time (e.g. a network request) on the main thread? You can’t just write <code>result = doNetworkRequest(params)</code>, because <code>doNetworkRequest</code> could lock up the UI for seconds. The default way of solving this is to write <code>doNetworkRequest(params, callback)</code>, where <code>callback</code> is a function (or an anonymous inner class, because this is Java) accepting <code>result</code> as a parameter. This leads to super-deep nesting of code.</p> <p><strong>New hotness:</strong> A <em>coroutine</em> (in Kotlin, a function prefixed by the <code>suspend</code> keyword) is a function that has the ability to “yield” to something else on the main thread, and be resumed at some point in the future. They are written like normal functions, and the compiler turns them into effectively the same type of callback-oriented code (called <em><a href="https://labs.pedrofelix.org/guides/kotlin/coroutines/coroutines-and-state-machines">continuation passing style</a></em>) under the hood.</p> <p>Every time normal Android development uses callbacks, we try to use coroutines instead—they lead to dramatically cleaner and more readable code.</p> <h3 id="typed-intents">Typed Intents</h3> <p><strong>Historical context:</strong> Different Android activities can only communicate with each other using “Intents.” An Intent is basically a dict with string keys and values that can be various different primitive types. Notably, Intents can’t carry objects, because they are supposed to be able to cross process/application boundaries. Even though they’re mostly used for communicating within applications. (Yeah, it doesn’t make much sense.) Like most other Android things, Activities also return results to their caller via a callback (<code>Activity.onActivityResult</code>), and it’s the same callback for every activity—to figure out which activity’s result you’re handling you need to switch on the intent’s “result code.”</p> <p><strong>New hotness:</strong> We built a system called <code>TypedIntents</code> that hides all the Intent nonsense behind (a) a coroutine-based wrapper for the callbacks, and (b) a type-safe system where you declare an activities “params” and “return” types as Kotlin data classes, and they get turned into Intents for you behind the scenes. Inherit from <code>TypedWaveActivity</code> to allow other people to call you with types. Use <code>Actions.call</code> to call another typed activity.</p> <h3 id="flows">Flows</h3> <p>Almost all of the business-logic functions in ViewModels follows the same formula:</p> <ul> <li>Do a bunch of (suspending) actions.</li> <li>If one of them raises a UserFacingError, display it to the user</li> <li>If the user cancelled the sequence of actions (e.g. by hitting “cancel” on a dialog), do nothing</li> <li>If any of your assumptions (about e.g. the data returned by the server) is violated, instead of crashing, display “an unknown error occurred”</li> </ul> <p>To factor out the common error handling, we wrote a wrapper <code>launchFlow</code>. Most ViewModel callbacks look like <code>fun foo(args) = launchFlow { ... }</code>. Inside the braces, user-facing errors will be caught and displayed in a dialog. And you can use <code>nullableObj.or(err)</code> as a replacement for <code>obj!!</code> to present “an unknown error occurred” instead of crashing if the object is null.</p> <hr> <div class="closer"> We work on Wave because we think it’s <a href="/en/blog/world/">an extremely effective way to improve the world</a>. If that’s how you want to spend your career too, <a href="/en/careers/">come work with us</a>! <hr> <p>If you liked this post, you can subscribe to <a href="https://www.wave.com/en/blog/index.xml">our RSS</a> or our mailing list:</p> <div class="subscribe"> <div id="mc_embed_signup"> <form action="https://wave.us10.list-manage.com/subscribe/post?u=9c1c2a3d440cde1068d9c58dd&id=3de6dee936" method="post" id="mc-embedded-subscribe-form" name="mc-embedded-subscribe-form" class="validate" target="_blank" novalidate> <div id="mc_embed_signup_scroll"> <div class="mc-field-group"> <input type="email" placeholder="Email Address" value="" name="EMAIL" class="required email" id="mce-EMAIL"> </div> <div id="mce-responses" class="clear"> <div class="response" id="mce-error-response" style="display:none"></div> <div class="response" id="mce-success-response" style="display:none"></div> </div> <div style="display: inline-block; position: absolute; left: -5000px;" aria-hidden="true"><input type="text" name="b_9c1c2a3d440cde1068d9c58dd_3de6dee936" tabindex="-1" value=""></div> <div class="clear"><input class="wavebutton" type="submit" value="Subscribe" name="subscribe" id="mc-embedded-subscribe" class="button"></div> </div> </form> </div> </div> </div> </article> </main> <footer> <div class="main-footer"> <section class="logo-lang"> <a class="logo" href="/en/"><img src="/img/nav-logo.png" class="logo-small" alt="Wave logo"></a> <div class="lang-menu btn-group dropup"> <button type="button" class="btn dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <h5>English</h5> </button> <div class="dropdown-menu"> <a class="dropdown-item" href="/en/blog/android/">English</a> </div> </div> <p class="copyright"> © Wave Mobile Money Inc. </p> </section> <section class="wavecol" id="footer-navigation"> <h5>Company</h5> <ul> <li><a href="/en/about/">About Us</a></li> <li><a href="/en/careers/">Careers</a></li> <li><a href="/en/blog/">Blog</a></li> </ul> </section> <section class="wavecol" id="footer-navigation"> <h5>Legal</h5> <ul> <li><a href="/en/terms_and_conditions/">Terms and Conditions</a></li> <li><a href="/en/security/responsible_disclosure">Responsible Disclosure</a></li> <li><a href="/en/wdf/">Wave Digital Finance</a></li> <li><a href="/en/privacy/">Privacy Notice</a></li> </ul> </section> <section class="wavecol"> </section> <section class="download-links"> <a href="https://play.google.com/store/apps/details?id=com.wave.personal"><img class="download-badge" src="/img/en/google-play-badge.png" alt="Get it on Google Play"></a> <a href="https://itunes.apple.com/sn/app/wave-mobile-money/id1523884528"><img class="download-badge" src="/img/en/app-store-badge.svg" alt="Download on the App Store"></a> </section> </div> </footer> <script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script> </body> </html>