如何在MVP中使用Shared Preferences而不依赖Dagger,并且不导致Presenter与Context相关?

41

我正在尝试在没有Dagger的情况下实现MVP(出于学习目的)。但是我遇到了一个问题-我使用Repository模式从缓存(Shared Preferences)或网络获取原始数据:

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

但是要使用Shared Preferences,我必须在某个地方放置类似于以下的代码行

presenter = new Presenter(getApplicationContext());

我使用onRetainCustomNonConfigurationInstance/getLastCustomNonConfigurationInstance这一对功能来保持Presenter“保留”。

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

    //...
}

不使用Dagger,且不使Presenter依赖于Context,如何在MVP中使用Shared Preferences?

4个回答

80

首先,您的Presenter本来就不应该依赖于Context。如果您的Presenter需要使用SharedPreferences,应该在构造函数中传递它们。如果您的Presenter需要一个Repository,同样也需要将其放在构造函数中。我强烈建议观看Google clean code talks,因为他们非常好地解释了为什么您应该使用适当的API。

这是正确的依赖管理,可以帮助您编写干净、可维护和可测试的代码。 无论您是使用dagger、其他DI工具还是自己提供对象,都没有关系。

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

使用工厂模式或像Dagger这样的DI框架,可以简化此对象创建过程。但是如上所述,Repository和您的Presenter都不依赖于Context。如果您想提供实际的SharedPreferences,则只有它们的创建会依赖于上下文。

您的存储库取决于某些API客户端和SharedPreferences,您的Presenter取决于Repository。通过向它们提供模拟对象,两个类都可以轻松地进行测试。

没有任何静态代码。 没有任何副作用。


5
如果您让您的代码依赖于存储接口而不是存储实现(比如 SharedPreferences),会更好吗?有一个接口将使得 Presenter 真正框架无关,因为只有存储接口的实现才知道 Android 框架。 - Nicolás Carrasco-Stevenson
3
当然,如果需要的话,您可以进一步抽象化。这个答案的主要目的是展示如何避免上下文依赖性,我不想使事情过于复杂化。 - David Medenjak
3
@DavidMedenjak,我对MVP不太了解,请帮助我理解这个解决方案的作用。因为对我来说,感觉像是presenter依赖于repository,而repository又依赖于shared pref...... 这是否意味着presenter间接地依赖于shared pref?还是我漏掉了什么核心内容? - eRaisedToX
2
@eRaisedToX 没错。但现在可以使用模拟存储库对演示文稿进行测试,而且本身不依赖于Android框架。您还可以随时从SharedPreferences切换到其他基于文件的设置,而无需更改演示文稿。这是关于清晰的代码、易于测试和单一职责原则。 - David Medenjak
我有同样的问题。我需要使用sharedPreferences,但是在model中我也需要获取本地字符串(context.getString(R.string...))。我该如何实现这一点? - Thracian

1
这是我的做法。我有一个单例“SharedPreferencesManager”类,用来处理所有读写操作共享偏好设置,如下所示:
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
}

每当需要访问共享首选项时,我就会在相关的Presenter构造函数中传递SharedPreferencesManager对象。例如:

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

希望这可以帮到你!

那么,你为什么选择将它设计成单例模式? - MrJre
很可能他这样做是为了确保应用程序中只存在一个共享首选项实例。 - eRaisedToX
@Much:你是怎么在字符串中输入ca7eed88-2409-4de7-b529-52598af76734的?这绝对不是什么快捷方式。 - Yatin

0

还可以在Android Architecture库中找到另一种方法:

由于Shared Preferences依赖于上下文,因此它仅应该知道它。为了将事物放在一个地方,我选择了Singleton来管理它。它由两个类组成:Manager(即SharePreferenceManager或ServiceManager或其他),以及注入上下文的initializer。

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

}

初始化程序基本上是一个空的提供者(https://developer.android.com/guide/topics/providers/content-providers.html),在AndroidManifest.xml中注册,在应用程序启动时加载:

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

所有函数都是默认实现,除了onCreate函数,它将所需的上下文注入到我们的管理器中。
使其正常工作的最后一步是在清单文件中注册提供程序:
<provider
            android:authorities="com.example.service-trojan"
            android:name=".interactor.impl.ServiceManagerInitializer"
            android:exported="false" />

这样,您的服务管理器与任何外部上下文初始化解耦。现在它可以完全被另一个与上下文无关的实现所替换。


0
这是我的实现方式。您可以设计一个接口,其中包含不同的应用程序和测试实现。我使用了名为PersistentStorage的接口,它提供了UI/tests所需的依赖项。这只是一个想法,随意修改。
从您的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);
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接