License to drive a car, or why applications should be Single-Activity
At AppsConf 2018 , which was held on October 8-9, I gave a presentation on the creation of android applications entirely in one Activity. Although the topic is well-known, there are many preconceptions about this choice - an overcrowded room and the number of questions after the presentation confirms this. In order not to wait for the video, I decided to make an article with a transcript of the speech.
What I will tell you
- Why and why it is necessary to switch to Single-Activity
- A universal approach to solving the problems that you are used to solve on multiple activities
- Examples of standard business tasks
- Narrow places where the code is usually propped up and not done honestly
Why is Single-Activity right?
Life cycle
All android developers know the scheme of the "cold" launch of the application. First, onCreate is called on the Application class, then the life cycle of the first Activity comes into effect.
If in our application there are several Activities (and most of such applications), the following happens:
App.onCreate()
ActivityA.onCreate()
ActivityA.onStart()
ActivityA.onResume()
ActivityA.onPause()
ActivityB.onCreate()
ActivityB.onStart()
ActivityB.onResume()
ActivityA.onStop()
This is the abstract startup log of ActivityB from ActivityA. Empty line - the moment when the launch of the new screen was called. At first glance, everything is fine. But if we turn to the documentation, it will become clear: to ensure that the screen is visible to the user, and he can interact with it, it is possible only after calling onResume
each screen:
App.onCreate()
ActivityA.onCreate()
ActivityA.onStart()
ActivityA.onResume() <--------ActivityA.onPause()
ActivityB.onCreate()
ActivityB.onStart()
ActivityB.onResume() <--------ActivityA.onStop()
The problem is that such a log does not help to understand the life cycle of an application. When the user is still inside, and when he has already moved to another application or has turned ours and so on. And this is necessary when we want to bind business logic to the life cycle of an application, for example, to keep a socket connection while the user is in the application, and close it when exiting
In the Single-Activity application, everything is simple - the lifecycle of the Activity becomes the lifecycle of the application. Everything you need for any logic is easy to link to the state of the application.
Running screens
As a user, I often come across the fact that a call from the phone book (and this is clearly the launch of a separate Activity) does not occur after clicking on a contact. It is not clear what this is connected with, but those whom I tried to get through unsuccessfully said they took the call and heard the sound of footsteps. At the same time, my smartphone has long been in my pocket.
The problem is that launching an Activity is a completely asynchronous process! There is no guarantee of instant launch, and even worse is that we can not control the process. Totally.
In the Single-Activity application, working with the fragment manager, we can control the process. transaction.commit()
- performs screen switching asynchronously, which allows you to open or close multiple screens in a row. transaction.commitNow()
- switches the screen synchronously, if you do not need to add it to the stack.
fragmentManager.executePendingTransactions () `allows you to perform all previously started transactions right now.
Screen stack analysis
Imagine that the business logic of your application depends on the current depth of the stack of screens (for example, nesting). Or at the end of some process you need to return to a certain screen, and if there are several identical ones, to the one closest to the root (the beginning of the chain).
How to get an Activity stack? What parameters need to be specified when starting the screen?
By the way, about the magic of startup parameters Activity:
- you can specify launch flags in Intent (and also mix them with each other, and change them from different places);
- you can add startup parameters in the manifest, because all Activity should be described there;
- add Intent filters here to handle external launch;
- and finally think about MultiTasks, when the Activity can run in different “tasks”.
Taken together, this creates confusion and problems with debugging support. You can never say with certainty exactly how the screen was launched, and how it affected the stack.
In the Single-Activity application, all screens are switched only through fragment transactions. You can analyze the current screen stack and saved transactions.
In the demo application of the Cicerone library you can see how the current state of the stack is displayed in the toolbar.
Note: in the latest version, the library support service closed access to the array of fragments inside the fragment manager, but if you really want, this problem can always be solved.
Activity is only one on the screen.
In real applications, we will definitely need to combine the “logical” screens in one Activity, then you cannot write a real application ONLY to the Activity. The ambivalence of the approach is always bad, since the same problem can be solved differently (somewhere, the layout is right in the Activity, and somewhere, the Activity is just a container).
Don't keep activities
This flag for testing really allows you to find some bugs in the application, but the behavior that it reproduces is NEVER encountered in reality! It does not happen that the application process remains, and at this moment the Activity, even if not active, is dying! An activity can only die with the application process. If the application is displayed to the user, and the system lacks resources, everything around will die (other inactive applications, services, and even the launcher), and your application will live to a victorious end, and if it has to die, then it will be complete.
You can check.
Heritage
Historically, the Activity has a huge amount of superfluous logic, which most likely will not be useful to you. For example, all you need to work with loaders
, actionBar
, action menu
and so on. This makes the class itself quite massive and heavy.
Animations
Anyone can make a simple shift animation when switching between activities. Here it is necessary to clarify that it is necessary to make a discount on the asynchronous launch of the Activity, which we talked about earlier.
If you need something more interesting, you can think of such examples of transition animation, which are made on the Activity:
But there is a big problem: customizing this animation is almost impossible. Designers and the customer is unlikely to please.
With fragments, everything is different. We can go straight to the level of the view hierarchy and make any animation you can imagine! Direct evidence here :
If you look at the source code, you will find that this is done in a conventional layout. Yes, the code is decent there, but animation is always quite difficult, and having this opportunity is always a plus. If you switch two Activities, then there is no common container in the application where you can make such transitions.
Configuration change on the fly
This point was not in my statement, but it is also very important. If you have a feature with language switching inside the application, then with several Activities it will be quite problematic to implement it, if, among other things, you don’t need to restart the application, but stay where the user was at the time the functionality was called.
In the Single-Activity application, it is enough to change the installed locale in the application context and call recreate()
the Activity in the rest; the rest will be done by the system itself.
At last
Google has a solution for navigation, the documentation of which explicitly states that it is desirable to write a Single-Activity application.
At this point, I hope you have no doubt that the classic approach with several Activities contains a number of shortcomings, which are usually taken as a blind eye, hiding behind the general tendency of Android dissatisfaction.
If so, then why is Single-Activity not yet a development standard?
Here I will quote my good friend:
Starting a new serious project, any leader is afraid of failure and avoids risky decisions. It is right. But I will try to provide a comprehensive plan for the transition to a Single-Activity.
Transition to Single-Activity
If you study this application, you can determine by its characteristic animations and behavior that it is written in several Activities. I could be wrong, and everything was done even on custom views, but this will not affect our reasoning.
And now attention! Do this like this:
We made only two changes: we added the AppActivity class and replaced all the Activities with FlowFragment. Consider each change in more detail.
What is AppActivity responsible for :
- contains only a container for fragments
- is the initialization point of UI scop objects (earlier it was necessary to do this in Application, which is wrong, because, for example, the services in our application do not exactly need such objects)
- is a lifecycle provider application
- brings all the advantages of Single-Activity.
What is FlowFragment :
- does exactly the same as the Activity that was created instead.
New navigation
The main difference from the old approach is navigation.
Previously, the developer had a choice: start a new Activity or a fragment transaction in the current one. The choice has not disappeared, but the methods have changed - now you have to decide whether to start the fragments transaction in the AppActivity or inside the current FlowFragment.
Similarly, with the processing of the Back button. Previously, the Activity passed the event to the current fragment, and if it did not process it, it made the decision itself. Now AppActivity sends the event to the current FlowFragment, and that, in turn, sends it to the current fragment.
Transfer the result between screens
For inexperienced developers, the issue of data transfer between screens is the main problem of the new approach, because before it was possible to use the startActivityForResult () functionality!
Not the first year various architectural approaches to writing applications are discussed. The main task here is the separation of the UI from the data layer and the business logic. From this point of view, startActivityForResult () breaks the canon, since the data between the screens of one application is transmitted on the side of the UI layer entities. I emphasize that it is exactly one application, since we have a common data layer, common models in a global scopa, and so on. We do not use these opportunities and drive ourselves into the framework of one Bundle (serialization, size, and more).
My advice : do not use startActivityForResult () inside the application! Use it only for its intended purpose - to launch external applications and get results from them.
How then to launch the screen with a choice for other screen? There are three options:
- TargetFragment
- Eventbus
- reactive model
TargetFragment is an out-of-box option, but the same data transfer is on the UI layer side. Bad option.
EventBus - if you can agree in a team and - most importantly - monitor the arrangements, then on the global data bus you can implement data transfer between screens. But since this is a dangerous move, the conclusion is a bad option.
Reactive model - this approach implies the presence of callbacks and nothing more. How you implement them is decided by the team of each project. But it is this approach that is optimal, as it provides control over what is happening and does not allow using the code for other purposes. Our choice!
Total
I love new approaches when they are simple and have clear benefits. I hope that in this case it is. The benefits are described in the first part, and the difficulty of judging you. It is enough to replace all the Activity with FlowFragment, keeping all logic unchanged. Slightly change the navigation code and think about working with transferring data between screens, if this has not already been done.
To show the simplicity of the approach, I myself translated the open application to a Single-Activity, and it took only a few hours (of course it is worth considering that this is not older than Legacy, and everything is more or less good with architecture).
What happened?
Let's see how to solve standard problems in a new approach now.
BottomNavigationBar and NavigationDrawer
Using the simple rule that we replace all Activities with FlowFragment, the side menu will now be in a certain fragment and switch nested fragments in it:
Similar to BottomNavigationBar.
Much more interesting is that we can invest some FlowFragment in others, because these are still ordinary fragments!
This option can be found in GitFox .
It is the ability to simply combine some fragments inside others without any problems to make a dynamic UI for different devices: tablets + smartphones.
Di scopes
If you have a flow of purchase of goods from several screens, and on each screen you need to show the name of the product, you probably already carried it into a separate Activity, which stores the product and provides it to the screens.
It will be the same with FlowFragment - it will contain a DI-scop with models for all nested screens. This approach eliminates the complex management of the lifetime of a scop, tying it to the lifetime of a FlowFragment.
Deep-links
If you used filters in the manifest to launch via a deep-link specific screen, you might have problems starting the Activity, which I wrote about in the first part. In the new approach, all deep-link fall into AppActivity.onNewIntent. Further, according to the data obtained, a transition occurs to the required screen (or a chain of screens. I suggest looking at this functionality in Chicheron ).
Death process
If the application is written on several Activities, you should know that when the application dies, then when the process is restored, the user will be on the last Activity, and all previous ones will be restored only when they are returned to them.
If this is not taken into account in advance, problems may arise. For example, if the scop needed for the last Activity was opened at the previous one, no one will recreate it. What to do? Bring it to the Application class? Make a few points of opening scopa?
Everything is simpler with fragments, as they are inside an Activity or another FlowFragment, and any container will be restored BEFORE re-creating the fragment.
Other practical problems can be discussed in the comments, because otherwise there is a chance that the article will be too voluminous.
And now the most interesting part.
Narrow places (remember and think).
Here are collected important things that you should think about in any project, but everyone is so accustomed to “subblocking” them in projects for several Activities, which is worth recalling and telling them how to solve them correctly in a new approach. And first on the list
Screen rotation
That most terrible fairy tale for fans to whine that Android recreates Activity when turning the screen. The most popular solution method is fixing the portrait orientation. Moreover, this proposal is no longer developers, but managers, frightened by phrases like "to support the turn is very difficult and costs several times more expensive ."
We will not argue about the correctness of such a decision. Another thing is important: fixing a turn does not exempt from death processing Activity! Since the same processes occur in a variety of other events: split-mode, when several applications are displayed on the screen, connecting an external monitor, changing the application configuration on the fly, and so on.
Moreover, the screen rotation allows you to check the correct “rubberiness” of the layout, so in our St. Petersburg team we do not disable the rotation on all debug builds, even if it isn’t in the release version. Not to mention the typical bugs that will still be found when checking.
Many solutions have already been written to handle the rotation, starting with Moxy and ending with various MVVM implementations. Make it no more difficult than anything else.
Consider another interesting case.
Imagine a product catalog application. We do it in Single-Activity. Everywhere the portrait mode is fixed, but the customer wants a feature when, when viewing a photo gallery, a user can watch them in landscape orientation. How to support this?
Someone will offer the first crutch :
<activity android:name=".AppActivity"
android:configChanges="orientation" />
overridefunonConfigurationChanged(newConfig: Configuration?) {
if (newConfig?.orientation == Configuration.ORIENTATION_LANDSCAPE) {
//ignore
} else {
super.onConfigurationChanged(newConfig)
}
}
Thus, we can not call super.onConfigurationChanged(newConfig)
, but process it ourselves and rotate only the necessary twists on the screen.
But with API 23, the project will crash with SuperNotCalledException
, so a bad choice .
In the statements above, an error was made:
I was reasonably corrected in the comments that it is enough to add android: configChanges = "orientation | screenSize", and then you can call super when the turn is over and the Activity will not be recreated. This is useful when on-screen WebView or a map that is initialized for a long time, and you want to avoid it.
This will help solve the described case with the gallery, but the main message of this section is: do not ignore the re-creation of the Activity , this can happen in many other cases.
Someone may suggest another solution:
<activity android:name=".AppActivity"
android:screenOrientation="portrait" />
<activity android:name=".RotateActivity" />
But in this way we move away from the Single-Activity approach for solving a simple task and deprive ourselves of all the benefits of the approach. This is a crutch, and a crutch is always a bad choice .
Here is the right decision:
<activity android:name=".AppActivity"
android:configChanges="orientation" />
overridefunonResume() {
super.onResume()
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR
}
overridefunonPause() {
super.onPause()
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
That is, when a fragment is opened, the application starts to spin, and when it returns, it is fixed again. According to my observations, this is how the AirBnB application works . If you open the view of photos of housing, cornering processing is activated, but in landscape orientation you can pull the photo down to exit the gallery. Below it, the previous screen in landscape orientation will be visible, which you usually will not find, because immediately after leaving the gallery the screen will turn into a portrait and fix.
This is where the timely preparation for screen rotation will help.
Transparent status bar
Only Activity can work with the system bar, and now it’s only one for us, so you should always specify
<item name="android:windowTranslucentStatus">true</item>
But on some screens there is no need to "crawl" under it, and you need to display all the content below. The flag comes to the rescue
android:fitsSystemWindows="true"
which indicates the layout that you should not draw under the system bar. But if you specify it in the layout of the fragment, and then you try to display the fragment through the transaction in the fragment manager, then you will be disappointed ... it will not work!
The answer is quickly googling.
I highly recommend that you familiarize yourself with a really comprehensive answer and many useful links.
A quick and working ( but not the most correct ) solution is to wrap the layout inCoordinatorLayout
<android.support.design.widget.CoordinatorLayoutxmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"android:layout_width="match_parent"android:layout_height="match_parent"android:fitsSystemWindows="true"></android.support.design.widget.CoordinatorLayout>
A better solution helps to handle and keyboard.
Change the layout when the keyboard appears
When the keyboard leaves, the layout should be changed so that the important elements of the UI do not remain out of reach. And if earlier we could specify different modes of reaction to the keyboard for different Activities, now we need to do this in Single-Activity. Therefore it is necessary to use
android:windowSoftInputMode="adjustResize"
If you use the approach from the previous section for processing the transparent status bar, you will find an annoying error: if the fragment successfully “crawled” under the status bar, then when the keyboard appears, it will be compressed above and below, since both the status bar and the keyboard inside the system work through SystemWindows
.
Note the title
What to do? Learn the documentation! And be sure to see the Chris Banes report on WindowInsets .
Using WindowInsets will allow
- find out the correct height bar status (rather than hardcore 51dp)
- prepare an application for any cuts in the screens of new smartphones
- find out the height of the keyboard (it's real!)
- receive events and react to the appearance of the keyboard.
Everyone learn WindowInsets!
Splash screen
If someone else doesn’t know, then the canonical Splash screen is not the first screen in the application that loads the data, but what the user sees when it starts up until the Activity content has time to draw. There are many articles on this topic.
But I want to note that with a Single-Activity, only one Splash screen is possible. Remember this and explain to the designers, because going through a deep-link to a dark screen and a bright Splash screen will see the transition between the colors.
Run your application from other applications
This is the most difficult place to understand , since it harbors cunning, which can be used both for good and for harm.
Imagine you created a typical social network application. Everything is done in a single-activity. The user switched to some far from the first screen and began to write a comment to a friend, but he was distracted, and he turned off the application.
The next day, he reads the news in another application and decided to share it on your social network ...
Intent is thrown, your application opens, and there is an unfinished comment on the first screen ...
What's next? There are several cases:
- We restore the screen with a comment, and from the top we open the screen with the “share” function. Then when you press "back", the user will see an unfinished comment. If it is necessary, then everything is OK!
- reset the saved stack of screens and believe that the user will forgive us ...
Both options should be discussed with the customer, as both are not perfect. But what if for different external launches it is necessary to choose different behavior? And there is another option - to save the previous stack for a future launch and after showing the “share” screen by the “back” button, exit the application.
What to do? The answer is, but read to the end.
We need to create a separate Activity!
Let us recall which task needs to be solved: to enable other applications to use our functionality, and if to launch some of our application's screens from other applications.
To launch the main application for this completely - an error, and it is worth creating a special application (the second Activity), which will display the necessary screens.
The second Activity is a separate application for sharing the main screens. It cannot be used from the main Activity, and it is necessary to separately configure the parameters in the manifest. I suggest to study this idea in detail.
Conclusion
The idea of this article (report) appeared because when it comes to applications within a single Activity, I often encounter the distrust of even experienced Android developers. So far, I have not met a full description of the approach, and even more detailed analysis of all the important points, and decided to do it myself.
I want to reassure all the distrustful: after the announcement of the architectural components of Google, I fixed all the critical bugs of the child fragments. As for the performance and large nesting - we are not the first project, adhering to this approach, and we have not encountered any problems related to the Activity.
I hope you can now assume that you have received a license to drive a car, and you can easily leave all your rivals behind the wheel! Thank!