Android Refactoring

Introduction:

The Engel & Völkers Property Search for mobile devices was the first software which was developed by an external provider and then insourced to the Engel & Völkers Technology GmbH. Although it was developed as native iOS application at the beginning, a port to Android started in March 2017 in order to reach more potential customers.

When the project was taken over a year later, technologies had already advanced. We analyzed the code base and though the application was runnable, we noticed a list of shortcomings, for example:


  • Bugs that lead to crashes


  • No functional unit tests


  • No clear architecture and tightly coupled components


  • Bad separation of concerns


Additionally, as the project was a simple copy of its iOS counterpart, it contained workflows and the look and feel of iOS application which might be uncommon to Android users. In short, there was a lot of room for improvement.

In the following article we would like to quickly present some concepts and libraries we used to refactor the code base with the goal to improve the maintainability and extensibility of the app in the future.

Basic steps

Our first goal was to prepare the app for a release to the Google Play Store. That means that all major bugs had to be fixed, new contact forms had to be added and adaptations concerning the European General Data Protection Regulation (GDPR) and legal requirements had to be made. Furthermore, the user interface had to be updated to follow the Engel & Völkers corporate identity guidelines. After version 1.0 hit the Play Store, bigger refactorings were tackled using the following tools.

Kotlin

Kotlin is a modern statically typed programming language which has been developed since 2011 and runs on the Java Virtual Machine (JVM). Since Google announced its official support for Android in 2017, Kotlin’s popularity among the Android Development community skyrocketed because of its characteristics and features like Null Safety, data classes or Extension Functions. It is a very concise language that eliminates a lot of boilerplate code while providing a very good readability.

Java-code example:

testButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
testTextView.setText("Hello World");
testTextView.setVisibility(View.VISIBLE);
}
});

Kotlin code example:

testButton.setOnClickListener{
testTextView.text = "Hello World"
testTextView.visibility = View.VISIBLE
}

Furthermore, its interoperability with Java makes it very easy to integrate into the existing codebase as Java code can simply be called from Kotlin and vice versa.

Our last reason for adopting Kotlin was rather simple and personal: we wanted to try out something new. We came to the agreement that every new feature should be implemented in Kotlin. Additionally, whenever an existing code part has to be modified or refactored, the associated class would have to be converted to Kotlin as well.

Android Architecture Components

Android Architecture Components are developed by Google and part of the Android JetPack. It is collection of libraries that have been developed to create robust and maintainable Android applications, that can be easily tested too. Although most libraries are fairly new and still under development, they are well documented with examples and codelabs that help you to get started. Each component might be used individually, but they are also designed to interact well with each other. We decided to evaluate what Android Architecture Components might be applicable to our project and ended up with implementing quite a few.

Hamburg - Engel & Völkers Technology

Model-View-ViewModel (MVVM) architecture

In the beginning, the given codebase of the project roughly followed a MVC approach which is almost given by default for Android applications. There were some activities, fragments and helper classes, but there was no bigger architectural design identifiable. Understanding and editing code parts often turned into a hassle as the fragments were quite complex and held too much responsibility by controlling view and business logic. Smaller view components were sometimes tied together and not reusable without making major changes.

Before starting to overhaul the basic architecture we evaluated two different concepts: Model-View-Presenter (MVP) and Model-View-ViewModel (MVVM). In the end we went for MVVM because of a couple of reasons:


  • It generates less boilerplate code than MVP


  • It can easily survive configuration changes, like screen rotations


  • After understanding the concept, it is straightforward to develop


  • It emphasizes the use of reactive programming concepts like RxJava or LiveData


  • It lowers the risk of memory leaks as ViewModels hold no references of View classes


  • Google provides a ViewModel component that is aware of the Android lifecycle


In the MVVM concept, the View is responsible for displaying information to the user and relaying user events to the ViewModel. The View should not hold any reference to the Model, it just only observes ViewModel properties and interacts with its exposed methods. The ViewModel manages the view logic and serves as a connector between the other two components. It can read and update data of the Model and listens to possible events from the Model. On the other hand, a ViewModel should never know about the view. By following this rule, the ViewModel is an isolated component that can be tested individually.

Hamburg - Engel & Völker Technology

By applying the MVVM architecture we were able to clean up the crowded View classes and put logic into the corresponding components .

Data binding

The Data Binding Library from Google is a powerful tool to remove UI framework methods from View classes by binding UI components directly to data sources, in our case the properties of a ViewModel. Normally, UI components need to be found in a layout and then programmatically assigned to a property or attribute:

val imprintTextView: TextView = view.findViewById(R.id.imprint_text)
imprintTextView.text = viewModel.imprintText

By using data binding properties can be assigned in a declarative way within layout files themselves, making the two steps mentioned above redundant:

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="{viewModel.imprintText}" />

It is also possible to bind method callbacks to click actions. The result is a less crowded view class, less exceptions as data bindings are null-safe and it opens the possibility for another benefit. Via LiveData, another Architecture Component, the binding can be made reactive so whenever the bound value changes the UI component will be automatically updated as well. We extensively use this feature to fill and update information that are asynchronously received via backend responses.

Another advantage of the Data Binding Library comes in the form of Custom binding adapters which allow to add own logic that is applied when bound values change. Furthermore, it offers the possibility to create own view element attributes, like setting an underline text style to a TextView.

By creating custom binding adapters like:

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="{viewModel.imprintText}" />

a new layout property is defined that can be applied to any TextView element:

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Imprint"
android:underlineText="@{true}" />

This removes any need to create references to TextView elements and set the paintFlags attribute programmatically within View classes over and over again. Instead of duplicated code we end up with a decoupled and reusable piece of code.

Besides the already presented one-way data binding and custom binding adapters, there is a third type of data binding that we use in our project, two-way Data Binding. It allows us to fill editable text fields with preset information while automatically propagating changes from user input back into the ViewModel. This is very handy, for example in our contact forms.

Room

It is very common for apps to store data in local files and database. In the Engel & Völkers Property Search app, information about search requests and houses or yachts on the watch list are saved in a local SQLite database which is wrapped by an Object Relational Mapping (ORM) system for easier and more robust access. We decided to exchange the existing GreenDao ORM system with the new Room Persistence Library from Google as it is designed to play well together with other Architecture Components like LiveData or RxJava. Room provides a clean API to easily define entities via annotations which gets even easier in combination with data classes from Kotlin. Entities can furthermore be annotated with type converters to alter data before storing them into SQL fields. We created several type converters to convert data objects to JSON format before saving them. This approach again helps to offload logic that was previously present in View and helper classes.

Data access objects (DAO) are realised by implementing annotated interfaces:

@Dao
interface SearchRequestDao : BaseDao<SearchRequest> {
@Query("SELECT * FROM search_requests WHERE name = :name")
fun getSearchRequestByName(name: String): Single<SearchRequest>
}

Based on the annotations room takes care of building the corresponding classes that used during runtime. It also had to be noticed, that room offers a clean management for data migrations and supports migration tests.

Repository pattern

In order to enforce a stronger separation of concerns we introduced the Repository pattern which focuses around adding an additional abstraction layer that decouples all data sources from the rest of the application. In our case it separates ViewModel classes from the local database or the Web API endpoints. The repository in between provides a central and consistent access to data. It holds all responsibility about how to decide which data source shall be queried or how to handle errors while ViewModel classes do not require any additional logic and simply consume the provided data.This architecture can quickly adapt to changes of data sources like the introduction of local caches without touching a single ViewModel class as the repository interface remains the same. Another advantage comes in form of testability as the repository can be covered by unit tests.

Hamburg - Engel &Völkers Technology

Retrofit 2

As the main purpose of the Engel & Völkers Property Search app is to display data coming from a backend API, reliable communication is a critical feature. The original implementation was setting up HTTP connections manually and scheduled them with asynchronous tasks. Overall, it involved a lot of complex, untested code. For more convenience and security we removed the old implementation and widely used Retrofit 2. It offers good integration with RxJava and reactive programming concepts so that we could achieve an implementation that handles requests and responses automatically off the main thread.

Similar to Room, communication requests can be easily created in form of annotated interfaces:

@GET("sapi/exposee")
fun exposeeRequest(@Query("language") language: String,
@Query("ids") ids: String,
@Query("country") country: String?,
@Query("currency") currency: String?,
@Query("version") version: String?,
@Query("nui") nui: Int?): Single<ExposeeModel>

Additionally, Retrofit 2 is providing good JSON parser integration so that the response from the backend is automatically converted into a data object and then published via RxJava.

Dagger 2

As we wanted to make our code more testable, we had to isolate the individual parts in a better way. We chose the dependency injection framework Dagger 2 for Android. Dependency injection is an architectural pattern based on the inversion of control technique, i.e. an external provider (injector) supplies all it needed dependencies to an object (client) instead of letting the object determine or instantiate dependencies by itself. The object only knows about the provided interface of the injected service but neither how the service was constructed nor which service is used exactly. This enforce the single responsibility principle and makes a class easier to test as all injected dependencies can be mocked during test cases.

In our project dependency injection is used for providing any kind of dependencies, for example, in our ViewModels it is used mostly for providing repositories and tracking services via constructor injection, like:

class ExposeeViewModel @Inject constructor(
private val searchWebService: SearchWebService,
private val watchListRepository: WatchListRepository,
private val usageTrackManager: UsageTrackManager) : ViewModel() {
}

Navigation component

The last Android Architecture Component we started to implement is the Navigation library. It defines screen transitions in a declarative xml format, thereby removing responsibility from View classes. It also manages navigation stack operations supports the implementation of Deep Links which might be added to our app in the near future. In order to make full use of the navigation component we started to shift over to a single activity approach. The ultimate goal is to have only one base activity that hosts a container to display content in the form of fragments.

Hamburg - Engel & Völker Technology

Summary

Taking over code from somebody else is never an easy task. There might be a lot issues to tackle, be it bugs, incomplete features, missing documentation or architectural flaws. Refactoring an existing project is a time consuming but we are very satisfied with the results we could achieve. Most of the presented Android Architecture Components worked out of the box and play well together. Although reactive programming with LiveData and the steep learning curve RxJava might be an obstacle you may spend several days on to overcome, the possibilities they are offering are worth it.

We enjoy working with Kotlin and highly recommend it to everyone willing to give it a try. Our project has about 84% Kotlin in our source code by the time of writing this article. Soon, we will very likely achieve to replace all remaining Java classes, that are not auto-generated, e.g. from data binding, with Kotlin implementations. It has to be noted that, despite of adding new features, the total number of lines of source code has only slightly increased during 10 months of development, thanks to much less boilerplate code. We could achieve a total unit test coverage of 22% by now. This is not considered to be sufficient yet, but at least a start and a motivation for the upcoming months.

All in all, the refactoring enabled us to develop new features in faster, more structured and testable way. In combination with the gathered experience by new applying patterns and technologies we feel very confident for the future development of the Engel & Völkers Property Search app.

Contact us now

Engel & Völkers Technology
Email
Back
Contact
Please enter your contact details here
Thank you for your request. We will contact you shortly.

Your Engel & Völkers Team
Salutation
  • Mr.
  • Mrs.
Send now

Follow us on social media


Array
(
[EUNDV] => Array
(
[67d842e2b887a402186a2820b1713d693dd854a5_csrf_offer-form] => MTM5MjE5NzU3NkJ4d29xancwTDVhZWFIRzEycXAxcW9SdElHdVBqMTdV
[67d842e2b887a402186a2820b1713d693dd854a5_csrf_contact-form] => MTM5MjE5NzU3NnlHcUR0Y2VlTXVPUndLMHZkMW9zMnRmRlgxaUcwaFVG
)
)