Действительно ли фрагментам нужен пустой конструктор?

258

У меня есть Fragmentс конструктором, который принимает несколько аргументов. Мое приложение работало нормально во время разработки, но на производстве мои пользователи иногда видят этот сбой:

android.support.v4.app.Fragment$InstantiationException: Unable to instantiate fragment 
make sure class name exists, is public, and has an empty constructor that is public

Я мог бы создать пустой конструктор, как следует из этого сообщения об ошибке, но это не имеет смысла для меня с тех пор, как мне пришлось бы вызывать отдельный метод для завершения настройки Fragment.

Мне любопытно, почему этот сбой случается только изредка. Может я ViewPagerнеправильно использую? Я сам создаю все экземпляры Fragmentи сохраняю их в списке внутри Activity. Я не использую FragmentManagerтранзакции, так как ViewPagerпримеры, которые я видел, не требовали этого, и все, казалось, работало во время разработки.

stkent
источник
22
В некоторых версиях Android (по крайней мере, ICS) вы можете перейти в настройки -> Параметры разработчика и включить «Не сохранять действия». Это даст вам детерминированный способ тестирования случаев, когда необходим конструктор без аргументов.
Кит
У меня была такая же проблема. Вместо этого я назначал данные связки переменным-членам (используя нестандартный ctor). Моя программа не зависала, когда я убивал приложение - это происходило только тогда, когда планировщик помещал мое приложение в дожигатель, чтобы «сэкономить место». Я обнаружил это, перейдя в Task Mgr и открыв тонну других приложений, а затем снова открыв мое приложение в режиме отладки. Он разбился каждый раз. Проблема была решена, когда я использовал ответ Криса Дженкинса, чтобы использовать аргументы связки.
Визурд
Вы можете быть заинтересованы в этой теме: stackoverflow.com/questions/15519214/…
Stefan Haustein
5
Дополнительное примечание для будущих читателей: если ваш Fragmentподкласс вообще не объявляет никаких конструкторов, то по умолчанию для вас неявно создается пустой открытый конструктор (это стандартное поведение Java ). Вам не нужно явно объявлять пустой конструктор, если вы также не объявили другие конструкторы (например, с аргументами).
Тони Чан
Я просто упомяну, что IntelliJ IDEA, по крайней мере для версии 14.1, предоставляет предупреждение, предупреждающее вас о том, что у вас не должно быть конструктора не по умолчанию во фрагменте.
RenniePet

Ответы:

349

Да, они делают.

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

Например:

public static final MyFragment newInstance(int title, String message) {
    MyFragment f = new MyFragment();
    Bundle bdl = new Bundle(2);
    bdl.putInt(EXTRA_TITLE, title);
    bdl.putString(EXTRA_MESSAGE, message);
    f.setArguments(bdl);
    return f;
}

И, конечно, схватить арги таким образом:

@Override
public void onCreate(Bundle savedInstanceState) {
    title = getArguments().getInt(EXTRA_TITLE);
    message = getArguments().getString(EXTRA_MESSAGE);

    //...
    //etc
    //...
}

Затем вы должны создать экземпляр из вашего менеджера фрагментов следующим образом:

@Override
public void onCreate(Bundle savedInstanceState) {
    if (savedInstanceState == null){
        getSupportFragmentManager()
            .beginTransaction()
            .replace(R.id.content, MyFragment.newInstance(
                R.string.alert_title,
                "Oh no, an error occurred!")
            )
            .commit();
    }
}

Таким образом, если отсоединено и повторно присоединено, состояние объекта может быть сохранено через аргументы. Очень похоже на связки, прикрепленные к Intents.

Причина - Дополнительное чтение

Я думал, что объясню, почему людям интересно, почему.

Если вы проверите: https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/app/Fragment.java

Вы увидите, что instantiate(..)метод в Fragmentклассе вызывает newInstanceметод:

public static Fragment instantiate(Context context, String fname, @Nullable Bundle args) {
    try {
        Class<?> clazz = sClassMap.get(fname);
        if (clazz == null) {
            // Class not found in the cache, see if it's real, and try to add it
            clazz = context.getClassLoader().loadClass(fname);
            if (!Fragment.class.isAssignableFrom(clazz)) {
                throw new InstantiationException("Trying to instantiate a class " + fname
                        + " that is not a Fragment", new ClassCastException());
            }
            sClassMap.put(fname, clazz);
        }
        Fragment f = (Fragment) clazz.getConstructor().newInstance();
        if (args != null) {
            args.setClassLoader(f.getClass().getClassLoader());
            f.setArguments(args);
        }
        return f;
    } catch (ClassNotFoundException e) {
        throw new InstantiationException("Unable to instantiate fragment " + fname
                + ": make sure class name exists, is public, and has an"
                + " empty constructor that is public", e);
    } catch (java.lang.InstantiationException e) {
        throw new InstantiationException("Unable to instantiate fragment " + fname
                + ": make sure class name exists, is public, and has an"
                + " empty constructor that is public", e);
    } catch (IllegalAccessException e) {
        throw new InstantiationException("Unable to instantiate fragment " + fname
                + ": make sure class name exists, is public, and has an"
                + " empty constructor that is public", e);
    } catch (NoSuchMethodException e) {
        throw new InstantiationException("Unable to instantiate fragment " + fname
                + ": could not find Fragment constructor", e);
    } catch (InvocationTargetException e) {
        throw new InstantiationException("Unable to instantiate fragment " + fname
                + ": calling Fragment constructor caused an exception", e);
    }
}

http://docs.oracle.com/javase/6/docs/api/java/lang/Class.html#newInstance () Объясняет, почему после создания экземпляра он проверяет, является ли метод доступа publicи что загрузчик классов разрешает доступ к нему.

В целом, это довольно неприятный метод, но он позволяет FragmentMangerубивать и восстанавливать Fragmentsсостояния. (Подсистема Android делает схожие вещи Activities).

Пример класса

Меня часто спрашивают о звонке newInstance. Не путайте это с методом класса. Весь этот пример класса должен показать использование.

/**
 * Created by chris on 21/11/2013
 */
public class StationInfoAccessibilityFragment extends BaseFragment implements JourneyProviderListener {

    public static final StationInfoAccessibilityFragment newInstance(String crsCode) {
        StationInfoAccessibilityFragment fragment = new StationInfoAccessibilityFragment();

        final Bundle args = new Bundle(1);
        args.putString(EXTRA_CRS_CODE, crsCode);
        fragment.setArguments(args);

        return fragment;
    }

    // Views
    LinearLayout mLinearLayout;

    /**
     * Layout Inflater
     */
    private LayoutInflater mInflater;
    /**
     * Station Crs Code
     */
    private String mCrsCode;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mCrsCode = getArguments().getString(EXTRA_CRS_CODE);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        mInflater = inflater;
        return inflater.inflate(R.layout.fragment_station_accessibility, container, false);
    }

    @Override
    public void onViewCreated(View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        mLinearLayout = (LinearLayout)view.findViewBy(R.id.station_info_accessibility_linear);
        //Do stuff
    }

    @Override
    public void onResume() {
        super.onResume();
        getActivity().getSupportActionBar().setTitle(R.string.station_info_access_mobility_title);
    }

    // Other methods etc...
}
Chris.Jenkins
источник
2
Если вы приостановите действие или уничтожите его. Таким образом, вы переходите на домашний экран, а затем Android убивает активность, чтобы сэкономить место. Состояние фрагментов будет сохранено (с использованием аргументов), а затем передано объекту (обычно). Таким образом, по возвращении к упражнению фрагменты должны попытаться воссоздать с использованием сохраненного состояния, нового Default (), затем onCreate и т. Д. Также, если упражнение пытается сохранить ресурсы (низкий уровень памяти). Это может привести к удалению только что приостановленных объектов. .. Commonsguy должен быть в состоянии объяснить лучше. Короче говоря, вы не знаете! :)
Chris.Jenkins
1
@mahkie Действительно, если вам нужно МНОЖЕСТВО объектов / моделей, вы должны получить их асинхронно из базы данных или ContentProvider.
Крис. Дженкинс
1
@ Chris.Jenkins Извините, если мне неясно ... моя точка зрения заключалась в том, что, в отличие от Activity, фрагменты не дают четкого представления о том, что конструкторы не должны использоваться для передачи / совместного использования данных. И хотя сброс / восстановление - это хорошо, я считаю, что хранение нескольких копий данных может иногда занимать больше памяти, чем восстановление восстановления. В некоторых случаях было бы полезно иметь возможность обрабатывать коллекцию Деятельностей / Фрагментов как единое целое, уничтожать их целиком или не уничтожать вообще - тогда мы могли бы передавать данные через конструкторы. На данный момент, что касается этой проблемы, пустой конструктор является единственным.
Каев
3
Зачем вам иметь несколько копий данных? Связки | Parcelable фактически передает ссылку на память, когда это возможно, между состояниями / фрагментами / действиями (на самом деле это вызывает некоторые странные проблемы с состоянием). Единственное время, когда Parcelable фактически эффективно «дублирует» данные, - это процессы и полный жизненный цикл. Например, если вы передаете объект своим фрагментам из вашей деятельности, ваша проходная ссылка не является клоном. Ваша единственная реальная дополнительная нагрузка - это дополнительные объекты фрагмента.
Chris.Jenkins
1
@ Chris.Jenkins Ну, тогда я не знал о Parcelable. Прочитав короткий Javadoc для Parcelable, и часть Parcel, расположенную недалеко от слова «реконструированный», я не дошел до части «Активные объекты», заключив, что это просто более низкоуровневый, более оптимизированный, но менее универсальный Serializable. Настоящим я надеваю шляпу стыда и бормотания: «Все еще не могу делиться неучастными вещами, и создание посылок может быть неприятным» :)
kaay
17

Как отмечает CommonsWare в этом вопросе https://stackoverflow.com/a/16064418/1319061 , эта ошибка также может возникать, если вы создаете анонимный подкласс фрагмента, поскольку у анонимных классов не может быть конструкторов.

Не делайте анонимных подклассов фрагмента :-)

JesperB
источник
1
Или, как упомянул CommonsWare в этом посте, убедитесь, что вы объявили внутренний Activity / Fragment / Reciever как «статический», чтобы избежать этой ошибки.
Тони Уикхем
7

Да, как вы можете видеть, пакет поддержки также создает фрагменты (когда они уничтожаются и открываются заново). Вашим Fragmentподклассам нужен общедоступный пустой конструктор, так как это то, что вызывается фреймворком.

Свейнунг Кваль Баккен
источник
Пустой фрагмент конструктора должен вызывать конструктор super () или нет? Я спрашиваю об этом, так как считаю, что пустой публичный конструктор обязателен. если вызов super () не имеет смысла для пустого открытого конструктора
TNR
@TNR, поскольку все абстракции Fragment имеют пустой конструктор super(), будут бесполезны, так как родительский класс нарушил правило пустого открытого конструктора. Так что нет, вам не нужно проходить super()внутри вашего конструктора.
Chris.Jenkins
4
Фактически это не является обязательным требованием явно определять пустой конструктор во фрагменте. В любом случае у каждого Java-класса есть неявный конструктор по умолчанию. Взято из: docs.oracle.com/javase/tutorial/java/javaOO/constructors.html ~ "Компилятор автоматически предоставляет конструктор по умолчанию без аргументов для любого класса без конструкторов."
Игорь Ганапольский
-6

Вот мое простое решение:

1 - Определите свой фрагмент

public class MyFragment extends Fragment {

    private String parameter;

    public MyFragment() {
    }

    public void setParameter(String parameter) {
        this.parameter = parameter;
    } 
}

2 - Создайте свой новый фрагмент и заполните параметр

    myfragment = new MyFragment();
    myfragment.setParameter("here the value of my parameter");

3 - Наслаждайся этим!

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

Alecs
источник
5
Это не обрабатывает перезагрузку фрагмента системой, хотя.
Видия