- PVSM.RU - https://www.pvsm.ru -
Разрабатывая под Android, всегда нужно быть начеку. Шаг влево / шаг вправо — и вот прошел ещё один час за дебагом. Кюветы могут быть какие угодно: начиная от обычных багов в SDK и заканчивая неочевидными именами методов с контекстно зависимым результатом (да-да, Fragment.getFragmentManager() [1], это я о тебе).
В предыдущей статье [2] были описаны кюветы «на поверхности» SDK, в которые угодить очень легко. На этот же раз кюветы будут поглубже, помудрёнее и поспецифичнее. Также будет несколько моментов, связанных с Retrofit 2 [3] & Gson [4].
Иногда так случается, что обычный способ создания объекта с кучей не подходит:
Такой ситуацией может быть, например, landscape отображение формы. Хотелось бы в таком случае иметь нечто вроде такого:
Как сделать такое же выравнивание 50 на 50? Существует несколько основных подходов [6]:
Однако они все имеют свои недостатки:
Более того, довольно часто необходимо использовать магию числа 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].
Как-то раз захотел я написать свой 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 для активити, которые вы больше никогда не увидите.
Обычная задача при реализации пагинации [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 — сделай сам. Но к счастью, «сделай сам» уже сделали за нас другие, так что давайте пользоваться.
Типичные проблемы и их решения:
Хотя для меня всё равно остается загадкой — почему нельзя было сделать какие-нибудь SimpleDivider / SimpleHeaderAdapter и т.д. сразу в SDK?
Нестолько проблема, сколько недостаток документации [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].
Задача — получить html-текст от сервера и вывести его на экран. Текст сервером отдается в виде "& lt;html& gt;". Всё. Это вся задача. Сложно? Существует же WebView, который может отобразить html в пару строк. Да что там, даже TextView [45]может это сделать! Раз-два и готово… да?.. нет?.. ну должно же?!
К сожалению, тут всё не так гладко:
(Ситуация специфична и напрямую к проблемам Android'а не относится, но в качестве затравки перед следующим кюветом решил добавить)
В ответ на один из api-запросов приходит битовая маска прав доступа в виде int'а. Нужно обрабатывать элементы этой маски.
Первое, что приходит в голову — int'овые константы и битовые операциями для проверок. Несомненно, оно всегда работает. Но что если хочется большего? Как насчет EnumSet?
«Без проблем» — ответит проггер-бородач и разобьет архитектуру моделей ещё на несколько уровней: POJO, Model, Entity, UiModel и чем ещё чёрт не шутит. Но если лень и хочется без доп. классов? Что тогда?
Создаём нужный нам enum, позаботившись о «битовости» имён в @SerializedName [53]:
public enum Access {
@SerializedName("1")
CREATE,
@SerializedName("2")
READ;
@SerializedName("4")
UPDATE;
@SerializedName("8")
DELETE;
}
Определяем JsonDeserializer [54]для десериализации из json в EnumSet:
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 = 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 */
}
Начнём с настройки. Gson формируется также, как и ранее. Retrofit создаётся вот так:
retrofit = new Retrofit.Builder()
.baseUrl(ApiConstants.API_ENDPOINT)
.client(httpClient)
.addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build();
А данные выглядят так:
public enum Season {
@SerializedName("3")
AUTUMN,
@SerializedName("1")
SPRING;
}
Благодаря возможности Gson к прямому парсингу enum через обычный @SerializedName, стало возможным избавиться от необходимости создавать всякие дополнительные классы-прослойки. Все данные будут сразу идти из запросов в Model. Всё прекрасно:
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] — ещё один конвертор, всё как всегда.
И напоследок, хотелось бы сказать одну вещь тем, кто пишет так:
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/
Нажмите здесь для печати.