Android app development points that old-fashioned programmers stumbled upon

What is this?

I used to mainly write 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 programmers who started developing traditional desktop and command line apps, there is a big cultural gap in developing Android apps. I'm wondering if that's why some people aren't doing what Android development wants or are frustrated. So, for those old-fashioned programmers, I would like to write some points of Android development that I would like to keep in mind.

Where is the entry point?

For old-fashioned programmers, computer programs are "clear beginnings and endings." For example, take the following C program.

#include <stdio.h>

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

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

#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 will be created, but MainActivity.kt (in the case of Kotlin) with the following contents will be 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 for some people to think that:

I'm curious that various files are created without permission, but maybe this ʻonCreate () is the entry point! That means the app must be closed when you exit this ʻonCreate () !

** Unfortunately not. ** **

No entry point or event loop (invisible)

Android apps also have a "beginning". But it's not the MainActivity # onCreate () in the code above. This method is a handler that is called when one of the screens of the app is created, not when the app starts. So where is the method called when the app starts?

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

Hmmm, but isn't that a matter of course?

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

The event handler for starting the application is ʻApplication # onCreate () `

I hope you understand that you will be writing event handlers when developing Android apps. So where is the handler called when the app starts? It is the ʻonCreate () method of a class that inherits from the ʻandroid.app.Application class.

What? There is no such class anywhere?

Yes, there is no source for such classes in projects created from templates. In such cases, Android accepts the implementation of the ʻandroid.app.Application class. As a result, the programmer does not seem to have a "handler called when the app starts". In most Android app development, you don't need to be aware of this handler. However, in some cases, you may want to write "initialization process for the entire application". In such cases, implement a class that inherits from 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 application to the OS

I understand the beginning of the program. How about the end? In the X window system etc., when there are no more windows to display on the screen, the event loop is often exited and the application is terminated. So does Android, like that, automatically quit the app when there's nothing to display on the screen?

This point is a little difficult to answer. The answer is no if you think "end of program = end of process". On Android, the process of the app does not end immediately when all screens (Activity) are closed. But if all the screens (Activity) and services (Service) are closed, the app will do virtually nothing. In that sense, you can think of the app as closed.

On Android, the end of the app process cannot (and should not be) controlled by the app. The process of the app will be "terminate when the OS wants to quit". It is not possible to explicitly terminate itself on the application side. It will be terminated by the OS.

Such a domineering!

I understand that feeling well. But Android has that kind of mechanism. However, foreground apps (currently running apps) are rarely terminated abruptly. Normally, apps that go to the background and have not been used for a long time are subject to termination. The OS does a good job everywhere.

The fact that you can't explicitly terminate an app's process can seem very strange to an old-fashioned programmer. However, considering the following points, this mechanism seems reasonable.

--On smartphones, there is almost no operation to "quit the app". ――It is natural for PCs to "close used apps", but smartphones do not have such a culture. ――Even if you press the home button to return to the home screen, it is expected that the screen just before returning to the home screen will be restored when you launch the application again. ――The same applies when you switch apps. --In the first place, in order for the user to explicitly terminate the application, the user needs to be aware of "what kind of application is running now". You can do that on a PC with a wide screen, but it is difficult on a smartphone with a narrow screen.

In other words, you cannot expect "user-operated application termination" on smartphones. If so, it makes sense for the OS to terminate the app at the right time.

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

--If you need to keep the state, make the state variable persistent. --Implement various life cycle methods (ʻActivity # onSaveInstanceState () , ʻActivity # onStop (), etc.) and save the state in the internal storage. --Restore the state saved in ↑ by implementing life cycle methods (ʻActivity # onRestoreInstanceState () , ʻActivity # onStart (), etc.). --Data that is used throughout the app without depending on a specific Activity or Service is saved in the internal storage as needed.

Also, I wrote that you can't explicitly terminate the process of the app, but you can terminate the Activity normally. I'll talk about what Activity is later, but in a nutshell, it's a "component that represents a single screen." An app can have multiple screens, or Activities, and each Activity can be explicitly terminated (ʻActivity # finish ()`). Therefore, it is possible to effectively close the application by terminating all activities.

What is Activity or Service?

I think there are quite a few people who wonder about this as well. I think that there are many people who can imagine Service from the name, but feel like "Hmm?" About Activity.

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

Service is a screenless component that is primarily used for screen-independent background processing. The app developer creates a derived class of ʻandroid.app.Service`, makes an instance of that class hold the state variable, and implements an event handler to achieve the desired background processing.

An app can have multiple Activities and multiple Services. You can create an app with only Activity, you can create an app with only Service, and of course you can create an app with both Activity and Service.

You can create another Activity from the Activity to realize "screen transition". You can also create a Service from within an Activity to execute a set of processes in the background. You can also create another Service from within the Service to process different processes in parallel. You can also create an Activity from within the Service to display the screen (with restrictions from Android 10).

Hmm. I understand that Activity is tied to the screen. But do you need Service? Why not create a thread and do background processing in that thread?

Yes, you can create threads normally, and you can use them for parallel processing in the background. However, threads created by the app are not managed by the OS (Android framework). An app that has completed all Activities and Services is more likely to be forced to terminate its process by the OS. In that case, ** even if the thread remains, it will end without any problem. ** **

Therefore, threads are usually used to perform processing that is completed within Activity or Service. For example, a thread started in an activity should be terminated by the time the activity ends, or if that is not possible, it should be terminated as soon as possible. Since it is not known when the thread will be forcibly terminated after the activity is terminated, it is necessary to implement it in consideration of that. [^ thread]

Service, on the other hand, is managed by the OS. The Service is (not often) forced to terminate until it is finished. [^ service_finish] Therefore, the process that does not depend on Activity and the process that you want to continue even if Activity ends are implemented as Service.

[^ thread]: Actually, even if the activity ends, the thread will continue to live if there are other activities and services in the app. However, Activities and Services should be implemented so that they are as independent (loosely coupled) as possible from other Activities and Services, so they should not be implemented in the hope that other Activities / Services will survive. ..

[^ service_finish]: 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 that way. Therefore, when implementing Service, it is necessary to implement it on the assumption that it may be forcibly terminated by the OS.

Does ʻApplication own ʻActivity or Service?

If you listen to the story so far, I think that some people think as follows.

An instance of the ʻApplication class (or its derived class) holds and owns an instance of ʻActivity and Service!

Would it look like this when written in code?

This doesn't work


class MainApplication : Application() {
    private var mainActivity: MainActivity? = null

    override fun onCreate() {
        super.onCreate()

        mainActivity = MainActivity()
        startActivity(mainActivity)
    }
}

From the perspective of someone who was developing a platform other than Android, isn't it "likely enough code"? But on Android, this is not allowed. Instantiation of ʻActivity and Service` is the job of the OS, and apps shouldn't do it on their own.

So how do you call Activity or Service?

** Use Intent. ** **

I think the existence of Intent is one of the points that people who develop Android apps for the first time stumble. It's a concept you don't often see on other platforms. For example, if you have an app that has two screens, ʻActivity1 and ʻActivity2, and you want to transition the screen from ʻActivity1 to ʻActivity2, you would 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 called class (ʻActivity2`).
  2. Set the Intent to the parameter you want to pass to the callee.
  3. Send the Intent to the 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 will realize the screen transition. Since the called Activity can get the Intent received by the ʻActivity # getIntent ()` method, it is possible to retrieve the parameters from that Intent.

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

The same is true when calling Service. For example, to call Service1 from ʻActivity1`, you need the following code.

    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. [^ launch_service]

[^ launch_service]: This example uses the startService () method, but the service started by this method cannot be processed for a long time on Android 8.0 or later. If you want the started service to stay alive for a long time, you need to start it as a Foreground service with the startForegroundService () method.

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

** The important thing here is that the concept of Intent is used as a cushion instead of calling directly, whether it is Activity or Service. ** **

This mechanism does not allow the caller to receive an instance of the calling Activity or Service. The called party also cannot receive the calling instance. Therefore, they cannot access instance methods and instance variables from each other. When cooperation between Activity / Service is required, set parameters in Intent and pass it as in the above example, or use another mechanism (startActivityForResult () method to return result information, You will be using (interacting via a derived class of Application, using Broadcast, etc.).

In addition, the parameters that can be set in the Intent (data that can be exchanged through the Intent) are Java primitive types, string types, and their array types. Data of other types must be passed in series. This means that you can't just pass an instance of an object.

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

There seem to be various reasons for this architecture, but I think the main reason is that Android originally targeted hardware with low memory. It is necessary to release the memory efficiently on the terminal with low memory. If each Activity is loosely coupled (without direct references to each other), you can always discard any activity that went to the background and free memory (with exceptions, of course). This allows you to efficiently free up memory without dropping the app itself. I think this is a big advantage of this architecture. Most modern hardware with excess memory may not enjoy this benefit very often. .. ..

What is "launching an app" in the first place?

I see. The thing is, create a derived class of 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 ** it's not. ** ** As I wrote earlier, it is rare to create a derived class of ʻApplication. You can launch the Activity on the first screen without doing that. To do this, in ʻAndroidManifest.xml, specify the Activity you want to launch first.

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 the same as it was created when the project was created from the template. This description defines the settings for the MainActivity class, but the important thing is the<action android: name = "android.intent.action.MAIN" />in the<intent-filter>element. It is a description. This description specifies that MainActivity is the first Activity to start. (By the way, the description <category android: name =" android.intent.category.LAUNCHER "/> below it specifies that MainActivity is an Activity that can be launched from the launcher.)

When the app is launched, the OS creates an Intent that "catch" this description and executes startActivity (). As a result, the MainActivity class is identified as the Activity to launch, and MainActivity is instantiated and displayed. This is what the OS does, so you don't have to code it on the app side.

Hmm? startActivity () is just a "start Activity" method, isn't it? Isn't "launching an app" another thing?

This is a bit confusing. When developing Android apps, we don't pay much attention to "launching apps". Instead, be aware of launching Activities and Services within your app.

For example, suppose you want to launch another app from your own app. To start other apps, write the following code.

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

PackageManager # getLaunchIntentForPackage () returns an Intent that identifies the first Activity to launch 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 "the first activity is launched" and "the app is launched".

Yeah! ?? But isn't the app still in the process? Is it okay to send an Intent to an app that hasn't started the process? ??

That's right. When sending an Intent, you don't have to worry about whether the destination app is running or not. Because ** The OS will do the process of "start if the process of the destination application is not started". ** In other words, if the process of the destination application is not started, if you startActivity () for the Activity of that application, the following processing will be executed.

  1. The app 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 and ʻActivity # onCreate ()` is called and displayed in the foreground.

This is a common "app launch" on Android.

In addition, it's normal to launch a specific Activity in another app directly. 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 do this, you will be taken to the screens of other apps as if the screen transitions occurred normally in your app. Perhaps the user may not be aware that the app has switched.

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

As you can see, on Android, the boundaries between apps are very thin. You can make screen transitions regardless of your own application or other applications. You can call the function regardless of your own application or other applications. I think that it is possible to link apps on other platforms as it is, but isn't Android the only one that can do so flexibly? I think this is what makes Android so interesting and addictive.

What does it mean to interact via a derived class of ʻApplication`?

In the previous section, I mentioned that one of the ways to exchange data between Activities and Services is to "exchange via a derived class of ʻApplication`". What does that mean??

In the previous explanation, I wrote that ʻApplication does not" own "`` Activity or Service. However, ʻApplication, ʻActivity, and Service have the following relationship.

Therefore, the data and objects that you want to share throughout the app should be stored in the instance of the derived class of ʻApplication`.

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()
    }
}

Needless to say, however, this technique can only be used to share data between activities and services within your app. If you want to share data with other apps, you can either give the Intent parameters as described above or call it Content Providers (https://developer.android.com/guide/topics/providers/content-providers). You will use the mechanism.

Shouldn't we just use static variables without such a hassle?

** Don't use it. ** **

Java static members and Kotlin objects can be accessed without specifying an instance of the class, so I'm sure some people will try to use it to share data. For example, the code below.

SharedData.kt


object 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 trying to share data using SharedData.hoge like a global variable. This is fine on other platforms, but Android doesn't allow it.

What? But I don't get a compile error and it works normally?

Yes, there are no errors. Sometimes it works as expected. But sometimes it doesn't work as expected. Since it works normally or does not work, it is easy to cause more confusion.

Android garbage collectors, unlike common Java garbage collectors, unload information from memory as well as the class itself as well as the instance. And the data held by the static variable when the class is unloaded is also destroyed. The code above causes SharedData.hoge to hold the string data, but the garbage collector can unload the SharedData object (class). Then, the stored character string data will also be discarded. If you then try to read SharedData.hoge, the SharedData object (class) will be loaded and initialized again at that time, so an empty string will be returned.

It may be the biggest point that people who have moved to Android development from other platforms will stumble. For these reasons, Java static variables and Kotlin objects should be used only in the following ways when developing Android apps.

--Constant definition --Use in a context where at least one instance of that class is guaranteed to be alive

In this way, the use of static variables and objects for data sharing (data transfer) between Activities and Services should be basically avoided. As explained so far, you can have the Intent passed to startActivity () or startService () as a parameter, have the derived class of ʻApplication have shared data, or store the data in SharedPreferences`. You can share it with others or save it as a normal file.

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

So far, we have explained how to start an Activity or Service. As a quick recap, you know that you can create an Intent and pass it to the startActivity () and startService () methods.

However, when linking between Activities and Services, it may not be possible to achieve the purpose simply by "starting the target Activity / Service". As an example, consider launching a Service from an Activity and letting the Activity know that the Service has finished background processing and is "finished". The Activity can start the Service with startService () (or startForegroundService ()) as we have seen. So how can the Service notify the Activity that it's done?

In this case, it is not always appropriate to use startActivity () because Activity has already started. The sendBroadcast () method is used in such cases. This method, like startActivity () and startService (), is for sending Intents, but to a BroadcastReceiver instead of an Activity or Service. BroadcastReceiver can be created and registered in Activity or Service, so use 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 = "Finished processing"
        }
    }

    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")
        )
        
        //Service start
        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?) {
        //Process various things
        Thread.sleep(1000)

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

Here, we used LocalBroadcastManager # sendBroadcast () because the destination of the broadcast is the Activity in the app, but if you want to send it to another app, use Context # sendBroadcast () (Context is ʻActivity. And Service` are common parent classes). [^ broadcast1] [^ broadcast2]

[^ broadcast1]: To tell the truth, if you want the Service to send an event to the Activity in your app, you don't necessarily have to use the BroadcastReceiver. This is because it is also possible to call an instance method of Activity from Service via a derived class of ʻApplication`. However, even if you do that, you should design your Activity and Service as loosely coupled as possible.

[^ broadcast2]: 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 neither Activity nor Service exists, it should be considered that there is no point in terminating the process when the process is exited from BroadcastReceiver # onReceive (), so if you want to process for a long time, You should start the Service in BroadcastReceiver # onReceive ()and perform time-consuming operations within that Service.

Summary

It's been long, so I folded it a lot, but if there's something I don't understand, I'll ask him to google it.

Android's architecture is quite unique, isn't it? At first, I could understand that the life cycle event was "well necessary", but as soon as I tried to link Activity / Service, it became "???". I hope it helps people who are stumbling in the same place.

Recommended Posts

Android app development points that old-fashioned programmers stumbled upon
Android app personal development kickoff
ROS app development on Android
Points I stumbled upon when creating an Android application [Updated from time to time]
Riot (chat app) development (settings) in Android Studio