- PVSM.RU - https://www.pvsm.ru -

Кюветы Android, Часть 2: SDK и Libraries

Разрабатывая под Android, всегда нужно быть начеку. Шаг влево / шаг вправо — и вот прошел ещё один час за дебагом. Кюветы могут быть какие угодно: начиная от обычных багов в SDK и заканчивая неочевидными именами методов с контекстно зависимым результатом (да-да, Fragment.getFragmentManager() [1], это я о тебе).

В предыдущей статье [2] были описаны кюветы «на поверхности» SDK, в которые угодить очень легко. На этот же раз кюветы будут поглубже, помудрёнее и поспецифичнее. Также будет несколько моментов, связанных с Retrofit 2 [3] & Gson [4].
image

1. GridLayout [5] не реагирует на layout_weight

Ситуация

Иногда так случается, что обычный способ создания объекта с кучей не подходит:

Обычная форма

image

Такой ситуацией может быть, например, landscape отображение формы. Хотелось бы в таком случае иметь нечто вроде такого:

Форма для landscape

image

Как сделать такое же выравнивание 50 на 50? Существует несколько основных подходов [6]:

Однако они все имеют свои недостатки:

  • Обилие LinearLayout приводит к монструозности xml'ки, а она приводит к смерте котиков.
  • RelativeLayout усложняет изменение в будущем (поменять местами несколько строк в форме или добавить разделитель будет той ещё задачкой. Про View.setVisibility(View.GONE) я и вовсе молчу).
  • Ну а TableLayout вообще никто не использует… или используют, но редко. Я таких людей не знаю.

Более того, довольно часто необходимо использовать магию числа 0dp & weight=1 [11], чтобы добиться гибкого дизайна. Ни TableLayout, ни RelativeLayout тут вам не помогут. При первой же попытке использовать что-то вроде TextView.setEllipsize() [12], начнутся проблемы и боль.
И тут вы наверное подметили, что я пропустил ещё один элемент. Казалось бы, на помощь приходит GridLayout, но и тот оказывается бесполезен из-за того, что не поддерживает свойство layout_weight. Так что же делать?

Решение

До некоторых пор делать было действительно нечего — либо мучайся с RelativeLayout, либо применяй LinearLayout , либо заполняй всё программный путем (для особо извращенных).
Однако с 21 версии GridLayout наконец-то начал поддерживать свойство layout_weight и, что самое важное, это изменение было добавлено в AppCompat в виде android.support.v7.widget.GridLayout!
К моменту когда я узнал об этом (и вообще о том, что обычный GridLayout чхать хотел на мой weight), я потратил по меньшей мере неделю, пытаясь понять почему мой layout поплыл вправо (как здесь [13]). Пожалуй, это одно из самых важных нововведений, которое, почему-то, осталось без должного внимания. К счастью, ответы на stackoverflow (1 [14], 2 [15]) уже начинают дописывать.
Также советую заглянуть на страничку к новым PercentRelativeLayout [16] и PercentFrameLayout [17] — это действительно бомба. Название говорит само за себя и позволяет сделать крайне адаптивный дизайн. iOS'ники оценят. И ах да, оно есть в AppCompat [18].

2. Fragment.isRemoving() [19] и Acitivity.isFinishing() [20] равны?

Ситуация

Как-то раз захотел я написать свой PresenterManager в виде синглтона (привет от MVP). Чтобы вовремя удалять Presenter'ов, я использовал Activity.isFinishing(), собирая id Presenter'ов фрагментов в активити и удаляя их вместе с ним. Естественно, такой способ плохо работал в случае с NavigationView [21] — фрагменты менялись через FragmentTransaction.replace() [22], Presenter'ы копились и всё шло коту под хвост.
Погуглив смальца [23], был найден метод Fragment.isRemoving() [19], который вроде бы делает то же самое, но для фрагментов. Я переписал код PresenterManager'а и был доволен. Конец…

Решение

… наступил моей спокойной жизни, когда я пытался заставить это работать. Честно, я пытался и так, и эдак, но поведение этого метода вкорне отличается от Activity.isFinishing(). Гугл был неправ. Если у вас когда-нибудь возникнет подобная задача, подумайте трижды прежде чем использовать Fragment.isRemoving(). Я серьезно [24]. Особенно уделите внимание логам при повороте экрана.

Кстати с Acitivity.isFinishing() [20] тоже не всё так гладко: сверните приложение с >1 активити в стэке, дождитесь ситуации нехватки памяти, вернитесь обратно и воспользуйтесь Up Navigation [25]и *вуаля*!.. Это был простой рецепт того, как поиметь Activity.isFinishing() == false для активити, которые вы больше никогда не увидите.

3. Header/Footer в RecyclerView [26]

Ситуация

Обычная задача при реализации пагинации [27] — необходимо отображать ProgressBar на время загрузки новых данных.
В отличие от ListView [28], RecyclerView обладает куда большими возможностями — чего только стоит RecyclerView.Adapter.notifyItemRangeInserted() [29] по сравнению с той самой головной болью [30] ListView.
Однако попробовав использовать его в проекте вместо ListView, сразу же натыкаешься на множество нюансов: где свойство ListView.setDivider() [31]? Где нечто вроде ListView.addHeaderView() [32]? Что ещё за RecyclerView.Adapter.getItemViewType() [33] и т.д., и т.п.
Разобраться то со всей этой свалкой новой информации несложно, однако кое-что неприятное всё равно остается. Добавление Divider/Header заствляет писать тучи кода. Что уж и говорить о сложных layout'ах? Довеча доводилось делать RecyclerView с 4-мя различными Header'ами и Footer'ом с контроллами. Скажем так, опечаленным и удрученным я ходил очень долго.

Решение

На самом деле всё не так плохо, если знать, что искать. Самая основная проблема RecyclerView (и оно же его основное преимущество) — с ним можно делать всё, что угодно. Нет практически никаких рамок. Отсюда и вытекает проблема: хочешь Header — сделай сам. Но к счастью, «сделай сам» уже сделали за нас другие, так что давайте пользоваться.
Типичные проблемы и их решения:

  • Заголовки для групп элементов (например, в словаре «А» будет являться заголовком для всех слов, начинающихся с этой буквы) — проще всего сделать через единственный item-layout, не добавляя 2-ой ненужный тип ViewHolder'а. Добавьте проверку на то, что текущий элемент ознаменует переход от одной буквы к другой и включите спрятанный в layout заголовок через View.VISIBLE.
  • Простой divider — копи-паст этого кода [34] в проект. Никаких лишних махинаций. Работает через RecyclerView.addItemDecoration() [35]
  • Добавлените Header / Footer / Drag&Drop и т.д. — если делать ручками, то либо заводить новый тип на каждый новый ViewHolder (не советую), либо делать WrapperAdapter (куда приятнее). Но ещё лучше посмотреть тут [36] и выбрать понравившуюся либу. Лично мне нравятся сразу две: FastAdapter [37]и UltimateRecyclerView [38]
  • Нужна пагинация, но лень возиться с Header / Footer для ProgressBar'ов — библиотека Paginate [39]от одного из разработчиков твиттера.

Хотя для меня всё равно остается загадкой — почему нельзя было сделать какие-нибудь SimpleDivider / SimpleHeaderAdapter и т.д. сразу в SDK?

4. Ускорение с RecyclerView.Adapter.setHasStableIds() [40]

Что с ним не так?

Нестолько проблема, сколько недостаток документации [41]. Вот что там написано:

Returns true if this adapter publishes a unique long value that can act as a key for the item at a given position in the data set. If that item is relocated in the data set, the ID returned for that item should be the same.

И тут люди делятся на два типа. Первые: всё ж ясно. Вторые: чо это вообще значит то?
Проблема в том, что даже если вы отнесли себя к первым людям, вас может поставить в тупик вопрос: а зачем этот метод? Да-да, чтобы вернуть уникальный ID! Я знаю. Но зачем оно надо? И нет, ответ «гугл пишет, что так быстрее скроллиться будет!» меня не устроит.

А вот в чём дело

Ускорение от RecyclerView.Adapter.setHasStableIds() действительно можно получить, но только в одном случае — если вы повсеместно используете RecyclerView.Adapter.notifyDataSetChanged() (а тут они соизволили написать, зачем нужны stable id) [42]. Если вы имеете статичные данные, то вам этот метод не даст ровным счетом ничего, а возможно даже и немного замедлит из-за внутренних проверок ID. Узнал я об этом только после чтения исходников, а чуть позже случайно наткнулся на эту статью [43].

5. WebView [44]

Ситуация

Задача — получить html-текст от сервера и вывести его на экран. Текст сервером отдается в виде "& lt;html& gt;". Всё. Это вся задача. Сложно? Существует же WebView, который может отобразить html в пару строк. Да что там, даже TextView [45]может это сделать! Раз-два и готово… да?.. нет?.. ну должно же?!

Решение

К сожалению, тут всё не так гладко:

  • Начнём с того, что нет метода типа HtmlUtils.unescape() в Android SDK. Если хочешь "& lt;" превратить в "<", то самый простой способ (кроме прописывания regex'а ручками) — подключить apache с его StringUtils.unescapeHtml4() [46].
  • Следующей проблемой будут артефакты при прокрутке. Совершенно внезапно (да, Android SDK?), WebView будет мигать черным цветом. Что делать — рассказывается тут [47]и тут [48]. Лично мне помогла только комбинация этих подходов.
  • И если вас ещё не удивило обилие проблем от столь простой задачи, то вот добивалочка: нужно отобразить ProgressBar, пока html-страничка не отрендерилась. И тут всё плохо. То есть реально плохо. Все представленые на stackoverflow решения работают через раз или не работаю вовсе (тык [49], тык [50]). Единственный работающий доселе способ был с применением WebView.setPictureListener () [51], однако тот теперь объявлен deprecated и тут уже ничего не попишешь.
    В итоге, единственное, что можно посоветовать — отказаться от ProgressBar'а. Либо, если уж совсем-совсем-совсем приспичит — добавить его прямо в html-код, проверяя через javascript готовность страницы. Но это уже для клуба элитных мазахистов.

6. Gson: битовая маска в виде EnumSet [52]

Когда/Где/Зачем?

(Ситуация специфична и напрямую к проблемам Android'а не относится, но в качестве затравки перед следующим кюветом решил добавить)
В ответ на один из api-запросов приходит битовая маска прав доступа в виде int'а. Нужно обрабатывать элементы этой маски.
Первое, что приходит в голову — int'овые константы и битовые операциями для проверок. Несомненно, оно всегда работает. Но что если хочется большего? Как насчет EnumSet?
«Без проблем» — ответит проггер-бородач и разобьет архитектуру моделей ещё на несколько уровней: POJO, Model, Entity, UiModel и чем ещё чёрт не шутит. Но если лень и хочется без доп. классов? Что тогда?

Решение

Создаём нужный нам enum, позаботившись о «битовости» имён в @SerializedName [53]:

enum Access

public enum Access {
    @SerializedName("1")
    CREATE,
    @SerializedName("2")
    READ;
    @SerializedName("4")
    UPDATE;
    @SerializedName("8")
    DELETE;
}

Определяем JsonDeserializer [54]для десериализации из json в EnumSet:

EnumMaskConverter

public class EnumMaskConverter<E extends Enum<E>> implements JsonDeserializer<EnumSet<E>> {
	Class<E> enumClass;

	public EnumMaskConverter(Class<E> enumClass) {
		this.enumClass = enumClass;
	}

	@Override
	public EnumSet<E> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
		long mask = json.getAsLong();
		EnumSet<E> set = EnumSet.noneOf(enumClass);

		for (E bit : enumClass.getEnumConstants()) {
			final String value = EnumUtils.GetSerializedNameValue(bit);
			assert value != null;

			long key = Integer.valueOf(value);
			if ((mask & key) != 0) {
				set.add(bit);
			}
		}
		return set;
	}
}

И добавляем его в Gson:

GsonBuilder

GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter((new TypeToken<EnumSet<Access>>() {}).getType(), new EnumMaskConverter<>(Access.class));
Gson gson = gsonBuilder.create();

В результате:

Использование

class MyModel {
        @SerializedName("mask")
	public EnumSet<Access> access;
}

/* ...some lines later... */

if (myModel.access.containsAll(EnumSet.of(Access.READ, Access.UPDATE, Access.DELETE))) { 
	/* do something really cool */ 
}

7. Retrofit: Enum в @GET [55] запросе

Ситуация

Начнём с настройки. Gson формируется также, как и ранее. Retrofit создаётся вот так:

new Retrofit.Builder()

retrofit = new Retrofit.Builder()
                .baseUrl(ApiConstants.API_ENDPOINT)
                .client(httpClient)
                .addConverterFactory(GsonConverterFactory.create(gson))
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                .build();

А данные выглядят так:

enum Season

public enum Season {
    @SerializedName("3")
    AUTUMN,
    @SerializedName("1")
    SPRING;
}

Благодаря возможности Gson к прямому парсингу enum через обычный @SerializedName, стало возможным избавиться от необходимости создавать всякие дополнительные классы-прослойки. Все данные будут сразу идти из запросов в Model. Всё прекрасно:

Retrofit Service

public interface MonthApi {
        @GET("index.php?page[api]=selectors")
        Observable<MonthSelector> getPriorityMonthSelector();

        @GET("index.php?page[api]=years")
        Observable<Month> getFirstMonth(@Query("season") Season season);
}

Применение

class MonthSelector {
        @SerializedName("season")
	public Season season;
}

/* ...some mouses later... */
MonthSelector selector = monthApi.getPriorityMonthSelector();
Season season = selector.season;

/* ...some cats later... */
Month month = monthApi.getFirstMonth(season);

А теперь, уважаемые знатоки, внимание вопрос! Что пошло не так и почему оно не работает?

Решение

Я специально опустил информацию о том, что именно здесь не работает. Дело в том, что если посмотреть в логи, то запрос monthApi.getFirstMonth(season) будет обработан, как index.php?page[api]=years&season_lookup=AUTUMN… «ээээ, что за дела?» — скажу я. А каков ваш ответ? Почему такой результат? Ещё не догадались? Тогда вы попали.
Когда я столкнулся с этой задачей, мне потребовалось несколько часов поисков в исходниках, чтобы понять одну вещь (или скорее даже вспомнить): да не используется Gson при отправке @GET / @POST и других подобных _запросов_ вообще! Ведь действительно, когда вы последний раз видели нечто вроде index.php?page[api]=years&season_lookup={a:123; b:321}? Это не имеет смысла. Retrofit 2 использует Gson только при конвертации Body, но никак не для самих запросов. В итоге? используется просто season.toString() — отсюда и результат.
Однако, если уж ооочень хочется (а я из таких) использовать enum с конвертацией через Gson в запросе, то вам сюда [56] — ещё один конвертор, всё как всегда.

8. Retrofit: передача auth-token

И напоследок, хотелось бы сказать одну вещь тем, кто пишет так:

Любой Retrofit Service

public interface CoolApi {
    @GET("index.php?page[api]=need")
    Observable<Data> 
    just(@Header("auth-token") String authToken);
    //           ^шлём auth-token

    @GET("index.php?page[api]=more")
    Observable<Data> 
    not(@Header("auth-token") String authToken);
    //           ^шлём auth-token ещё раз

    @GET("index.php?page[api]=gold")
    Observable<Data> 
    doIt(@Header("auth-token") String authToken);
    //           ^шлём auth-token в 101ый раз!
}

Начните уже использовать Interceptor [57]'оры! Я понимаю, что Retrofit использовать очень просто и поэтому никто не читает документацию, но когда 3 часа сидишь и вычищаешь код не только от auth-token, но и ото всяких специфических current_location, battery_level, busy_status — настигает великая печалька (не спрашивайте, зачем передавать battery_level в каждый запрос. Сам в шоке). Почитать об этом можно тут [58].

Вместо заключения

Что ж, на этот раз вышло куда больше текста, чем я планировал. Некоторые менее интересные кюветы пришлось выкинуть, другие же я решил оставил для следующего раза.
Вопреки посылу предыдущей части [2], в этот раз я старался заставить вас не «гуглить в первую очередь», а прежде всего подумать «а зачем я это делаю?». Иногда проблему создает не SDK или библиотека, а сам программист и, к сожалению, в этом случае всё куда плачевнее. Не стоит недооценивать выбранный инструментарий, как и не стоит переоценивать его.
В общем, если вам нравится андроид и/или вы планируете им заняться — всегда держите себя в курсе мировых трендов [59]. Ну или поищите здесь [60] более удобный для себя новостной ресурс. Там же вы можете найти много информации об Android SDK [61], популярных библиотеках [62] и т.д., и т.п.

Автор: Yoto

Источник [63]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/java/117465

Ссылки в тексте:

[1] Fragment.getFragmentManager(): http://developer.android.com/intl/ru/reference/android/app/Fragment.html#getFragmentManager%28%29

[2] предыдущей статье: https://habrahabr.ru/post/279811/

[3] Retrofit 2: http://inthecheesefactory.com/blog/retrofit-2.0/en

[4] Gson: https://github.com/google/gson

[5] GridLayout: http://developer.android.com/intl/ru/reference/android/widget/GridLayout.html

[6] основных подходов: http://developer.android.com/intl/ru/training/improving-layouts/optimizing-layout.html

[7] LinearLayout: http://developer.android.com/reference/android/widget/LinearLayout.html

[8] RelativeLayout: http://developer.android.com/reference/android/widget/RelativeLayout.html

[9] TableLayout: http://developer.android.com/reference/android/widget/TableLayout.html

[10] GridLayout: http://developer.android.com/reference/android/widget/GridLayout.html

[11] 0dp & weight=1: http://stackoverflow.com/questions/6975607/what-does-layout-height-0dp-mean

[12] TextView.setEllipsize(): http://stackoverflow.com/questions/13313996/what-does-ellipsize-mean-in-android

[13] здесь: https://code.google.com/p/android/issues/detail?id=79177#c3

[14] 1: http://stackoverflow.com/a/10033481/2653714

[15] 2: http://stackoverflow.com/a/10348166/2653714

[16] PercentRelativeLayout: http://developer.android.com/intl/ru/reference/android/support/percent/PercentRelativeLayout.html

[17] PercentFrameLayout : http://developer.android.com/intl/ru/reference/android/support/percent/PercentFrameLayout.html

[18] AppCompat: http://developer.android.com/intl/ru/tools/support-library/features.html#percent

[19] Fragment.isRemoving(): http://developer.android.com/intl/ru/reference/android/app/Fragment.html#isRemoving%28%29

[20] Acitivity.isFinishing(): http://developer.android.com/intl/ru/reference/android/app/Activity.html#isFinishing%28%29

[21] NavigationView : http://developer.android.com/intl/ru/reference/android/support/design/widget/NavigationView.html

[22] FragmentTransaction.replace(): http://developer.android.com/intl/ru/reference/android/app/FragmentTransaction.html#replace%28int,%20android.app.Fragment%29

[23] Погуглив смальца: http://stackoverflow.com/questions/25450763/what-is-the-fragment-equivalent-of-activity-isfinishing

[24] Я серьезно: http://stackoverflow.com/a/34822148/2653714

[25] Up Navigation : http://developer.android.com/intl/ru/training/implementing-navigation/ancestral.html

[26] RecyclerView: http://developer.android.com/intl/ru/reference/android/support/v7/widget/RecyclerView.html

[27] пагинации : https://ru.wikipedia.org/wiki/%D0%9F%D0%B0%D0%B3%D0%B8%D0%BD%D0%B0%D1%86%D0%B8%D1%8F

[28] ListView: http://developer.android.com/intl/ru/reference/android/widget/ListView.html

[29] RecyclerView.Adapter.notifyItemRangeInserted(): http://developer.android.com/intl/ru/reference/android/support/v7/widget/RecyclerView.Adapter.html#notifyItemRangeInserted%28int,%20int%29

[30] той самой головной болью: http://stackoverflow.com/questions/6264795/animate-newly-added-items-in-listview

[31] ListView.setDivider(): http://developer.android.com/intl/ru/reference/android/widget/ListView.html#setDivider%28android.graphics.drawable.Drawable%29

[32] ListView.addHeaderView(): http://developer.android.com/intl/ru/reference/android/widget/ListView.html#addHeaderView%28android.view.View%29

[33] RecyclerView.Adapter.getItemViewType(): http://developer.android.com/intl/ru/reference/android/support/v7/widget/RecyclerView.Adapter.html#getItemViewType%28int%29

[34] этого кода: https://gist.github.com/lapastillaroja/858caf1a82791b6c1a36

[35] RecyclerView.addItemDecoration(): http://developer.android.com/intl/ru/reference/android/support/v7/widget/RecyclerView.html#addItemDecoration%28android.support.v7.widget.RecyclerView.ItemDecoration%29

[36] тут: http://guides.codepath.com/android/Must-Have-Libraries#recyclerview

[37] FastAdapter : https://github.com/mikepenz/FastAdapter

[38] UltimateRecyclerView : https://github.com/cymcsg/UltimateRecyclerView

[39] Paginate : https://github.com/MarkoMilos/Paginate

[40] RecyclerView.Adapter.setHasStableIds(): http://developer.android.com/intl/ru/reference/android/support/v7/widget/RecyclerView.Adapter.html#setHasStableIds%28boolean%29

[41] документации: http://developer.android.com/intl/ru/reference/android/support/v7/widget/RecyclerView.Adapter.html#hasStableIds%28%29

[42] RecyclerView.Adapter.notifyDataSetChanged() (а тут они соизволили написать, зачем нужны stable id): http://developer.android.com/intl/ru/reference/android/support/v7/widget/RecyclerView.Adapter.html#notifyDataSetChanged%28%29

[43] эту статью: https://blog.stylingandroid.com/material-part-7/

[44] WebView: http://developer.android.com/intl/ru/reference/android/webkit/WebView.html

[45] TextView : http://stackoverflow.com/questions/2116162/how-to-display-html-in-textview

[46] StringUtils.unescapeHtml4(): http://commons.apache.org/proper/commons-lang/javadocs/api-3.1/org/apache/commons/lang3/StringEscapeUtils.html#unescapeHtml4%28java.lang.String%29

[47] тут : http://stackoverflow.com/a/17317706/2653714

[48] тут: http://stackoverflow.com/a/16217522/2653714

[49] тык: http://stackoverflow.com/questions/4065134/is-there-a-listener-for-when-the-webview-displays-its-content

[50] тык: http://stackoverflow.com/questions/21000566/android-webview-detect-when-rendering-is-finished

[51] WebView.setPictureListener (): http://stackoverflow.com/a/4945875/2653714

[52] EnumSet: http://developer.android.com/intl/ru/reference/java/util/EnumSet.html

[53] @SerializedName: https://google.github.io/gson/apidocs/com/google/gson/annotations/SerializedName.html

[54] JsonDeserializer : https://google-gson.googlecode.com/svn/trunk/gson/docs/javadocs/com/google/gson/JsonDeserializer.html

[55] @GET: https://square.github.io/retrofit/2.x/retrofit/

[56] сюда: http://stackoverflow.com/a/35801262/2653714

[57] Interceptor: https://github.com/square/okhttp/wiki/Interceptors

[58] тут: https://futurestud.io/blog/retrofit-token-authentication-on-android

[59] мировых трендов: http://android-developers.blogspot.ru/

[60] здесь : http://guides.codepath.com/android/Keeping-Updated-with-Android

[61] Android SDK: http://guides.codepath.com/android

[62] популярных библиотеках: http://guides.codepath.com/android/Must-Have-Libraries

[63] Источник: https://habrahabr.ru/post/280190/