SharedPreferences.onSharedPreferenceChangeListener não está sendo chamado de forma consistente

267

Estou registrando um ouvinte de alteração de preferências como este (na onCreate()minha atividade principal):

SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);

prefs.registerOnSharedPreferenceChangeListener(
   new SharedPreferences.OnSharedPreferenceChangeListener() {
       public void onSharedPreferenceChanged(
         SharedPreferences prefs, String key) {

         System.out.println(key);
       }
});

O problema é que o ouvinte nem sempre é chamado. Ele funciona pelas primeiras vezes que uma preferência é alterada e não é mais chamada até eu desinstalar e reinstalar o aplicativo. Parece que não é necessário reiniciar o aplicativo para corrigi-lo.

Encontrei um segmento de lista de discussão relatando o mesmo problema, mas ninguém realmente respondeu. O que estou fazendo de errado?

synic
fonte

Respostas:

612

Este é sorrateiro. SharedPreferences mantém os ouvintes em um WeakHashMap. Isso significa que você não pode usar uma classe interna anônima como ouvinte, pois ela se tornará o destino da coleta de lixo assim que você sair do escopo atual. Ele funcionará no início, mas, eventualmente, coletará o lixo, será removido do WeakHashMap e deixará de funcionar.

Mantenha uma referência ao ouvinte em um campo da sua classe e você ficará bem, desde que sua instância da classe não seja destruída.

ou seja, em vez de:

prefs.registerOnSharedPreferenceChangeListener(
  new SharedPreferences.OnSharedPreferenceChangeListener() {
  public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
    // Implementation
  }
});

faça isso:

// Use instance field for listener
// It will not be gc'd as long as this instance is kept referenced
listener = new SharedPreferences.OnSharedPreferenceChangeListener() {
  public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
    // Implementation
  }
};

prefs.registerOnSharedPreferenceChangeListener(listener);

A razão pela qual o cancelamento do registro no método onDestroy corrige o problema é porque, para isso, você tinha que salvar o ouvinte em um campo, evitando, portanto, o problema. É salvar o ouvinte em um campo que resolve o problema, não o cancelamento do registro no onDestroy.

ATUALIZAÇÃO : os documentos do Android foram atualizados com avisos sobre esse comportamento. Portanto, o comportamento excêntrico permanece. Mas agora está documentado.

Blanka
fonte
20
Isso estava me matando, pensei que estava enlouquecendo. Obrigado por postar esta solução!
Brad Hein
10
Este post é ENORME, muito obrigado, isso pode ter me custado horas de depuração impossível!
Kevin Gaudin
Impressionante, é exatamente isso que eu precisava. Boa explicação também!
Stealthcopter
Isso já me mordeu, e eu não sabia o que estava acontecendo até ler este post. Obrigado! Boo, Android!
Nakedible
5
Ótima resposta, obrigado. Definitivamente deve ser mencionado nos documentos. code.google.com/p/android/issues/detail?id=48589
Andrey Chernih 25/11
16

esta resposta aceita está ok, pois para mim está criando uma nova instância sempre que a atividade é retomada

Então, que tal manter a referência ao ouvinte dentro da atividade

OnSharedPreferenceChangeListener myPrefListner = new OnSharedPreferenceChangeListener(){
      public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
         // your stuff
      }
};

e em seu onResume e onPause

@Override     
protected void onResume() {
    super.onResume();          
    getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(myPrefListner);     
}



@Override     
protected void onPause() {         
    super.onPause();          
    getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(myPrefListner);

}

isso será muito parecido com o que você está fazendo, exceto que estamos mantendo uma referência rígida.

Samuel
fonte
Por que você usou super.onResume()antes getPreferenceScreen()...?
Yousha Aleayoub
@YoushaAleayoub, leia sobre android.app.supernotcalledexception que é exigido pela implementação do Android.
Samuel
O que você quer dizer? usando super.onResume()é necessário ou usá-lo ANTES getPreferenceScreen()é necessária? porque eu estou falando sobre o lugar certo. cs.dartmouth.edu/~campbell/cs65/lecture05/lecture05.html
Yousha Aleayoub
Lembro-me de lê-lo aqui developer.android.com/training/basics/activity-lifecycle/… , veja o comentário no código. mas colocá-lo na cauda é lógico. mas até agora não enfrentei nenhum problema nisso.
Samuel
Muito obrigado, em todos os outros lugares que encontrei nos métodos onResume () e onPause () que eles registraram thise não listener, causou erro e eu poderia resolver o meu problema. Btw estes dois métodos são públicos agora, não protegidos
Nicolas
16

Como esta é a página mais detalhada para o tópico que eu quero adicionar meu 50ct.

Tive o problema de que o OnSharedPreferenceChangeListener não foi chamado. Minhas Preferências Compartilhadas são recuperadas no início da Atividade principal por:

prefs = PreferenceManager.getDefaultSharedPreferences(this);

Meu código PreferenceActivity é curto e não faz nada, exceto mostrar as preferências:

public class Preferences extends PreferenceActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // load the XML preferences file
        addPreferencesFromResource(R.xml.preferences);
    }
}

Sempre que o botão de menu é pressionado, crio a PreferenceActivity a partir da Atividade principal:

@Override
public boolean onPrepareOptionsMenu(Menu menu) {
    super.onCreateOptionsMenu(menu);
    //start Preference activity to show preferences on screen
    startActivity(new Intent(this, Preferences.class));
    //hook into sharedPreferences. THIS NEEDS TO BE DONE AFTER CREATING THE ACTIVITY!!!
    prefs.registerOnSharedPreferenceChangeListener(this);
    return false;
}

Observe que o registro do OnSharedPreferenceChangeListener precisa ser feito DEPOIS de criar a PreferenceActivity nesse caso; caso contrário, o manipulador na atividade principal não será chamado !!! Levei um tempo doce para perceber que ...

Bim
fonte
9

A resposta aceita cria uma chamada SharedPreferenceChangeListenertoda vez onResume. O @Samuel resolve isso fazendo SharedPreferenceListenerum membro da classe Activity. Mas há uma terceira e mais simples solução que o Google também usa neste codelab . Faça sua classe de atividade implementar a OnSharedPreferenceChangeListenerinterface e substituir onSharedPreferenceChangeda Atividade, tornando efetivamente a Atividade em si SharedPreferenceListener.

public class MainActivity extends Activity implements SharedPreferences.OnSharedPreferenceChangeListener {

    @Override
    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s) {

    }

    @Override
    protected void onStart() {
        super.onStart();
        PreferenceManager.getDefaultSharedPreferences(this)
                .registerOnSharedPreferenceChangeListener(this);
    }

    @Override
    protected void onStop() {
        super.onStop();
        PreferenceManager.getDefaultSharedPreferences(this)
                .unregisterOnSharedPreferenceChangeListener(this);
    }
}
PrashanD
fonte
1
exatamente, deveria ser isso. implemente a interface, registre-se no onStart e cancele o registro no onStop.
Junaed
2

Código Kotlin para o registro SharedPreferenceChangeListener que ele detecta quando a mudança ocorrerá na chave salva:

  PreferenceManager.getDefaultSharedPreferences(this)
        .registerOnSharedPreferenceChangeListener { sharedPreferences, key ->
            if(key=="language") {
                //Do Something 
            }
        }

você pode colocar esse código em onStart () ou em outro lugar .. * Considere que você deve usar

 if(key=="YourKey")

ou seus códigos dentro do bloco "// Do Something" serão executados incorretamente para cada alteração que ocorrer em qualquer outra chave em sharedPreferences

Hamed Jaliliani
fonte
1

Portanto, não sei se isso realmente ajudaria alguém, mas resolvi o meu problema. Mesmo tendo implementado o OnSharedPreferenceChangeListenerque foi declarado pela resposta aceita . Ainda assim, eu tinha uma inconsistência com o ouvinte sendo chamado.

Eu vim aqui para entender que o Android envia para coleta de lixo depois de algum tempo. Então, olhei para o meu código. Para minha vergonha, eu não tinha declarado o ouvinte globalmente, mas dentro do onCreateView. E isso foi porque eu ouvi o Android Studio me dizendo para converter o ouvinte em uma variável local.

Asghar Musani
fonte
0

Faz sentido que os ouvintes sejam mantidos no WeakHashMap. Como na maioria das vezes, os desenvolvedores preferem escrever o código dessa maneira.

PreferenceManager.getDefaultSharedPreferences(getApplicationContext()).registerOnSharedPreferenceChangeListener(
    new OnSharedPreferenceChangeListener() {
    @Override
    public void onSharedPreferenceChanged(
        SharedPreferences sharedPreferences, String key) {
        Log.i(LOGTAG, "testOnSharedPreferenceChangedWrong key =" + key);
    }
});

Isso pode não parecer ruim. Mas se o contêiner OnSharedPreferenceChangeListeners não fosse WeakHashMap, seria muito ruim. Se o código acima foi gravado em uma Activity. Como você está usando uma classe interna não estática (anônima), que contém implicitamente a referência da instância anexa. Isso causará vazamento de memória.

Além disso, se você mantiver o ouvinte como um campo, poderá usar registerOnSharedPreferenceChangeListener no início e chamar unregisterOnSharedPreferenceChangeListener no final. Mas você não pode acessar uma variável local em um método fora de seu escopo. Então você só tem a oportunidade de se registrar, mas não tem a chance de cancelar o registro do ouvinte. Assim, o uso do WeakHashMap resolverá o problema. É assim que eu recomendo.

Se você criar a instância do listener como um campo estático, evitará o vazamento de memória causado pela classe interna não estática. Mas, como os ouvintes podem ser múltiplos, deve ser relacionado à instância. Isso reduzirá o custo de manipulação do retorno de chamada onSharedPreferenceChanged .

androidyue
fonte
-3

Ao ler os dados legíveis do Word compartilhados pelo primeiro aplicativo, devemos

Substituir

getSharedPreferences("PREF_NAME", Context.MODE_PRIVATE);

com

getSharedPreferences("PREF_NAME", Context.MODE_MULTI_PROCESS);

no segundo aplicativo para obter valor atualizado no segundo aplicativo.

Mas ainda não está funcionando ...

shridutt kothari
fonte
O Android não suporta o acesso a SharedPreferences de vários processos. Fazer isso causa problemas de simultaneidade, que podem resultar na perda de todas as preferências. Além disso, MODE_MULTI_PROCESS não é mais suportado.
Sam
@ Sam esta resposta tem 3 anos, por favor, não faça voto negativo, se não estiver funcionando para você nas versões mais recentes do Android. Quando a resposta foi escrita, foi a melhor abordagem para fazê-lo.
shridutt kothari
1
Não, essa abordagem nunca foi segura em vários processos, mesmo quando você escreveu esta resposta.
Sam
Como o @Sam afirma, corretamente, prefs compartilhados nunca foram processados ​​com segurança. Também para shridutt kothari - se você não gostar dos votos negativos, remova sua resposta incorreta (de qualquer maneira, ela não responde à pergunta do OP). No entanto, se você ainda deseja usar prefs compartilhadas de uma maneira segura para o processo, é necessário criar uma abstração segura para o processo acima, por exemplo, um ContentProvider, que é seguro para o processo, e ainda permite usar preferências compartilhadas como mecanismo de armazenamento persistente. , Eu já fiz isso antes e, para pequenos conjuntos de dados / preferências, executa o sqlite por uma margem justa.
Mark Keen
1
@ MarkKeen A propósito, tentei usar provedores de conteúdo para isso e os provedores de conteúdo tendiam a falhar aleatoriamente na Produção devido a erros do Android, então hoje em dia eu apenas uso transmissões para sincronizar a configuração com processos secundários, conforme necessário.
Sam