Show Me What You Kot, Episode 1

Posted by Elte on October 09, 2021 · 27 mins read

I’ve been a mobile app developer on the Xamarin Native platform for well over five years now, and honestly, I like to complain about Xamarin a lot. If you’re hoping for a post with cynical bashing about why this is the case, I have to disappoint you, this is not that post. Instead I figured I would venture into the world of native, native Android (double layered native with extra nativesauce) to see how things are over there, and if my complaints are justified. I’ve also been looking for an excuse to do more with Kotlin, because I’m a big fan of every JetBrains IDE I’ve ever worked with (I have used IntelliJ IDEA, PhpStorm, PyCharm and Rider) so I naturally have big hopes for that language. As I’m typing this, I’m tapering for my first race in three years after injury and lockdown. Taper means I both have a little bit more time on my hands (less workout volume) and I tend to get antsy from the built up energy, so this will be a good distraction.

That’s why I’m starting the “Show Me What You Kot” blog series. I’ll be investigating some aspect of Android development with Kotlin in each instalment, by developing a mini app and talking about it. Why would you listen to anything I have to say, you ask? Well, while my experience with Kotlin and double layered native Android with extra nativesacue may be limited, I do have quite some experience developing Android apps, so even though I kinda don’t know what I’m talking about, I also kinda do. As fastai’s Rachel Thomas correctly points out, if you’re new to these topics you may gain more insights from somebody newly investigating them than from somebody already deeply familiar with the fundamentals, as my questions will be closer to the ones you’ll be asking. I may be the best of both worlds here (fingers crossed!). In addition I’ll be providing the perspective of a Xamarin developer and compare how things might be done over there. Finally, I’m actually writing this mostly for myself.

As for the title… well, as anybody who knows me will tell you, I’m a punny guy. The punisher, they call me. I’ll see myself out now.

Episode 1: Lifecycle madness

So just to dive head first into the shallow end of the pool, let’s talk about the Android lifecycle. This topic is close to my heart, so to say, as it causes a lot of issues everywhere. Case in point: if I’m watching a video using the YouTube app on my phone, I switch to my browser to look up some info before switching back to the YouTube app, there’s a decent chance I’ll end up on the home screen with my video nowhere to be found. I actually don’t recall this happening recently, but it was definitely happening a few months ago. What’s going on here, if even the YouTube folks can’t get this right? Let’s dive in.

Tangent alert…

If I’m honest, many aspects of Android come off as over engineered, it’s one of my most major gripes with the platform (that and the inexplicable desire to want to express anything and everything in XML). Oftentimes the APIs and architecture provide a lot of elegance and flexibility at the expense of usability. As a recent example, there is a way to determine what parts of the screen are covered by system UI-elements. I won’t go into detail here, but for example, to find out how many pixels are taken off the bottom of the screen by the home button, keyboard, etc, you used to call something like:

windowInsets.getSystemWindowInsetBottom()

I say used to, because this API was deprecated in Android R (API level 30). Instead, somebody must have figured that it’s more flexible to be able to select various aspects of the system UI with a bit mask. So now instead you’re supposed to use this new call:

windowInsets.getInsetsIgnoringVisibility(WindowInsets.Type.systemBars()).bottom

This is indeed way more flexible, except:

  • I’m now calling two functions and a property instead of one fairly descriptive getter
  • I have to remember (or more likely look up, because it’s not likely you need this often enough to remember) WindowInsets.Type values.
  • The first hit on Google when searching for the deprecation warning is that StackOverflow post I just linked to, not the Android documentation.
  • This second alternative is in fact not equivalent to the original, because the documentation states that the original includes the IME (i.e. the keyboard) and the second one does not. That information is indirectly present if you read the documentation carefully, but the deprecation warning does simply state to switch to that alternative without elaborating further. And most likely you didn’t end up reading the documentation anyway, because Google served up a no-context answer on Stack Overflow instead.

This kind of thing happens quite a bit on Android. You kind of see where it’s coming from, the API is flexible and elegant in certain ways but requires a lot of niche knowledge to use correctly. Searching for answers will oftentimes land you on StackOverflow rather than official documentation, and the average quality of answers there is really quite low (there’s a lot in the works-but-misses-the-point-entirely category).

To Google’s credit, they have a bunch of ready to go components as part of AndroidX that take care of a lot of these things for you in common scenarios. I do find these tend to swing the pendulum a bit too far in the other direction though, having not enough flexibility. But I digress, this may be a topic for a different post altogether.

What does this have to do with the lifecycle?

To get back on topic, the Android lifecycle is like this: very flexible, kind of elegant, kind of hard to use. Android apps are made up of isolated components called Activitys, which can contain Fragments. You can think of activities as separate app screens and of fragment as isolated UI modules, although the lines have blurred the more functional Fragments have become, and the recommended approach these days is actually to use Fragments for almost everything. Activities and fragments all have their own lifecycle, which is basically a set of states corresponding to how they’re currently used: they can be just created, started, active, in the background, destroyed, etc. Mobile phones are resource constrained environments. Not nearly as resource constrained as they were when Android came out, but the complexity and size of apps have kept up with the better hardware, so still kind of resource constrained. When your app isn’t used and some other needs more memory, the operating system may decide to kill your process. Except it may not actually kill your entire process, it may just kill those pesky UI-heavy activities that use a lot of memory, and leave the rest of your process alone. Or it may just kill a single one. Or a couple. Essentially, the Android OS can kill whatever parts of your app it feels necessary. It will tell each component when it’s about to be killed though, and provide it with a bundle object that will be persisted, so it can restore its state from it the next time the user returns to the app. If and when the app is eventually brought back to life, it will also restore individual activities as needed, so when you return to an app the activity for the latest top screen may be restored, but not any other activities that were open (not even the parent that created it!). The only guarantee is that they will be restored when they are revisited.

It’s easy to see why this is flexible and elegant: you have a bunch of individual isolated components that can be incrementally sacrificed and restarted for performance. It’s also not hard to see how it leads to confusing issues: an app typically doesn’t consist of individual isolated components. You’ll likely want to have at least some components talk to each other. While it’s tempting to communicate through the broader app process, this approach will break down if the component you’re communicating with is not actually currently alive to accept your communications. Of course there’s a mechanism for it in activities (which I just found out is now deprecated!), as well as an entire article for Fragments but you might not know to look for it if you’ve never encountered an issue. You’d be forgiven for the oversight - in fact the MvvmCross framework I’ve worked with for the past years seems content to ignore the issue entirely which has led to me implement a separate component to make it work in the past.

To make matters worse, there used to be no proper way to test this behavior. For a long time the closest thing to the real deal was a developer option called “Don’t keep activities”, that kills activities as soon as they are left (even if the views are still part of the stack). This isn’t quite like any real behavior I’ve ever encountered though, I’ve never actually seen individual activities killed in the wild. To simulate “proper” process death I used to run an app that progressively allocated larger blocks of memory, until the OS decided to start killing things. For what it’s worth, there appears to be an adb command to do it now (and alternatively an easier way than flooding memory), which just to prove an earlier point is only found hidden in a codelab - or outside of the Android developer documentation.

Are you going to write some code or what?

Yeah okay I’m rambling. And I don’t want to hate too much on Android either, I actually like Android a lot! I like it so much that I hold it to a higher standard, okay?

The first app in this series I’m going to keep rather simple, because I just want to implement some result passing and state persistence. I’ll be avoiding data binding, because I actually want to look into this in the next post, and I can already tell that one is going to be a can of snakes. So, my first entirely useless app is going to consist of the following:

  1. A MainActivity that holds a HomeFragment with some buttons and text.
  2. A RandomActivity that generates a random number and displays it. Once the number is generated, it shouldn’t ever change, unless the activity is finished and started again. There will be a button in this activity to send the random number back to the HomeFragment, where it will be displayed.
  3. A RandomFragment, that does the same thing as the RandomActivity. The difference here is it’ll be communicating data from the RandomFragment to the HomeFragment directly.

I’m going to be implementing this in a few steps to illustrate what can go wrong if these things aren’t taken into account. The final code for this episode can be found on GitHub, I’ve also kept tags along the way for the separate semi-implemented versions I want to talk about.

So to start I just created a new project from a template in Android Studio, and got rid of some boilerplate I didn’t need (though probably not all, pesky boilerplate is everywhere). Then I started coding away. Some thoughts about that process compared to Xamarin.Android development in Rider:

  • IDE support, holy crap, this is nice. Both Microsoft and JetBrains are trying hard to get nifty Android code completion for Xamarin into their IDEs, but with all the runtime translations that are going on the bar for it all to break down entirely is incredibly low. I’ve never worked on anything substantial where it kept working (or even worked from the start as far as I can remember). Having XML-files where things are actually more than just strings waiting for me to put a typo in is quite a treat. I can actually have the IDE rename a view ID and it changes across the board. If you’re working in Android Studio all the time you’re probably rolling your eyes right now, but… well, I said this wasn’t a “complain about Xamarin” post. The only time where the IDE messed up was when I renamed a layout file: it also renamed references to it, but not references to the generated view binding classes. Guess I should check if that’s already in their bug tracker.
  • Speaking of view bindings: sweet, I can use view bindings. Xamarin technically has this, but I spent a day trying to enable it once, and after doubling my already slow build times it broke down anyway, so, yeah, FindViewById from there on. Here, of course, it just works. Small victories. I do wonder how it affects performance (both build and runtime) in a large app, I haven’t looked into how this is implemented yet. I went down too many rabbit holes already.
  • Kotlin feels like a mad scientist - concise, powerful, clever, but the potential for chaos is most definitely there. It shares some concepts with C#, but that language is a bit more rigid and a bit less magic. IDE support here is fantastic of course, but IDE support for C# in Rider is brilliant too.

Fragment result passing and persistence

For version 1, I implemented a rudimentary (1) and (3), so there’s fragment to fragment communication. Link to the v1 snapshot code. In the HomeFragment I register a result listener as follows:

requireActivity().supportFragmentManager
    .setFragmentResultListener(RANDOM_FRAG_REQUEST_KEY, this) { _, bundle ->
        val num = bundle.getInt(NUMBER_KEY, -1)
        binding.returnedNumber.text = resources.getString(R.string.selected_number, num)
    }

Like I said before, I’m explicitly ignoring data binding here, so I’m going old school on setting text. It’s not all bad, it’s super clear what’s going on this way after all.

The RandomFragment sends its numbers like this:

binding.buttonSend.setOnClickListener {
    // Create a result, set it, and pop the backstack to return home
    val fm = requireActivity().supportFragmentManager
    val resultBundle = bundleOf(Pair(NUMBER_KEY, _myRandomNumber))
    fm.setFragmentResult(RANDOM_FRAG_REQUEST_KEY, resultBundle)
    fm.popBackStack()
}

This is what it looks like:

V1 walkthrough

Cool! Now let’s see what happens if I open the RandomFragment, press the home button and run adb shell am kill me.elte.kotit.eps1:

V1 after killing

Oops. Not only has my number changed (ok you can’t tell from this one screenshot, but it did), but I’m seeing two views at the same time! The issue here is that the fragment manager saves and restores its own state. The onCreate method of my MainActivity looks like this:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    binding = ActivityMainBinding.inflate(layoutInflater)
    setContentView(binding.root)

    setSupportActionBar(binding.toolbar)

    // Adds the transaction regardless of persisted state!
    supportFragmentManager.beginTransaction()
        .add(R.id.fragment_container, HomeFragment())
        .commit()
}

As the comment I just added indicates, I always add a fragment to the container, but upon restoring state the fragment manager does the same. So now I’m displaying two fragments at the same time, and there’s another HomeFragment on the backstack still. This is quite easily solved by conditionally adding the fragment:

if (savedInstanceState == null) {
    supportFragmentManager.beginTransaction()
        .add(R.id.fragment_container, HomeFragment())
        .commit()
}

If I’d never killed the app, I might have never noticed this though! Keeping the number is slightly more involved. Upon “shelving” our activities and fragments, Android will call onSaveInstanceState(Bundle) on them. That same bundle is then passed to onCreateView (or onCreate in case of an activity). So to the fragment I add:

override fun onSaveInstanceState(outState: Bundle) {
    super.onSaveInstanceState(outState)
    outState.putInt(NUMBER_KEY, _myRandomNumber)
}

And in its OnCreateView:

// Try to fetch our random number from the saved instance state
_myRandomNumber = savedInstanceState?.getInt(NUMBER_KEY, -1) ?: -1;
if (_myRandomNumber < 0) {
    generateNumber()
}

Note that I’m using -1 as a “no value” throughout, which isn’t a necessity (nothing wrong with negative random numbers after all), but it saves me from checking if the key exists everywhere (getInt must return some Int as a default, it won’t return null). For the example I’m just generating numbers between 0 and 1000. It’s as convenient as that in the world of fake, useless apps.

This leads to version 2, which survives an adb shell am kill (hard to prove in a GIF, you’ll have to trust me on this one). That leaves one last part:

Activity result passing and persistence

Although “fragments for everything” is all the hype these days, it still pays to know how to do these things in an activity. Not in the least because you might need something from another app (a picture from the camera, a file from a browser, etc) and that will most definitely involve activity result passing. Little did I know when I started writing this that there is a new API for this as well now. I won’t elaborate on the difference with the old approach too much, this article seems to do a decent job at that. This new API gets rid of the need to manage request codes (I had even already introduced a constant for it, which I no longer need), and introduces a modicum of type safety; although in the end safely serializing and deserializing that data into the correct type is still the responsibility of the developer. It also adds some extra boilerplate, so it’s a bit of a mixed bag. Anyway, the activity result contract is defined as follows:

class IntegerResultContract : ActivityResultContract<Unit, Int>() {
    override fun parseResult(resultCode: Int, intent: Intent?): Int = when(resultCode) {
        Activity.RESULT_OK -> intent?.getIntExtra(NUMBER_KEY, -1) ?: -1
        else -> -1
    }

    override fun createIntent(context: Context, input: Unit): Intent {
        return Intent(context, RandomActivity::class.java)
    }
}

Android bundles a bunch of these contracts for standard use cases in the androidx.activity.result.contract.ActivityResultContracts class. The default contracts without an input seem to use Void rather than Unit, but that seems more Javaesque, so I stuck to Unit here. Nothing is also an option, but that leads to the awkwardness of having to pass null at invocation. Unit is fine.

The RandomActivity is nothing special, it’s almost exactly like the RandomFragment. I even reused its layout, which then forced me to find out how view bindings deal with include. Oh, and this time I didn’t “forget” to implement onSaveInstanceState either. And there you have it, the fully functional useless app according to design:

Final walkthrough

Conclusion

Despite keeping it simple for this first episode, I went down more rabbit holes than I care to admit, and I rambled a bit more than I would’ve liked. I had to actively drag myself away from the data binding / LiveData documentation to get this first part finished. I’m having fun though! The whole experience seems a lot more stable and ironed out on this side of the fence, although in all fairness it’s hard to compare these things fairly in a toy app with only a few dependencies.

The next episode should be a good one - to create anyway, if it’ll be good to read, no idea. I’ll be able to compare what I’ve been doing in Xamarin.Android + MvvmCross with the fully native Android architecture. Spoiler: it’s kind of similar, with a lot less magic and IDE support on the .NET side of things. It might take me a while longer than this one though, I feel a lot of tangents and rabbit holes coming up. My browser will shudder under the weight of all those open tabs. Thanks for reading this far, until the next!