Кюветы Android, Часть 1: SDK

    Довольно долгое время я никак не мог понять, в чём же разница между «библиотекой» и «фреймворком». Нет-нет, я умел и читать, и гуглить, но до меня всё никак не доходил смысл этих понятий. Начав же программировать под андроид, я наконец понял, что значат слова «библиотеку использует программист, но программиста использует фреймворк».
    В этой серии статьей я хочу рассказать о проблемах, с которыми мне пришлось столнулся при разработке под андроид. Моей целью является не предоставление каких-либо убер-решений приведенных проблем, а лишь информирование о том, с какими проблемами может столкнуться тот, кто посягнет на святой грааль Android SDK. Не думаю, что суровые синьоры откроют для себя Америку, но как говорится: «повторение — мать учения».
    image


    1. Dismiss для DatePickerDialog вызывает обработчик OnDateSetListener


    Ситуация

    Довольно неприятная проблема для начинающего. Особенно если вы рассчитываете, что SDK работает как часы.
    В своё время пришлось повозиться, чтобы понять, в чём дело. Проблема усугубилась тем, что после установки времени не было никакой обратной связи в приложении (на экране новое время не отображалось). Все данные сразу заносились в объект, который сохранялся в БД и спустя несколько экранов читался обратно.
    Несложно представить, откуда начинался дебаг — с экрана отображения (т.к. для тестов использовалась дата Date.now() — это внесло дополнительный конфуз), а затем по цепочке.

    Решение

    На самом деле в Lollipop баг был устранен, однако кого это устраивает? В AppCompat добавлять гугл фикс не планирует, поэтому нужен обходной путь. И он есть — скопировали весь файл целиком и понеслать. Информацию о реализации можно прочитать на stackoverflow.
    DatePickerDialogFragment
    /*
     * Copyright 2012 David Cesarino de Sousa
     *
     * Licensed under the Apache License, Version 2.0 (the "License");
     * you may not use this file except in compliance with the License.
     * You may obtain a copy of the License at
     *
     *      http://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an "AS IS" BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     * See the License for the specific language governing permissions and
     * limitations under the License.
     */
    package net.davidcesarino.android.common.ui;
    
    import android.annotation.TargetApi;
    import android.app.Activity;
    import android.app.DatePickerDialog;
    import android.app.DatePickerDialog.OnDateSetListener;
    import android.app.Dialog;
    import android.content.DialogInterface;
    import android.os.Build;
    import android.os.Bundle;
    import android.support.v4.app.DialogFragment;
    import android.widget.DatePicker;
    
    /**
     * <p>Provides a usable {@link DatePickerDialog} wrapped as a {@link DialogFragment},
     * using the compatibility package v4. Its main advantage is handling Issue 34833 
     * automatically for you.</p>
     * 
     * <p>Current implementation (because I wanted that way =) ):</p>
     * 
     * <ul>
     * <li>Only two buttons, a {@code BUTTON_POSITIVE} and a {@code BUTTON_NEGATIVE}.
     * <li>Buttons labeled from {@code android.R.string.ok} and {@code android.R.string.cancel}.
     * </ul>
     * 
     * <p><strong>Usage sample:</strong></p>
     * 
     * <pre>class YourActivity extends Activity implements OnDateSetListener
     * 
     * // ...
     * 
     * Bundle b = new Bundle();
     * b.putInt(DatePickerDialogFragment.YEAR, 2012);
     * b.putInt(DatePickerDialogFragment.MONTH, 6);
     * b.putInt(DatePickerDialogFragment.DATE, 17);
     * DialogFragment picker = new DatePickerDialogFragment();
     * picker.setArguments(b);
     * picker.show(getActivity().getSupportFragmentManager(), "fragment_date_picker");</pre>
     * 
     * @author davidcesarino@gmail.com
     * @version 2015.0904
     * @see <a href="http://code.google.com/p/android/issues/detail?id=34833">Android Issue 34833</a>
     * @see <a href="http://stackoverflow.com/q/11444238/489607"
     * >Jelly Bean DatePickerDialog — is there a way to cancel?</a>
     *
     */
    public class DatePickerDialogFragment extends DialogFragment {
        
        public static final String YEAR = "Year";
        public static final String MONTH = "Month";
        public static final String DATE = "Day";
        
        private OnDateSetListener mListener;
        
        @Override
        public void onAttach(Activity activity) {
            super.onAttach(activity);
            this.mListener = (OnDateSetListener) activity;
        }
        
        @Override
        public void onDetach() {
            this.mListener = null;
            super.onDetach();
        }
        
        @TargetApi(11)
        @Override
        public Dialog onCreateDialog(Bundle savedInstanceState) {
            Bundle b = getArguments();
            int y = b.getInt(YEAR);
            int m = b.getInt(MONTH);
            int d = b.getInt(DATE);
            
            // Jelly Bean introduced a bug in DatePickerDialog (and possibly 
            // TimePickerDialog as well), and one of the possible solutions is 
            // to postpone the creation of both the listener and the BUTTON_* .
            // 
            // Passing a null here won't harm because DatePickerDialog checks for a null
            // whenever it reads the listener that was passed here. >>> This seems to be 
            // true down to 1.5 / API 3, up to 4.1.1 / API 16. <<< No worries. For now.
            //
            // See my own question and answer, and details I included for the issue:
            //
            // http://stackoverflow.com/a/11493752/489607
            // http://code.google.com/p/android/issues/detail?id=34833
            //
            // Of course, suggestions welcome.
            
            final DatePickerDialog picker = new DatePickerDialog(getActivity(),
                    getConstructorListener(), y, m, d);
    
            if (isAffectedVersion()) {
                picker.setButton(DialogInterface.BUTTON_POSITIVE,
                        getActivity().getString(android.R.string.ok),
                        new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                DatePicker dp = picker.getDatePicker();
                                mListener.onDateSet(dp,
                                        dp.getYear(), dp.getMonth(), dp.getDayOfMonth());
                            }
                        });
                picker.setButton(DialogInterface.BUTTON_NEGATIVE,
                        getActivity().getString(android.R.string.cancel),
                        new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {}
                        });
            }
            return picker;
        }
    
        private static boolean isAffectedVersion() {
            return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN
                    && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP;
        }
    
        private OnDateSetListener getConstructorListener() {
            return isAffectedVersion() ? null : mListener;
        }
    }
    



    2. Кнопка требует двойной клик прежде чем сработать


    Ситуация

    Ещё один случай из разряда «да как так то!». Представьте себе ситуацию, когда в открытом диалоговом окне вы заполняете данные в нескольких EditText'ах, а затем нажимаете OK. Что может пойти не так? Ну например кнопка OK может игнорировать ваше нажатие! Но только первое… и не всегда… и только раз в месяц.... И конечно же опять начинается дебаг и дебаг, и дебаг…

    Решение

    А решение донельзя простое. Нужно знать о магии setFocusableInTouchMode() и что вообще это такое. Многие из нас не уделяют должного внимания свойству focusable. Действительно, оно чувствуется само по себе и в большинстве своём ведёт себя как следует. И вот тут то нас и ловят:
    • Когда юзер кликает по кнопке, он перемещает фокус на неё.
    • Когда юзер кликает по экрану, но не по какому-либо элементу — фокус передается на экран, то бишь рассеивается, то бишь фокус переходит в режим Touch Mode или в режим отсутствия фокуса.

    Просто? Не тут то было, всегда есть исключения. В некоторых случаях, фокус может присутствовать и в этом режиме (который, повторюсь, называется «режим отсутствия фокуса»). Самый яркий пример — EditText. Такое поведение необходимо для возможности одновременного взаимодействия и клавиатуры, и EditText'а. Иначе бы написать что-то не вышло.
    В итоге, решением является focusableInTouchMode=true для кнопки. Решение выглядит простым, но когда не знаешь, с чего начать, всё обретает иные краски. Более детально можно почитать на android-developers.blogspot.ru.

    3. Bundle.putParcelable() — не всегда сериализация


    Ситуация

    Есть диалоговое окно, есть активити. Вы, как бравый товарищ, решаете передать свой объект класса VeryComplexModel в диалоговое окно, чтобы проделать над ним какие-либо действия (например, редактирование), а затем вернуть обратно, чтобы в активити сохранить в БД новую версию.
    И опять же магия происходит во время закрытия диалога. Казалось бы, локальная копия объект должна остаться старой, но нет. Она изменилась.

    Решение

    Всё дело в неверном представлении механизмов в Bundle'а. В моём понимании, Bundle, как и всякие Serializable и JSONObject — всегда создают объект с нуля, если сделать последовательно serialize() и deserialize(). Во всяком случае, так я думал. Однако то ли из соображения оптимизаций, то ли по какой-то другой причине, Bundle может нести в себе указатель на ваш объект без сериализации. Отсюда и изменение данных в диалоговом окне несмотря на dismiss. Ожидалось, что пострадает лишь копия, но Android SDK распорядился иначе.

    4. getFragmentManager() внутри фрагмента


    Ситуация

    Пожалуй, это самая распространенная проблема среди начинающих (и не только!) программистов. Стоит немного расслабиться и часок-другой дебага обеспечен.
    FragmentManager используется для управления фрагментами внутри активити, а также для управления вложенными фрагментами внутри других фрагментов.
    У активити есть метод getFragmentManager(), у фрагмента есть метод getFragmentManger() — вызвал, использовал, работает… или нет?.. Что-то опять сломалось. SDK, пожалей!

    Решение

    К сожалению, злую шутку здесь играют две вещи:
    • ожидание того, что активити и фрагменты работаю примерно одинаково
    • игнорирование принципа RTFM

    Если глянуть в документацию, то сразу видно, что getFragmentManager() для фрагмента возвращает… FragmentManager родителя! Чтобы получить нормальный, работающий так, как ожидается, FragmentManager необходимо использовать getChildFragmentManager().

    5. Модификация Drawable в runtime


    Ситуация

    С данной проблемой мне довелось столкнутся во время создания разноцветных background'ов для разных объектов. Представить подобное можно на примере чата, где bubble (пузырь сообщения) для вашего сообщения окрашен в серый, а для собеседника в красный.
    Конечно, самым простым решением будет создать 2 независимых ресурса. Но что если это ваш домашний проект «на раз», а художник из вас как балерина? Тут и приходят на помощь различные методы вида setColorFilter(). Верно?.. Нет.

    Решение

    Просто так взяв да применив setColorFilter() на каком-нибудь R.drawable.bg_bubble, будет произведено изменение над всеми bg_bubble на районе в проекте.
    Дело в том, что если пользователь видит 100 сообщений с bg_bubble, это не значит, что имеется 100 копий этого ресурса. Это просто не имеет смысла. В оптимизационных целях, копия хранится лишь одна и поэтому изменения в bg_bubble коснутся сразу всех сообщений.
    Простейшее решение — создать локальную копию:
    Drawable clone = drawable.getConstantState().newDrawable();
    

    Более подробно суть проблемы описывается здесь на другом примере.

    6. Выравнивание TextView по TextView, невзирая на разные размеры/шрифты


    Что? Ничего не понял
    image

    Без лишних слов, сразу ссылку — Watch That Baseline Alignment. А то и две.
    Сказать о baseline ровным счетом нечего, но только когда знаешь, что оно существует. А вот если не знаешь… вот тогда начинается веселье с padding & margin. Лично видел подобное. Даже «зверем» такой код язык не поворачивается назвать.

    7. Spinner без дефолтного значения


    Ситуация

    Есть Spinner для которого нужно добавить «защиту от дурака» в виде подсказки, которая не является значением самого спиннера.
    Spinner с подсказкой
    image

    Можно подумать, что метод Spinner.setPrompt() делает дело, но не тут то было. Работает он только для диалоговых окон, да и не на всех версиях андроида отображается. Что же делать?

    Решение

    «А ничего. Живи с этим» (с) Android SDK.
    Как обычно, необходим хак. Первое, что приходит в голову: добавить 1 элемент с описанием в начала массива. Однако это плохое решение. Мало того, что «подсказку» теперь можно будет выбрать в качестве значения Spinner'а, так ещё и начинаются проблемы при использовать R.array / CursorAdapter.
    И как всегда, самый лучший источник хаков на stackoverflow.
    NothingSelectedSpinnerAdapter
    import android.content.Context;
    import android.database.DataSetObserver;
    import android.view.LayoutInflater;
    import android.view.View;
    import android.view.ViewGroup;
    import android.widget.ListAdapter;
    import android.widget.SpinnerAdapter;
    
    /**
     * Decorator Adapter to allow a Spinner to show a 'Nothing Selected...' initially
     * displayed instead of the first choice in the Adapter.
     */
    public class NothingSelectedSpinnerAdapter implements SpinnerAdapter, ListAdapter {
    
        protected static final int EXTRA = 1;
        protected SpinnerAdapter adapter;
        protected Context context;
        protected int nothingSelectedLayout;
        protected int nothingSelectedDropdownLayout;
        protected LayoutInflater layoutInflater;
    
        /**
         * Use this constructor to have NO 'Select One...' item, instead use
         * the standard prompt or nothing at all.
         * @param spinnerAdapter wrapped Adapter.
         * @param nothingSelectedLayout layout for nothing selected, perhaps
         * you want text grayed out like a prompt...
         * @param context
         */
        public NothingSelectedSpinnerAdapter(
          SpinnerAdapter spinnerAdapter,
          int nothingSelectedLayout, Context context) {
    
            this(spinnerAdapter, nothingSelectedLayout, -1, context);
        }
    
        /**
         * Use this constructor to Define your 'Select One...' layout as the first
         * row in the returned choices.
         * If you do this, you probably don't want a prompt on your spinner or it'll
         * have two 'Select' rows.
         * @param spinnerAdapter wrapped Adapter. Should probably return false for isEnabled(0)
         * @param nothingSelectedLayout layout for nothing selected, perhaps you want
         * text grayed out like a prompt...
         * @param nothingSelectedDropdownLayout layout for your 'Select an Item...' in
         * the dropdown.
         * @param context
         */
        public NothingSelectedSpinnerAdapter(SpinnerAdapter spinnerAdapter,
                int nothingSelectedLayout, int nothingSelectedDropdownLayout, Context context) {
            this.adapter = spinnerAdapter;
            this.context = context;
            this.nothingSelectedLayout = nothingSelectedLayout;
            this.nothingSelectedDropdownLayout = nothingSelectedDropdownLayout;
            layoutInflater = LayoutInflater.from(context);
        }
    
        @Override
        public final View getView(int position, View convertView, ViewGroup parent) {
            // This provides the View for the Selected Item in the Spinner, not
            // the dropdown (unless dropdownView is not set).
            if (position == 0) {
                return getNothingSelectedView(parent);
            }
            return adapter.getView(position - EXTRA, null, parent); // Could re-use
                                                     // the convertView if possible.
        }
    
        /**
         * View to show in Spinner with Nothing Selected
         * Override this to do something dynamic... e.g. "37 Options Found"
         * @param parent
         * @return
         */
        protected View getNothingSelectedView(ViewGroup parent) {
            return layoutInflater.inflate(nothingSelectedLayout, parent, false);
        }
    
        @Override
        public View getDropDownView(int position, View convertView, ViewGroup parent) {
            // Android BUG! http://code.google.com/p/android/issues/detail?id=17128 -
            // Spinner does not support multiple view types
            if (position == 0) {
                return nothingSelectedDropdownLayout == -1 ?
                  new View(context) :
                  getNothingSelectedDropdownView(parent);
            }
    
            // Could re-use the convertView if possible, use setTag...
            return adapter.getDropDownView(position - EXTRA, null, parent);
        }
    
        /**
         * Override this to do something dynamic... For example, "Pick your favorite
         * of these 37".
         * @param parent
         * @return
         */
        protected View getNothingSelectedDropdownView(ViewGroup parent) {
            return layoutInflater.inflate(nothingSelectedDropdownLayout, parent, false);
        }
    
        @Override
        public int getCount() {
            int count = adapter.getCount();
            return count == 0 ? 0 : count + EXTRA;
        }
    
        @Override
        public Object getItem(int position) {
            return position == 0 ? null : adapter.getItem(position - EXTRA);
        }
    
        @Override
        public int getItemViewType(int position) {
            return 0;
        }
    
        @Override
        public int getViewTypeCount() {
            return 1;
        }
    
        @Override
        public long getItemId(int position) {
            return position >= EXTRA ? adapter.getItemId(position - EXTRA) : position - EXTRA;
        }
    
        @Override
        public boolean hasStableIds() {
            return adapter.hasStableIds();
        }
    
        @Override
        public boolean isEmpty() {
            return adapter.isEmpty();
        }
    
        @Override
        public void registerDataSetObserver(DataSetObserver observer) {
            adapter.registerDataSetObserver(observer);
        }
    
        @Override
        public void unregisterDataSetObserver(DataSetObserver observer) {
            adapter.unregisterDataSetObserver(observer);
        }
    
        @Override
        public boolean areAllItemsEnabled() {
            return false;
        }
    
        @Override
        public boolean isEnabled(int position) {
            return position != 0; // Don't allow the 'nothing selected'
                                                 // item to be picked.
        }
    
    }
    


    Пример использования
    Spinner spinner = (Spinner) findViewById(R.id.spinner);
        ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.planets_array, android.R.layout.simple_spinner_item);
        adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
        spinner.setPrompt("Select your favorite Planet!");
    
        spinner.setAdapter(
          new NothingSelectedSpinnerAdapter(
                adapter,
                R.layout.contact_spinner_row_nothing_selected,
                // R.layout.contact_spinner_nothing_selected_dropdown, // Optional
                this));
    



    8. Решето под названием SupportMapFragment


    Ситуация

    По воле случая, нужно было иметь дело с картами… благо, было это на один раз, но даже этого хватило, чтобы облысеть чуть-чуть повырывать волосы из-за стресса.
    Как всегда считая, что SDK абсолютно безбажно и работает как часы, я схватил мем-лик. На тот момент я обрадовался недавно поставленному LeakCanary, похвалил его мысленно и принялся изучать логи. Они были странными (например, была строка com.google.android.gms.location.internal.zzk), но говорили о том, что утекает мой MapFragment. Что же я нашел в итоге после часу изучения своих исходников вдоль и поперёк? Ну, думаю, ответ и так ясен.

    На самом деле..

    А на самом деле виновна SDK и еже с ней. Каюсь, моя ошибка, стоило сразу обратить внимание на странные логи, но как-то не срослось. Логи в LeakCanary часто не особо понятны, кроме последних строчек, где видны именно «свои» ссылки, поэтому всё остальное было благополучно проигнорировано. Лично я столкнулся со следующими проблемами, которые, кстати, схватил за один раз:
    1. Утечка
    2. OutOfMemoryError №1
    3. OutOfMemoryError №2
    4. BadParcelableException

    Особенно неприятен был последний баг. Впервые используя библиотеку Parceler, я решил, что баг в ней или в том, что я неправильно её готовлю использую. Идея о том, что баг возник из-за SupportMapFragment у меня ну никак не возникала — согласитесь, причем тут карты и BadParcelableException, который возникает когда ты лично добавляешь и вытаскиваешь какие-то данные из Bundle? И так я снова потратил несколько часов, изучая исходники Parceler и Bundle.putParcelable() как умалишенный.

    Заключение


    Несмотря на все приведенные здесь проблемы и странности, а также общий тон изложения статьи, мне всё равно нравится программировать под андроид. Да, иногда SDK даёт пощечину-другую, но в целом оно предоставляет много других, хорошо реализованных (!), возможностей. Чего только стоят новые Toolbar, NavigationDrawer и Behavior? Что и говорить о Shared Element Activity Transition!
    Статьей я хотел добиться лишь одного — чтобы если вы ещё не сталкивались с подобными проблемами, столкнувшись, сразу лезли в гугл, а не сидели битый час поуши в дебаге. Я планирую написать ещё 2 части «кюветов»: SDK+libraries и RxJava, но, конечно же, всё зависит от результатов этой части.
    Для новичков, да и для программистов средняков, крайне рекомендую почитать на досуге CodePath Android Cliffnotes. Оно не затрагивает именно «кюветы» (хотя и не без этого), но приводит очень детальное описание всего SDK.
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 21
    • –5
      Как-то раз разрабатывал приложение с помощью ionic, а после этого (уже другое) пересел на native. Косяков и здесь и там хватает, однако такой ад может быть только в native. Для меня странно вручную заниматься обработкой кнопок диалогов, передачей объектов между стейтами, императивным выставлением бекграундов для сообщений. А уж вся эта заваруха с адаптерами и спиннерами, охох...
      • +5
        Недавно назначил один и тот же Drawable фоном двум View — потом не мог понять, почему у одной из View фон «обрезан» — оказалось, всё дело в общих bounds; помог getConstantState().newDrawable(). ДЛя меня разработка для Android — хождение по полю, усеянному невидимыми граблями разной длины; и хорошо, если ударит в грудь. RTFM здесь обязателен, хотя и здесь не без чудес: зачастую документация по методу doSomething() ограничивается фразой «Does something», описания аргументов и возвращаемых значений порой отсутствует или врёт, иногда у метода вообще нет описания.

        Весь SDK написан в нескольких разных стилях, кое-где авторы не знают про существование generics, классы часто сложно или невозможно расширять за пределами простейших вещей типа перехвата прокрутки. Относительно мало готовых компонентов, часто SDK тебе даёт бревно и перочиный ножик, а бруски для стула ты должен делать сам. Как-то понадобилось сделать редактор rich text с вложенными цитатами и блоками кода — оказалось, нет даже какого-нибудь базового RichEditText пришлось кастовать причудливые заклинания со Spannable.

        В общем, на слова «разработка для Android» у меня первая ассоциация — «геморрой». Радует только наличие качественной IDE, за которую спасибо в первую очередь JetBrains, и интегрированный в неё Android Lint.
        • +1
          Самый глубокий кювет для меня был при работе с arm64 девайсом и нативными либами.
          Несмотря на то, что все arm64 девайсы могут без проблем юзать arm-v7 либы, приложение крашилось, т.к. по какой-то причине искало именно x64 версию одной библиотеки.
          После 3 дней курения проблемы, оказалось, что если у вас в проекте есть хоть одна нативная либа собранная для х64, то все остальные нативные библиотеки андроид тоже начинает искать в папке для х64, и победить это нельзя вообще никак, только если заменить все на х64, либо все на arm-v7.
          Прийти к решению было очень непросто, так как напрямую мы подключали только одну нативную библиотеку, о остальные просто втихую использовались какими-то другими зависимостями.
          Так и не нашел где такое поведение задокументировано...
          • 0
            Блин. Я даже не знаю — смеяться или плакать. А в какой документации вы прочитали, что солнце восходит на западе, а заходит на востоке? Где прописано что рыбы плавают, а птицы летают?
            Это ж до какой степень нужно не понимать как в принципе операционки устроены чтобы ожидать, что вы сможете в один процесс грузить и arm64 и arm-v7 либу?
            Не то, что это относится к категории "это в принципе невозможно". Возможно. В программирование вообще мало невозможного. Есть трюки, позволяющие это сделать на некоторых системах (нет, не на arm64, но на x86 — nspluginwrapper и всё такое), но как сказал один мой знакомый: «говорить о том, что надёжность у этих трюков "как у чего-то скрепленного скотчем или проволокой" — это оскорблять надёжные, проверенные строительные инструменты».
            Ни Windows, ни GNU/Linux, ни MacOS, ни вообще какая-бы-то-ни-была известная операционка ничего такого по умолчанию не позволяет и позволять никогда не будет — почему Android должен быть исключением? Потому что у него "главная программа" — это не нативный бинарник, а Java-класс? А почему это должно как-то влиять на нативный код?
            • 0
              Тише, тише.
              Как я уже сказал, в проекте напрямую не было видно, что используются какие-то другие нативные библиотеки.
              О том, что какая-то из подключаемых в Gradle библиотек имеет внутри скомпиленную под arm64 .so'шку, можно было только догадываться.
              К тому же, нет возможности указать какую либу использовать, arm64 или arm-v7. По-моему в ситуации, когда есть 5 либ под v7 и одна под x64 (которая в том числе есть и под v7), было бы логичнее сделать fallback на v7, а не пытаться все 5 найти под v7 и не давать возможности изменить это поведение.
              Так что давайте не будем тут про понимание устройства ОС.
              • 0
                пардон,
                а не пытаться все 5 найти под x64*
                • 0
                  А сколько и каких либ грузится под x64 — это уже ваше приложение решать будет. Хочет — загрузит одну, хочет — все пять. Хозяин — барин.
                • 0
                  По-моему в ситуации, когда есть 5 либ под v7 и одна под x64 (которая в том числе есть и под v7), было бы логичнее сделать fallback на v7, а не пытаться все 5 найти под v7 и не давать возможности изменить это поведение.
                  Это с какого-такого перепугу? Вы включили в APK'шку arm64-v8a библиотеку и, тем самым, сказали Android'у: да, архитектура arm64-v8a мною поддерживается. А что вместо 5 либ у вас теперь одна — ну так кто ж вам судья? Может те 4 либы раньше реализовывали логику, которая сейчас просто не нужна (ну там эмутяцию петабайтных массивов в стиле EMS или еще чего).
                  Никто и никогда не принимает решение «грузить arm64 либу» или «грузить arm-v7 либу».
                  Принимается решение: запускать 32-битный процесс или 64-битный процесс. Всё остальное — следует уже из этого решения. Какая виртуальная машина запускается, сколько памяти выделается и т.д. и т.п. Вы можете вообще никакую нативную библиотеку никогда не грузить — но, тем не менее, получить ограничение в 4GiB памяти на процесс если у вас файлики в arm-v7 каталоге лежат.
                  P.S. Скаживе спасибо, что Android — не iOS. Там эта проблема решена ещё проще: если есть arm-v7 либа, то должна быть и arm-v8 либа, иначе вам опубликовать приложение просто не дадут :-)
              • 0
                Кстати насчёт документации.
                Сейчас пошёл, проверил — всё подробно описано там, где, собственно, и должно быть описано: в разделе «Android Platform ABI support» подробно описано — как и когда выбираются библиотки, следующий раздел «Automatic extraction of native code at install time» подробно объясняет — как и куда они копируются при установке.
                Так что это поведение и логично и подробно описано — не знаю на что у вас ушли три дня...
                • 0
                  Хорошо, по-вашему логично, что у меня есть в проекте arm-v7 библиотека, а Android пытается найти ее х64 версию.
                  Что бывает очевиднее. И да, это поведение там конкретно не описано.
                  When installing an application, the package manager service scans the APK, and looks for any shared libraries of the form:
                  lib/<primary-abi>/lib.so
                  If none is found, and you have defined a secondary ABI, the service scans for shared libraries of the form:
                  lib/<secondary-abi>/lib.so
                  Вот у меня есть secondary ABI, но Android искал только primary.
                  • 0
                    Вот у меня есть secondary ABI, но Android искал только primary.
                    Недосточно того, чтобы был «secondary-abi». Нужно ещё чтобы «primary-abi» отсутствовал как класс (if none is found and… ). Если у вас так было — файлите баг.
                • 0
                  64 битный процесс не может использовать 32 битные .so(.dll). Это справедливо для всех операционных систем, не только андроида. Если в вашем приложении есть хотя-бы одна 64 битная библиотека то система запустит ваше приложение в 64 битном режиме. А определить каких библиотек не хватает, не запуская приложение, сложная задача. Единственное решение которое приходит в голову, это добавить warning в lint что у вас разное количество библиотек на разные архиткетуры. Хотя в некоторых проектах это будет ложным срабатыванием.
                  • 0
                    Можно составить список native экспортов в Java коде и сопоставить с собранными бинарниками, а также проверить, чтобы сами бинарники не потеряли зависимостей (а-ля ldd). Но кто же это писать то будет...
                    • 0
                      Никто не будет потому что это бред. Наличие неразрешённых экспортов в Java коде само по себе ошибкой не является (представьте что у вас есть несколько классов, который, блин, реализуют петабайтные массивы на 32-битной платформе в стиле EMS… на 64-битной платформе они перестанут быть нужны). А вот наличие библиотек, которые нельзя использовать — ошибкой является. Зачем они в вашей .APK'шке если вы никогда и ни при каких условиях не собираетесь их использовать?
                      Потретить кучу времени и сил для того, чтобы кому-то поломать правильно написанную программу — сомнительное удовольствие.
                    • +1
                      Добавление хитрых эвристик в подобные места — плохая затея. Так иногда делают, чтобы "обойти конкурета на повороте" — но это не делает затею менее плохой. Пример — Internet Explorer и его "гениальная" идея о том, что нужно угадывать тип файла если вебмастер идиот и неправильно настроил web-сервер. Помогло в своё время отыграть пару процентов рынка, но в результате теперь все создатели веб-браузеров и все web-разработчики вынуждены это учитывать. Первые — должны реализовывать довольно-таки сложный алгоритм и писать кучу тестов на него. Вторые — должны о нём не забывать и/или постоянно затыкать возникающие из-за него бреши в безопасности. Причём — это уже навсегда.
                      Причём я уверяю вас: они всё равно рано или поздно дадут сбой. И если Dimezis потратил три дня на борьбу с простым и задокументированных подходом, то сбой в сложной программе "вынюхивания" он бы занёс в категорию "злые божества сломали мою программу и починить её теперь нельзя никак, она теперь использовать amd64 не сможет никогда".
                      Не надо так делать.
                  • +1
                    Если глянуть в документацию, то сразу видно, что getFragmentManager() для фрагмента возвращает… FragmentManager родительского активити (не родителя, а именно активити!).
                    Вообще-то в документации сказано:
                    If this Fragment is a child of another Fragment, the FragmentManager returned here will be the parent's getChildFragmentManager().
                    Т.е. для вложенных фрагментов возвращается getChildFragmentManager родительского фрагмента.
                    developer.android.com/intl/ru/reference/android/app/Fragment.html#getFragmentManager%28%29
                    • 0
                      Вы правы. Почему-то помнилось, что оно возвращает именно менджер активити. Исправил.
                    • +2
                      Стиль изложения шикарен — и весело, и без воды.
                      На фразе «А ничего. Живи с этим» подавился чаем :)
                      Плюсовать нечем, но в избранное добавил.
                      Реквестирую обе анонсированные части — ради таких статей собственно и читаю хабр.
                      • 0
                        Жду продолжения.
                        • +1
                          А я пытаюсь запилить облегченный фреймворк, базирующийся на нативных WebView, в отличии от того же PhoneGap или Crosswalk. Так я с определением открыта ли софтверная клавиатура, сначала намучился. Но в итоге сделал гибрид через onclick/onblur на html input и onMeasure на лайоте, на котором развернулся WebView.

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

                          И конечно переопределил onConfigurationChanged, чтобы WebView не создавался заново при смене ориентации.

                          В итоге, у меня WebView крешится. Если открыть софтверную клавиатура, а после начать менять ориентацию устройства, то может уже на 2-3 раз смены ориентации отвалиться. А может после 10 раза ) Правда у меня девайс очень старый на 4.0.3, вот жду Xiaomi на 5.1, может хоть там такого бага не будет. А вот в эмуляторах на всех версиях работает без крешей.
                          • 0
                            Вопрос не в версии устройства. На "очень старых девайсах" может памяти не хватать. Клавиатуру убъют и пересоздатут — а вы этого не заметите. На Galaxy Nexus поэтому очень сложно пользоваться Chrome'ом: ввод чего-либо превращается в ад. Любое действие (ну там сообщение от GMail'а что почта пришла) нарушает хрупкий баланс и клавиатура убирается. После пары тапов — возвращается и можно ввести ещё пару букв...

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.