Проекты Android могут быть большими.
Иногда очень большой.
Один из наших проектов — новостное приложение, которое разрабатывается одновременно для двух альтернативных платформ: Android и FireOS от Amazon. Это позволяет расширить круг читателей новостей, ведь пользователи электронных книг Kindle Fire любят читать :).
Однако это также накладывает обязанность учитывать особенности каждой платформы.
Например, если на Android вы используете GCM для push-сообщений, то на FireOS для этого необходимо использовать Amazon AWS. То же самое касается систем покупок в приложениях: биллинг Google в приложении или покупка внутри приложения.
Но большой размер проекта! = большой размер приложения! В этой статье мы покажем, как мы используем альтернативные сборки для оптимизации приложений и их поддержки без ущерба для процесса разработки.
Что мы готовим?
При разработке мультиплатформенного приложения разработчик может иметь дело с кодом, который работает только на одной из платформ, но при запуске на других будет лежать мертвым грузом.
Помимо своего существования, такой код, скорее всего, принесет в проект все свои зависимости с такой же нагрузкой.
Само по себе это не очень хорошо, а учитывая специфику разработки под Android: «проблему 65к», лучше всего делать размер загружаемого файла как можно меньшим, с таким кодом обязательно нужно что-то делать.
И я хочу видеть бесконечные проверки ifAndroid() или ifAmazon() чуть реже, чем когда-либо.
Если вы опытный Android-разработчик, вы наверняка уже сталкивались с таким вариантом плагина Android Gradle, как ProductFlavor. Flavour (англ.
) – вкус, аромат. Эта опция позволяет создавать альтернативные сборки одного и того же проекта, включая в сборку файлы из разных каталогов, в зависимости от названия собираемой версии.
ProductFlavor часто используется для различного рода «брендинга» приложения, замены ресурсов (картинок, текстов, ссылок).
Другой распространенный случай — когда приложение делится на демо-версию и полную версию, потому что имя собранной версии автоматически попадает в поле класса BuildConfig.FLAVOR. Его значение позже можно будет проверить во время выполнения, и оно не позволит вам выполнять какие-либо действия в демо-версии.
Не только ресурсы, но и код можно разделить на разновидности.
Но нужно понимать, что код, используемый во вкусе1, никогда не сможет взаимодействовать с кодом из вкуса2. А код, лежащий в основном модуле, всегда может видеть только один вариант одновременно.
Все это означает, например, что вы не можете написать набор служебных методов в одном варианте и использовать их в другом.
Разделять код нужно грамотно и очень аккуратно, максимально изолированно, чтобы переключение альтернативных билдов происходило незаметно для основного модуля.
В этом нам очень поможет паттерн Dependency Injection. Следуя ему, мы оставим в основном модуле только общие интерфейсы, а конкретные реализации разложим на вкусы.
Давайте рассмотрим весь процесс на примере создания простого приложения для поиска репозиториев на GitHub.
Ингредиенты
Итак, нам нужно:- Этап с полем ввода, кнопкой и списком результатов (1 шт.).
- Класс для работы с Github Web API: его макет и реальная реализация (всего 2 шт.).
- Класс для кэширования результатов поиска: также реальные и макетные реализации (всего 2).
- Иконки, тексты, индикаторы выполнения – на ваш вкус.
view для презентации, .
models для моделей бизнес-логики и .
data для классов контент-провайдеров.
В пакете данных нам также понадобятся 2 пакета сервисов и хранилищ.
В результате вся конструкция должна выглядеть так:
Нам нужна только одна модель: «Репозиторий».
Вы можете хранить в нем все, что захотите, но мы хотели, чтобы в нем было описание, имя и htmlUrl. Теперь давайте определим интерфейс класса сервиса, который будет искать репозитории AppService:
Сразу создадим интерфейс для класса, кэширующего результаты поиска RepositoryStorage:public interface AppService { List<Repository> searchRepositories(String query); }
public interface RepositoryStorage {
void saveRepositories(String query, List<Repository> repositoryList);
List<Repository> getRepositories(String query);
}
Мы создадим и сохраним наш сервис и репозиторий внутри класса Application:
public class App extends Application {
private AppService appService;
private RepositoryStorage repositoryStorage;
public AppService getAppService() {
return appService;
}
public RepositoryStorage getRepositoryStorage() {
return repositoryStorage;
}
}
Для подготовительного этапа останется только создать сам экран и написать на нем, как получать и отображать результаты.
Для нашего демонстрационного приложения AsyncTask будет достаточно для выполнения фоновой работы, но вы всегда можете использовать свой любимый подход. public class MainActivity extends AppCompatActivity {
@Bind(R.id.actionSearchView) Button actionSearchView;
@Bind(R.id.recyclerView) RecyclerView recyclerView;
@Bind(R.id.searchQueryView) EditText searchQueryView;
@Bind(R.id.progressView) View progressView;
private SearchResultsAdapter adapter;
private AppService appService;
private SearchTask searchTask;
private RepositoryStorage repositoryStorage;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
appService = ((App) getApplication()).
getAppService(); repositoryStorage = ((App) getApplication()).
getRepositoryStorage(); recyclerView.setLayoutManager(new LinearLayoutManager(this)); adapter = new SearchResultsAdapter(); recyclerView.setAdapter(adapter); searchQueryView.setOnEditorActionListener(new TextView.OnEditorActionListener() { @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { querySearch(searchQueryView.getText().
toString()); return true; } }); } @OnClick(R.id.actionSearchView) void onActionSearchClicked() { querySearch(searchQueryView.getText().
toString()); } private void querySearch(String query) { if (TextUtils.isEmpty(query)) { return; } if (searchTask != null) { return; } InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); imm.hideSoftInputFromWindow(searchQueryView.getWindowToken(), 0); searchTask = new SearchTask(); searchTask.execute(query); showProgress(true); } private void showData(List<Repository> repositories) { searchTask = null; adapter.setData(repositories); } private void showProgress(boolean inProgress) { progressView.setVisibility(inProgress ? View.VISIBLE : View.GONE); actionSearchView.setEnabled(!inProgress); } private void showError(@Nullable ApiException exception) { searchTask = null; new AlertDialog.Builder(this) .
setMessage(exception != null ? exception.getMessage() : getString(R.string.unknown_error)) .
setTitle(R.string.error_title) .
show();
}
private class SearchTask extends AsyncTask<String, Void, SearchTaskResult> {
@Override
protected SearchTaskResult doInBackground(String. params) {
String q = params[0];
SearchTaskResult result = new SearchTaskResult();
try {
result.repositories = appService.searchRepositories(q);
repositoryStorage.saveRepositories(q, result.repositories);
} catch (ApiException e) {
result.exception = e;
//try to show some cached results
result.repositories = repositoryStorage.getRepositories(q);
}
return result;
}
@Override
protected void onPostExecute(SearchTaskResult result) {
if (result.exception != null) {
showError(result.exception);
}
showData(result.repositories);
showProgress(false);
}
}
private class SearchTaskResult {
List<Repository> repositories;
ApiException exception;
}
}
Реализацию адаптера и весь демо-проект в целом можно посмотреть по адресу GitHub .
На этом этапе наш проект уже можно скомпилировать и запустить, но в этом нет смысла, поскольку мы не написали никакой реализации наших интерфейсов.
Служба приложений И РепозиторийХранилище , так что сейчас самое время это сделать.
Добавление вкуса
Сначала вам нужно открыть build.gradle в основном модуле проекта и добавим к нему наши варианты.
Назовем их, например, "насмехаться" И «продюсер» productFlavors {
mock {}
prod {}
}
Их надо добавить в раздел андроид {.
} на том же уровне, что и типы сборки {.
} .
Обязательно нажмите кнопку «Синхронизировать проект с файлами Gradle».
Как только синхронизация завершится, в окне «Варианты сборки» появятся новые варианты.
Теперь давай выберем макетОтладка .
После того, как мы определили разновидности продуктов в проекте, мы можем создать для них одноименные каталоги на том же уровне, что и основной .
Файлы будут взяты из этих каталогов при сборке одной из версий.
Давайте добавим папку насмехаться , повторяя в нем структуру пакета услуги И хранилища :
Наконец, мы можем начать издеваться над нашими интерфейсами: public class AppServiceImpl implements AppService {
@Override
public List<Repository> searchRepositories(String query) {
if (query.equals("error")) {
throw new ApiException("Manual exception");
}
List<Repository> results = new ArrayList<>();
for (int i = 1; i <= 10; i++) {
results.add(new Repository("Mock description " + i, "Mock Repository " + i, " http://mock-repo-url "));
}
return results;
}
}
public class MockRepositoryStorage implements RepositoryStorage {
@Override
public void saveRepositories(String q, List<Repository> repositoryList) {}
@Override
public List<Repository> getRepositories(String q) {
return null;
}
}
Как видите, мок-сервис дает нам 10 очень информативных моделей репозитория, а мок-хранилище вообще ничего не делает. Мы инициализируем их в нашем классе App: @Override
public void onCreate() {
super.onCreate();
appService = new AppServiceImpl();
repositoryStorage = new MockRepositoryStorage();
}
Теперь наше приложение готово к сборке и запуску.
Теперь мы можем протестировать и настроить пользовательский интерфейс.
Теперь мы.
можем перейти к реальной реализации наших интерфейсов.
В окне «Варианты сборки» выберите параметр prodDebug и похоже на папку насмехаться давайте создадим папку подталкивать с теми же пакетами и классами:
Мы будем использовать Retrofit2 для сетевых запросов, он будет работать внутри нашей реализации AppServiceImpl: public class AppServiceImpl implements AppService {
private final RetroGithubService service;
public AppServiceImpl() {
service = new Retrofit.Builder()
.
baseUrl(" https://api.github.com/ ") .
addConverterFactory(GsonConverterFactory.create()) .
build().
create(RetroGithubService.class);
}
@Override
public List<Repository> searchRepositories(String query) {
Call<ApiRepositorySearchEntity> call = service.searchRepositories(query);
try {
Response<ApiRepositorySearchEntity> response = call.execute();
if (response.isSuccess()) {
ApiRepositorySearchEntity body = response.body();
List<Repository> results = new ArrayList<>();
RepositoryMapper mapper = new RepositoryMapper();
for (RepositoryEntity entity : body.items) {
results.add(mapper.map(entity));
}
return results;
} else {
throw new ApiException(response.message());
}
} catch (Exception e) {
throw new ApiException(e);
}
}
}
public interface RetroGithubService {
@GET("search/repositories")
Call<ApiRepositorySearchEntity> searchRepositories(@Query("q") String query);
}
Как видно из кода, мы создали еще несколько вспомогательных классов: *Сущность для анализа ответов и РепозиторийMapper для сопоставления ответов с моделью Репозиторий .
Обратите внимание, что все классы, связанные с реальной работой с сервером, такие как RepositoryEntity, RepositoryMapper, RetroGithubService, расположены в папке «prod» флейвора.
Это означает, что при сборке любого другого варианта, например макета, эти классы не попадут в результирующий apk-файл.
.
Внимательный читатель может заметить, что имя класса, реализующего реальную работу на сервере, и имя его мок-аналога совпадают: AppServiceImpl.java .
Это сделано специально и благодаря этому ничего не нужно менять в основном коде проекта, который находится в основной папке, при смене флейвора.
С выбранным вкусом насмехаться приложение видит класс AppServiceImpl, расположенный в папке макета, и не видит класс, расположенный в папке подталкивать .
Аналогично для выбранного вкуса.
подталкивать .
Не менее внимательный читатель может заметить, что мы назвали класс реализации кэша MockRepositoryХранилище и, возможно, опечатка.
Но нет, мы сделали это специально, чтобы показать один из вариантов того, как можно иметь разные имена классов реализации и даже разные конструкторы для каждого из них.
Хитрость по сути проста: мы создадим одноимённый класс для разных вкусов.
РепозиторийStorageBuilder , что, в зависимости от выбранного варианта, даст нам необходимую реализацию.
продуктВкус = продукт public class RepositoryStorageBuilder {
private int maxSize;
public RepositoryStorageBuilder setMaxSize(int maxSize) {
this.maxSize = maxSize;
return this;
}
public RepositoryStorage build() {
return new InMemoryRepositoryStorage(maxSize);
}
}
ProductFlavor = макет public class RepositoryStorageBuilder {
public RepositoryStorageBuilder setMaxSize(int maxSize) {
return this;
}
public RepositoryStorage build() {
return new MockRepositoryStorage();
}
}
И инициализация, общая для обоих в приложении: @Override
public void onCreate() {
super.onCreate();
.
repositoryStorage = new RepositoryStorageBuilder() .
setMaxSize(5) .
build();
}
Теперь «честную» реализацию работы можно считать выполненной, но если мы остановимся на этом, то не воспользуемся всей мощью ProductFlavor. Дело в том, что при честной реализации поиска используются библиотеки, которые заявлены в разделе зависимости , попадаем в нашу сборку вне зависимости от выбранного вкуса.
К счастью, мы можем указать для каждой зависимости отдельно, хотим ли мы видеть ее в сборке, добавив желаемое имя разновидности перед словом компиляция: dependencies {
compile fileTree(dir: 'libs', include: ['*.
jar'])
testCompile 'junit:junit:4.12'
prodCompile 'com.squareup.retrofit:retrofit:2.0.0-beta2'
prodCompile 'com.squareup.retrofit:converter-gson:2.0.0-beta2'
prodCompile 'com.google.code.gson:gson:2.5'
compile 'com.android.support:appcompat-v7:23.1.1'
compile 'com.android.support:recyclerview-v7:23.1.1'
compile 'com.jakewharton:butterknife:7.0.1'
}
Это не только уменьшит размер приложения, но и увеличит скорость его сборки, если зависимости действительно большие.
За что?
Зачем использовать этот подход к Dependency Injection, если есть Dagger2, Roboguice, если его даже можно написать вручную? Конечно, ключевое отличие этого подхода в том, что реализации определяются на этапе компиляции и в сборку включаются только те зависимости, которые будут реально использоваться, со всеми вытекающими отсюда последствиями.В то же время вы можете продолжать использовать свою любимую среду DI для определения зависимостей во время выполнения.
Правдивая история
Как мы упоминали вначале, один из наших проектов мы разрабатываем сразу для двух платформ: Android и Amazon FireOS. Эти операционные системы в принципе похожи друг на друга (конечно, мы все понимаем, кто на кого похож :)), но каждая из них имеет свою реализацию push-уведомлений и свой механизм встроенных покупок.Для этих и других различий между платформами мы, как и в демо-проекте, оставили в основном модуле только общие интерфейсы: та же регистрация устройства на сервере push-сообщений, тот же процесс покупки подписки и сохраняем конкретную платформу.
-зависимые реализации в соответствующих вариантах.
Мы уже давно пользуемся этим подходом и готовы поделиться впечатлениями от использования:
плюсы
- Исключает из полученной сборки весь код и его зависимости, которые никогда не будут использоваться ни на одной платформе.
- Сокращение времени сборки проекта, т.к.
собирается только выбранный (активный) вариант.
- Все преимущества использования IoC, отделение интерфейса от реализации и отсутствие уродливого ветвления в стиле isAndroid().
- Android Studio одновременно видит только выбранный вариант и его каталог.
Из-за этого автоматический рефакторинг, поиск по классам Java и поиск по ресурсам не работают полноценно.
Не работает в том смысле, что не применимо к неактивным ароматизаторам.
После рефакторинга иногда приходится переключаться между вариантами и повторять рефакторинг отдельно для каждого из них.
-
Драйвер Видеокарты: Так Чей Это Баг?
19 Oct, 24 -
Идеальная Система Управления Проектами
19 Oct, 24 -
Политика Безопасности Паролей В Carbonio
19 Oct, 24 -
Новый Ионный Векслер. Открытие
19 Oct, 24