How to use Shared Preferences in MVP without Dagger and not causing Presenter to be Context dependent?
Asked Answered
S

4

41

I'm trying to implement MVP without Dagger (for learning purposes). But I got to the problem - I use Repository patter to get raw data either from cache (Shared Preferences) or network:

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

But to put my hands on Shared Preferences I have to put somewhere line like

presenter = new Presenter(getApplicationContext());

I use onRetainCustomNonConfigurationInstance/getLastCustomNonConfigurationInstance pair to keep Presenter "retained".

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

    //...
}

So how to use Shared Preferences in MVP without Dagger and not causing Presenter to be Context dependent?

Saleem answered 10/5, 2016 at 11:46 Comment(0)
G
80

Your Presenter should not be Context dependent in the first place. If your presenter needs SharedPreferences you should pass them in the constructor.
If your presenter needs a Repository, again, put that in the constructor. I highly suggest watching Google clean code talks since they do a really good job explaining why you should use a proper API.

This is proper dependency management, which will help you write clean, maintainable, and testable code. And whether you use dagger, some other DI tool, or supply the objects yourself is irrelevant.

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

This object creation can be simplified by using a factory pattern, or some DI framework like dagger, but as you can see above neither Repository nor your presenter depends on a Context. If you want to supply your actual SharedPreferences only their creation of them will depend on the context.

Your repository depends on some API client and SharedPreferences, your presenter depends on the Repository. Both classes can easily be tested by just supplying mocked objects to them.

Without any static code. Without any side effects.

Granjon answered 13/5, 2016 at 17:52 Comment(5)
Would it be much better if you make your code depend on a storage interface rather than on a storage implementation (like SharedPreferences is)? Having an interface would make the presenter truly framework agnostic since only the implementation of your storage interface would know about the Android frameworkDunderhead
@NicolásCarrasco of course you could add further abstraction if and where needed. This answer was primarily to show how not to be context dependent and I don't want to overcomplicate things.Granjon
@DavidMedenjak I m new to MVP ,kindly help me understand how this solution helps..because it feels to me like presenter depends on respository and repository depends on shared pref ...does that not mean presenter depends on shared pref (indirectly) ..OR I m missing the coreOsy
@Osy That's right. But the presenter can now be tested with a mocked repository and has no dependency on the Android framework by itself. You could also switch from SharedPreferences to some other file based settings at any time, without changing the presenter at all. It's about clean code, easy testing, and the single responsibility princibleGranjon
I have the same issue. I need sharedPreferences but also i need to get local strings(context.getString(R.string...) in model. How can i achieve this?Hammerfest
T
1

This is how I do it. I have a singleton "SharedPreferencesManager" class that will handle all the read write operations to shared prefs like below

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
}

Then whenever access to shared preference is needed I pass the SharedPreferencesManager object in the relevant Presenter's constructor. For example :

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

Hope this helps!

Tweeddale answered 10/5, 2016 at 12:29 Comment(3)
So, why did you choose to make it a singleton?Rajiv
probably..,he did so, to make sure only one instance of shared pref exists throughout the appOsy
@Much : How did you typed ca7eed88-2409-4de7-b529-52598af76734 in String ... its definitely not typed some sort of short cutNugent
H
0

Another approach can also be found in the Android Architecture libraries:

As the Shared Preferences depends on a context, it solely should know about it. To have things in one place, I choose a Singleton to manage this. It consists of two classes: the Manager (i.e. the SharePreferenceManager or ServiceManager or whatever), and an initializer which injects the Context.

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...

}

The initializer is basically an empty Provider (https://developer.android.com/guide/topics/providers/content-providers.html), which is registered in the AndroidManifest.xml and loaded when the app starts:

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

All function are default implementations except the onCreate, which injects the required context into our manager.

Last step to get this working is to register the provider in the manifest:

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

This way, your service manager is decoupled from any external context initialization. It now can be completely replaced with another implementation which is context-independent.

Hypertensive answered 25/3, 2018 at 19:33 Comment(0)
C
0

This is how I implement it. You can design it with an interface where you have different implementation for your app and test. I have used interface PersistentStorage which I provide depdencdy from UI/tests. This is just an idea, feel free to modify it.

From your Activity/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);
    }
}
Cerement answered 4/6, 2019 at 8:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.