[Java] Old-fashioned programmer’s stumbling block for Android application development

22 minute read

What is this talking about?

Originally I was mainly writing applications that run on the PC desktop or command line in C/C++ or Java, but now I mostly write Android apps. From the perspective of a programmer who has been involved in traditional desktop and command line app development, there is a huge cultural gap in Android app development. I’m wondering if that’s why Android development isn’t going the way it should or is frustrated. So, for those old-fashioned programmers, I would like to write the points of Android development that I would like to keep in mind.

Where is the entry point?

For old-fashioned programmers, a computer program is “a thing with a clear beginning and end.” For example, let’s look at the following C program.

# include <stdio.h>

int main(int argc, char* argv[]){
    printf("Hello, world!");
    return 0;
}

The program starts at the beginning of the main function and ends by exiting the main function. It’s as clear as it gets. The same applies to GUI programs. For example, if you use the X window system, the code would be:

# include <X11/Intrinsic.h>
# include <X11/StringDefs.h>
# include <X11/Xaw/Label.h>

int main(int argc, char* argv[]){
    XtAppContext context;
    Widget root, label;

    root = XtVaAppInitialize(&context, "Hello, world!", NULL, 0, &argc, argv, NULL, NULL);
    label = XtVaCreateManagedWidget("Hello, world!", labelWidgetClass, root, NULL);
    XtRealizeWidget(root);
    XtAppMainLoop(context);
    return 0;
}

This also starts at the beginning of the main function and ends by exiting the event loop and exiting the main function.

But what about Android? For now, let’s create a new project in Android Studio. There are several templates to choose from when creating a project, but for now let’s choose Empty Activity. Then, various files are created, but MainActivity.kt (for Kotlin) with the following contents is displayed on the screen.

MainActivity.kt


package test.app.helloworld

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle

class MainActivity :AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

It wouldn’t be strange if somebody thought something like this.

I am worried that files are created arbitrarily, but maybe onCreate() is the entry point! That means the app will exit when you exit this onCreate()!

**Unfortunately not. **

There is no entry point or event loop (invisible)

Android apps also have a “begin”. But that’s not the MainActivity#onCreate() in the code above. This method is a handler that is called when one of the screens that the app has is created, not when the app starts. So where is the method called when the app starts?

In fact, when developing an Android app, you don’t write an entry point equivalent to the main function in C. Talking about the internal processing of the OS, since the Android application operates as an independent Linux process, there should be an entry point equivalent to the main function internally, but the Android OS (Android framework) handles it. I’m hiding it. Therefore, the application developer does not write the entry pointer. Similarly, the event loop is hidden by the OS, so we will not write it. App developers will focus on writing handlers for events dispatched from the event loop.

Hmm, but that’s pretty obvious, isn’t it?

I agree. Hiding entry points and event loops is nothing special in Imadoki’s GUI framework. However, even such frameworks often provide a way to access the event loop. Android, on the other hand, does not provide a way for apps to access the event loop. So you can’t create your own event loop that other platforms can. maybe.

The application start event handler is Application#onCreate()

As you can see, developing an Android app involves writing event handlers. So again, where is the handler that is called when the app starts? That is the onCreate() method of the class that extends the android.app.Application class.

Eh? There is no such class anywhere?

Yes, projects created from templates do not have source for such classes. In such cases, Android will just use the implementation of the android.app.Application class as is. As a result, the programmer appears to have no “handler called when the app starts.” Most Android app developers do not need to be aware of this handler. However, there are times when you want to write “initialization processing for the entire application”. In such a case, implement a class that inherits the android.app.Application class and specify that class in AndroidManifest.xml.

MainApplication.kt


package test.app.helloworld

import android.app.Application
import android.util.Log

class MainApplication :Application() {
    override fun onCreate() {
        super.onCreate()
        Log.d("MainApplication", "Start application!")
    }
}

AndroidManifest.xml


    <application
        android:name=".MainApplication"

Leave the OS to the end of the application

I understand the beginning of the program. What about the end? In X window system etc., if there are no more windows to display on the screen, it is likely that the application will exit from the event loop and the application will end. Then, does Android, like that, automatically terminate the app when there is nothing to display on the screen?

This is a bit difficult to answer. The answer is NO if you think “program end = process end”. On Android, even if all screens (Activity) are closed, the application process does not end immediately. However, if all screens (Activity) and services (Service) are finished, the application will virtually do nothing. In that sense, you can think of the app as terminated.

On Android, the end of the app process cannot be controlled (not controlled) by the app. The process of the application will be “terminate when the OS wants to terminate”. The application cannot explicitly terminate itself. It will be terminated by the OS without permission.

Such domineering!

I understand that feeling. But Android has that mechanism. However, it is rare that a foreground application (currently running application) is suddenly terminated. Normally, apps that go to the background and have not been used for a long time are targeted for termination. The OS does a good job of everywhere.

The fact that you can’t explicitly terminate an app’s process seems very strange to a traditional programmer. However, this mechanism seems reasonable considering the following points.

  • There is almost no operation to “close the application” on a smartphone. -It’s common for PCs to “close down used apps,” but smartphones don’t have that culture. -Even if you press the home button to return to the home screen, the smartphone is expected to “restore the screen just before returning to the home screen” when you launch the app again. -The same applies when switching between apps. -In the first place, the user needs to be aware of “what kind of application is currently running” in order for the user to explicitly terminate the application. You can do it with a PC with a wide screen, but it is difficult with a smartphone with a narrow screen.

In other words, it is not possible to expect “end of application by user operation” with a smartphone. If so, it makes sense for the OS to terminate the app at the right time.

The fact that it is terminated by the OS arbitrarily means that it is necessary to implement it so that it can be terminated at any time in application development. Basically, the following method is used.

  • Persist state variables if state retention is needed. -Implement various life cycle methods (Activity#onSaveInstanceState(), Activity#onStop() etc.) and save the state in internal storage. -Implement the lifecycle method (Activity#onRestoreInstanceState(), Activity#onStart() etc.) to restore the state saved in ↑.-Data that is used throughout the application without depending on a specific activity or service is saved in the internal storage as needed.

I also mentioned that you can’t explicitly terminate the process of your app, but you can terminate Activity normally. I will talk about what an Activity is later, but in a nutshell, it is a “component that represents one screen”. An app can have multiple screens, or Activities, and each Activity can be explicitly terminated (Activity#finish()). Therefore, it is possible to put the application in a practically closed state by closing all Activities.

What is # Activity or Service? I wonder if there are many people who wonder this too. It seems that there are many people who can think of Service as something, but feel like “Fuumm?” about Activity.

As I wrote above, Activity is a component that represents one screen. It holds all screen state and receives and processes events that occur on that screen. The app developer creates a class derived from android.app.Activity, makes an instance of that class hold a state variable, and implements an event handler to achieve the target screen.

Service is a component that does not have a screen and is mainly used for background processing that does not depend on the screen. The app developer creates a class derived from android.app.Service, makes an instance of that class hold a state variable, and implements an event handler to achieve the desired background processing.

An app can have multiple Activities and multiple Services. You can make an app that has only an activity, you can make an app that has only a service, and of course you can make an app that has both an activity and a service.

You can create another Activity from the Activity and realize “screen transition”. You can also create a Service from within an Activity and execute a set of processes in the background. It is also possible to create another Service from within a Service and perform different processing in parallel. You can also create an Activity from Service to display the screen (with Android 10 there are restrictions).

Hmm. I know the Activity is tied to the screen. But is Service necessary? Why not create a thread and perform background processing in that thread?

Yes, you can create a thread normally, and you can use it to do parallel processing in the background. However, the threads created by the application are not managed by the OS (Android framework). If the Activity or Service is all terminated, the OS will be more likely to be forced to terminate the process. In that case, ** even if there are threads left, it will end without regard. **

Therefore, threads are usually used to execute the processing that is completed in Activity or Service. For example, a thread started within an Activity should end by the time the Activity ends, or if that is not possible, end it as soon as possible. Since the Activity is finished, we don’t know when the thread will be forcibly terminated, so we need to implement it accordingly. 1

On the other hand, Service is managed by the OS. The Service is (not) forced to terminate until it’s done. Because it is 2, the processing that does not depend on the Activity and the processing that you want to continue even after the Activity ends are implemented as a Service.

Does Application own Activity and Service?

If you hear the story so far, I think that some people think the following.

It means that an instance of the Application class (or its derived class) holds and owns an instance of Activity or Service!

Is it like this when written in code?

```kotlin: This won’t work class MainApplication :Application() { private var mainActivity: MainActivity? = null

override fun onCreate() {
    super.onCreate()

    mainActivity = MainActivity()
    startActivity(mainActivity)
} } ```

From the perspective of people who have been developing platforms other than Android, isn’t it “likely possible code”? But Android doesn’t allow this. Instantiating Activity and Service is a task of the OS, and the application cannot do it without permission.

Then how to call Activity or Service?

**Use Intent. **

I think Intent is one of the stumbling blocks for those who develop Android apps for the first time. That’s a concept I don’t see on other platforms. For example, if you have an application that has two screens, Activity1 and Activity2, and you want to change the screen from Activity1 to Activity2, write the following code.

class Activity1 :AppCompatActivity() {
    fun callActivity2() {
        val intent = Intent(this, Activity2::class.java)
        intent.putExtra("param1", "some data")
        startActivity(intent)
    }
}

This code (callActivity2() method) does the following:

  1. Create an Intent that holds the information of the call destination class (Activity2).
  2. Set the parameter you want to pass to the callee to Intent.
  3. Send Intent to OS.

The OS will then do the following:

  1. Select the class to be called from the sent Intent information. This time, the class is explicitly specified, so the Activity2 class is selected.
  2. Instantiate the class.
  3. Pass the Intent sent by the caller to the instantiated Activity.
  4. Display the instantiated Activity in the foreground.

This enables screen transitions. Since the called Activity can get the Intent received by the Activity#getIntent() method, it is possible to take the parameter from the Intent.

        val param1 = getIntent().getStringExtra("param1")

The same is true when calling Service. For example, the following code is required to call Service1 from Activity1.

    fun callService1() {
        val intent = Intent(this, Service1::class.java)
        intent.putExtra("param1", "some data")
        startService(intent)
    }

As you can see, it’s almost the same as in Activity. 3

In the above example, the class of Activity or Service to be called is explicitly specified, but instead of explicitly specifying it, the category etc. can be specified. You can also call Activity and Service implemented in other apps. It is easy to think of Intent as a medium for exchanging information between Activity/Service.

**The important thing here is that, whether it’s an Activity or a Service, we’re not going to call it directly, but rather the idea of an Intent as a cushion. **

With this mechanism, the caller cannot receive the Activity or Service instance that was called. The called side also cannot receive the calling instance. Therefore, the instance methods and instance variables cannot access each other. If you need to link between Activity/Service, set the parameter to Intent and pass it as in the above example, or return the result information by another mechanism (startActivityForResult() method, You will interact with it through derived classes of Application`, use Broadcast, etc.).Furthermore, the parameters that can be set in Intent (data that can be exchanged via Intent) are Java primitive types, string types, and their array types. Other types of data must be serialized and passed. That means you can’t just pass an instance of an object.

Thanks to this, each Activity/Service will be loosely coupled at a high level.

There seem to be various reasons for such an architecture, but I think the main reason is that Android originally targeted hardware with low memory. It is necessary to release the memory efficiently in the terminal with a small memory. If each Activity is loosely coupled (no direct reference to each other), you can always destroy background Activities and release memory (with exceptions of course). This allows you to release memory efficiently without dropping the app itself. I think this is a big advantage of this architecture. Modern hardware, which has the most available memory, may not enjoy this benefit very often. .. ..

What is “launching an app”?

I see. All you have to do is create a class derived from the Application class, create an Intent that specifies the activity of the screen to be displayed first in the onCreate() method, and do startActivity()! ?

I think it’s natural to think this way from the flow of the story so far, but not **. ** As I wrote earlier, I rarely make a derived class of Application. You can launch the Activity for the first screen without doing that. To do so, specify the Activity you want to start first in AndroidManifest.xml.

AndroidManifest.xml


        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

This description is exactly what was created when the project was created from the template. This description defines the settings of the MainActivity class, but the important thing is to say <action android:name="android.intent.action.MAIN" /> in the <intent-filter> element. The description. This description specifies that the MainActivity is the first Activity to launch. (By the way, the description under that is <category android:name="android.intent.category.LAUNCHER" /> specifies that MainActivity is an Activity that can be launched from the launcher.)

When the app is launched, the OS will create an Intent that will “catch” this description and execute startActivity(). As a result, the MainActivity class is specified as the Activity to launch, and the MainActivity is instantiated and displayed. This is what the OS does, so you don’t have to do any coding on the app side.

Hmm? startActivity() is just a method that “activates the Activity”, right? Isn’t it another thing to “launch an app”?

This is a little difficult to understand. When developing an Android app, you don’t really care about “launching the app”. Instead, be aware of launching Activities and Services within your app.

For example, consider launching another app from your app. To launch other apps, you would write code like:

        val intent = packageManager.getLaunchIntentForPackage("test.app.other_app")
        startActivity(intent)

PackageManager#getLaunchIntentForPackage() returns an Intent that identifies the Activity to launch first for the app specified in the argument. So by passing that Intent to startActivity(), the first Activity of the target app will be launched. In other words, it is regarded as “equal to start of the first activity” and “start of the application”.

Yeah! ? But isn’t the process of the app still up? Is it okay to send an Intent to an app whose process has not started yet? ?

That’s right. When sending an Intent, you don’t need to consider whether the destination app is running or not. Because **, the OS does the processing of “start up the process of the destination application if it is not running”. ** In other words, if the process of the destination app is not running, startActivity() will be executed for the activity of the app, and the following process will run.

  1. The application process starts.
  2. The Application class (or its derived class) is instantiated and Application#onCreate() is called.
  3. The class of Activity specified by Intent is instantiated, Activity#onCreate() is called, and it is displayed in the foreground.

This is a common “launching app” on Android.

In addition, it’s no surprise that you can directly launch a specific Activity in another app. You can also pass parameters.

        val intent = Intent().apply {
            setClassName("test.app.other_app", "test.app.other_app.Activity2")
            flags = Intent.FLAG_ACTIVITY_NEW_TASK
            putExtra("param1", "hoge")
        }
        startActivity(intent)

When you execute this, it will move to the screen of another application as if the screen transition had occurred normally in your own application. Perhaps the user may not notice that the app has switched.

Similarly, it is possible to directly start a specific service of another app.

As you can see, on Android, the boundaries between apps are very thin. You can transition the screen regardless of your application or other applications. You can call the function regardless of your application or other applications. I think that the linkage between apps can be done on other platforms, but I think Android is the only one that can be so flexible. I think that’s where Android is interesting and also where it’s quirky.

What does it mean to communicate through a derived class of Application?

In the previous section, as one of the methods to exchange data between Activities and Services, “Transaction via the derived class of Application” was mentioned. What does that mean??

In the above explanation, I wrote that the Application does not “own” the Activity or the Service. However, Application and Activity, Service have the following relationship.

  • An application always has only one instance of the Application class (or its derived class).
  • Every Activity and Service instance can get an instance of its Application class using the getApplication() method.

Therefore, all the data and objects that you want to share with the entire application should be stored in an instance of the Application derived class.

MainApplication.kt


class MainApplication :Application() {
    val sharedHoge = Hoge()
}

Activity1.kt


class Activity1 :AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_1)
        
        (application as MainApplication).sharedHoge.fuga()
    }
}

Service1.kt


class Service1: Service() {
    override fun onCreate() {
        super.onCreate()

        (application as MainApplication).sharedHoge.fuga()
    }
}

However, it goes without saying that this method can only be used to share data between Activities and Services within an app. If you want to share data with other apps, you can either add parameters to Intent as described above or call content providers. I will use the mechanism.

Shouldn’t it be nice to use static variables without doing that kind of trouble?

** Don’t use it. **

Since Java static members and Kotlin objects can be accessed without specifying an instance of the class, some people would like to use it to share data. For example, the following code.

SharedData.ktobject SharedData {


    var hoge: String = ""
}

Activity1.kt


class Activity1 :AppCompatActivity() {
    fun onClickFugaButton() {
        SharedData.hoge = "fuga"
    }
}

Activity2.kt


class Activity2 :AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_2)
        findViewById<TextView>(R.id.textViewHoge).text = SharedData.hoge
    }
}

In this example we are going to share data using SharedData.hoge like a global variable. This is fine on other platforms, but Android does not allow this.

Eh? But there is no compile error and it works normally?

Yes, there are no errors. Sometimes it works as expected. But sometimes it doesn’t work as expected. It is easy to cause confusion because it moves normally or does not move normally.

Unlike the general Java garbage collector, the Android garbage collector unloads not only the instance but also the class itself from memory. And when the class is unloaded, the data held by the static variable is also destroyed. In the above code, SharedData.hoge holds the string data, but the garbage collector may unload the SharedData object (class). Then, the stored character string data will also be destroyed. After that, if you try to read SharedData.hoge, the SharedData object (class) is loaded and initialized again at that time, and an empty string is returned.

It may be the biggest point that people who have moved to Android development from other platforms stumble. For the above reasons, the use of Java static variables and Kotlin objects should be limited to the following when developing Android apps.

  • Constant definition
  • Use in a context that guarantees that at least one instance of that class is alive

Basically, you should avoid using static variables and objects for data sharing (data passing) between Activities and Services like this. As described above, you can have it as a parameter in the Intent passed to startActivity() or startService(), have shared data in the derived class of Application, or store the data in SharedPreferences. You can share it with others or save it as a file.

So what if you want to fire an event in an Activity from a Service, for example?

So far, we have explained how to start an Activity or Service. After a quick review, you’ll understand that you should create an Intent and pass it to the startActivity() and startService() methods.

However, when linking between Activities and Services, the purpose may not be achieved simply by “launching the target Activity/Service”. As an example, let’s consider starting a Service from an Activity, letting the Service finish its background processing, and letting the Activity know that it’s done. An Activity can start a Service with startService() (or startForegroundService()) as we’ve seen. So how can the Service tell the Activity that it’s done?

In this case, the Activity is already running, so using startActivity() is not always appropriate. The sendBroadcast() method is used in such cases. This method is also for sending Intent like startActivity() and startService(), but the destination is BroadcastReceiver instead of Activity or Service. BroadcastReceiver can be created and registered in Activity or Service, so it uses it to receive Intent.

MainActivity.kt


class MainActivity :AppCompatActivity() {
    private val myBroadcastReceiver = object: BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent?) {
            findViewById<TextView>(R.id.textView1).text = "Processing finished"
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Register BroadcastReceiver
        LocalBroadcastManager.getInstance(this).registerReceiver(
            myBroadcastReceiver,
            IntentFilter("test.app.actions.SERVICE_FINISHED")
        )
        
        // start service
        startService(Intent(this, Service3::class.java))
    }

    override fun onDestroy() {
        LocalBroadcastManager.getInstance(this).unregisterReceiver(myBroadcastReceiver)
        super.onDestroy()
    }
}

Service3.kt


class Service3: IntentService("Service3") {
    override fun onHandleIntent(intent: Intent?) {
        // do a lot of processing
        Thread.sleep(1000)

        // send a broadcast
        LocalBroadcastManager.getInstance(this).sendBroadcast(
            Intent("test.app.actions.SERVICE_FINISHED")
        )
    }
}

I used LocalBroadcastManager#sendBroadcast() here because the destination of the broadcast is Activity in the app, but use Context#sendBroadcast() when sending to other apps (Context is Activity Or Service common parent class). 4 5

Summary

Since it became long, I folded it a lot, but if there is a place I do not understand halfway, I will ask you to do it properly ….

Android’s architecture is quite unique. At first, I understood that I needed a lifecycle event, but when I tried to link Activity/Service, it became “???”. I would be happy to help people who are stumbling in the same place.

  1. Even if the Activity ends, the thread will continue to live if there are other Activities or Services in the application. However, Activity and Service should be implemented so as not to depend on other Activity and Service as much as possible (being loosely coupled), so it should not be implemented that expects the survival of other Activity/Service. .. 

  2. Service(Foreground service) may be forcibly terminated by the OS when the entire system runs out of memory. This is not a system malfunction, and Android was originally designed for that. Therefore, when implementing Service, it is necessary to implement it on the assumption that it may be forcibly terminated by the OS. 

  3. This example uses the startService() method, but the service started by this method can not process for a long time on Android 8.0 or later. If you want to keep the started Service alive for a long time, you need to start it as a Foreground service with the startForegroundService() method. 

  4. Actually, if you want to send an event from Service to Activity in your app, you don’t necessarily have to use BroadcastReceiver. Because it is also possible to call the instance method of Activity from Service via the derived class of Application. However, even if you do such things, you should carefully design Activity and Service to be as loosely coupled as possible. 

  5. BroadcastReceiver can also receive system events fired by the OS. For example, it is possible to execute some processing immediately after starting the terminal. However, if there is no activity or service, it should be considered that there is no need to terminate the process when the process exits from BroadcastReceiver#onReceive(), so if you want to process for a long time, ` You should start your Service inside BroadcastReceiver#onReceive()` and perform some time-consuming operations within that Service.