41 votes

Comment utiliser les préférences partagées dans le MVP sans Dagger et sans que le Presenter soit dépendant du contexte ?

J'essaie d'implémenter MVP sans Dagger (à des fins d'apprentissage). Mais je me suis heurté à un problème - j'utilise Repository patter pour obtenir des données brutes soit à partir du cache (Shared Preferences), soit à partir du réseau :

Shared Prefs| 
            |<->Repository<->Model<->Presenter<->View
     Network|

Mais pour mettre la main sur les préférences partagées, je dois placer quelque part une ligne du genre

presenter = new Presenter(getApplicationContext());

J'utilise onRetainCustomNonConfigurationInstance / getLastCustomNonConfigurationInstance pour que le présentateur soit "retenu".

public class MyActivity extends AppCompatActivity implements MvpView {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        //...
        presenter = (MvpPresenter) getLastCustomNonConfigurationInstance();

        if(null == presenter){
            presenter = new Presenter(getApplicationContext());
        }

        presenter.attachView(this);
    }

    @Override
    public Object onRetainCustomNonConfigurationInstance() {
        return presenter;
    }

    //...
}

Comment utiliser les préférences partagées dans le MVP sans Dagger et sans que le Presenter soit dépendant du contexte ?

78voto

David Medenjak Points 4553

Votre présentateur ne doit pas être Context dépendante en premier lieu. Si votre présentateur a besoin SharedPreferences vous devriez les faire passer les constructeur .
Si votre présentateur a besoin d'un Repository Encore une fois, il faut mettre cela dans le constructeur . Je vous conseille vivement de regarder Discussions sur le code propre de Google car ils font un très bon travail d'explication pourquoi vous devez utiliser une API appropriée.

Il s'agit d'une bonne gestion des dépendances, qui vous aidera à écrire un code propre, facile à maintenir et à tester. Que vous utilisiez Dagger, un autre outil de gestion des dépendances ou que vous fournissiez les objets vous-même n'a aucune importance.

public class MyActivity extends AppCompatActivity implements MvpView {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        SharedPreferences preferences = // get your preferences
        ApiClient apiClient = // get your network handling object
        Repository repository = new Repository(apiClient, preferences);
        presenter = new Presenter(repository);
    }
}

Cette création d'objets peut être simplifiée par l'utilisation d'un modèle de fabrique, ou d'un cadre DI comme dagger, mais comme vous pouvez le voir ci-dessus, ni l'un ni l'autre n'ont été créés. Repository ni votre présentateur ne dépendent d'un Context . Si vous souhaitez fournir votre SharedPreferences seulement leur création dépend du contexte.

Votre référentiel dépend d'un client API et SharedPreferences votre présentateur dépend de la Repository . Les deux classes peuvent être facilement testées en leur fournissant simplement des objets simulés.

Sans code statique. Sans effets secondaires.

5 votes

Ne serait-il pas préférable que votre code dépende d'une interface de stockage plutôt que d'une implémentation de stockage (comme c'est le cas pour SharedPreferences) ? Avoir une interface rendrait le présentateur vraiment agnostique puisque seule l'implémentation de votre interface de stockage connaitrait le framework Android.

3 votes

@NicolásCarrasco Bien sûr, vous pouvez ajouter une abstraction supplémentaire si et là où c'est nécessaire. Cette réponse visait principalement à montrer comment ne pas être dépendant du contexte et je ne veux pas compliquer les choses à l'excès.

3 votes

@DavidMedenjak Je suis novice en matière de MVP, aidez-moi à comprendre en quoi cette solution est utile, car j'ai l'impression qu'il s'agit de presenter dépend de respository et repository dépend de shared pref ...cela ne signifie-t-il pas presenter dépend de shared pref (indirectement) ou je ne comprends pas l'essentiel

2voto

Much Overflow Points 2241

Voici comment je procède. J'ai une classe singleton "SharedPreferencesManager" qui va gérer toutes les opérations de lecture et d'écriture des préférences partagées comme ci-dessous

public final class SharedPreferencesManager {
    private  static final String MY_APP_PREFERENCES = "ca7eed88-2409-4de7-b529-52598af76734";
    private static final String PREF_USER_LEARNED_DRAWER = "963dfbb5-5f25-4fa9-9a9e-6766bfebfda8";
    ... // other shared preference keys

    private SharedPreferences sharedPrefs;
    private static SharedPreferencesManager instance;

    private SharedPreferencesManager(Context context){
        //using application context just to make sure we don't leak any activities
        sharedPrefs = context.getApplicationContext().getSharedPreferences(MY_APP_PREFERENCES, Context.MODE_PRIVATE);
    }

    public static synchronized SharedPreferencesManager getInstance(Context context){
        if(instance == null)
            instance = new SharedPreferencesManager(context);

        return instance;
    }

    public boolean isNavigationDrawerLearned(){
        return sharedPrefs.getBoolean(PREF_USER_LEARNED_DRAWER, false);
    }

    public void setNavigationDrawerLearned(boolean value){
        SharedPreferences.Editor editor = sharedPrefs.edit();
        editor.putBoolean(PREF_USER_LEARNED_DRAWER, value);
        editor.apply();
    }

    ... // other shared preference accessors
}

Ensuite, lorsque l'accès aux préférences partagées est nécessaire, je passe l'objet SharedPreferencesManager dans le constructeur du présentateur concerné. Par exemple :

if(null == presenter){
    presenter = new Presenter(SharedPreferencesManager.getInstance(getApplicationContext()));
}

J'espère que cela vous aidera !

0 votes

Alors, pourquoi avoir choisi d'en faire un singleton ?

0 votes

Probablement, il l'a fait pour s'assurer qu'il n'existe qu'une seule instance de préfabrication partagée dans toute l'application.

0 votes

@Much : Comment as-tu tapé ca7eed88-2409-4de7-b529-52598af76734 dans String ... ce n'est certainement pas une sorte de raccourci.

0voto

Dinesh Bob Points 1073

Vous pouvez utiliser Application contexte à Repository sans passer par la couche Presenter comme indiqué ici . Commencez par sous-classer votre classe Application et enregistrez son instance dans une variable statique.

public class MyApplication extends Application {
    private static context = null;

    public void onCreate(...) {
        context = this;
        ...
    }

    public static Context getContext() {
        return context;
    }
}

Mentionnez ensuite le nom de votre classe d'application dans le champ AndroidManifest ,

<application
    android:name=".MyApplication"
    ...
    >

</application>

Vous pouvez maintenant utiliser le contexte de l'application dans le référentiel (soit dans les préférences partagées, soit dans la base de données SQLite, soit dans l'accès au réseau), en utilisant la fonction MyApplication.context .

0voto

Denis Loh Points 1226

Une autre approche peut également être trouvée dans les bibliothèques de l'architecture Android :

Étant donné que les préférences partagées dépendent d'un contexte, celui-ci doit en être informé. Pour que tout soit au même endroit, j'ai choisi un Singleton pour gérer cela. Il se compose de deux classes : le gestionnaire (c'est-à-dire le SharePreferenceManager ou ServiceManager ou autre), et un initialisateur qui injecte le contexte.

class ServiceManager {

  private static final ServiceManager instance = new ServiceManager();

  // Avoid mem leak when referencing context within singletons
  private WeakReference<Context> context

  private ServiceManager() {}

  public static ServiceManager getInstance() { return instance; }

  static void attach(Context context) { instance.context = new WeakReference(context); }

  ... your code...

}

L'initialisateur est en fait un Provider ( https://developer.Android.com/guide/topics/providers/content-providers.html ), qui est enregistré dans la base de données de l AndroidManifest.xml et chargé au démarrage de l'application :

public class ServiceManagerInitializer extends ContentProvider {

    @Override
    public boolean onCreate() {
        ServiceManager.init(getContext());

        return false;
    }

    @Nullable
    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
        return null;
    }

    @Nullable
    @Override
    public String getType(@NonNull Uri uri) {
        return null;
    }

    @Nullable
    @Override
    public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
        return null;
    }

    @Override
    public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
        return 0;
    }

    @Override
    public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
        return 0;
    }
}

Toutes les fonctions sont des implémentations par défaut, à l'exception de onCreate, qui injecte le contexte requis dans notre gestionnaire.

La dernière étape consiste à enregistrer le fournisseur dans le manifeste :

<provider
            android:authorities="com.example.service-trojan"
            android:name=".interactor.impl.ServiceManagerInitializer"
            android:exported="false" />

De cette manière, votre gestionnaire de services est découplé de toute initialisation de contexte externe. Il peut maintenant être complètement remplacé par une autre implémentation indépendante du contexte.

0voto

junto Points 651

Voici comment je le mets en œuvre. Vous pouvez le concevoir avec une interface où vous avez une implémentation différente pour votre application et votre test. J'ai utilisé l'interface PersistentStorage que je fournis de manière dépendante de l'interface utilisateur/des tests. Ce n'est qu'une idée, n'hésitez pas à la modifier.

Depuis votre activité/fragment

public static final String PREF_NAME = "app_info_cache";

@Inject
DataManager dataManager;

void injectDepedendency(){
    DaggerAppcompnent.inject(this);//Normal DI withDagger
    dataManager.setPersistentStorage(new PersistentStorageImp(getSharedPreferences()));
}

//In case you need to pass from Fragment then you need to resolve getSharedPreferences with Context
SharedPreferences getSharedPreferences() {
    return getSharedPreferences(PREF_NAME,
            Context.MODE_MULTI_PROCESS | Context.MODE_MULTI_PROCESS);
}

//This is how you can use in Testing

@Inject
DataManager dataManager;

@Before
public void injectDepedendency(){
    DaggerTestAppcompnent.inject(this);
    dataManager.setPersistentStorage(new MockPersistentStorageImp());
}

@Test
public void testSomeFeature_ShouldStoreInfo(){

}

    /**
    YOUR DATAMANAGER
*/

public interface UserDataManager {

    void setPersistentStorage(PersistentStorage persistentStorage);
}

public class UserDataManagerImp implements UserDataManager{
    PersistentStorage persistentStorage;

    public void setPersistentStorage(PersistentStorage persistentStorage){
        this.persistentStorage = persistentStorage;
    }
}

public interface PersistentStorage {
    /**
        Here you can define all the methods you need to store data in preferences.
    */
    boolean getBoolean(String arg, boolean defaultval);

    void putBoolean(String arg, boolean value);

    String getString(String arg, String defaultval);

    void putString(String arg, String value);

}

/**
    PersistentStorage Implementation for Real App
*/
public class PersistentStorageImp implements PersistentStorage {
    SharedPreferences preferences;

    public PersistentStorageImp(SharedPreferences preferences){
        this.preferences = preferences;
    }

    private SharedPreferences getSharedPreferences(){
        return preferences;
    }

    public String getString(String arg, String defaultval) {
        SharedPreferences pref = getSharedPreferences();
        return pref.getString(arg, defaultval);
    }

    public boolean getBoolean(String arg, boolean defaultval) {
        SharedPreferences pref = getSharedPreferences();
        return pref.getBoolean(arg, defaultval);
    }

    public void putBoolean(String arg, boolean value) {
        SharedPreferences pref = getSharedPreferences();
        SharedPreferences.Editor editor = pref.edit();
        editor.putBoolean(arg, value);
        editor.commit();
    }

    public void putString(String arg, String value) {
        SharedPreferences pref = getSharedPreferences();
        SharedPreferences.Editor editor = pref.edit();
        editor.putString(arg, value);
        editor.commit();
    }
}

/**
    PersistentStorage Implementation for testing
*/

public class MockPersistentStorageImp implements PersistentStorage {
    private Map<String,Object> map = new HashMap<>();
    @Override
    public boolean getBoolean(String key, boolean defaultval) {
        if(map.containsKey(key)){
            return (Boolean) map.get(key);
        }
        return defaultval;
    }

    @Override
    public void putBoolean(String key, boolean value) {
        map.put(key,value);
    }

    @Override
    public String getString(String key, String defaultval) {
        if(map.containsKey(key)){
            return (String) map.get(key);
        }
        return defaultval;
    }

    @Override
    public void putString(String key, String value) {
        map.put(key,value);
    }
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X