Since Google introduced Android Architecture Components and finally became more opinionated about software architecture, some people started referring to it as modern android development. Although those libraries were important to the Android community, specially for newcomers to the Android world, they also created an abstraction on top of the Android SDK.
I've been working with Android development for nearly 9 years and I am often surprised for the lack of understanding Android developers have about the Android SDK. When interviewing I am always focused on the basics, not only regarding Android development, but also software engineering practices and software architecture. Libraries will come and go, but the Android SDK will always be there, also when developing a complex application a deep understanding of the Android SDK is necessary to come up with a proper solution.
So what are the basics? A good starting point is to evaluate the five main Android components: Application, Activity, Broadcast Receiver, Content Provider and Service. Some developers when asked which are the basic components also include the Fragment or the View, but all the components share an important characteristic: the are declared in the Android Manifest.
The basic components work as an interface of your application with the Android OS. They communicate what your application wishes to expose to third party applications installed in the device and with the Android OS. So let's go and evaluate each of those components one-by-one.
When the user taps an icon in the home screen of an Android device, the first component to be created will be the Application one. Actually a process is created with a Dalvik VM or Art VM instance, in case there was no process already created for your application.
Application object is unique within the process and its onCreate method is called only once during the process lifecycle. Actually Application's onCreate is the first of all other components' creational methods to be called. So we can say Application's onCreate is the starting point of an Android application and that Application object is a good place to reference other objects that should live during the entire lifecycle of the process.
Instead of triggering logic only when an Activity is displayed, a good part of your business logic should start being processed on Application's onCreate method. Also it is not by chance that at least one DaggerComponent is usually instantiated within Application and most importantly referenced by its instance, so we can have those nice Singleton objects that are unique within the application.
Activity is the place where Google expects you to focus, concentrate all your work and logic, at least it is what you get by reading the Android documentation. All jokes aside an Activity is a door you open to your application UI, you can have many doors, or just one door.
The Android OS, or external applications are the ones responsible to open those doors. For example via a deep link from a Browser, a Google Assistant action, or any other Intent that matches the Intent Filter of your Activity. The most obvious Activity is the one with Main action and Launcher category in its Intent Filter. That's the door the launcher application uses to enter your application.
So the right motivation one should have to declare a new Activity in the Android Manifest is to create an external door. The recent single Activity movement is motived by that ideology, that in fact is the original idea behind Activities. Many Android applications nowadays use a single Activity and Fragments or Views for internal navigation.
Actually using multiple Activities for internal navigation is a waste of memory, since the View Hierarchy associated with an Activity is kept in memory while in the Back Stack.
The reason for the Fragment's complicated lifecycle is an attempt to solve that problem. Fragments are more efficient for internal navigation because their View Hierarchy is destroyed when they are moved to the back stack by the Fragment Manager. That's why Fragments have the onCreateView and the onDestroyView while Activities don't.
Many developers believe the Android OS "kills" Activities that are in the back stack. That is not true. The Android OS kills processes only. If you have too many Activities in the back stack you will eventually reach the maximum amount of memory reserved to your process. That will end-up causing the famous OutOfMemoryError.
In a short sentence the Broadcast Receiver is an optimisation. It is an attempt to avoid starting a Service when it is not necessary. Communication with Broadcast Receivers is done via Intents, just like a Service, so basically we have an Intent Filter which states which Intents that BroadcastReceiver is interested in, but we could theoretically use the same Intent Filter for a Service.
Everything would work, but sometimes we need more logic in order to be sure it is worth to fire a Service or not. Or we just don’t want to start a Service at all, the logic related to the Intent can be processed within the onReceive method of the Broadcast Receiver.
According to Android documentation a the onReceive method should run for around 10 seconds max, so use onReceive for fast calculations based on the received Intent or to decide whether a Service should be started to deal with the Intent.
Broadcast Receiver could be registered dynamically in an Activity, Fragment or Service, instead of in the Android Manifest. Also it can be used for internal communication within your application. I would strongly recommend not to use Broadcast Receivers to solve those problems, instead create a simpler communication structure that fits the use cases of your application and that can be tested independently of the Android SDK.
This is probably the most misunderstood of all components. A Service is a way to communicate the Android OS that your application is doing background work. There is absolutely no difference if the thread performing the work is associated with a Service or any other component, although obviously associating the thread with an Activity or a Broadcast Receiver wouldn't be a great idea, since the have a short lifecycle.
Android OS has a priority associated with each application process. Having a running Service in your application increases that priority, therefore it makes it less likely for the Android OS to kill your process. If you want an even higher priority you can use a Foreground Service, which requires a Notification to be displayed to the user. The highest priority possible is when your application is in foreground.
So for example, let's say your use case is to download a large file from the internet. You could encapsulate that logic in a DownloadManager object, that would take care of storing the download information, whether it is idle, finished, paused or resumed. DownloadManager would have a start method that would look for stored on-going download information and resume it. That DownloadManager instance would be associated with the Application component and aside from that, a Service could be started and stopped by DownloadManager via an interface (e.g. BackgroundMode) whenever the download was active or not. BackgroundMode would have activate and deactivate methods with implementations that would basically start and stop a Service.
By having a Service running, your process priority would be increased making your application less likely to be killed while the file is being downloaded. You could even use START_STICKY flag in your Service, so in case the process is killed anyway a new process would be automatically created. When the new process is created, a new Application instance is created, the onCreate is called, the Service is re-started. The download is resumed in Application's onCreate where DownloadManager start method is called.
Most applications don't need a Content Provider. The main idea of the Content Provider is to expose data to other applications. It is an overkill to use a Content Provider to access data that is private to your application, instead use SQLiteOpenHelper directly or any other famous library for Database access that you want.
A classic Content Provider usage is in the Contacts application that use it to expose Contact information to external applications.
Android Agnostic Architecture
All the aforementioned components were created to help your application to communicate with the external world: other applications and the Android OS. Android components communicate via Intents that under the hood use Inter Process Communication and Data Serialisation in order to allow components living in different processes (applications) to seamlessly communicate with each other.
Your internal business logic should not be built around those components, instead it should be agnostic to them. Once we understand how Android SDK components work, we can design an application where most of the code is Android agnostic and therefore free of the burden related to it. Also Android agnostic code can be easily tested and developed without the dependency to the Android emulator leveraging TDD and other software engineering practices.