如何在没有 Dagger 的情况下在 MVP 中使用共享首选项并且不会导致 Presenter 依赖于上下文?

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

我正在尝试在没有 Dagger 的情况下实现 MVP(出于学习目的)。但我遇到了问题 - 我使用 Repository patter 从缓存(共享首选项)或网络获取原始数据:

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

但要将我的手放在“共享首选项”上,我必须在某处添加一行

presenter = new Presenter(getApplicationContext());

我使用 onRetainCustomNonConfigurationInstance/getLastCustomNonConfigurationInstance 对来保留 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;
    }

    //...
}

那么如何在没有 Dagger 的情况下在 MVP 中使用共享首选项并且不会导致 Presenter 依赖于上下文?

我就是这样做的。我有一个单例 "SharedPreferencesManager" class 它将处理所有对共享首选项的读写操作,如下所示

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

希望对您有所帮助!

您的 Presenter 一开始就不应该 Context 依赖。如果您的演示者需要SharedPreferences,您应该将它们传递给构造函数
如果您的演示者需要 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 和你的演示者都不依赖于 Context。如果你想提供你的实际 SharedPreferences 只有它们的 创建 将取决于上下文。

您的存储库依赖于某些 API 客户端和 SharedPreferences,您的演示者依赖于 Repository。 类 都可以通过向它们提供模拟对象来轻松测试。

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

另一种方法也可以在 Android 架构库中找到:

由于 Shared Preferences 取决于上下文,因此它应该只知道它。为了将事情放在一个地方,我选择了一个单例来管理它。它由两个 类 组成:Manager(即 SharePreferenceManager 或 ServiceManager 或其他),以及一个注入 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...

}

初始化器基本上是一个空的Providerhttps://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" />

这样,您的服务管理器就与任何外部上下文初始化分离了。它现在可以完全替换为另一个实现 context-independent。

我就是这样实现的。您可以使用一个界面来设计它,您可以在其中为您的应用程序和测试提供不同的实现。我使用了我从 UI/tests 提供依赖项的接口 PersistentStorage。这只是一个想法,欢迎随时修改。

来自你的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);
    }
}