top of page
Search

Framework For Thinking About Android

Writer: E ChanE Chan

This blog is going to be how I think about Android and to give people a framework for understanding the litany of information and literature out there.


This is different from your step-by-step Android tutorial because I’m not going to tell you how to write an Android app or use these components. I’m simply going to give you a framework for organizing all the Android information you read, a basic understanding of how these components tend to work, what problems they intend to solve, and why they exist in the form that they do.


What most people do is just write Android code after Android code, spam the compiler, copy paste Gradle configurations, but have no understand of the bigger picture. Sure, they can rattle off something about how Fragments are managed in the backstack or to use testImplementation in Gradle. But few people understand what those features actually mean .

Simply put, I think that everyone is thinking about Android completely incorrectly. They see it as a monstrous behemoth that requires 3 programming languages to learn and 100 libraries when the reality is much so simpler.


I have worked on Android for 10 years (I still remember the jump from 2.1 to 3.0) and worked on almost every part of it ranging from writing simple applications to writing views, layout managers, and even the compilation stage. I don’t promote myself to be an expert by any means. In fact, I can safely say that there are plenty of details I am ignorant about. Despite this, I am also reasonably confident that I’ve probably read more code in the Android SDK and core code than your average Android app developer.


This is just an opinion piece of how I organize the information in my head. I’m going to do this by giving you the few underlying principles that I think drive 80% of the Android architecture, tools, libraries, and decisions that are taken by the community.


Let’s get started


Compilers


Most guides would rather you start with views and “Hello World” apps. I take the opposite approach for one reason: most people are familiar with Java and compiling code in any language. Yet this is probably what is the most forgotten about behind the build system, IDE abstractions, XMLs, libraries, and Android SDK.


Also it gives me a chance to flex. Who do you know has actually read the source code for Android build system and compilers?


Firstly, what is behind all those buttons in the IDE?


Fundamentally, you are trying to take a bunch of Java code, combine it with some non Java code, compile it into a bunch of class files, and then zip the mess together into an executable binary that runs on a JVM.


That’s it. Seriously. It’s all one big glorified Java code management program that runs a simple jar compile command. If you know how to troubleshoot paths, projects, and interpreters in general, you should be able to eliminate 90% of the troubles you run into when working with IntelliJ/Android Studio. Most of these issues you learn via a trial by fire (I’m looking at you Gradle).


You may also remember that in Java, it is possible to reference code in 3rd party libraries that are also jars. Without the right binaries or jars included at runtime, you will hit the famous RuntimeException where a class cannot be found.


Modern build systems try to be smart about this by breaking up the code into modules, compiling each of the modules into jars, and then gluing the jars together at the end. These jars can either be created by compiling the source code locally or by downloading a prebuilt jar from a repository (ie. Maven or Github).


In that regard, you can effectively think of the Android SDK as just one enormous 3rd party library that you are entirely reliant on who’s API and function you’re calling are promised at runtime.


jar cmf AndroidManifest.mf Android.jar Main.class Main.java Module1.jar Module2.jar …

At the end of the day, there is fundamentally no difference between stuffing all the java files into 1 command and compiling it and compiling 10,000 jar files that contain all the java class files in all of them.


This is done so that incremental changes will be faster. Imagine having to compile 10,000 files each time you made 1 change to a single java file. By breaking up the code this way and implementing heuristics to skip compilation (ABIs), you can get the same results faster.

Diagram of how ABIs can interact in a dependency graph generated by the Buck build system. Also, as of writing this, the open source version of Buck does a LOT of things wrong.

Where things get complicated is when you start adding resources, xmls, assets, and so on and optimize the builds for modules. These non-java assets have a reference name and are packaged into the binary and then referenced via a R.java file. For instance, you may declare a drawable/foo.jpg. Android will then generate a R.drawable.foo for you to reference in your code. You can effectively think of this as a URI (indirect reference to file) with special transformative powers accessed via the Android context. Want to do some image manipulation on a drawable resource?

ResourcesCompat.getDrawable(getResources(), R.drawable.foo, null).doSomethingHere();

Keep in mind, Android uses .dex files instead of .class files for its Dalvik JVM but you can effectively think of them as interchangeable if you have no intention of messing around with byte code.


Android also does a few spectacular optimizations as well on the byte code of these files for various operations like obfuscation with Proguard (you don’t want someone peeking into your APK binary and copying your code) and desugaring (how do you bring newer language features into old Java/Android runtimes?). There is nothing stopping you from writing your own compiler plugins to do this and to respect annotations but this effectively will boil down the discussion into a Java compilation discussion so we will omit it.


Even with tests and instrumented tests, the fundamental idea does not change. All that is different is how you declare it in the top level build file. Most build systems will package the correct testing tools for you for each of the modules.


At the end of the day, it's all Java anyway. If you’re interested in reading more about build systems and the philosophy behind them and how they work, I recommend Software Engineering at Google.


Code

Ok so now that you understand that you’re effectively just writing Java code, what is so special? Everyone is afraid of the litany of APIs and components and language that exist ranging from the poorly conceptualized Fragment to view rendering and so on. That they need to learn the newest, latest, and greatest APIs.


Which is utterly bullshit. Asking someone to keep up with all the hottest 3rd party libraries is like asking them to try and follow every new fashion trend. You can do just fine knowing the basic principles.


Similarly if you just understand color blocking and fit, you understand 90% of fashion without continually buying the latest Balenciaga merchandise.


Blockchain?? Are you fucking serious. Fucking bullshit.


In my mind, there are only a few fundamental ideas in Android. If there is any one idea you want to take away from all of this is that Android is always trying to tackle the idea of resource efficiency using these principles.


1. Lifecycles


Note that in this discussion, I’m going to use the colloquial term for lifecycles despite the fact that Android has an actual concept called lifecycles.


Mobile phones suck at power management. So in an effort to help developers be smarter about their resources and manage this, Android has given developers the power to shoot themselves in the foot by separating a lot of the operations into discrete steps, otherwise known as lifecycles.


The short version is that Android gives you a chance to respond when it's about to background, destroy, create, or do some kind of operation to save power.


Where people get this wrong is that they try to just write apps without being mindful of this interaction. For instance, in Activities (think of activities as screens for now), while it's entirely appropriate to create and inflate layout/views in onCreate(), people try to do this in a completely different lifecycle step onStart() and forget that there are multiple ways to relaunch activities. One particular instance is due to the interaction of onSavedInstanceState() where activities can be killed and serialized in the background because Android needs to reclaim memory.


Bottom line: respect your lifecycles so you aren’t in limbo when Android changes states.


Yet if your goal was to just write apps or follow tutorials, you might not have any exposure or understanding of this. This is because most “disposable apps” don’t care about the nuances of Android architecture and the standard for understanding how to write Android revolves around “it just shows up on the screen.”


But it isn’t just individual activities/screens. Think about how a view is drawn on the screen. How exactly does Android lay out these items and render them?


Turns out views themselves have a lifecycle too!

View Lifecycle


And even the famed RecyclerView that everyone uses has a bunch of internal guts (like scrapping and detaching individual views themselves) as well that most people have never had the chance to really bother with. Mostly you are dealing with the ViewHolder object as a proxy for this interaction.

ViewHolder Lifecycle

This has not changed and is unlikely to change. 10 years after its inception, this idea is STILL being used in the recommended Android components.


ViewModel themselves are lifecycle aware ways of storing data in a MVVM architecture. So you no longer have to manually serialize and deserialize data between steps in the lifecycle.


ViewModel Lifecycle

While you may not actually run into these issues yourself and hopefully you will never have to write a LayoutManager yourself (ick), you should at least understand that mostly everything in Android is built around predictable flows and steps.


So when you run into some weird process, view, or architecture, ask yourself how this works with the idea of lifecycles and how this helps Android save resources.


2. Architecture.


You are responsible for managing the logic that drives the end to end interaction. Therefore, there are certain parts of your code that will need to be split up. In this section, keep in mind the idea of “separation of concerns”, “single responsibility principle”, and “law of demeter” as you will see this over and over again.


Most people are probably used to building either a monolith of code where one file houses both business and UI logic (Activity monolith antipattern). Or are put into a box at a big tech company where they only focus on one service and never venture into how the service works at large.


A context is just, well, the context. The setting or environment that some Android code runs in. Or, more specifically, its the level of access to internal resources. In that regard, you can think of an Activity as a screen.



Shamelessly stolen from here.


Without really understanding or having written any of these components, you already have an intuitive understanding of what you’re allowed to do and what you aren’t based on the names. But the bottom line is that different components and contexts have different things they’re allowed to do.


Let’s address the whole XML thing. First, imagine a world where you didn’t have the separation of XML and Java and you could only write your views in Java.


Every view would be written to the effect of:


linearlayout1.setDividers(true);
linearlayout1.setPixelSize(5.0);
linearlayout1.setWidth(500);
linearlayout1.setHeight(500);
…
linearlayout69.setDividers(5);
linearlayout1.addChild(linearlayout69);
…

linearlayout1.setText(“HelloWorld”);


Oh god imagine doing this for 10,000 possible views on initialization every time you just wanted to inflate the layout. The code would be endless and would be mixed in with the actual business logic! Try refactoring THAT.


With XML we now have a separate file that is more easily readable and separates the UI decoration from the actual core logic:


<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".HelloWorldActivity" >

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/hello_world"
        android:textSize="25sp" />

</RelativeLayout>

Now we can reserve our Java code for the more interesting on-the-fly stuff. Separation of concerns!


Again, this idea doesn’t change (mostly because this is a fundamental concept in computer engineering). Even Kotlin Jetpack Compose is just syntactic sugar with coroutines designed to achieve exactly this. Just with different code and style.


But this doesn’t only extend to views. Consider overall app architecture. Early on, everyone used to create Activity monoliths and mesh business logic with UI logic.

Imagine:


layout.setColor(COLOR.WHITE);
networkClient.makeRequest(it-> {
    if(album_count > 30){
        artist.album_count += it.count;
        layout.setText(album_count);
    }
});


Looks benign but you can easily see where race conditions come in and where code confusion can start (callback may be on a different thread if you’re not careful). Fortunately, ViewModels help us in this regard where all the business logic is in one place and view manipulation can stay in another.



Activity:
viewModel.layoutObservable{ it-> layout.setText(it)}

ViewModel:
networkClient.makeRequest{it-> {
    if(layoutObservable.count  > 30){
        layoutObservable.count  += it.count;
    }
});

Looks so much nicer and easier to read right?


Anyway, this is just a small example. The point here is that you are managing an application. A mini universe if you will and that doesn’t just apply to UI + ViewModels.


How do you want to handle network requests/caching/interruptions? This is where Rooms comes in to try and create an architectural pattern for persistent storage of non-sensitive queried data that is also lifecycle aware (hopefully you didn’t forget about lifecycles!).


Navigation Graph and NavigationController are, as the name implies, a way separate the control of the navigation between fragments from the fragment’s ui itself.


But hopefully, with this understanding of separation of concerns/law of demeter, you can work out what solutions will work for your app.


3. User Interaction


One of the blessings of Android is that everything is UI testable. Which means you can actually interact with the built code yourself and test to make sure everything works. Nifty right?


Fortunately Android has published a list of supported UI elements and interactions that it can detect and work with. This becomes very nifty when you want to control and track gestures like drag and drop (you may recognize the lifecycle of the drag action here!).


The cardinal sin in this area is the overuse of the UI thread. What does this mean?

Android UI interaction is dynamic so there must be thread processing and re-rendering all the user interactions and stuff on screen. This thread updates and refreshes the screen every 16ms. How responsive the screen is is dependent on if the app can continually refresh at this rate. So if you load a bunch of heavy tasks like network tasks that wait for a response on the UI thread, your screen will freeze.


Of course, the solution is to move the heavy work to a background thread and somewhat overlaps with the idea of resource efficiency.


What you’ll also find is that Android tries to be very smart about this as well. On some fling gestures, instead of continually re-rendering layouts per fling distance every 16ms, Android sometimes plain gives up and just re-renders everything from scratch instead of rendering things incrementally (ie. move some views up and add a view below versus redrawing & relaying out everything). Usually you will see this message:


I/Choreographer: Skipped <number> frames! The application may be doing too much work on its main thread.

It effectively means that for the number of frames, the UI froze and no changes were submitted. So you want to avoid this as much as possible.



3rd party libraries


Jake Wharton-sama


This picture literally summarizes everything you need to know about 3rd party libraries in the Android world. If you have not seen his Github repository, I encourage you to do so. Many of the libraries he owns and has heavily contributed to end up being widely used. There is a lot you can learn simply by looking at his code.


RxJava: My god, I could not understand RxJava until 1 year of playing around with it. But effectively, you’re just creating a “pipeline” of information and transforming the pipeline to do fancier transformations of the data that passes through to it. How it does this underneath is another matter (hint: everything is an object in Java).


You will also need the accompanying RxAndroid to enable some fancier features like lifecycle-related scheduled observable.


EventBus: A fast (“copy paste code”) way to do non-lifecycle broadcasts but is too easily abusable. In my personal opinion, this is basically an anti-pattern of a library for proper architecture. Regardless, its good to know that this exists.


Dagger + Hilt /Koin/Dependency Injection(DI). This isn’t only used for Android but also for Java in general (remember: Android is just fancy Java!). I won’t dive into the specifics but at the very least, you must understand what dependency injection is. After that, I recommend you play with these DI tools.


OkHttp: Honestly, this is just an HTTP client. It's pretty widely used. Useful to know it exists.


Testing


As I write this, I realize that testing in Android as it exists today is taught using wonderfully easy to teach ideas that don’t quite match up to the real world. More on that later.


For those who aren’t familiar with thinking about testing, imagine you’re studying a fish and how the fish behaves. But instead of studying it in the ocean, you put it into an aquarium so you can study the fish. You subject the fish to a bunch of individual tests like putting food flakes in the tank, moving a rock or fake piece of seaweed, etc. Maybe some interactions with other fishes.


In this case, our fish is the code under test and the environment and other variables we throw at the fish are our tests. It's just that some tests are more complex and less isolated than others and as we increase complexity we get closer to simulating an actual habitat.


There are 3 variations of tests: unit, integrated, and instrumented.


  • A unit test is a test that tries to test a single unit in isolation. Most often it is a class.

  • Integration tests determine if independently developed units of software work correctly when they are connected to each other. In Android world, we tend to put tests that require the use of the Android SDK in this category.

  • An instrumented (sometimes called a UI test) requires the run and installation of an actual device or a simulation of Android. Usually this requires an actual view that is human observable.


So then, how do you use the litany of tools out there? Here are the tools you should be familiar with. The bolded ones are the ones you should have tried to play around with once.

  • Junit (unit test): Solid. I don’t think you need to argue against this. Verify outputs of objects and functions.

  • Mockito: An addendum to Junit. Deterministically specify objects and function outputs and behaviors.

  • Powermock: Mock all the things you (probably) shouldn’t. Using this is a big anti-pattern.

  • Robolectric (integrated test): Use real Android code underneath. Tests take longer because the Android code needs to be glued in via bytecode. Can still use Junit verification.

  • Espresso (instrumented test): Flaky UI tests. Use to verify behavior.


In my opinion, I would just separate your tests into Junit, Robolectric, and manual UI testing. Use Mockito as an addendum across the Junit and Robolectric tests.


This will work for 90% of app developers and is what Google has in mind with Gradle and what most build systems tend to do. The last 10% will be for big companies that can invest in the infrastructure to maintain flaky unit tests (and get around Espresso, ick).


Consider the lists I gave you a hierarchy where tests and tools that are at the top of the list can exist in tests below it but not the other way around. I can put Junit tests in an integrated or instrumented test folder because all I need is Java. But I cannot put a Robolectric/integrated test in the unit test folder because I would be missing a lot of Android SDK calls. Junit has no knowledge of the Android SDK.


Where people mess up is by being lazy and overreaching for what they need. That is, they put the tests in the wrong folders and modules. They put Junit tests that only test Java code in the instrumented tests folder or something to that effect.


The consequence for doing this is that Android instrumented tests take a long time to run because you’re building a binary and installing the app. Whereas if you skipped that and just ran on your local JVM, you could save yourself a lot of time.


So test organization is very important! Next time you write a test, ask yourself how much context you really need and organize accordingly.


Bonus Topics


Memory Leaks


The point here is that every step and component in Android serves a purpose and 80% of the time, its mostly done to try and be more efficient with resources.


So do become familiar with common Java memory leaks. My favorite one that most people overlook is the anonymous inner class.


Cancel Discarded Requests


This most commonly applies to RecyclerViews + ImageLoading whilst scrolling. I’ll leave the implementation and the why as an exercise to the viewers.


Q&A:


You could talk more about the UI and how to write views.


I could, but I’d rather you write your own code, make your own mistakes, and learn. I am simply giving you the nuances you need to be aware of. The implementation details, I leave to you. Get out of tutorial hell sir.


Why did you not mention Kotlin?


Because for most people, the differences between Java and Kotlin are language specific and have no real bearing on your experience of writing basic Android. Sure you’ll get access to Jetpack Compose. But effectively it's just super syntactic sugar for Java.*


At the end of the day, do you really care about how Kotlin code gets transformed into JVM byte code? Most people don’t: they just like a sexy new language that works with Java and the language features they need to get over some of the hurdles like @JvmName annotation.


(*ok yes I know that there is a giga bias towards Kotlin b/c of nullability and coroutines but at the end of the day, you are gonna write the exact same shit just “kotlin-ified”. What if I told you that as of writing, Kotlin itself isn’t super optimized for large monorepo codebases and that the chasm between startupland and big tech companies for Kotlin is kinda big?)


What about other testing tools and frameworks?


I was going to write a huge dissertation on the shortcomings of Robolectric and Espresso but found that it was going to just confuse the reader. UI tests are notoriously flaky which has lead companies like Airbnb to create Happo which, in some ways, violate the idea of UI tests since it hooks directly into the underlying Android implementation and reads the layout instead of just what is on the UI. As for Robolectric, this comment by Jake Wharton should suffice as to why its not completely accurate to label Robolectric as a “integrated test.”



 
 
 

Comentários


bottom of page