- A Single-Activity Android Application. Why not?!
- Obvious Things about Activity
- What you’re signing up for or taking control of it
- Will I get those precious 60 fps?
- Modernising A Legacy Android App Architecture, Part One: The Single Activity
- Background
- Rewrite Or Refactor?
- Navigation Soup
- The Collective Noun For Fragments: A Hierarchy
- Properly Defined Responsibilities
- Single Activity
- Jetpack Navigation?
- Up Next
A Single-Activity Android Application. Why not?!
The reason behind writing this article was one of the I/O 2016 sessions where a speaker told about Google’s plans to neutralize the UX differences between Native Android App and Web. In its turn, availability of stable working libraries and my solid development experience finally served as a call to action. Though in all fairness, experience is not always the best motivator for making changes to the development process which has been ‘debugged to shine’.
Obvious Things about Activity
A basic Android application project can consist of one or more Activities. Activity may or may not contain Fragments. Fragment can include some UI that can be reused in an unspecified Activity or Fragment. It seems to be all logical and taken from the Bible. The question is: When should I create a new Activity? Let’s see what options we might have:
- An independent screen such as SettingsActivity which exists as one-off instance in the application.
- A screen that I can create as multiple instances, such as ProfileActivity.
- A screen that can be accessed using a Deep Link.
- A screen with unique settings of Status Bar, Navigation Bar and other window parameters.
Seems to be all, doesn’t it?
Based on our experience and the options above we can point out the following benefits (will reduce the development time and the number of bugs):
- Can set style for out-of-the-box screens using xml of Status Bar and Navigation Bar to input parameters.
- Built-in transition between screens.
- Can reuse or replace a previously launched Activity.
If there are advantages, then there should be some disadvantages as well. Let’s find out what they are.
- One of the main drawbacks is asynchronous launch. No matter how ‘heavy’ your Activity is, Android will decide itself on what the delay should be before the launch.
- The Shared Element Transition does not work between Fragment and Activity. That’s a bit of a headache when building complex UI/UX.
- If screen layout is changed, the system re-creates screens for all previous Activities along with all the fragments relating to the backward navigation. By the way the same is true when restoring an app after it’s killed by the system.
Summarizing the above, the disadvantages can be brought down to a common denominator — the loss of control over the application by developer.
What you’re signing up for or taking control of it
Fragments do solve these problems, but also bring all the advantages that we’ve used to live with to nought! One has to pay in full for a complete control over the application whereas the chances of making a mistake are doubled. In addition to all the advantages of a Multiple-Activity which turn into disadvantages when you use a Single-Activity, you will have to solve the following tasks:
- Writing or selecting a library to navigate between screens.
- Breaking down the application into DI (Dependency Injection) modules and creating relations between them both for data transfer and semantically.
- Controlling UI of the main Activity depending on the state which the application is in.
If you have enough skills and proficiency, the estimate for developing an app will be one and half times higher in terms of the number of screens and their relationships between each other. That is, if you have 2 screens in the application and it takes 8 hours to implement them (layout, animations, saving state), then in a Single-Activity App the solution will require 12 hours as such.
Will I get those precious 60 fps?
Animation smoothness between the screens will depend only on a smart arrangement of the views, their number as well as the data binding (text, pictures, audio, video). Messing up is only possible in the background. For example, one of the apps or services would take hold of the CPU or an I/O channel.
Activity or Fragment launching is practically the same under ideal circumstances. But you may ask why? Activity is actually “heavy” and has many properties and components. That’s all fine, but let’s get down the facts.
There are 3 measurable values that are absolutely important for us as they reflect the resource ‘efficiency’ of using an Activity or a Fragment.
To get the above values, let’s use the Android Studio 3.2.1 Profiler and Battery Historian.
Analyze a screen with a typical layout that consists of: TextView, ImageView, Button, ProgressBar, and RecyclerView — and a default fadeIn/Out animation to render the transition between screens. It is important that it is drawn within ≤16 ms to maintain the experimental integrity.
Let’s analyze not only how an Activity or a Fragment becomes visible, but also their closing to simulate the user’s actions. Therefore, we will go through the following scenario.
Running the scenarios with a Single and Multiple Activity several times we’ve obtained the following results from Battery Historian.
Источник
Modernising A Legacy Android App Architecture, Part One: The Single Activity
Background
Hello. I’m Rob. I work on the Android version of BBC Sport. Our codebase started its life in 2013 and it’s fair to say we have amassed some tech debt over that time. This is the story of how we modernised a core part of its architecture. It’s aimed at a technical audience who have at least some familiarity with Android.
Obligatory disclaimer: views expressed are my own, not the BBC’s or the team’s.
The fundamentals of the app were not unusual for that era of Android development:
- multiple activities
- some use of fragments
- some use of viewmodels
- various UI patterns over time: MVP, MVVM, MVI
What we wanted to achieve was:
- consistency with modern Android development practices
- consistency with other modern BBC app architectures
- clarity for developers — being able to look at the codebase and see quickly that this is the pattern for how we build features
- usual product qualities: maintainability, reliability, extensibility
The absence of some of these characteristics — not all of them, and not totally — was a problem, and for various reasons, this slowly escalated from typical developer gripes to a more pressing business need. The organisation recognised the need to take action, and therefore we as a team were granted some time to address our architectural issues.
Rewrite Or Refactor?
This sometimes got referred to within the business as a ‘rewrite’, and it was of sorts, but it was one within strict parameters: not a start-again-from-scratch development, and an activity with a particular timeframe in mind, at least for this focused phase. Really this meant ‘refactor’ rather than ‘rewrite’.
We have built apps from scratch before in my time at the BBC. Sounds is a good example. Rather than overhauling its predecessor, iPlayer Radio, we started a ‘greenfield’ development and took our time to do it carefully. Though I say so myself, we did that job extremely well and it’s the best engineered software product I’ve worked on, largely the culmination of a lot of other people’s prior experience building BBC apps.
For Sounds, a fresh start was the right call, but it was also one driven by lots of factors beyond engineering concerns. It was a reinvention of the product in its entirety — the look and feel, the marketing, the backend and other components far beyond the app.
In our case we’re solving an engineering problem whilst trying to maintain product consistency, for now, and although a greenfield opportunity would be wonderful, you can understand it’s a luxury we can’t always afford. What we were able to do is take experiences like the Sounds development as an ideal model and re-apply many of those core techniques and architectural principles in a compressed and accelerated way.
This overall story is about how we put this into action, and solved at least some of our problems.
This is the first in a short sequence of articles on the subject, and over the course of these, we will run through a few different things in turn:
- this article: the problems of multiple activities, and getting away from this
- next article : what we think a good UI design pattern looks like
- third article : the overall phase of work and some specific experiences we had with doing this
It’s about 10,000 words in total, so quite a lot of detail especially in the last one, but let’s start off simple.
Navigation Soup
When you have lots of Activities, two things happen. One is that the overall navigation graph gets quite complex. The other is that you have to duplicate core responsibilities, in a few different ways.
The diagram below is an approximation — and subset — of the core Activities (purple) that we had. There were various entry points (green)into the app, and also inherently multiple interactions with Android’s lifecycle system.
Unsurprisingly, this navigation arrangement is quite difficult to understand, both in design and implementation. Let’s suppose a user starts the app. The set of app services isn’t fully set up yet so it shows the splash screen whilst that happens. The splash screen finishes and should transition to the main page, but we then want to prompt the user to sign in, so they get directed to the sign in page, which they dismiss, so we go back to the main page, which has to make sure it doesn’t show sign-in again this time. Clear?
This also implicitly tells you about the duplication of responsibility. You could in theory leave and return (resume) to any of these activities, and the app could have been torn down in the interim, so do they all need to worry about whether the app is started yet, and hand the user to the splash screen? Do they all need to worry about signed-in status?
Wouldn’t it be better to give them a sort of scope, so they only exist under the relevant circumstances and only have to worry about doing their own job?
The Collective Noun For Fragments: A Hierarchy
When we think about some of these navigation decisions, we can often come up with a set of behaviours that involve exclusivity but perhaps aren’t immediately obvious. These are some of ours:
- if the core app services aren’t finished initialising yet, the user should only be able to see the splash screen
- if the app has been disabled by remote config (e.g. it needs the user to go and upgrade it in the store), the user shouldn’t be able to access any other UI besides the message
- if the app is enabled, we might ask the user to sign in
- if we do ask them to sign in, it shouldn’t matter what content they’re looking at, the sign in page should appear on top
From this you can start to derive some ideas.
Depending on the app’s initialisation status, you can have a splash screen OR a ‘disabled’ screen OR the app is enabled. If the app is enabled it will show sign-in OR some actual content.
We can build a hierarchy around this.
We set out with a top level fragment that’s always present, MainFragment. It (or something it owns) decides which kind of child to show as its content, and that decision is based on the initialisation status, aka ‘bootstrap state’.
If we are initialised and enabled, we bring the first content layer into play — AppEnabledFragment in this diagram. It too is responsible for decision making, but at a lower level: whether to either show sign-in, or a further layer of content.
Properly Defined Responsibilities
This probably already seems like a vaguely sensible thing to do, but the benefit is worth calling out explicitly.
AppEnabledFragment only operates in a world where the app is enabled. It doesn’t care or even know about splash screens or showing upgrade messages because this is outside of its scope, and it either doesn’t exist or isn’t active in those circumstances.
Conversely, the MainFragment doesn’t care about any of the detail of how content is displayed, only that it has been put into motion or taken out of service. Decisions are made once in one place.
Single Activity
In the above system, there is one activity. One activity means one set of interactions with the lifecycle — you leave the app and return and it’s one overarching activity that gets onPause and onResume calls. We can re-evaluate the layers of decisions we made at this point.
If for example the application has been cleaned up, we will recreate the UIs from the ground up, and not have to worry about handling an ‘uninitialised’ state in the content components.
Similarly, if we get a deep link intent, then from an activity perspective, we handle it ‘in house’ rather than having to work out how to summon the right activity without a conventional step-by-step user journey behind it. This yields consistent user journeys.
Jetpack Navigation?
If you’re an Android dev, you might be looking at this and thinking ‘what about Jetpack Navigation?’. Well, if you look at what JN prescribes, the above and particularly the single activity model is one of the most important elements, for exactly the reasons already given. JN in turn is effectively becoming the intended model for modern Android navigation.
We aren’t yet using JN, but we might in future. What we’ve done takes us much closer to compatibility with it. What JN would really give us from this point onwards is a set of design tools and frameworks that replace the detailed fragment transactions we use to power it.
You might think that to rework our navigation but not adopt JN now is a missed opportunity. That’s possibly true, but the fundamentals of this refactor work already put enough technical risk in flight. There are some big, overarching transformations here that are of a larger size and scope than we would normally be comfortable with. It’s only because we have experience in these areas that this becomes acceptable. For us, we don’t have that with JN yet and we’d rather do one thing at a time, controlling our exposure to risk as much as possible. It is absolutely still on the cards to investigate.
Up Next
In the next article in this series, we’ll look at how we structured the fragments themselves; primarily what presentation design pattern we decided upon, but also what to do about the frameworks that might make it happen.
Источник