📜 ⬆️ ⬇️

Asynchronous operations and recreations of Activity in Android

In one article on Habra ( 274635 ), a curious solution was demonstrated for transferring an object from onSaveInstanceState to onRestoreInstanceState without serialization. It uses the writeStrongBinder(IBInder) method of the android.os.Parcel class.

This solution functions correctly until Android downloads your application. And he has the right to do it.
... system processes
( http://developer.android.com/intl/ru/reference/android/app/Activity.html )


However, this is not the main thing. (If the application does not need to restore its state after such a restart, then this solution will also be appropriate).

But the purpose for which such “non-serializable” objects are used there seemed strange to me. There, calls from asynchronous operations are transferred through them to the Activity to update the displayed state of the application.
')
I have always thought that since the time of Smalltalk, any developer recognizes this typical design problem. But it seems I was wrong.

Task



Special features

In the latter case, the results should be displayed when you next open the Activity .

Decision


MVC (with active model) and Layers.

Detailed solution


The rest of the article is an explanation of what MVC and Layers are.

Let me explain with a specific example. Let us need to build an application “Electronic ticket to electronic queue”.
  1. The user enters the branch of the bank, clicks the button “Take a ticket” in the application. The application sends a request to the server and receives a ticket.
  2. When the queue comes up in the application displays the number of the window in which you want to apply.

I will get the ticket from the server using an asynchronous operation. Also asynchronous operations will be reading the ticket from the file (after restarting) and deleting the file.

You can build such an application from simple components. For example:
  1. Component where the ticket will be located ( TicketSubsystem )
  2. TicketActivity where the ticket will be displayed and the button "Take a ticket"
  3. Class for Ticket (ticket number, position in the queue, window number)
  4. Class for Asynchronous Ticket Retrieval

The most interesting thing is how these components interact.

The application does not have to contain the TicketSubsystem component at TicketSubsystem . Ticket could be
in the static field Ticket.currentTicket , or in the field in the heir class android.app.Application .
However, it is very important that the status is / no ticket originated from an object capable of performing the role
from MVC — that is, generate notifications when a ticket appears (or is replaced).

If you make TicketSubsystem model in MVC terms, then the Activity will be able to subscribe to events and update the ticket display when it is loaded. In this case, the Activity will perform the role of View ( ) in terms of MVC .

Then the asynchronous operation “Getting a new ticket” can simply record the received ticket in TicketSubsystem and not take care of anything else.

Model


Obviously, the model must be a ticket. However, in the application ticket can not "hang" in the air. In addition, the ticket does not initially exist, it appears only after the completion of the asynchronous operation. From this it follows that in the application there must be something else where the ticket will be. Let it be TicketSubsystem . The ticket itself must also be presented somehow, let it be the Ticket class. Both of these classes must be able to fulfill the role of an active model.

Ways to build an active model


Active model - the model notifies the idea that changes have occurred in it. wikipedia

In java there are several auxiliary classes for creating an active model. For example:
  1. PropertyChangeSupport and PropertyChangeListener from the java.beans package
  2. Observable and Observer from java.util
  3. BaseObservable and Observable.OnPropertyChangedCallback from android.databinding

I personally like the third way. It supports strict naming of observable fields, thanks to the android.databinding.Bindable annotation. But there are other ways, and they all fit.

And in Groovy there is a great annotation groovy.beans.Bindable . Together with the possibility of a brief declaration of object properties, a very concise code is obtained (which is based on PropertyChangeSupport from java.beans ).

 @groovy.beans.Bindable class TicketSubsystem { Ticket ticket } @groovy.beans.Bindable class Ticket { String number int positionInQueue String tellerNumber } 

Representation


TicketActivity (as almost all objects related to the presentation) appears and disappears at the will of the user. The application only needs to correctly display the data at the time the Activity appears and when the data changes, while the Activity is displayed.

So in TicketActivity you need:
  1. Update UI widgets when changing data in the ticket
  2. Connect the listener to the ticket when it appears
  3. Connect the listener to TicketSubsytem (to refresh the view when the ticket appears)

1. Update UI widgets.


In the examples in the article I will use PropertyChangeListener from java.beans for the sake of demonstration
details. And in the source code, the link at the bottom of the article will use the android.databinding library,
as providing the most concise code.

 PropertyChangeListener ticketListener = new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent event) { updateTicketView(); } }; void updateTicketView() { TextView queuePositionView = (TextView) findViewById(R.id.textQueuePosition); queuePositionView.setText(ticket != null ? "" + ticket.getQueuePosition() : ""); ... } 

2. Connecting the listener to the ticket



 PropertyChangeListener ticketSubsystemListener = new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent event) { setTicket(ticketSubsystem.getTicket()); } }; void setTicket(Ticket newTicket) { if(ticket != null) { ticket.removePropertyChangeListener(ticketListener); } ticket = newTicket; if(ticket != null) { ticket.addPropertyChangeListener(ticketListener); } updateTicketView(); } 

The setTicket method when replacing a ticket removes a subscription to events from an old ticket and subscribes to events from a new ticket. If you call setTicket(null) , then TicketActivity unsubscribe from ticket events.

3. Connecting the listener to the TicketSubsystem



 void setTicketSubsystem(TicketSubsystem newTicketSubsystem) { if(ticketSubsystem != null) { ticketSubsystem.removePropertyChangeListener(ticketSubsystemListener); setTicket(null); } ticketSubsystem = newTicketSubsystem; if(ticketSubsystem != null) { ticketSubsystem.addPropertyChangeListener(ticketSubsystemListener); setTicket(ticketSubsystem.getTicket()); } } @Override protected void onPostCreate(@Nullable Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); setTicketSubsystem(globalTicketSubsystem); } @Override protected void onStop() { super.onStop(); setTicketSubsystem(null); } 

The code is pretty straightforward. But without using special tools, you have to write quite a few operations of the same type. For each element in the model hierarchy, you have to start a field and create a separate listener.

Asynchronous operation "Get a ticket"


The asynchronous operation code is also pretty simple. The basic idea is to write the results to the upon completion of the asynchronous operation. A will be updated on the notification from the .

 public class GetNewTicket extends AsyncTask<Void, Void, Void> { private int queuePosition; private String ticketNumber; @Override protected Void doInBackground(Void... params) { SystemClock.sleep(TimeUnit.SECONDS.toMillis(2)); Random random = new Random(); queuePosition = random.nextInt(100); ticketNumber = "A" + queuePosition; // TODO     ,    //     . return null; } @Override protected void onPostExecute(Void aVoid) { Ticket ticket = new Ticket(); ticket.setNumber(ticketNumber); ticket.setQueuePosition(queuePosition); globalTicketSubsystem.setTicket(ticket); } } 

Here the globalTicketSubsystem link (also referred to in TicketActivity ) depends on the way subsystems are built in your application.

Restoring state upon restart


Suppose a user clicked the button “Take a ticket”, the application sent a request to the server, and at that time there was an incoming call. While the user answered the call, the answer came from the server, but the user does not know about it. Moreover, the user pressed "Home" and launched some application that ate all the memory and the system had to unload our application.

And our application should display the ticket received before the restart.

To provide this functionality, I will write the ticket to a file and read it after the application starts.

 public class ReadTicketFromFileextends AsyncTask<File, Void, Void> { ... @Override protected Void doInBackground(File... files) { //     number, positionInQueue, tellerNumber } @Override protected void onPostExecute(Void aVoid) { Ticket ticket = new Ticket(); ticket.setNumber(number); ticket.setPositionInQueue(positionInQueue); ticket.setTellerNumber(tellerNumber); globalTicketSubsystem.setTicket(ticket); } } 

Layers


This template defines the rules by which one class is allowed to depend on other classes, so that excessive code entanglement does not occur. In general, this is a family of templates, and I am guided by the variant of Craig Larman from the book “UML Application and Design Patterns”. Here is a chart .

The basic idea is that classes from lower levels cannot depend on classes from upper levels. If we place our classes in the Layers levels, we’ll get something like this:

Please note that all the arrows that cross the level boundaries are pointing straight down! TicketActivity creates a GetNewTicket - down arrow. GetNewTicket creates a Ticket - down arrow. Anonymous ticketListener implements the PropertyChangeListener interface - the down arrow. Ticket notifies listeners PropertyChangeListener - down arrow. Etc.

That is, any dependencies (inheritance, use as a member type of a class, use as a type of a parameter or type of a return value, use as a type of a local variable) are allowed only to classes at the same level or lower levels.

Another drop of theory, and move on to the code.

Level assignment


Objects at the Domains level reflect business entities with which the application works. They should be independent of how our application is organized. For example, the presence of the positionInQueue field of a Ticket is due to business requirements (and not the way we wrote our application).

The Application level is the boundary of where the application logic can be located (except for the appearance formation). If you need to do some useful work, the code should be here (or below).

If you need to do something with appearance, then this is the class for the Presentation level. So this class can contain only the mapping code, and no logic. For logic, he will have to access classes from the Application level.

Belonging of a class to a certain level of Layers is conditional. The class is at a given level as long as it fulfills its requirements. That is, as a result of the editing, the class may move to another level, or become unsuitable for one level.

How to determine at what level should be given class? I will share a modest heuristic, and generally recommend to study the available theory. Start at least here .

Heuristic
  1. If an application removes the View Level, it should be able to perform all of its functions (except for demonstrating the results). Our application without View Level will still contain both the code for requesting a ticket, the ticket itself, and access to it.
  2. If an object of some class displays something, or reacts to the actions of the user, then its place is at the View Level.
  3. In case of inconsistencies, divide the class into several.

Code


The repository https://github.com/SamSoldatenko/habr3 contains the application described here, built using android.databinding and roboguice . Look at the code, and here I briefly explain what choice I made and for what reasons.
Brief explanations
  1. The com.android.support:appcompat-v7 dependency is added because commercial development relies on this library to support older android versions.
  2. The com.android.support:support-annotations dependency is added to use the @UiThread annotation (there are many other useful annotations).
  3. Dependency org.roboguice:roboguice is a library for dependency injection. Used to compose an application from parts using Inject annotations. Also, this library allows you to embed resources, links to widgets and contains a mechanism for sending messages similar to CDI Events from JSR-299.
    • TicketActivity using annotation @Inject receives a link to TicketSubsystem .
    • The asynchronous ReadTicketFromFile task using the @InjectResource annotation @InjectResource file name from the resources from which the ticket is to be loaded.
    • TicketSubsystem using @Inject gets a Provider that it uses to create a ReadTicketFromFile .
    • and etc.

  4. The org.roboguice:roboblender creates a database of all annotations for org.roboguice:roboguice at compile time, which is then used at run time.
  5. Added app/lint.xml with settings for suppressing warnings from the roboguice library.
  6. The dataBinding option in app/build.gradle enables special syntax in layout files similar to Expression Language ( EL ) and includes the android.databinding package, which is used to make Ticket and TicketSubsystem an active model. As a result, the presentation code is greatly simplified and replaced with declarations in the layout file. For example:

     <TextView ... android:text="@{ts.ticket.number}" /> 

  7. The .idea folder is in .gitignore to use any version of Android Studio or IDEA . The project is perfectly imported and synchronized via the build.gradle files.
  8. The configuration of the gradle wrapper is left unchanged (the files gradlew , gradlew.bat and the folder gradle ). This is a very effective and convenient mechanism.
  9. Setting unitTests.returnDefaultValues = true in app/build.gradle . This is a trade-off between protection against random errors in unit tests and the brevity of unit tests. Here I prefer the shortness of the unit tests.
  10. The org.mockito:mockito-core library is used to create stubs in unit tests. In addition, this library allows you to describe “System Under Test” using the annotations @Mock and @InjectMocks . When using Dependency Injection, components “expect” that they will be injected dependencies before using them. Before the tests also need to implement all the dependencies. Mockito can create and implement stubs in the class under test. This greatly simplifies the test code, especially if the fields being injected have limited visibility. See GetNewTicketTest.
  11. Why Mockito , and not Robolectric ?
    1. Android developers recommend writing local unit tests in this way.
    2. This is the way to get the fastest pass of the “edit” cycle - “test run” - “result” (important for TDD).
    3. Robolectric is more suitable for integration testing than modular.

  12. org.powermock:powermock-module-junit library org.powermock:powermock-module-junit and org.powermock:powermock-api-mockito . Some things cannot be replaced with plugs. For example, replace the static method or suppress the call to the base class method. For these purposes, PowerMock replaces the class loader and corrects the byte code. In TicketActivityTest , using PowerMock suppresses the call to RoboActionBarActivity.onCreate(Bundle) and sets the return value from the call to the static method DataBindingUtil.setContentView
  13. Why do many class fields have package local scope?
    1. This is application code, not a library. That is, we control all the code that uses our classes. Consequently, there is no need to hide the fields.
    2. The visibility of test fields simplifies the writing of unit tests.

  14. Why then all fields are not public?
    A public member of a class is a commitment made by the class to all other classes that exist and those that will appear in the future. And package local is a commitment only to those who are in the same package at the same time. Thus, you can change the package local field (rename, delete, add a new one) if you update all the classes in the package.
  15. Why is the LogInterface log variable not static?
    1. There is no need to write the initialization code yourself. DI does the job better.
    2. To make it easier to replace the logger plug. The output to the log in certain cases is “specified” and checked in tests.

  16. Why do we need LogInterface and LogImpl which are just descendants of similar classes from RoboGuice?
    To set the Roboguice configuration with the @ImplementedBy annotation @ImplementedBy(LogImpl.class) .
  17. Why is the @UiThread annotation for the Ticket and TicketSubsystem ?
    These classes are sources of onPropertyChanged events that are used in UI components to update the display. It is necessary to ensure that calls will be made in the UI stream.
  18. What happens in the TicketSubsystem constructor?
    After starting the application, you need to download data from the file. In the Android application, this is the Application.onCreate event. But in this example, such a class has not been added. Therefore, the moment when you need to read the file is determined by when the TicketSubsystem is created (only one copy is created, TicketSubsystem it is marked with the @Singleton annotation). However, in the TicketSubsystem constructor, TicketSubsystem cannot create a ReadTicketFromFile , since it needs a link to a not yet created TicketSubsystem . Therefore, the creation of ReadTicketFromFile postponed to the next UI flow cycle.
  19. To check how the application works after restarting:
    1. Click "Take a Ticket"
    2. Without waiting for it to appear, click "Home"
    3. In the console, run adb shell am kill ru.soldatenko.habr3
    4. Launch the application



thank

Source: https://habr.com/ru/post/281290/


All Articles