Back to Home

Asynchronous operations and recreation of Activity in Android

android · mvc · layers

Asynchronous operations and recreation of Activity in Android

    In one article on the hub ( 274635 ), a curious solution was demonstrated for transferring an object from onSaveInstanceStateto onRestoreInstanceStatewithout serialization. The writeStrongBinder(IBInder)class method is used there android.os.Parcel.

    Such a solution functions correctly until Android unloads your application. And he has the right to do it.
    ... system may safely kill its process to reclaim memory for other foreground or visible processes ...
    ( http://developer.android.com/intl/en/reference/android/app/Activity.html )


    However, this is not the point. (If the application does not need to restore its state after such a restart, then this solution is also suitable).

    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 Activityto update the displayed state of the application.

    I always thought that since Smalltalk, any developer will recognize this typical design task. But it seems I was wrong.

    Task


    • On command from user ( onClick()), start the asynchronous operation
    • At the end of the operation, display the result in Activity

    Features
    • Activitydisplayed at the time the operation is completed may be
      • the same from which the team came
      • another instance of the same class (screen rotation)
      • an instance of another class (the user switched to a different screen in the application)

    • At the time the operation is completed, it may turn out that none Activtyof the application is displayed

    In the latter case, the results should be displayed at the next opening Activity.

    Decision


    MVC (with active model) and Layers.

    Detailed solution


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

    I will explain with a specific example. Let us need to build the “Electronic ticket to electronic queue” application.
    1. The user enters the bank branch, clicks the button “Get a ticket” in the application. The application sends a request to the server and receives a ticket.
    2. When the queue comes up, the application displays the number of the window that you need to contact.

    I will receive a 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 instance:
    1. Component where the ticket will be located ( TicketSubsystem)
    2. TicketActivity where the ticket will be displayed and the button "Get ticket"
    3. Class for the Ticket (ticket number, line item, window number)
    4. Class for Asynchronous Ticket Acquisition

    The most interesting thing is how these components interact.

    An application is not required to contain a component at all TicketSubsystem. The ticket could be
    in a static field Ticket.currentTicket, or in a field in the successor class android.app.Application.
    However, it is very important that the condition there is / is not a ticket coming from an object capable of playing a role
    Модельof MVC- that is, generate notifications when a ticket appears (or is replaced).


    If you make the TicketSubsystemmodel in terms MVC, you Activitycan subscribe to events and update the ticket display when it is loaded. In this case, it Activitywill fulfill the role of View( Представление) in terms of MVC.

    Then the asynchronous operation “Obtaining a new ticket” will be able to simply record the received ticket in TicketSubsystemand not to worry about anything else.

    Model


    Obviously, the ticket should be the model. However, in the application, the ticket cannot “hang” in the air. In addition, the ticket does not initially exist, it appears only upon completion of the asynchronous operation. It follows from this that there must be something else in the application where the ticket will be located. Let it be TicketSubsystem. The ticket itself must also be somehow presented, let it be a class Ticket. Both of these classes must be able to play 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

    There are several helper classes in java for creating an active model. For example:
    1. PropertyChangeSupportand PropertyChangeListenerfrom the packagejava.beans
    2. Observableand Observerfrom the packagejava.util
    3. BaseObservableand Observable.OnPropertyChangedCallbackfromandroid.databinding

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

    Groovy has a wonderful annotation groovy.beans.Bindable . Together with the possibility of a brief declaration of the properties of the object, a very concise code is obtained (which relies on PropertyChangeSupportfrom java.beans).

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

    Representation


    TicketActivity(like 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 of appearance Activityand while changing the data is displayed Activity.

    So in TicketActivityneed:
    1. Update UI widgets when data changes in ticket
    2. Connect listener to Ticket when it appears
    3. Connect listener to TicketSubsytem(to refresh view when appears ticket)

    1. Update UI widgets.


    The examples in the article, I'll use PropertyChangeListenerfrom java.beansthe sake of demonstration
    details. And the source code for the link at the bottom of the article will be used by the library android.databinding,
    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();
    }
    

    setTicketWhen replacing a ticket, the method deletes the event subscription from the old ticket and subscribes to events from the new ticket. If called setTicket(null), it will TicketActivityunsubscribe from events ticket.

    3. Connecting a listener to 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 the use of special tools, one has to write quite a lot of operations of the same type. For each element in the model hierarchy, you have to create a field and create a separate listener.

    Asynchronous operation “Take a ticket”


    The asynchronous operation code is also quite simple. The basic idea is to write the results to at the end of the asynchronous operation Модель. And it Представлениеwill be updated by notification from Модели.

    public class GetNewTicket extends AsyncTask {
        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 link globalTicketSubsystem(it was also mentioned in TicketActivity) depends on how the layout of the subsystems in your application.

    Restore state upon restart


    Suppose that the user clicked the “Take a Ticket” button, the application sent a request to the server, and at that time an incoming call occurred. While the user answered the call, a response came from the server, but the user does not know about it. Not only that, the user clicked “Home” and launched some application that gobbled up all the memory and the system had to unload our application.

    And now 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 {
    ...
        @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 there is no excessive code entanglement. In general, this is a family of templates, and I focus on the version of Craig Larman from the book "Application of UML and design patterns." There is a diagram here .

    The basic idea is that classes from the lower levels cannot be dependent on classes from the upper levels. If we place our classes by levels Layers, we get something like this diagram:

    Note that all the arrows that cross the boundaries of the levels are directed strictly down! TicketActivitycreates GetNewTicket- down arrow. GetNewTicketcreates Ticket- down arrow. Anonymous ticketListenerimplements the interface PropertyChangeListener- down arrow.Ticketnotifies listeners PropertyChangeListener- down arrow. And so on.

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

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

    Level Assignment


    Objects at the level Domainsreflect the business entities with which the application works. They should be independent of how our application is arranged. For example, the presence of the positionInQueuey field Ticketis due to business requirements (and not the way we wrote our application).

    A level Applicationis the boundary of where the application logic can be located (except for shaping the appearance). If you need to do some useful work, then the code should be here (or below).

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

    Belonging to a certain levelLayers- conditional. A class is at a given level as long as it fulfills its requirements. That is, as a result of editing, the class can go to another level, or become unsuitable for any level.

    How to determine at what level a given class should be? I will share a modest heuristic, but in general I recommend studying an accessible theory. Begin even here .

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

    The code


    The repository https://github.com/SamSoldatenko/habr3 contains the application described here, built using android.databindingand roboguice. Look at the code, and here I will briefly explain what choice I made and for what reasons.
    Brief explanation
    1. Dependency com.android.support:appcompat-v7added because commercial development relies on this library to support older versions of android.
    2. A dependency has been com.android.support:support-annotationsadded to use annotations @UiThread(there are many other useful annotations).
    3. Dependency org.roboguice:roboguice- a library for implementing dependencies. Used to compose an application from parts using Inject annotations . Also, this library allows you to embed resources, links to widgets and contains a message forwarding mechanism similar to CDI Events from JSR-299.
      • TicketActivityc using the annotation @Injectgets a link to TicketSubsystem.
      • ReadTicketFromFileUsing an annotation, @InjectResourcean asynchronous task retrieves the file name from the resources from which the ticket must be loaded.
      • TicketSubsystemusing @Injectgets Providerwhich uses to create ReadTicketFromFile.
      • and etc.

    4. The dependency org.roboguice:roboblendercreates a database of all annotations for org.roboguice:roboguiceat compile time, which is then used at run time.
    5. Added app/lint.xmlsettings file to suppress warnings from the library roboguice.
    6. The option dataBindingto app/build.gradlepermit a special syntax in the layout files like Expression Language( EL) and connects the package android.databinding, which is used to make Ticketand the TicketSubsystemactive model. As a result, the view code is greatly simplified and replaced with declarations in the layout file. For instance:


    7. The folder is .ideaincluded in .gitignoreorder to use any versions Android Studioor IDEA. The project is perfectly imported and synchronized through files build.gradle.
    8. The gradle wrapper configuration is left unchanged (files gradlew, gradlew.batand folder gradle). This is a very effective and convenient mechanism.
    9. Setting unitTests.returnDefaultValues = truec app/build.gradle. This is a compromise between the protection against random errors in unit tests and the brevity of unit tests. Here I preferred the brevity of unit tests.
    10. The library is org.mockito:mockito-coreused to create stubs in unit tests. In addition, this library allows you to describe the “System Under Test” using annotations @Mockand @InjectMocks. When using Dependency Injection, components “expect” that dependencies will be implemented before they are used. Before the tests, you also need to implement all the dependencies. MockitoCan create and implement stubs in the tested class. This greatly simplifies the test code, especially if the embedded fields have limited visibility. See GetNewTicketTest.
    11. Why Mockitonot Robolectric?
      1. Android developers recommend writing local unit tests this way.
      2. This results in the fastest pass through the “edit” cycle - “test run” - “result” (important for TDD).
      3. Robolectric is more suitable for integration testing than for unit testing.

    12. Library org.powermock:powermock-module-junitand org.powermock:powermock-api-mockito. Some things cannot be replaced with plugs. For example, substitute a static method or suppress a call to a base class method. For these purposes, PowerMockreplaces the class loader and corrects the bytecode. In TicketActivityTestusing the PowerMockcall is suppressed RoboActionBarActivity.onCreate(Bundle), and the return value is set from the static method callDataBindingUtil.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. Therefore, there is no need to hide the fields.
      2. Visibility of fields from tests makes writing unit tests easier.

    14. Why then are all the fields 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 classes in the package.
    15. Why is the variable LogInterface lognot static?
      1. There is no need to write the initialization code yourself. DI does this better.
      2. To make it easier to replace the logger with a stub. Log output in certain cases is "specified" and checked in tests.

    16. Why do we need LogInterfaceand LogImplwhich are just descendants of similar classes from RoboGuice?
      To prescribe Roboguice configuration annotation @ImplementedBy(LogImpl.class).
    17. Why annotation @UiThreadfor classes Ticketand TicketSubsystem?
      These classes are event sources onPropertyChangedthat are used in UI components to update the mapping. It must be guaranteed that calls will be made in the UI thread.
    18. What happens in the constructor TicketSubsystem?
      After starting the application, you need to load data from the file. In an Android application, this is an Application.onCreate event. But in this example, such a class was not added. Therefore, the moment when you need to read the file is determined by when it is created TicketSubsystem(only one copy is created, because it is marked with an annotation @Singleton). However, the constructor TicketSubsystemcannot be created ReadTicketFromFile, because it needs a link to the one that has not yet been created TicketSubsystem. Therefore, creation is ReadTicketFromFiledeferred to the next cycle of the UI thread.
    19. To check how the application works after restarting:
      1. Click "Get a ticket"
      2. Without waiting for it to appear, click "Home"
      3. In the console, execute adb shell am kill ru.soldatenko.habr3
      4. Launch the application



    thanks

    Read Next