Android (7)


Наследование зависимостей в Kodein

При внедрении зависимостей с Kodein в Activity, Fragment или т.п., мы переопределяем инстанс kodein. Важно иметь ввиду, что умолчанию наследования объявленных на родительском уровне объектов не происходит.
Если вы хотите получить доступ к этим объектам, вы можете использовать метод extend

Пример

Предположим, у меня есть инстанс настроек, объявленный на уровне приложения

class App : Application(), KodeinAware {

    override val kodein = Kodein {
        import(androidXContextTranslators)
        bind<ISettings>() with singleton { Settings(this@App) }
    }

Далее в активити мы его можем получить следующим образом:

class MainActivity : Activity(), KodeinAware {

    private val appKodein by kodein(App.instance)

    override val kodein = Kodein {
        // или так: 
        // extend(appApplication.appKodein(), allowOverride = true)
        extend(appKodein)
        // после строчки выше мы наследуем все от уровня App
        //...
    }

    private val settings: ISettings by instance() // корректная инициализация зависимости



Dependency Injection с Kodein

Если вы пишите Android-приложение на Kotlin и хотите использовать инъекцию зависимостей, взгляните на платформу Kodein. Она создана специально для Android и Kotlin. Не буду глубоко погружаться в паттерн dependency injection в этом посте. Если вы с ним не знакомы, рекомендую загуглить Dagger 2 для ознакомления.
Сегодня мы вместе с Вами взглянем на Kodein в действии на примере. Предположим, мы хотим занижектить singleton инстанс retrofit-а в каком-либо месте нашего приложения.

Сначала, добавим Kodein зависимости в gradle файл:

 implementation "org.kodein.di:kodein-di-generic-jvm:6.1.0"
 implementation "org.kodein.di:kodein-di-framework-android-core:6.1.0"
 implementation "org.kodein.di:kodein-di-framework-android-x:6.1.0"

Имплементим интерфейс KodeinAware в классе приложения Application. Будет необходимо переопределить kodein val и в нем инициализировать инстанс Retrofit-а:

class App : Application(), KodeinAware {

    override val kodein = Kodein {
        import(androidXContextTranslators)
        bind<Retrofit>() with singleton {
            Retrofit.Builder()
                .client(OkHttpClient().newBuilder().build())
                .baseUrl("htttp://example.com")
                .addCallAdapterFactory(CoroutineCallAdapterFactory())
                .build()
        }
    }
}

Все зависимости внутри блока Kodein формируются по следующему паттерну:

 bind<TYPE>() with

далее идет метод создания и использования объекта: «singleton», «provides» or «factory»
«singleton» говорит сам за себя, «provides» каждый раз генерит новый объект. Если нужен кастомный объект, тогда используйте фабрику: «factory«; как и «provides» он создает каждый раз новый объект, но с конструктором.

Теперь, когда мы объявили зависимости, давайте использовать их!
Заинжектим наш инстанс Retrofit-а в Activity/Fragment или, если вы используете MVP паттерн, то в presenter. Разница в наличии контекста: в презентере его нет по умолчанию. Попробуем сначала внедрить Retrofit в Activity.

И снова заимплементим интерфейс KodeinAware и получим инстанс:

class MainActivity : AppCompatActivity, KodeinAware{

    override val kodein: Kodein by kodein()

    private val retrofit: Retrofit by instance()
    ...
    // use retrofit

А теперь, если мы хотим использовать ретрофит в классе без Android контекста,
то нам нужно всего лишь передать туда Context для того, чтобы создать инстанс Kodein. Для этого я использовал applicationContext.
Добавьте следующие строки в класс Application:

    ...
    override fun onCreate() {
        super.onCreate()
        instance = this
    }
    
    companion object {
        lateinit var instance: App
            private set
    }
    ...

И теперь в презентере:

class MyPresenterImpl : IMyPresenter, KodeinAware {

    override val kodein by kodein(App.instance)

    override val kodeinContext = kcontext(App.instance)

    private val retrofit: Retrofit by instance()

    ...
    // use retrofit

Ссылка на официальную документацию




Адаптируем Android приложение под Wear OS

В марте 2018 Google произвела ребрендинг платформы Android Wear. Новое название — Wear OS. По статистике, на данный момент в мире умных часов лидирует watchOS от Apple, занимая 16,2 %, на долю Wear OS приходится меньше 7%, Однако, рынок умных часов еще новый и определенно следует ждать увеличения пользователей. Это значит, мы можем писать приложения, а также watchface для носимых гаджетов.
Рассмотрим пример создания модуля Wear OS для существующего приложения. У меня как раз есть подходящее — все тот же секундомер бодибилдера.

Создание модуля

Итак, создаем новый модуль в нашем проекте и выбираем wearable app

В build.gradle созданного модуля добавляем зависимости:

dependencies {
    implementation 'com.android.support:wear:27.1.1'
    implementation 'com.google.android.support:wearable:2.3.0'
    compileOnly 'com.google.android.wearable:wearable:2.3.0'
}

Wearable app может быть в трех состояниях:

  • Полностью независимо от мобильного преложения
  • Полу-независимо (телефон не требуется, но может предоставлять дополнительные функции)
  • Зависимым от мобильного приложения

Для первых двух, то есть для независимых, в Android Manifest для тега application надо прописать:

 <meta-data
 android:name="com.google.android.wearable.standalone"
 android:value="true" />

иначе приложение будет недоступно для пользователей с iPhone.
То есть, если этот флаг false, то приложение можно будет установить только на устройстве, связнным с телефоном, на котором установлен Play Store.

Помимо этого, для wear-модуля необходимо добавить В AndroidManifest.xml <uses-feature> со значением:

<manifest>
...
 <uses-feature android:name="android.hardware.type.watch" />
...
</manifest>


Packaging

Ранее, apk под Wear 1.0 были встраиваемыми в APKs для телефонов. Wear OS позволяет нам загружать apk скомпилированные под Wear 1.0 сразу в Play Store. Это позволяет уменьшить размер apk основного приложения и дает дополнительную гибкость в версионировании и дистрибуции нашего приложения. Подробнее здесь

Приложение должно быть самостоятельным apk, но это не значит, что мы не можем использовать код нашего приложения повторно. Просто выносим общую логику в библиотечный модуль, и импортируем его в модули app и wear. Таким образом, в модулях app и wear остается в основном только UI-часть приложения.

 

UI

Для создания красивых отзывчивых приложений в wear os есть свои UI-элементы . Например, BoxInsetLayout — layout, автоматически адпатирующийся под круглые и прямоугольные экраны.

BoxInsetLayout      BoxInsetLayout

Для создания списков в wear устройствах имеется аналог RecyclerView — WearableRecyclerView

WearableRecyclerView

 

 

Итог

Помните, что из-за аппаратных ограничений часы не предназначены для больших по функциональности приложений. Они идеально подходят для уведомлений, быстрых сообщений, или простых приложений. Имейте это ввиду, и публикуйте новые приложения или адаптированные под Wear OS версии существующих в Google Play!

Вот и все на сегодня!




A/B Тестирование с Firebase. Просто. Быстро.

Иногда нам нужно протестировать новую функциональность в приложении. Посмотреть, как повлияет, например, новый дизайн на retention и DAU, понравится он пользователям, или нет. Для этого используются AB-тесты.
В упрощенном виде это выглядит так: на бэкенд-стороне администратор выставляет какой части пользователей показывать вариант A (новый дизайн), какой части — вариант B (старый дизайн). Клиент-сторона запрашивает у сервера, какой вариант, А или B, он должен показать. И далее обычной if- или switch- проверкой запускается нужный код.
К счастью для многих мобильных разработчиков, многофункциональный сервис Firebase предоставляет в числе прочего возможность AB-тестов.
Чтобы создать эксперимент, идем в консоль Firebase.
Заходим в нужный проект. Если у Вас еще нет проекта, создайте его.
Для наглядности, я буду добавлять эксперимент по изменению цвета UI-элемента в своем проекте секундомера.
Итак, слева, в разделе Grow выбираем пункт A/B Testing и жмем Create experiment->Remote config. Вводим название эксперимента, описание, выбираем наше приложение и вводим процент целевой аудитории

Жмем далее. Можно добавить не только два, но и больше вариантов. В моем случае хватит двух Добавляем параметр red_circle_enabled. Выставляем значение этого параметра для созданных вариантов:

Остается выбрать цели, по которым мы будем отслеживать результативность тестов:

Жмем Review и запускаем эксперимент.

Теперь нам нужно имплементировать A/B логику на клиенте.

В Codelabs есть хорошее описание как это сделать на Android
Все, что нам нужно, лежит в пакете com.google.firebase.remoteconfig, поэтому добавляем зависимость в build.gradle:

 implementation 'com.google.firebase:firebase-config:15.0.0'

Затем инициализируем инстанс Remote Config там, где будет реализована логика A/B теста:

FirebaseRemoteConfig mFirebaseRemoteConfig = FirebaseRemoteConfig.getInstance();FirebaseRemoteConfigSettings firebaseRemoteConfigSettings =
 new FirebaseRemoteConfigSettings.Builder()
 .setDeveloperModeEnabled(true)
 .build();
// Инициализируем данные по умолчанию
Map<String, Object> defaultConfigMap = new HashMap<>();
defaultConfigMap.put("red_circle_enabled", false);
firebaseRemoteConfig.setConfigSettings(firebaseRemoteConfigSettings);
firebaseRemoteConfig.setDefaults(defaultConfigMap);
fetchConfig();

Метод fetchConfig запрашивает конфигурационные данные у Firebase

public void fetchConfig() {
    long cacheExpiration = 3600;
    if (firebaseRemoteConfig.getInfo().getConfigSettings()
            .isDeveloperModeEnabled()) {
        cacheExpiration = 0;
    }
    firebaseRemoteConfig.fetch(cacheExpiration)
            .addOnSuccessListener(aVoid -> {
                firebaseRemoteConfig.activateFetched();
                applyRedCircleTest();
            })
            .addOnFailureListener(e -> {
                Log.w(TAG, "Error fetching config: " +
                        e.getMessage());
                applyRedCircleTest();
            });
}

Метод applyRedCircleTest непосредственно реализует логику в зависимости от полученного значения A/B теста

private void applyRedCircleTest() {
    Boolean redCircleEnabled = firebaseRemoteConfig.getBoolean(Constants.AB_TEST_RED_CIRCLE);
    circleStrokeTextView.setRedCircleEnabled(redCircleEnabled.booleanValue());
}

Вот мы и добавили A/B тест в приложение за 15 минут с помощью Firebase!




RxJava. ConcatMap для зависимых Observable

Расскажу сегодня, как оператор concatMap помогает преобразовать имеющуюся последовательную зависимую синхронную цепочку методов в реактивную и многопоточную.
Предположим, мы запрашиваем булево значение, и выполняем по результату какое-то UI событие:

  ...
  boolean state = mPresenter.getBooleanState(context)
  if(state) {
      showViewA();
    } else {
      showViewB();
  }

Класс, предоставляющий данные, пусть это будет презентер, имеет зависимую структуру методов, выполнение второго зависит от результата выполнения первого:

public class MyPresenter<MyView> {

  ...

  private int getIntValue(Context context) {
      int retValue = someCalculationMethod(context);
      return retValue;
  }
  public boolean getBooleanState(Context context) {
      int intValue = getIntValue(context);
      return performSomeCalculationWith(intValue);
  }
}

Если метод someCalculationMethod выполняется моментально, то мы можем выполнить его в главном потоке. Если же это, например, запрос к серверу, и на нем можно ожидать задержку по времени, то следует вывести его в отдельный фоновый поток.
С помощью Rx это можно легко сделать, предварительно преобразовав методы getIntValue и getBooleanState в Observable:

  private Observable<Integer> getIntValue(Context context) {
      return Observable.fromCallable(() -> {
         int retValue = someCalculationMethod(context);
         return retValue;
      });
  }

  private Observable<Boolean> getBooleanState(Context context, Integer intValue) {
     return Observable.fromCallable(() -> {
        boolean retValue = performSomeCalculationWith(intValue);
        return retValue;
     });
  }

Как же получить Observable метода getBooleanState, не имея результатов getIntValue? Вся магия  по объединению двух Observable реализуется с помощью оператора concatMap

  private Observable<Boolean> rxGetBooleanState(Context context) {
      Observable integerObservable = getIntValue(context)
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread());

      Observable retObservable = integerObservable
        .concatMap(intValue -> getBooleanState(context, intValue));
      return retObservable;
  }

ConcatMap работает аналогично flatMap. Важная отличительная особенность — сохранение последовательности элементов.

Обратимся к документации

Returns a new Observable that emits items resulting from applying a function that you supply to each item emitted by the source Observable, where that function returns an Observable, and then emitting the items that result from concatenating those resulting Observables.

Посмотрим для наглядности на схемы работы оператора concatMap:

concatMap

concatMap scheme

concatMap создает одну цепочку, и выполняет на элементах исходного Observable заданную функцию (тоже Observable), сохраняя порядок элементов. Итоговый Observable содержит преобразованные элементы в соответствующей последовательности.

Теперь нам остается только вызвать наш метод из требуемого контекста:

Disposable disposable = mPresenter.rxGetBooleanState(this)
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe { state ->
            if (required) {
                showViewA();
            } else {
               showViewB();
            }
        }

Update
На самом деле, наиболее простой способ выполнить два зависимых Observable — это оператор .flatMap, как описано здесь: https://github.com/ReactiveX/RxJava/issues/442


getIntValue(context).flatMap( intValue -> {
   return getBooleanState(context, intValue)
})



RxJava. Использование оператора share

Сегодня расскажу про удобный реактивный подход в обработке ввода текста в EditText. В качестве примера возьмем поисковую строку в SearchView.

Задача

Поиск обычно занимает какое-то время. Неважно, ходим мы за данными на сервер, или читаем локальную базу. Пользователь может вводить символы в поисковую строку гораздо быстрее, чем будут отрабатывать такие запросы. Поэтому нам нужно установить какое-то пороговое значение временной задержки, чаще которой не вызывать поиск. Помимо этого, нам может потребоваться отображать в UI изменения в режиме реального времени, без задержек.

Решение

Без использования Rx в Android этого можно добиться с помощью обычного Runnable и Handler, что не очень удобно и громоздко. Благодаря библиотеке RxBinding мы можем применить известный оператор debounce и получить изящное решение в виде:

RxSearchView.queryTextChanges(searchView) 
 .debounce(500, TimeUnit.MILLISECONDS) // задержка в 500 мс 
 .subscribe(query -> mPresenter.searchRequest(query));

Теперь, независимо от скорости ввода текста, запросы на сервер будут уходить не чаще двух раз в секунду.
Однако, что делать, если нам потребуется делать обновления в UI при каждом вводе нового символа? Например, менять подсказку. При текущей реализации эти обновления UI будут происходить с той же задержкой в 500 мс, что совсем не user-friendly.

На наше счастье, в Rx есть возможность широковещательного уведомления нескольких подписчиков. Для этого нужно вызвать оператор .share() на нашем Observable и вуаля!

Observable<String> sharedTextChanges = RxSearchView.queryTextChages(searchViw).share()
 
sharedTextChanges 
 .debounce(500, TimeUnit.MILLISECONDS) // use debounce 
 .subscribe(query -> mPresenter.searchRequest(query)); 
 
sharedTextChanges 
 .subscribe(query -> mPresenter.updateUI(query));

Мы два раза подписались на Observable: с debounce и без него. Теперь UI обновляется в режиме реального времени, при каждом вводе символа, а запросы на сервер уходят не чаще раза в 500 мс.

Что под капотом?

Оператор .share() — это обертка на цепью операторов .publish().refcount(). Они позволяет “расшарить” испускаемые потоком объекты. Рассмотрим эти операторы подробнее.

Оператор .publish( ) — превращает Observable в ConnectableObservable.

rxjava publish operator scheme

“ConnectedObservable” — это такой Observable, который не выпускает данные, пока на нем не вызовут оператор .connect(). Таким образом, мы можем дождаться, пока все Subscriber-ы не подпишутся на Observable, и только затем испускать данные.

Оператор .refcount() берет на себя функции по управлению несколькими подписчиками. Согласно документации,

Returns an Observable that stays connected to this ConnectableObservable as long as there is at least one subscription to this ConnectableObservable.

RxJava operator refcount

refcount() — держит в памяти количество подписчиков на результирующий Observable и не отключается от источника ConnectedObservable пока все не отпишутся.

 

Вот и все. Очень эффективно и элегантно, как всегда с rx. Надеюсь, этот пост оказался полезным для вас!




Настройка proxy в Android Studio

На днях мне пришлось разворачивать Android Studio IDE на машине с корпоративным прокси. Тема не нова, и уже обсуждалась на stackoverflow и в блогах. Однако, эта задача не решается сходу — приходиться покопаться. Поэтому я решил создать пошаговую инструкцию по настройке IDE Android-разработчика в условиях прокси.

Все действия мы будем проводить на windows-машине. Для linux алгоритм будет аналогичный.

Итак, при первом запуске Android Studio предлагает настроить прокси

Жмем Setup Proxy, вводим адрес прокси-сервера и свои учетные данные:

Адрес прокси можно узнать с помощью команды (windows)

ipconfig /all | find /i "Dns Suffix"

Протестируйте соединение с помощью кнопки Check connection на этом же окне. Если все ок, идем дальше.

 

Все ок, идем дальше. В появившемся после запуска IDE окне прокси нужно снова прописать параметры proxy для http и https:


Эти же настройки можно прописать в файле gradle.properties:

systemProp.http.proxyPassword=<PASSWORD>
systemProp.http.proxyHost=<PROXY_URL>
systemProp.http.proxyUser=<LOGIN>
systemProp.http.proxyPort=<PORT>
systemProp.https.proxyPassword=<PASSWORD>
systemProp.https.proxyHost=<PROXY_URL>
systemProp.https.proxyUser=<LOGIN>
systemProp.https.proxyPort=<PORT>

Однако, имейте ввиду, что настройки прокси IDE перезаписывают настройки проекта.

Если сейчас попытаться собрать проект, то, скорее всего, сборка закончится неуспешно с ошибкой

SSLHandshakeException: sun.security.validator.ValidatorException: PKIX fix

Gradle пытается достучаться до серверов репозиториев, не имея сертификатов. Нам необходимо добавить их в хранилища. Для этого сначала добавляем в gradle.properties следующие строчки:

systemProp.javax.net.ssl.trustStore=<ANDROID STUDIO PATH>\\jre\\lib\\security\\cacerts
systemProp.javax.net.ssl.trustStorePassword=changeit

Здесь мы указываем путь и пароль к хранилищу сертификатов. Пароль по умолчанию — changeit. Если вы не меняли его, он остался таким же.

Как же добавить сертификаты в хранилище?

Установка сертификатов

При запуске проекте IDE предлагает принять сертификаты. Их следует принять, однако это не поможет автоматически. Нам необходимо импортировать сертификаты в хранилище сертификатов cacerts IDE и JVM. Для этого необходимо выполнить следующие шаги:

  1. Скачать сертификат. Сделать это можно с помощью браузера или openssl
  2. Импортировать сертификат в в хранилища с помощью keytool

Чтобы импортировать загруженный на шаге 1 сертификат, на Windows-машине необходимо запустить командную строку от администратора и выполнить:

keytool -import -alias <alias> -keystore C:\Progra~1\Android\Android Studio3.0\jre\jre\lib\security\cacerts -file <path_to/certificate_file>

Также, необходимо добавить этот сертификат в другие хранилища cacerts (JVM и Android Studio):

keytool -import -alias <alias> -keystore <path_to_studio>\.AndroidStudio3.0\system\tasks\cacerts -file <path_to/certificate_file>
 keytool -import -alias <alias> -keystore "C:\Progra~1\Java\jre_V.V.V\lib\security\cacerts" <path_to/certificate_file>

альтернативно, вместо того, чтобы добавлять, можно копировать сертификаты между хранилищами с помощью команды:

keytool -importkeystore -srckeystore <path_to_studio>\.AndroidStudio3.0\system\tasks\cacerts -destkeystore C:\Progra~1\Java\jre_V.V.V\lib\security\cacerts -v
 password changeit

После импорта сертификатов почистите кэш gradle в папке C:\Users\<User>\.gradle и перезагрузите систему. Если при попытке обратиться к хранилищу cacerts IDE выдает ошибку Access denied, запустите Android Studio от администратора.

Запускаем сборку… Проект успешно собирается!

В случае, если импорт сертификатов не помогает, можно заменить адрес загрузки репозиториев с секьюрного https на обычный http:

jcenter {
 url "http://jcenter.bintray.com/"
 }

Git

Помимо gradle, проблемы могут возникнуть и с системой контроля версий. В случае с git необходимо необходимо добавить в глобальные настройки git параметры proxy. Для этого выполнить команду:

 git config --global http.proxy http[s]://userName:password@proxyaddress:port

Если при попытке при попытке pull/push из/в GitLab возникает ошибка

SSL certificate problem: self signed certificate in certificate chain

то следует выполнить следующую команду от администратора:

 git config --system http.sslCAPath <path_to_studio>/.AndroidStudio<v.No>/system/tasks/cacerts

Для возможности push/pull через IDE Android Studio необходимо в настройках  Settings->Version Control->Git в пункте SSH executable указать Native

На этом все, можно работать. Надеюсь, статья оказалась полезной для вас. Буду рад вашим мнениям и комментариям!