Os fragmentos realmente precisam de um construtor vazio?

258

Eu tenho um Fragmentcom um construtor que leva vários argumentos. Meu aplicativo funcionou bem durante o desenvolvimento, mas, na produção, meus usuários às vezes veem essa falha:

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

Eu poderia criar um construtor vazio como essa mensagem de erro sugere, mas isso não faz sentido para mim desde então, eu teria que chamar um método separado para concluir a configuração Fragment.

Estou curioso para saber por que esse acidente ocorre ocasionalmente. Talvez eu esteja usando o ViewPagerincorretamente? Eu instanciar todos os Fragments eu mesmo e salvá-los em uma lista dentro do Activity. Não uso FragmentManagertransações, pois os ViewPagerexemplos que eu vi não exigem isso e tudo parecia funcionar durante o desenvolvimento.

stkent
fonte
22
Em algumas versões do Android (pelo menos ICS), você pode acessar as configurações -> opções do desenvolvedor e ativar "Não manter atividades". Fazer isso fornecerá uma maneira determinística de testar os casos em que um construtor não-arg é necessário.
10111 Keith
Eu tive esse mesmo problema. Em vez disso, eu estava atribuindo os dados do pacote às variáveis ​​de membro (usando um ctor não padrão). Meu programa não estava travando quando eu matei o aplicativo - isso só acontecia quando o agendador colocava meu aplicativo no backburner para "economizar espaço". A maneira como descobri isso foi acessando o Gerenciador de tarefas e abrindo vários outros aplicativos, depois reabrindo meu aplicativo na depuração. Falhava toda vez. O problema foi resolvido quando eu usei a resposta de Chris Jenkins para usar argumentos de pacote.
Wizurd 23/07/14
Você pode estar interessado neste tópico: stackoverflow.com/questions/15519214/…
Stefan Haustein
5
Uma observação adicional para futuros leitores: se sua Fragmentsubclasse não declarar nenhum construtor, por padrão, um construtor público vazio será implicitamente feito para você (esse é o comportamento padrão do Java ). Você não precisa declarar explicitamente um construtor vazio, a menos que tenha declarado outros construtores (por exemplo, aqueles com argumentos).
Tony Chan
Mencionarei apenas que o IntelliJ IDEA, pelo menos para a versão 14.1, fornece um aviso alertando você para o fato de que você não deve ter um construtor não padrão em um fragmento.
RenniePet

Respostas:

349

Sim, eles fazem.

Você realmente não deveria substituir o construtor de qualquer maneira. Você deve ter um newInstance()método estático definido e passar quaisquer parâmetros por meio de argumentos (pacote configurável)

Por exemplo:

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;
}

E, claro, pegando os argumentos dessa maneira:

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

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

Então você instanciaria do seu gerenciador de fragmentos da seguinte forma:

@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();
    }
}

Dessa forma, se desanexado e reconectado, o estado do objeto pode ser armazenado através dos argumentos. Muito parecido com pacotes anexados aos Intents.

Razão - Leitura extra

Eu pensei em explicar o porquê das pessoas que se perguntam o porquê.

Se você marcar: https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/app/Fragment.java

Você verá que o instantiate(..)método na Fragmentclasse chama o newInstancemétodo:

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 () Explica por que, por instanciação, verifica se o acessador é publice se esse carregador de classes permite acesso a ele.

É um método bastante desagradável, mas permite FragmentMangermatar e recriar Fragmentscom estados. (O subsistema Android faz coisas semelhantes Activities).

Classe de exemplo

Me perguntam muito sobre ligar newInstance. Não confunda isso com o método de classe. Este exemplo de classe inteira deve mostrar o uso.

/**
 * 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
fonte
2
Se você pausar a atividade ou destruí-la. Então você vai para a tela inicial e a atividade é eliminada pelo Android para economizar espaço. O estado dos fragmentos será salvo (usando os argumentos) e depois gc no objeto (normalmente). Portanto, ao retornar à atividade, os fragmentos devem tentar ser recriados usando o estado salvo, o novo Default () e o onCreate etc ... Além disso, se a atividade estiver tentando economizar recursos (telefone com pouca memória) Ele pode remover os objetos que foram pausados .. Commonsguy deve ser capaz de explicar melhor. Em suma, você não sabe! :)
Chris.Jenkins
1
@mahkie Realmente, se você precisar de MUITO Objetos / Modelos, deve obtê-los de forma assíncrona de um banco de dados ou de um ContentProvider.
Chris.Jenkins
1
@ Chris.Jenkins Desculpe se eu não estava claro ... meu argumento era que, diferentemente das Atividades, os Fragmentos não deixam tão claro que os construtores não devem ser usados ​​para transmitir / compartilhar dados. E embora o despejo / restauração seja bom, acredito que manter várias cópias de dados às vezes pode ocupar mais memória do que a destruição de recuperação pode recuperar. Em alguns casos, pode ser útil ter a opção de tratar uma coleção de Atividades / Fragmentos como uma unidade, para ser destruída como um todo ou de forma alguma - então poderíamos passar dados através de construtores. Por enquanto, com relação a esse problema, um construtor vazio é o único a ter.
Kaay
3
Por que você estaria segurando várias cópias de dados? Pacotes | Parcelable, na verdade, passa a referência de memória quando é possível entre estados / fragmentos / atividades (na verdade, causa alguns problemas de estado estranhos). Por exemplo, se você passar um objeto para seus fragmentos de sua atividade, sua referência de passagem não será um clone. Sua única sobrecarga extra real são os objetos de fragmento adicionais.
precisa saber é o seguinte
1
@ Chris.Jenkins Bem, essa era minha ignorância do Parcelable. Depois de ler o pequeno javadoc de Parcelable, e uma parte do Parcel não muito além da palavra "reconstruído", eu não havia alcançado a parte "Objetos ativos", concluindo que era apenas um serializador de nível mais otimizado, mas menos versátil. Tenho a honra de vestir o chapéu de vergonha e murmurar "ainda não pode compartilhar nonparcelables e fazendo parcelables pode ser um incômodo" :)
Kaay
17

Conforme observado pelo CommonsWare nesta pergunta https://stackoverflow.com/a/16064418/1319061 , esse erro também pode ocorrer se você estiver criando uma subclasse anônima de um Fragmento, pois as classes anônimas não podem ter construtores.

Não faça subclasses anônimas de Fragment :-)

JesperB
fonte
1
Ou, como o CommonsWare mencionado nessa postagem, declare uma Atividade / Fragmento / Recebedor interno como "estática" para evitar esse erro.
Tony Wickham
7

Sim, como você pode ver, o pacote de suporte instancia os fragmentos também (quando eles são destruídos e reabertos). Suas Fragmentsubclasses precisam de um construtor público vazio, pois é isso que está sendo chamado pela estrutura.

Sveinung Kval Bakken
fonte
O Construtor de fragmento vazio deve chamar o super () Construtor ou não? Estou perguntando isso, pois considero que o Construtor público vazio é obrigatório. se chamar super () não faz sentido para o construtor público vazio
TNR
@TNR como todas as abstrações de fragmentos têm um construtor vazio super()seria infrutífero, pois a classe pai quebrou a regra do construtor público vazio. Portanto, não, você não precisa passar super()dentro do seu construtor.
Chris.Jenkins
4
De fato, não é um requisito definir explicitamente um construtor vazio em um Fragmento. Toda classe Java tem um construtor padrão implícito de qualquer maneira. Retirado de: docs.oracle.com/javase/tutorial/java/javaOO/constructors.html ~ "O compilador fornece automaticamente um construtor padrão sem argumento para qualquer classe sem construtores."
IgorGanapolsky
-6

Aqui está minha solução simples:

1 - Defina seu fragmento

public class MyFragment extends Fragment {

    private String parameter;

    public MyFragment() {
    }

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

2 - Crie seu novo fragmento e preencha o parâmetro

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

3 - Aproveite!

Obviamente, você pode alterar o tipo e o número de parâmetros. Rápido e fácil.

Alecs
fonte
5
Porém, isso não lida com o recarregamento do fragmento pelo sistema.
Vidia