In this post we will observe Activity through a prism of Single Responsibility Principle of Object Oriented design, and attempt to understand what is the one single responsibility that is best associated with Activity in Android.
The following statement opens the official “Activities” page by Google:
An Activity is an application component that provides a screen with which users can interact in order to do something, such as dial the phone, take a photo, send an email, or view a map.
After such an introduction, it is only natural that we will be thinking about
Activities in terms of application’s UI. Today, when the topic of modular design became especially popular and many abbreviations (e.g. MVP) were promoted to a rank of “buzz-word”, perception of
Activity as UI element is especially notable.
I myself started developing for Android with these concepts in mind, but after a while I felt that there are some fundamental issues with this approach, and I began to wonder whether, maybe, after all, UI implementation details do not belong to Activities? This was not an easy concept to adopt, but after I got used to it and implemented it in a real application – I never looked back.
Single Responsibility Principle:
Single Responsibility Principle, as formulated by Robert “Uncle Bob” Martin, states:
A class should have only one reason to change.
For the purpose of our current discussion, we will narrow down the “reasons for change” list to just two reasons:
- Redesign of application’s UI which doesn’t change the general application’s functionality (“facelift”)
- Change in functionality which doesn’t require any change in application’s UI
We will designate as “UI logic” all the logic which could potentially change due to reason #1 (UI changes). We will designate as “business logic” all the logic which could potentially change due to reason #2 (functionality changes).
In order to adhere to the “narrowed” Single Responsibility Principle, classes in our applications must not contain “UI logic” and “business logic” together.
Why separate UI and business logic:
You might wonder what we, the developers, gain by adhering to the Single Responsibility Principle. Well, while it is true that UI and business logic can coexist, there are clear benefits to their complete separation. Some of these benefits are:
- Easy UIs changes – you will be able to update or even completely replace UI while keeping the business logic (almost) intact if they are separated
- UI logic “pollutes” and “obscures” business logic – both your business logic and UI logic will be much more readable and maintainable if they are separated
- UI is tricky to unit test – you definitely want to unit test your business logic, but if UI implementation is not “abstracted out”, you will need to apply test scenarios on UI elements in order to exercise your business logic, which is tricky (and your tests will need to be changed every time UI changes)
The above list of benefits is neither complete, nor the points listed there are necessarily the main benefits, but it shows that separation of UI and business logic is something that we would definitely like to have in our applications.
The “standard” way of implementation of Activities:
Now, once we agreed that separation of UI and business logic is a desirable system’s property in general, we can get back to
Activities. Maybe the way we usually implement
Activities already produces the desired outcome? I don’t think so.
Let’s take a look at one simple example. The following two responsibilities can be found in a single
Activity in almost any application:
- Register listeners for user’s interactions with application’s UI
- Perform actions in response to user’s interactions with application’s UI
It is very natural that
Activity both registers listeners and handles user’s interactions, right? But let’s look at this from a slightly different angle.
In order to register listeners with UI components
Activity must be aware of their IDs (
R.id.*). This is a clear dependency on UI implementation detail –
Activity “knows” the names of UI components (and, usually, it will also know their type:
Button, etc.). Therefore, responsibility #1 from the above list falls into “UI logic bucket”.
In response to user’s interactions with application’s UI,
Activity can perform various UI manipulations (e.g. change colors and shapes), but it will, usually, also initiate (or perform by itself) some actions that provide additional value to the user (e.g. note taking application would store a note on button click). These actions are not UI manipulation and do not depend on UI in any way – they are derived from the more global definition of application’s “functionality”. These are application’s “business rules”. Therefore, in the most general case, responsibility #2 from the above list falls into “business logic bucket”.
What we saw now is that even the simplest scheme of registering listeners with UI components and handling interactions with these components in the same class makes UI and business logic entangled. And every developer, who ever had to debug Activities having 500+ lines of code, most of which are UI manipulations with several lines of business logic embedded here and there, knows the pain of trying to figure out what the problem is and how to fix it (without breaking anything else).
Why Activities are not UI elements:
In order to adhere to our “narrowed” Single Responsibility Principle,
Activities in our applications should contain either UI logic only, or business logic only. Which approach is better?
Turns out that Android platform team has already decided for us. The below list of
Activity's dependencies is sufficient in order to make separation of business logic from Activity practically impossible:
The exhaustive list of all the features that make Activity and app’s business logic inseparable is much longer (e.g. request runtime permission, integration with LoaderManager, etc.), but this one is sufficient by itself. It might be surprising that such a basic fact that all of us got accustomed to is of such importance, but it is really that simple.
Functionally, Context objects provide access to most platform’s features that third party applications can use. This is a generalization, but we don’t need a more detailed description for the discussion in this post.
Since Activity is Context’s subclass, our applications use its API in order to take control over a subset of features and resources of the platform. And the logic that “orchestrates” these features and resources is our business logic. Therefore, no matter how hard we try, we will not be able to completely separate business logic from Activities.
Since we can’t separate business logic from Activities, we shall separate all UI logic from it. This is not a trivial task, but it is very well worth the effort in a long run.
In order to be able to discuss separation of UI and business logic quantitatively, we should define some sort of “dirtiness metric” – a measurable quantity which can serve as an indication of how “dirty” our logic is. In fact, we will define two metrics: one for business logic and one for UI logic.
Business logic dirtiness test (for Activities):
Any occurrence of one of the following in your Activities is one “dirtiness point”:
- Lookup in R.layout.* (layout files)
- Lookup in R.id.* (Views’ IDs)
- Dependency on any class or interface which doesn’t score 0 in tests 1 and 2 above
Note that this test is “transitive” – not only your Activities should not be aware of UI implementation details, but the classes that you reference in Activities can’t posses this knowledge either. Therefore, you can’t simply put all “dirty code” in some “helper” class which you instantiate in Activity (unless you don’t aim for 0 in this test).
UI logic dirtiness test (for classes that encapsulate UI logic):
Any occurrence of one of the following in classes that encapsulate UI logic is one “dirtiness point”:
- Dependency on Activity
- Dependency on any class or interface which doesn’t score 0 in test 1 above
Note that this test is also “transitive” – there should be no “dependency chain” from classes that encapsulate UI logic to Activities. Unfortunately, I can’t define dependency on Context as “dirtiness point” – you’ll need to provide a Context to UI encapsulating classes because there is no way to inflate a View without a reference to some Context (Context are God Objects, remember?).
You should always aim for 0 in both the above tests. It is not always feasible to score 0, but you should be explicitly aware of all “dirtiness points” in your applications and have a good reasons for not cleaning them away.
In this post we discussed why Activities in Android should not contain UI logic. We started by agreeing that separation of UI logic from business logic is a desirable feature, and then showed that it is practically impossible to separate business logic from Activity due to a very tight coupling of Activity to various parts of Android framework. We also defined “dirtiness metrics” for business and UI logic in order to be able to measure the “dirtiness” in our apps.
The concepts discussed in this post are very “high level” and it might be not that evident how our conclusions can be applied in practice. Therefore, I also wrote a series of posts that demonstrate one possible architectural pattern that is built upon the ideas discussed here and can be used for actually writing applications.
Please leave your comments and questions below, and consider subscribing to our newsletter if you liked the post.