Android - txgz999/Mobile GitHub Wiki
https://developer.android.com/training/basics/intents/filters
When your app is installed on a device, the system identifies your intent filters and adds the information to an internal catalog of intents supported by all installed apps.
https://developer.android.com/guide/components/activities/intro-activities
An activity is started by an intent, we can find the intent information by calling the following in onCreate:
Intent intent = getIntent();
The Override annotator in Java is optional, see https://stackoverflow.com/questions/8578057/is-it-compulsory-to-annotate-inherited-methods-with-override. What matters is the method signature.
https://android.jlelse.eu/intent-vs-pendingintent-8ef2ad5824ed
I feel the Android version is hard to remember, until I realized the first letter of each version is named alphabetically. The current version is 10.0 (Q), and the corresponding SDK version is 29. The one before that is 9.0 (P: Pie) and SDK version 28. The one coming some is 11.0 (R) and SDK version 30.
When we create an emulator, we need to choose a System Image for certain Android version, as well as a Device (e.g. Google Pixel 2 Smartphone), since there are various Android devices and each may have different version of Android OS installed.
When we choose the System Image, we better to choose the one having Google Play Store. It is not needed to support FCM, but we may have the need to download useful software, e.g. home launcher, from the store.
Apps, often called launchers, that replace the Home screens on your Android device and give you access to the contents and features of your devices.
The splash screen is the first screen user would see when fires up the app. It would be disappear when the main screen is loaded. See https://android.jlelse.eu/right-way-to-create-splash-screen-on-android-
Each app defines one or several activities. When we run an app, an instance of the main activity of this app appears in screen. From there we can jump to another activity instance (i.e. using Intent), which can be an instance of the same activity, or an instance of a different activity of the same app, or even an instance of an activity of a different app.
When jumping to a difference activity instance, the original activity instance still exists in memory. They forms an activity stack, with the new activity adds to the top of the stack, so called first in last out. The activity stack is also called back stack, because when user clicks the Back button, the new activity instance is popped up from the activity stack and is destroyed, the original activity instance now is on the top of the activity stack and thus appears on screen. This activity stack forms a task.
A task can be brought to background when user presses the Home button. So what are in the foreground or in the background are tasks not apps, although each task is initialized through an app.
Once we understand task and activity stack, we can understand the need of the intent flags. Intent brings activity instance to the screen, i.e. on top of the activity stack of the foreground task. How would it affect the activity stack. The default behavior is to create a new activity instance and add to the activity stack. But sometimes that is not what we want, then we can use intent flag to change this behavior.
As examples, assume the intent is for activity A and has no flag, and we call the instance of A created before the intent execution Ao, and the one created by the intent An:
- if the activity stack contains B,C (in the order from top to bottom), then after the execution, the stack contains An,B,C
- if the activity stack contains Ao,B,C, then after the execution, the stack contains An,Ao,B,C
- if the activity stack contains B,Ao,C, then after the execution, the stack contains An,B,Ao,C
- FLAG_ACTIVITY_SINGLE_TOP
If the activity being started is the current activity (i.e. the one currently at the top of the activity stack), then no new instance of activity would’ve be created. The existing instance on the top of the activity stack receives a call to onNewIntent().
As examples, assume the intent is for activity A and use the single top flag alone:
- if the activity stack contains B,C, then after the execution, the stack contains An,B,C
- if the activity stack contains Ao,B,C, then after the execution, the stack has no change and still contains Ao,B,C
- if the activity stack contains B,Ao,C, then after the execution, the stack contains An,B,Ao,C
- FLAG_ACTIVITY_CLEAR_TOP
If the activity being started is already running in the current task (i.e. the activity stack contains an instance of that activity), then all of the other activities on top of that existing instance of that activity are destroyed and this intent is delivered to the resumed instance of the activity (now on top), through onNewIntent().
Therefore clear top means removes all instances of other activities on top of the existing instance of the activity that the intent targets, in this way this instance becomes the top one on the activity stack.
As examples, assume the intent is for activity A and use the clear top flag alone:
- if the activity stack contains B,C, then after the execution, the stack contains An,B,C
- if the activity stack contains Ao,B,C, after executing the intent, the stack contains An,B,C
- if the activity stack contains B,Ao,C, then after the execution, the stack contains An,C
If we use both flags together, then (see the discussion in https://stackoverflow.com/questions/12572227/flag-activity-clear-top-calls-oncreate-instead-of-onresume)
- if the activity stack contains B,C, then after the execution, the stack contains An,B,C
- if the activity stack contains Ao,B,C, then after the execution, the stack contains Ao,B,C
- if the activity stack contains B,Ao,C, then after the execution, the stack contains Ao,C
For details, see https://developer.android.com/guide/components/activities/tasks-and-back-stack
https://developer.android.com/topic/libraries/data-binding
I have a class that extends FirebaseMessagingService
, which receives Firebase messages in its overridden onMessageReceived
method. How can the new message be displayed on the screen in an activity? The solution is to use LocalBroadcastManager
to broadcast a message in the service, then in the activity add a receiver to get the message.
- in service class
public void onMessageReceived(RemoteMessage remoteMessage) {
Map<String, String> data = remoteMessage.getData();
sendLocalNotification(data.get("title"), data.get("content"));
LocalBroadcastManager manager = LocalBroadcastManager.getInstance(this);
Intent intent =
new Intent(getResources().getString(R.string.new_notification_intent))
.putExtra("messageId", remoteMessage.getMessageId())
.putExtra("sentTime", remoteMessage.getSentTime())
.putExtra("title", data.get("title"))
.putExtra("content", data.get("content"));
manager.sendBroadcast(intent);
}
Notice that the message is defined as an Intent
.
- define a receiver class, in the following it is a inner class of the activity class:
class NewNotificationReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String messageId = intent.getStringExtra("messageId");
long sentTime = intent.getLongExtra("sentTime", 0);
String title = intent.getStringExtra("title");
String content = intent.getStringExtra("content");
MainActivity.this.addNotification(messageId, sentTime, title, content);
}
}
- inside the activity class register and unregister an receiver instance:
@Override
protected void onCreate(Bundle savedInstanceState) {
...
newNotificationReceiver = new NewNotificationReceiver();
LocalBroadcastManager.getInstance(this).registerReceiver(newNotificationReceiver,
new IntentFilter(getResources().getString(R.string.new_notification_intent)));
}
@Override
protected void onDestroy() {
LocalBroadcastManager.getInstance(this).unregisterReceiver(newNotificationReceiver);
...
}
- the activity class also decides what to do when message comes
private void addNotification(String messageId, long sentTime, String title, String content) {
Notification n = new Notification(0, messageId, sentTime, title, content, false);
this.notifications.add(0, n);
this.mAdapter.notifyDataSetChanged();
...
}
How can I hide keyboard, which was brought up by user typing in a text field, when user clicks a button? See the discussion in https://stackoverflow.com/questions/13593069/androidhide-keyboard-after-button-click:
InputMethodManager imm = (InputMethodManager)getSystemService(INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);
It seems that the Volley library is the recommended way. See https://developer.android.com/training/volley. First add
- implementation 'com.android.volley:volley:1.1.1'
to the build.gradle at app level and do a sync. Then we can create either StringRequest or JsonObjectRequest, depends on the response data type, and send requests to server. The good thing of Volley if the sending request and waiting for response are done in worker threads, while the call to Volley is done at main thread, and the result is sending back to the main thread. So the use of work thread is just a implementation detail that the use of the Volley library does not need to care about.
There are several ways to achieve that, see
- https://stackoverflow.com/questions/2468874/how-can-i-update-information-in-an-android-activity-from-a-background-service/2469646
- https://www.websmithing.com/2011/02/01/how-to-update-the-ui-in-an-android-activity-using-data-from-a-background-service/
- https://stackoverflow.com/questions/8802157/how-to-use-localbroadcastmanager
It seems that using LocalBroadcastManager
is the recommended approach.
On the service side, define and send intent
LocalBroadcastManager manager = LocalBroadcastManager.getInstance(this);
Intent intent = new Intent("NEW_STUDENT_REGISTERED")
.putExtra("LastName", "Smith")
.putExtra("FirstName", "John");
manager.sendBroadcast(intent);
On the activity side, define and register a receiver
public class MainActivity extends AppCompatActivity {
private MyBroadcastReceiver receiver;
@Override
protected void onCreate(Bundle savedInstanceState) {
...
receiver = new MyBroadcastReceiver();
LocalBroadcastManager.getInstance(this).registerReceiver(receiver, new IntentFilter("NEW_STUDENT_REGISTERED"));
}
@Override
protected void onDestroy() {
LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver);
super.onDestroy();
}
private void addStudent(String firstName, String lastName) {
...
}
class MyBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String firstName = intent.getStringExtra("FirstName");
String lastName = intent.getStringExtra("LastName");
MainActivity.this.addStudent(firstName, lastName);
}
}
}
Activity is defined in a java class, layout is defined in an xml file. There are related through the following code in OnCreate
:
setContentView(R.layout.activity_xxx);
where activity_xxx
is the name of the xml file. When we compile the project, Android automatically create a View instance from each of the xml layout file. Therefore R.layout.activity_xxx
is a View instance.
To illustrate that a View instance does not have to come from a xml layout file, consider the following code where a View instance is created in code directly:
Button button = new Button(this);
button.setText("Click Me");
button.setBackgroundColor(Color.parseColor("#FF0000"));
setContentView(button);
In conclusion, a xml layout file represents a View instance.
In Android, controls are called views.
TextView viewById = (TextView) this.findViewById(R.id.textView);
viewById.setText("This is my test");
Create an Intent
object to describe where you want to go, then call startActivity
(remember activity means page/window/screen)
public void next(View view) {
Intent intent = new Intent(this, MainActivity.class);
startActivity(intent);
}
In a .Net project, there is no standard way to define project level constants, used by multiple classes. In Android, we can define them in res/values/strings.xml, then we can refer to these strings anywhere in code using R.string.
. A bigger advantage of approach is to support localization.
RecyclerView replaces ListView and GridView. There are multiple elements involved in creating a RecyclerView:
- Dataset
- Adapter
- ViewHolder
- View
- LayoutManager
A view holder holds a view, which provides a visual representation of one data item. The adapter is responsible to convert data set to a set of view holders and fill data to the view. The layout manager is responsible to ask the adapter to create view holders and arrange their positions on the screen.
Comparing to the ListView and GridView, RecyclerView is especially good to show large amount of data set. It does not create one view holder for each data item. It only creates a few more than what the screen can display. When user scroll the screen, a data item may move out of the screen, the view holder that shows that data item may be reused to show another data item coming to the screen.
ListView and GridView are a little bit simpler, they uses the following elements only
- Dataset
- View
- Adapter
ListView and GridView are both subset of AdapterView, and they know how to arrange views. Now in RecyclerView this task is assigned to an external separate layout manager. This provides more flexibility of using RecyclerView. Meanwhile Android provides some built-in layout manager, such as LinearLayoutManager and GridLayoutManager, and thus in simple uses, developers don't need to implement their own layout manager. The reason RecyclerView can cover both ListView and GridView is RecyclerView abstracts the difference of these two legacy views into layout manager level.
There are some built-in adapters for ListView and GridView, such as ArrayAdapter, but when using RecyclerView, developers have to create their own adapter class.
The basic steps to use RecyclerView as follows (see https://developer.android.com/guide/topics/ui/layout/recyclerview):
- modify the activity xml layout file to include a RecyclerView
- create an xml layout file for showing a data item
- create an adapter class that based on given data set, create view holders and populate views
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder> {
private String[] mDataset;
// Provide a reference to the views for each data item
// Complex data items may need more than one view per item, and
// you provide access to all the views for a data item in a view holder
public static class MyViewHolder extends RecyclerView.ViewHolder {
// each data item is just a string in this case
public TextView textView;
public MyViewHolder(TextView v) {
super(v);
textView = v;
}
}
// Provide a suitable constructor (depends on the kind of dataset)
public MyAdapter(String[] myDataset) {
mDataset = myDataset;
}
// Create new views (invoked by the layout manager)
@Override
public MyAdapter.MyViewHolder onCreateViewHolder(ViewGroup parent,
int viewType) {
// create a new view
TextView v = (TextView) LayoutInflater.from(parent.getContext())
.inflate(R.layout.my_text_view, parent, false);
MyViewHolder vh = new MyViewHolder(v);
return vh;
}
// Replace the contents of a view (invoked by the layout manager)
@Override
public void onBindViewHolder(MyViewHolder holder, int position) {
// - get element from your dataset at this position
// - replace the contents of the view with that element
holder.textView.setText(mDataset[position]);
}
// Return the size of your dataset (invoked by the layout manager)
@Override
public int getItemCount() {
return mDataset.length;
}
}
- in the activity class, add code to create and use data set, layout manager and adapter
recyclerView = (RecyclerView) this.findViewById(R.id.my_recycler_view);
layoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(layoutManager);
List<Person> data = new ArrayList<Person>();
for(int i=0; i<=100; i++) {
data.add(new Person("Smith" + i, "John", LocalDate.of(1980+ i, Month.MARCH, 12)));
}
mAdapter = new MyAdapter(myDataset);
recyclerView.setAdapter(mAdapter);
The adapter has a base class, and thus has a very clear methods signature to override:
- onCreateViewHolder
- onBindViewHolder
- getItemCount
The reason that RecyclerView can be used to display huge amount of data is it does not create one view holder for each data item. It only create enough that can occupy the screen and plus a few more, and when user scroll the screen, those view holders are reused to display new data item coming to the screen. The onCreateViewHolder
method is called by the layout manager to create view holder, and the onBindViewHolder
is called by the layout manager when it is going to display a new data item. We can easily verify that be loading a large amount of data, then in these two methods, log the position value passed to these method.
How can we split the view of each data item into two portions: the top portion is always visible, but the bottom becomes visible only when user clicks the top portion? I found the following example:
- https://android.jlelse.eu/get-expandable-recyclerview-in-a-simple-way-8946046b4573
- https://github.com/RohitSurwase/AndroidDesignPatterns/blob/master/dynamic-recycler-view/src/main/java/com/rohitss/dynamicrecycler/RecyclerDataAdapter.java
The item view xml layout file contains both the top and the bottom portions. Where should we initially hide the bottom portion?
- Can we do that in the view xml file to set the visibility for the bottom portion? We can't. This file is used when we create a view holder. If we do that way, when the view holder is reused to represent a new data item, the bottom portion of the new data item would be visible automatically if the one for the previous data item is visible
- Can we do that in the
onBindViewHolder
method to hide the bottom portion? We can't. If we do that way, say we have a data item with visible bottom portion, now we scroll it off the screen then scroll it back to screen. At that time, this data item might be bound to a new view holder, and then the bottom portion is not visible anymore.
To correct resolve this issue, we need to realize that whether the bottom portion is visible or not is a data item level information, and thus should be stored with the data item.
public class Person {
public String lastName;
public String firstName;
public LocalDate birthDate;
public boolean detailVisible;
public Person(String lastName, String firstName, LocalDate birthDate) {
this.lastName = lastName;
this.firstName = firstName;
this.birthDate = birthDate;
detailVisible = false;
}
}
public class PersonViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener{
public View personView;
private LinearLayout mainView;
private LinearLayout detailView;
private Person mData;
public PersonViewHolder(View view) {
super(view);
this.personView = view;
mainView = view.findViewById(R.id.main);
mainView.setOnClickListener(this);
detailView = view.findViewById(R.id.detail);
detailView.setOnClickListener(this);
}
@Override
public void onClick(View view) {
if(view.getId()==R.id.main) {
if (detailView.getVisibility() == View.GONE) {
detailView.setVisibility(View.VISIBLE);
}
else
{
detailView.setVisibility(View.GONE);
}
mData.detailVisible = !mData.detailVisible;
}
else {
}
}
public void bind(Person p) {
mData = p;
((TextView) personView.findViewById(R.id.firstname)).setText(p.firstName);
((TextView) personView.findViewById(R.id.lastname)).setText(p.lastName);
((TextView) personView.findViewById(R.id.birthdate)).setText(p.birthDate.toString());
detailView.setVisibility((p.detailVisible)?View.VISIBLE:View.GONE);
}
}
public class PersonAdapter extends RecyclerView.Adapter<PersonViewHolder> {
private List<Person> mDataset;
public PersonAdapter(List<Person> dataset) {
this.mDataset = dataset;
}
@NonNull
@Override
public PersonViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext())
.inflate(R.layout.person, parent, false);
PersonViewHolder vh = new PersonViewHolder(v);
return vh;
}
public void onBindViewHolder(@NonNull PersonViewHolder holder, int position) {
Log.w("Adapter", Integer.toString(position));
Person p = this.mDataset.get(position);
holder.bind(p);
}
@Override
public int getItemCount() {
return mDataset.size();
}
}
We make the view holder as a click listener, and make it listens to the click of the top and bottom parts, then respond accordingly. For example, when user clicks the top part, we will make the bottom part visible. Notice that android does not support event bubbling.
When data changes, we notify the screen to change according by calling the notifyDataSetChanged
method of the adapter:
private void addPerson(String lastName, String firstName, LocalDate birthDate) {
Person p = new Person(lastName, firstName, birthDate);
this.myDataset.add(0, p);
this.mAdapter.notifyDataSetChanged();
}
Intent Filter
https://stuff.mit.edu/afs/sipb/project/android/docs/guide/topics/manifest/manifest-intro.html
Components advertise their capabilities — the kinds of intents they can respond to — through intent filters. Since the Android system must learn which intents a component can handle before it launches the component, intent filters are specified in the manifest as elements. A component may have any number of filters, each one describing a different capability.
An intent that explicitly names a target component will activate that component; the intent filter doesn't play a role. But an intent that doesn't specify a target by name can activate a component only if it can pass through one of the component's intent filters.
One thing confused me is the AndroidManifest.xml contains a little different FCM related stuff (e.g. permission and intent filter) in different approaches. The reason is the packages they use can also bring permissions and intent filters. These things are combined with the ones set in AndroidManifest.xml to form the whole permissions and intent filters for the app.