Hi, my name is Andrew and I are working on Tinkoff and Tinkoff Junior apps for the Android platform. I want to talk about how we collect two similar applications from the same code base.
— , ̆ 14 . , (, ), , , (, ).
.
At the start of the project, we considered various options for its implementation and made a number of decisions. It immediately became apparent that the two applications (Tinkoff and Tinkoff Junior) would have a significant portion of the common code. We did not want to fork from the old application, and then copy the bug fixes and the new common functionality. To work with two applications at once, we considered three options: Gradle Flavors, Git Submodules, Gradle Modules.
Many of our developers have already tried using Flavors, plus we could apply multi-dimensional (multi-dimensional flavors) for use with existing flavors.
However, Flavors have one fatal flaw. Android Studio considers the code only the code of the active flavor - that is, what lies in the main folder and in the flavor folder. The rest of the code is considered text along with comments. This imposes restrictions on some studio tools: search for code usage, refactoring, and others.
Another implementation of our idea is to use the GIT submodules: put the common code into a separate repository and connect it as a submodule to two repositories with the code of a specific application.
This approach increases the complexity of working with project source code. Also, developers would still have to work with all three repositories to make edits when changing the API of a common module.
The last option is to switch to multi-module architecture. This approach is devoid of the disadvantages that the other two have. However, the transition to a multi-module architecture requires time spent on refactoring.
At the time we started working on Tinkoff Junior, we had two modules: a small API module describing working with the server, and a large monolithic application module that focused the main part of the project code.
As a result, we wanted to get two application modules: adult and junior, and a kind of common core- module. We have identified two options:
We had time in stock, and we decided to start developing the first version ( common module) with the condition to go to the quick version, when we’re out of time for refactoring.
In the end, what happened: we moved part of the project to the common module, and then turned the remaining application module into a library. As a result, we now have the following project structure:
We have modules with features, which allows us to distinguish between "adult", common or "child" code. However, the application module is still quite large, and now about half of the project is stored there.
In the documentation there is a simple instruction on how to turn an application into a library. It contains four simple points and, it would seem, there should be no difficulties:
build.gradle
fileapplicationId
from module configurationapply plugin: 'com.android.application'
with apply plugin: 'com.android.library'
However, the conversion took several days and the final diff was:
First of all, in libraries, resource identifiers are not constants . In libraries, as in applications, the R.java file is generated with a list of resource identifiers. And in libraries, identifier values ​​are not constant. Java does not allow making a switch by non-constant values, and all switches should be replaced with if-else.
// Application int id = view.getId(); switch(id) { case R.id.button1: action1(); break; case R.id.button2: action2(); break; } // Library int id = view.getId(); if (id == R.id.button1) { action1(); } else if (id == R.id.button2) { action2(); }
Next, we encountered a packet collision.
Suppose you have a library with package = com.example , and an application with package = com.example.app depends on this library. Then in the library the class com.example.R will be generated, and in the application, respectively, com.example.app.R . Now we will create com.example.MainActivity in the application, in which we will try to access the R-class. Without an explicit import, the R-class of the library will be used, in which the application resources are not specified, but only the library resources. However, Android Studio does not highlight the error, and when you try to go from the code to the resource, everything will be okay.
We use Dagger as a framework for dependency injection.
In each module containing activites, fragments and services, we have the usual interfaces, which describe inject- methods for these entities. In the application modules ( adult and junor ), the dagger component interfaces are inherited from these interfaces. In modules, we bring the components to the interfaces required for this module.
The development of our project greatly simplifies the use of multi-combining.
In one of the common modules, we define the interface. In each module of the application ( adult , junior ) we describe the implementation of this interface. Using the @Binds
annotation, @Binds
indicate to Dagger that every time, instead of an interface, it is necessary to inject its concrete implementation for a child or adult application. We also often collect a collection of interface implementations (Set or Map), while such implementations are described in different application modules.
For different purposes, we collect several variants of the application. The flavors described in the base module must also be described in the dependent modules. Also, for Android Studio to work correctly, you need to select compatible build options in all modules of the project.
In a short time we have implemented a new application. Now we ship the new functionality in two applications, writing it once.
At the same time, we spent some time refactoring, simultaneously reducing technical debt, and switched to multi-module architecture. Along the way, we faced restrictions from the Android SDK and Android Studio, which we successfully coped with.
Source: https://habr.com/ru/post/454128/
All Articles