FREEDOM LOGIC

Информация должна быть свободной

Путь к RxJava

| Comments

Команда текущего проекта, для которой я недавно делал подборку материалов по монадам, в рекордные сроки освоила эту концепцию и захотела еще. В этот раз им понадобилось изучить RxJava и меня опять попросили сделать подборку хороших материалов на эту тему. Не курить, пристегнуть ремни!

С чего начать?

Для понимания основной концепции и ответа на вопрос “А оно мне надо?” я бы посоветовал вот этот цикл статей Grokking RxJava от Dan Lew:

Часто бывает, что люди, начинающие изучать RxJava - лопатят кучу мануалов, статей, видео, спрашивают у “гуру”, но понимание не приходит. Специально для них была написана вот эта статья: The introduction to Reactive Programming you’ve been missing. Ну, а если по какой-то причине просветление и к этому моменту не наступило, то настоятельно рекомендую посмотреть отличный доклад от нашего соотечественника Артема Зинатулина - RxJava доставляет.

В качестве закрепления знаний рекомендую так же прочесть цикл статей Crunching RxAndroid от Roberto Orgiu:

Для успешного старта этих материалов должно хватить.

Продвинутые материалы

Материалы из этого раздела описывают уже более частные вещи и предназначены для более глубокого изучения RxJava

После всего этого можно пользоваться вот этой “шпаргалкой”: Awesome RxJava.

Остались вопросы? Хочется еще больше RxJava? Все здесь.

Удачного всем изучения и чистых функций!

Монады, функторы и прочая эзотерика

| Comments

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

Все приведенные выше комментарии о статьях являются только моим мннеием (возможно неправильным) и мне жаль, если я оскорбил чьи-то чувства.

Smali: Путь джедая. Часть первая.

| Comments

Давно хотел стартануть такой цикл статей, но не мог найти на это времени. Теперь оно нашлось и я его наконец-то запускаю. Продумывать заранее список тем мне лень, поэтому буду писать как пишется. Постараюсь конечно соблюсти порядок “от простого к сложному”, но ничего не обещаю =). О чем вообще пойдет речь? О smali конечно же. Понимание байткода Dalvik VM может пригодиться как при исследовании приложений, так и для оптимизации своих. Плюс понимание того, во что превратится ваш код после компиляции, даст вам +10 к качеству кода +5 к уважению среди других программистов и, как следствие, +100500 к ЧСВ ;). Сразу хочу оговориться по поводу уровня подготовки читателей. От вас потребуется понимание самых основ, потому что объяснять откуда есть пошел байткод, мне опять же лень. Необходимый теоретический минимум вы найдете в конце этой статьи.

Готовим инструментарий

Чтобы общение с байткодом проходило весело и приятно, нужно подготовить весь необходимый инструментарий. Нам понадобится:

Это все, что понадобится на первых парах. Весь остальной чудо-софт, который предназначен для разбора APK файлов и т.п. буду вводить по необходимости.

Hello smali!

Уже не помню кто придумал начинать изучение языков с hello world, но традицию продолжу. Писать байткод с нуля мы пока не будем, поработаем с уже сгенерированным. Стартуем студию, создаем новый проект с Empty Activity, открываем MainActivity и приводим метод onCreate к следующему виду:

1
2
3
4
5
6
7
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    Toast.makeText(this, "Hello Smali!", Toast.LENGTH_SHORT).show();
}

А теперь посмотрим какой байткод из этого получится. Для этого воспользуемся установленным ранее плагином java2smali.

В результате будет создан файл MainActivity.smali

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
.class public Lcom/example/fi5t/myapplication/MainActivity;
.super Landroid/support/v7/app/AppCompatActivity;
.source "MainActivity.java"


# direct methods
.method public constructor <init>()V
    .registers 1

    .prologue
    .line 7
    invoke-direct {p0}, Landroid/support/v7/app/AppCompatActivity;-><init>()V

    return-void
.end method


# virtual methods
.method protected onCreate(Landroid/os/Bundle;)V
    .registers 4
    .param p1, "savedInstanceState"    # Landroid/os/Bundle;

    .prologue
    .line 11
    invoke-super {p0, p1}, Landroid/support/v7/app/AppCompatActivity;->onCreate(Landroid/os/Bundle;)V

    .line 12
    const v0, 0x7f040019

    invoke-virtual {p0, v0}, Lcom/example/fi5t/myapplication/MainActivity;->setContentView(I)V

    .line 14
    const-string v0, "Hello Smali!"

    const/4 v1, 0x0

    invoke-static {p0, v0, v1}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;

    move-result-object v0

    invoke-virtual {v0}, Landroid/widget/Toast;->show()V

    .line 15
    return-void
.end method

Кода стало несколько больше =). В целом это все еще читаемо и “натренированный” глаз способен воспринимать такой код не хуже чем кода на Java. Основное отличие этого листинга от исходного файла в том, что здесь мы видим конструктор по умолчанию, который в явном виде отсутствует в исходном классе. Самое “вкусное” находится в методе onCreate, поэтому с ним и будем разбираться.

.method protected onCreate(Landroid/os/Bundle;)V - в целом выглядит достаточно понятно за исключением букв L и V. Lfully/qualified/Name; - это просто специальный синтаксис для квалифицированного имени класса, а V говорит нам о том, что тип возвращаемого значения этого метода будет void.

Далее идет не очень понятное рядовому программисту (или программисту плохо знакомому с языком ассемблера) слово .registers. Циферка справа от него показывает сколько всего регистров будет использовано в этом методе. Методом пристального взгляда убеждаемся в том, что регистров используется действительно 4: v0, v1, p0, p1. Поскольку Dalvik является регистровой машиной, то эти самые регистры используются для хранения параметров методов.

Следующая строчка хоть и понятна где-то на подсознательном уровне, но все же требует некоторых пояснений. .param p1, "savedInstanceState" # Landroid/os/Bundle; - эта директива “связывает”(associate) параметр метода со специальным регистром p1, что позволит далее работать уже с регистром. А нехитрый комментарий # Landroid/os/Bundle; намекает нам на тип этого параметра.

.prologue как и .line, являются отладочной информацией и живым людям чаще всего не нужны. Но для формализации повествования, скажу, что .prologue это специальная метка, которая говорит, что в этом месте заканчивается “пролог” этого метода (аналог DBG_SET_PROLOGUE_END). .line указывает отладчику на какой строке исходного файла находиться эта конструкция. Данное утверждение легко проверить посмотрев сначала на то, что находится под .line 11, а потом взглянув на исходный класс.

invoke-super {p0, p1}, Landroid/support/v7/app/AppCompatActivity;->onCreate(Landroid/os/Bundle;)V - является вызовом метода onCreate родительского класса. Тут фигурирует уже знакомый нам p1 и незнакомый p0, который является указателем на ту activity у которой эти методы вызываются. Внимательный читатель уже понял откуда он взялся. Невнимательным покажу: invoke-direct {p0}, Landroid/support/v7/app/AppCompatActivity;-><init>()V. Все гениальное просто =)

const v0, 0x7f040019 - тут в работу включается еще один регистр в который мы пишем некое шестнадцатеричное значение. Что это за значение, можно понять если взглянуть на R.java в каталоге build

1
2
3
4
5
6
7
public static final class layout {
  ...
    public static final int abc_select_dialog_material=0x7f040018;
    public static final int activity_main=0x7f040019;
    public static final int notification_media_action=0x7f04001a;
      ...
}

invoke-virtual {p0, v0}, Lcom/example/fi5t/myapplication/MainActivity;->setContentView(I)V - вызываем метод setContentView, в который передаем нашу константу v0. Единственным непонятным местом здесь может быть буква I, которая означает, что метод принимает целое число в качестве параметра.

const-string v0, "Hello Smali!" - просто строковая константа, которая переиспользует регистр v0

const/4 v1, 0x0 - берет 4 бита от переданной константы и кладет в регистр. Тут уже поработал оптимизатор, т.к. нет смысла для 0 (а именно такое значение у константы Toast.LENGTH_SHORT) брать больше битов чем необходимо.

invoke-static {p0, v0, v1}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast; - вызов статического метода makeText которому передается 3 параметра. Все предельно просто.

move-result-object v0 - в ход опять идет магия байткода и помещает результат предыдущей операции в регистр v0 (регистры не имеют типов, поэтому пихать туда можно все что угодно)

invoke-virtual {v0}, Landroid/widget/Toast;->show()V - и в качестве вишенки на торте, вызываем метод show() у получившегося на предыдущем шаге объекта.

Далее идет никому не интересный формализм в виде вызова return-void и метки говорящей о том, что метод закончился.

Подводим итоги

Как вы смогли заметить - не так страшен байткод, как его генераторы. Все это вполне читаемо и при грамотном подходе (RTFM), не вызывает никаких трудностей в понимании. Конечно поначалу это кажется какой-то кашей, но со временем приходит осознание красоты и лаконичности таких листингов. Но это достигается только практикой. Как и обещал, привожу ссылки на полезные источники:

Мой взгляд на Code Review

| Comments

Сейчас чаще чем раньше приходится проводить code review, поэтому было решено этот процесс оптимизировать и задокументировать полученный результат. Существует куча разных способов от просмотра diff-ов в git до использования специального софта вроде Upsource и ему подобных. И все это хорошо, но не сильно для меня удобно. Поэтому и был придуман этот способ.

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

1
2
// REVIEW: Изменить имя функции на более говорящее
fun doSomething() = 1 + 2

Но все это не имело бы никакого смысла, если бы не поддержка IDE. Чтобы все выглядело красиво нам нужно настроить специальные фильтры в TODO toolbar.

Открываем окно настройки фильтров

И в появившемся окне, добавить следующие значения

Для того, чтобы привнести еще большее удобство в этот процесс, добавить live template для быстрого добавления этого специального комментария.

Вот и все. Путем нехитрых манипуляций мы получаем достаточно удобную систему для проведения code review не вылезая из любимой IDE.

Изменение Dev ветки по умолчанию в SourceTree + GitFlow

| Comments

Понадобилось мне тут динамично менять dev ветку в SourceTree при использовании gitflow. Руками это делать лениво, поэтому был написан скрипт, которым и спешу поделиться.

Чтобы воспользоваться этой прелестью, в SourceTree необходимо добавить новый action. Для этого нужно зайти в Preference->Custom Actions и указать в полях вот такие значения

1
2
3
Menu Caption: Set default branch
Script to run: /usr/bin/python
Parameters: /tmp/set_dev_branch.py.py $REPO

Теперь для того чтобы вызвать этот скрипт, нужно запустить этот action из главного меню.

1
Actions->Custom Actions->Set default branch

Так же можно повесить это действие на горячую клавишу.

Реализация ActionMode

| Comments

/!\ После долгой летаргии, блог переходит в режим коротких и по возможности частых заметок “по делу” /!\

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

Подготовка ресурсов

Для начала создадим меню контекстных действий

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
    <item
        android:id="@+id/stop_the_earth"
        app:showAsAction="always"
        android:orderInCategory="100"
        android:title="Остановить землю" />
</menu>

И подправить стили в styles.xml. В основную тему добавляем следующие строки

1
2
3
4
        <item name="windowActionModeOverlay">true</item>
        <item name="actionModeStyle">@style/ActionModeStyle</item>
        <item name="android:actionModeBackground">@drawable/background_top</item>
        <item name="android:actionModeCloseDrawable">@android:drawable/ic_menu_close_clear_cancel</item>

Стоит обратить внимание, что windowActionModeOverlay должен быть без префикса android: иначе чуда не произойдет и рулить видимостью ActionBar-а придется вручную. Фон может быть задан сплошным цветом, но для красоты я его сделал таким:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="utf-8"?>
<inset xmlns:android="http://schemas.android.com/apk/res/android"
    android:insetBottom="0dp"
    android:insetLeft="-5dp"
    android:insetRight="-5dp"
    android:insetTop="-5dp" >

    <shape>
        <solid android:color="@color/colorPrimary" />

        <stroke
            android:width="1dp"
            android:color="@color/colorAccent" />
    </shape>

</inset>

Аттрибут actionModeStyle позволяет нам поиграть со стилями текста, добраться другим способом к тексту на панельке ActionMode-а нельзя

1
2
3
4
5
6
7
    <style name="ActionModeStyle" parent="Widget.AppCompat.ActionMode">
        <item name="titleTextStyle">@style/ActionTextStyle</item>
    </style>

    <style name="ActionTextStyle" parent="TextAppearance.AppCompat.Widget.ActionBar.Title">
        <item name="android:textColor">@color/colorAccent</item>
    </style>

Вот что получилось в итоге:

Теперь напишем немного кода чтобы вдохнуть жизнь в этот бездушный дизайн. Для начала в нужную activity/fragment/etc (нужное подчеркнуть) добавляем 2 поля.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
private ActionMode mActionMode;
private final ActionMode.Callback mActionModeCallback = new ActionMode.Callback() {

    @Override
    public boolean onCreateActionMode(final ActionMode mode, final Menu menu) {
        final MenuInflater inflater = mode.getMenuInflater();
        inflater.inflate(R.menu.photo_gallery_edit_menu, menu);

        mode.setTitle("ActionMode example");

        return true;
    }

    @Override
    public boolean onPrepareActionMode(final ActionMode mode, final Menu menu) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            getWindow().setStatusBarColor(getResources().getColor(R.color.colorAccent));
        }
        return true;
    }

    @Override
    public boolean onActionItemClicked(final ActionMode mode, final MenuItem item) {
        if (item.getItemId() == R.id.stop_the_earth) {
            mode.finish();
            return true;
        } else {
            return false;
        }
    }

    @Override
    public void onDestroyActionMode(final ActionMode mode) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            getWindow().setStatusBarColor(getResources().getColor(R.color.colorPrimaryDark));
        }

        mActionMode = null;
    }
};

Существует примерно бесконечное количество способов совершить вышеописанные действия, но у меня так. Все эти вырвиглазные жонглирования с цветом статусбара и прочие дизайнерские “изыски” призваны показать спектр возможностей и не являются обязательными. После чего, в метод onOptionsItemSelected(MenuItem item) добавляем такой код:

1
2
3
4
if (mActionMode != null) {
  return false;
}
mActionMode = startActionMode(mActionModeCallback);

Запускаем, радуемся.

Kotlin, или Туда и обратно

| Comments

Спустя 2.5 проекта написанных на чистом (99%) Kotlin, у меня накопилась некая критическая масса впечатлений о которых я и хотел бы написать.

Для тех, кто не в курсе (хотя зачем вы тогда это читаете?) - kotlin это новый, модный, молодежный язык от JetBrains. Он пришел в этот мир что бы решить многие проблемы Java, потеснить Scala и стать эдаким Swift-ом для бедных Android программистов. Получилось у них или нет пока не совсем понятно. Релиз языка должен быть в этом году, а пока что он имеет массу проблем при написании чего-то более сложного чем println(“Hello Kotlin!”). При этом конечно несправедливо было бы говорить что язык сырой и ни на что не годный. Это не так. На нем уже можно смело писать production-код, но надо понимать, что порою это займет у вас больше времени чем написание аналогичного кода на Java.

Основной проблемой, с которой я столкнулся при написании kotlin-кода под Android, была очень глючная отладка. Работает она из рук вон плохо. На эту тему даже был заведен тикет. Суть проблемы в том, что при попытке вычислить выражение или добавить переменную в Watches возникает вот такая ошибка

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

Отдельным пунктом хотелось бы отметить жуткие тормоза при работе с файлами в 3к и более строк. Умники могут щас начать меня тыкать носом в книги Фаулера и призывать писать “нормальный код”, но это не убирает проблему. Аналогичный файл на Java переваривается студией легко и приятно.

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

Как было дело в Java

1
2
3
4
5
6
7
8
9
10
11
public class GcmBroadcastReceiver extends WakefulBroadcastReceiver {

  @Override
  public void onReceive(Context context, Intent intent) {
      ComponentName comp = new ComponentName(context.getPackageName(),
      GcmIntentService.class.getName());

      startWakefulService(context, (intent.setComponent(comp)));
      setResultCode(Activity.RESULT_OK);
  }
}

После чего можно было спокойно делать вот такие вызовы

1
GcmBroadcastReceiver.completeWakefulIntent(intent);

В Kotlin так делать нельзя. Может быть проблема не в языке, а во мне и я совсем не знаю и не понимаю Kotlin, но победить я это смог только написав костыль c делегатами

1
2
3
4
5
6
7
public class GcmBroadcastReceiver: WakefulBroadcastReceiver() {
  class object {
      fun startWakefulService(context: Context, intent: Intent): ComponentName = WakefulBroadcastReceiver.startWakefulService(context, intent)
      fun completeWakefulIntent(intent: Intent): Boolean = WakefulBroadcastReceiver.completeWakefulIntent(intent)
  }
...
}

Конечно же идея с костялями пришла не сразу. Хотелось сделать “по-человечески” и я сконвертировал класс с нужными мне вызовами и оно сконвертировалось в GcmBroadcastReceiver.completeWakefulIntent(intent). И конечно же отказалось потом работать. Я понимаю, что конвертер не учитывает контекст, но все же я надеялся на его помощь =) К слову говоря, он все же чаще помогает нежели вредить. Конечно код, который он генерирует использовать в чистом виде нельзя ибо это ужас дикий. Но вот при изучении языка он очень сильно помогает, показывая как должен выглядеть тот или иной Java-код на Kotlin.

Но и это еще не все. В какой-то момент у меня сглючил Kotlin-плагин и полностью потухла подсветка Kotlin-синтаксиса. При этом в создаваемых рядом Java-файла она была, а в Kotlin-файлах нет. Что я только не делал… Не помогала даже переустановка Kotlin-плагина(!). В результате были вычищены все кэши студии, переустановлено все, что только можно было переустановить и тогда подсветка вернулась.

В целом за весь рабочий день мое отношение к Kotlin-у меняется раз 10. Вроде пишешь все круто и тут он подкидывает “сюрприз” от которого диву даешься и хочется обратно в Java. Короче пока что Kotlin ведет себя как капризная девка и поделать с этим ничего нельзя. Будем ждать релиза и надеяться на лучшее.

Tips and Tricks for Android Material Support Library

| Comments

Оригинал статьи

Приложения в “материальном дизайне” (Material designed) являются сейчас последним писком моды. Прошли времена дизайна Gingerbread и если ваше приложение выполнено в дизайне Holo то вы уже в числе отстающих. Все люди (включая вашего менеджера проектов) хотят новенькое “материальное” приложение - но что насчет тех людей, которые все еще используют старые Android устройства? Что бы не оставлять их позади прогресса на помощь приходит Material Support Library.

Установка

Для того что бы использовать Material Support Library вы должны обновить ваш саппорт репозиторий и добавить зависимости к вашему приложению. Если вам нужна помощь с этим, то лучший гайд, который я нашел, пришел прямо от самого Криса Бейнса (Chris Banes).

Добавьте саппорт библиотеку в ваш build.gradle

1
2
3
dependencies {
    compile 'com.android.support:appcompat-v7:21.0.+'
}

Смените тему вашего приложения на тему Theme.Appcompat

1
2
3
4
5
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
  <item name="colorPrimary">@color/primary</item>
  <item name="colorPrimaryDark">@color/primary_dark</item>
  <item name="colorAccent">@color/accent</item>
</style>

Измените ваши Activites на ActionBarActivites. ActionBarActivity является подклассом FragmentActivity поэтому не должно быть никаких проблем если вы уже используете FragmentActivities

1
2
3
public class BaseActionBarActivity extends ActionBarActivity {
  // Implementation
}

Вот и все! Теперь вы используете Material Support Library. Попробуйте запустить ваше приложение чтобы узреть всю “материальную” добродетель.

Упс. Без паники, мы можем это исправить.

getActionBar() -> getSupportActionBar()

или “Почему мое приложение упало при запуске?”.

При вызове getActionBar() у ActionBarActivity вы получите null объект. Это является причиной кучи NullPointerExceptions в существующем коде, так что вместо этого вызова вам нужно использовать getSupportActionBar(). К сожалению, это означает, что вы должны испольовать приведение getActivity() в ваших фрагментах:

1
2
3
4
5
6
7
if (getActivity() instanceof ActionBarActivity) {
  ActionBar actionBar = ((ActionBarActivity) getActivity()).getSupportActionBar();

  if (actionBar != null) {
  // Do something with the ActionBar
  }
}

android:showAsAction -> app:showAsAction

или “Все дело в выпадающем меню”.

Если вы откроете один из ваших файлов menu.xml, то увидите, что showAsAction подчеркнуто красным. Исправить это можно очень просто. Достаточно изменить “android” на “app” a добавить пространство имен “app”. Все потому что showAsAction был представлен в API 11 и саппорт библиотека использует свое собственное определение showAsAction.

1
2
3
4
5
6
7
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <item android:id="@+id/menu_compose"
        android:icon="@drawable/header_icon_compose_normal"
        app:showAsAction="ifRoom"
        android:title="@string/menu_action_compose" />
</menu>

PreferenceActivity -> ActionBarActivity + PreferenceFragment

или “Где находится ActionBar в моем PreferenceActivity?”

У нас в Hootsuite (речь идет о приложении автора оригинальной статьи. примечание переводчика) мы используем несколько устаревшую PreferenceActivity. Это пережиток тех времен, когда мы поддержиали API 8+, но с тех пор мы перешли на поддержку API 14+ и можем использовать PreferenceFragment. Это позвоялет нам использовать любой тип activity, а так же общую тему AppCompat, которую мы установили ранее.

1
2
3
4
5
6
7
8
9
10
11
12
13
public class PreferencesActivity extends BaseActionBarActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_preferences);
        if (savedInstanceState == null) {
            getFragmentManager().beginTransaction()
                    .add(R.id.container, new PreferencesFragment())
                    .commit();
        }
    }
}

Все это довольно просто настроить, но вы должны использовать getFragmentManager() вместо getSupportFragmentManager() и PreferenceFragment должен наследовать android.app.Fragment, а не android.support.v4.app.Fragment. Если вы все еще поддерживаете Gingerbread, я бы порекомендовал использовать custom layout или использовать один из доступных портов(backports)

DrawerLayout + Toolbar

или “Сделайте Романа Нурика (Roman Nurik) счастливым”.

Посмотрите Material Design Checklist и вы заметите, что вам нужно изменить существующий drawer layout что бы он был над панелью инструментов и под панелью состояния. Есть полезный пост на StackOverflow объясняющий как это реализовать.

Я обнаружил, что лучше иметь отдельную тему которую вы будете использовать в вашей DrawerActivity. Вы можете использовать обычную тему с ActionBar и тему NoTitleBar если вы вручную добавляете Toolbar:

1
2
3
4
5
6
7
8
9
10
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
  <item name="colorPrimary">@color/primary</item>
  <item name="colorPrimaryDark">@color/primary_dark</item>
  <item name="colorAccent">@color/accent</item>
</style>

<style name="AppTheme.NoTitleBar">
  <item name="windowActionBar">false</item>
  <item name="android:windowNoTitle">true</item>
</style>

Так же я обнаружил, что большинство приложений используют тему Light.DarkActionBar. При использовании этой темы вам нужно будет добавить некоторые атрибуты к Toolbar. Добавление app:theme позволит вам установить Dark версию ActionBar-а, а app:popupTheme позволит установить стиль всплывающих окон таким же как выпадающее меню.

1
2
3
4
5
6
7
8
<android.support.v7.widget.Toolbar
    android:id="@+id/toolbar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="?attr/colorPrimary"
    android:minHeight="?attr/actionBarSize"
    app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
    app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />

Для того что бы использовать нечто большее чем просто сплошной цвет под панелью статуса вам нужен custom layout. Когда вы устанавливаете fitsSystemWindows в true, fitSystemWindows() будет вызван в вашей View. Вам нужно переопределить это в custom ViewGroup и нарисовать полупрозрачное наложение, пример этого эффекта можно увидеть в Google IO app.

Перевод вашего приложения на использование Material Support Libraty может потребовать определенное количество времени и сил, но в конце-концов это стоит того. Приложение выполненное в современном стиле будет выделяться на фоне остальных приложений в Play Store и обеспечит наилучший пользовательский опыт (user experience). Увидеть обновленное в материальном стиле Hootsuite Android App можно будет в ближайшее время!

Style Resources

или “Помогите! Я не дизайнер!”

Anything and everything you need to know about colours, style, and keylines
Hundreds of Google-created icons
Easy creation of Material colors.xml
Many more examples to give you ideas

Google Play Services and DEX Method Limits

| Comments

Оригинал статьи

Ограничением для некоторых Android приложений является общее количество методов, которое может содержать скомпилированный .dex файл. Это ограничение на 16 бит или 65,535 значений.

Когда вы подключаете внешнюю библиотеку в ваше прилоежние, вы получаете все их методы в вашем .dex файле. Большие библиотеки API, такие как Google Play сервисы, “сожрут” этот лимит очень быстро.

Узнать больше об этой проблеме и способах ее обхода с помощью билд-системы Adnroid Studio 1.0 здесь

Кроме того, начиная с версии 6.5 сервисы Google Play могут подключаться в виде нескольких необольших клиентских библиотек, так что в ваш .dex файл попадут только те методы API, которые вы действительно используете.

До версии 6.5, в вашем build.gralde файле обычно была такая строчка:

1
compile 'com.google.android.gms:play-services:6.5.87'

Начиная с версии Google Play сервисов 6.5 вы можете выбирать из ряда отдельных API, какие из них имеют свои собственные подключаемые файлы, можно посмотреть в документации. Например если вы хотите использовать только карты, то вместо подключения всей библиотеки можно подключить только карты:

1
compile 'com.google.android.gms:play-services-maps:6.5.87'

Заметьте, что вместе с этим будут транзитивно подключены “базовые” библиотеки которые используют все API. Вы так же можете подключить их самостоятельно с помощью следующей строки:

1
compile 'com.google.android.gms:play-services-base:6.5.87'

Полный список API представлен ниже. Подробнее можно почитать на Android Developer site

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
com.google.android.gms:play-services-base:6.5.87
com.google.android.gms:play-services-ads:6.5.87
com.google.android.gms:play-services-appindexing:6.5.87
com.google.android.gms:play-services-maps:6.5.87
com.google.android.gms:play-services-location:6.5.87
com.google.android.gms:play-services-fitness:6.5.87
com.google.android.gms:play-services-panorama:6.5.87
com.google.android.gms:play-services-drive:6.5.87
com.google.android.gms:play-services-games:6.5.87
com.google.android.gms:play-services-wallet:6.5.87
com.google.android.gms:play-services-identity:6.5.87
com.google.android.gms:play-services-cast:6.5.87
com.google.android.gms:play-services-plus:6.5.87
com.google.android.gms:play-services-appstate:6.5.87
com.google.android.gms:play-services-wearable:6.5.87
com.google.android.gms:play-services-all-wear:6.5.87

Заметьте: на момент написания, корректной для использования версией является 6.5.87. Т.к. это очень детальный(granular) номер версии, то обновляться он будет очень часто, поэтому убедитесь, что используете последнюю версию когда пишите код. Часто люди используют “+” для обозначения версии такой как например 6.5.+ что бы использовать последний для 6.5 билд. Однако, как правило, не рекомендуется использовать “+”, т.к. это может привести к неконсистентности.

Так же есть некоторые изменения в именах билиотек, которые повлияют на вас если вы будете собирать приложения для Android Wear. Ранее, вы могли использовать play-services-wearable чтобы подключить всю библиотеку сервисов Google Play для ваших носимых устройствах и если вы хотите продолжать делать так, вы теперь должны использовать play-services-all-wear. Вы можете продолжить использовать play-services-wearable, что даст вам доступ только к Wearable Data Layer API (подробнее здесь). Вы должны сделать это если вы так же хотите продолжать работать с другими Google Play сервисами в будущем, например такими как Location APIs на ваших носимых устройствах, вам нужно добавить add play-services-location

FAN So Fun

| Comments

FAN = Facebook Audience Network

Партия сказала: надо!

Эта история началась с одного известного приложения Applifto, в разработке которого я принимал непосредственное участие. По секретному заданию руководства необходимо было реализовать поддержку банерной сети фейсбука посредством библиотеки AudienceNetwork. Судя по документации, реализовать поддержку банеров можно в 3 строчки кода и все будет работать отлично. И мы наивно в это поверили.

Приключения начинаются

Банеры были реализованы, приложение ушло в Play Store, а команда ушла на выходные (деплой был в пятницу, все как полагается). И конечно же одна полярная лиса с побережья Северного Ледовитого океана не заставила себя долго ждать и пришла во всей своей красе. Посыпалась куча крэшей при отображении полноэкранных банеров и мой напарник мужественно бросился на ликвидацию последствий =)

Очень скоро стало ясно, что проблема “не на нашей стороне” и падение происходит на уровне ОС Android(!) из-за какого-то кода, который скрывается внутри библиотеки AudienceNetwork. Библиотека, в отличие от всего остального Facebook SDK закрыта и представлена в виде JAR-файла. На этом можно было бы закончить повествование, отказаться от этой библиотеки и пойти например написать заявление на увольнение ;) но это не наш метод. Забрав у напарника отладочный проект я решил применить некоторые уловки “темной стороны” для решения этой “нерешаемой” проблемы.

Другая сторона силы

Все написанное ниже является плодом моего больного воображения и никогда не происходило на самом деле

Для того что бы понять что же приводит к падениям необходимо было получить исходники библиотеки. Сделать это достаточно просто. Нам потребуется декомпилятор Procyon и немного постучать по клавишам.

1
java -jar procyon.jar AudienceNetwork.jar -o AudienceNetwork

В результате в каталог AudienceNetwork упадет куча *.java файлов, которые можно подключить в проект и использовать (с небольшими исправлениями ошибок). Осталось быстренько понять где происходит ошибка и дело в шляпе. Но все оказалось несколько сложнее. В “фирменном” callback-hall от фейсбука пришлось изрядно покопаться, прежде чем удалось локализовать место где потенциально могла быть ошибка. Все усугублялось тем, что ошибка возникала на уровне ниже библиотеки и найти концы было очень сложно. В результате стало понятно, что ошибка возникает сразу после загрузки разметки. Ряд экспериментов с разметкой показал, что проблема кроется в 2х строчках

1
2
@-webkit-keyframes vcard1frames{0%,25%,75%,100%{opacity:1}30%,70%{opacity:0}}
@-webkit-keyframes vcard2frames{0%,25%,75%,100%{opacity:0}30%,70%{opacity:1}}

С помощью этой магии можно создавать анимацию в webkit. Какой-либо стоящей информации, о том почему с этими строками так плохо работает webkit под android, я не нашел. Поэтому в качестве рабочей версии была принята такая: баг в android реализации webkit-а. Разобрались, выдохнули, но проблему то решать все еще нужно.

Решение в данном случае достаточно простое. Необходимо на лету выпиливать эти строки из разметки. При этом мы жертвуем незначительной анимацией, которая при желании может быть реализована на чистом JavaScript. Для того что бы это сделать мне нужно было сделать свой класс наследующий InterstitiaAd из AudienceNetwork и переопределить реализацию метода show(). Но и тут ждала небольшая засада. Дело в том, что класс, с точки зрения ООП, написан достаточно грамотно. Все нужные мне для подмены поля естественно имели модификтаоры private. Эту проблему я оставил на сладкое и продолжил реализацию класса. В результате получилось примерно следующее

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
/**
* /!\ * Deobfuscated code * /!\
* Class "y" = HtmlAdDataModel
* Field "a" = markup
* Method "d" = getDataModel()
* Method "c" = getMarkup()
* Method "a" = addToIntentExtra()
*/
public class FixedInterstitialAd extends InterstitialAd {
  private Context mContext;

  public FixedInterstitialAd(Context context, String s) {
      super(context, s);
      mContext = context;
  }

  @Override
  public boolean show() {
      if (!isAdLoaded()) {
          if (this.adListener != null) {
              this.adListener.onError(this, AdError.INTERNAL_ERROR);
          }
          return false;
      }


      final Intent intent = new Intent(mContext, (Class)InterstitialAdActivity.class);

      y sourceModel = ((y)this.adResponse.d());         //d = getDataModel()
      sourceModel.a = getGoodMarkup(sourceModel.c()); //c() = getMarkup
      sourceModel.a(intent);                           //a() = addToIntentExtra()

      this.adLoaded = false;

      final WindowManager wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
      final DisplayMetrics displayMetrics = new DisplayMetrics();

      final Display display = wm.getDefaultDisplay();
      display.getMetrics(displayMetrics);
      intent.putExtra("displayRotation", display.getRotation());
      intent.putExtra("displayWidth", displayMetrics.widthPixels);
      intent.putExtra("displayHeight", displayMetrics.heightPixels);
      intent.putExtra("adInterstitialUniqueId", this.uniqueId);

      mContext.startActivity(intent);

      return true;
  }

  private String getGoodMarkup(String dirtyMarkup) {
      String resultString = "";

      try {
          resultString = dirtyMarkup.replaceAll("@-webkit-keyframes\\svcard.*?\\}{2}", "");
      } catch (PatternSyntaxException | IllegalArgumentException | IndexOutOfBoundsException ex) {
          ex.printStackTrace();
      }

      return resultString;
  }
}

Код библиотеки практически весь обфусцирован, поэтому я вставил пояснения. Я не стал сильно заморачиваться и просто переделал нужные мне поля в protected. Тестовые запуски показали, что банеры отображаются нормально и можно было переходить к завершающему этапу этого балета. Я не хотел использовать распакованую библиотеку в проекте, а решил пропатчить классы непосредственно в jar-файле. С удобными, кроссплатформенными редакторами байткода в стране беда поэтому пришлось заюзать dirtyJOE, версии которого есть только под Win. Сам патч вышел достаточно простым. Я изменил модификаторы полей с private на protected после чего собрал библиотеку заново и подключил ее в проект. Банеры ожидаемо завелись.

Выводы

Задача была решена и теперь можно долго ругать фейсбук за то, что “не предусмотрели”, “не протестировали”, “не предсказали будущее”, но я не буду этого делать. Все совершают ошибки и одно из главных качеств хорошего разработчика это уметь исправлять эти ошибки. Неважно свои они или чужие. В данном случае ошибка не была исправлена как таковая, но было разработано вполне рабочее решение, которое вполне сможет дожить до тех светлых времен, когда выйдет фикс для webkit или AudienceNetwork.