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

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

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

1
2
3
Shared Prefs|
            |<->Repository<->Model<->Presenter<->View
     Network|

但要把手放在共享首选项上,我必须把它放在某个地方

1
presenter = new Presenter(getApplicationContext());

我使用onRetainCustomNonConfigurationInstance / getLastCustomNonConfigurationInstance对来保持Presenter"保留"。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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依赖于上下文?


您的Presenter首先不应该Context依赖。如果您的演示者需要SharedPreferences,您应该在构造函数中传递它们。
如果您的演示者需要Repository,请再次将其放入构造函数中。我强烈建议您观看谷歌清洁代码会谈,因为他们非常好地解释了为什么要使用适当的API。

这是正确的依赖关系管理,它将帮助您编写干净,可维护且可测试的代码。
无论你使用匕首,其他一些DI工具,还是自己提供物品都是无关紧要的。

1
2
3
4
5
6
7
8
9
10
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);
    }
}

通过使用工厂模式或某些DI框架(如匕首)可以简化此对象创建,但正如您在上面看到的那样,Repository和您的演示者都不依赖于Context。如果您想提供实际的SharedPreferences,那么它们的创建将取决于上下文。

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

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


我就是这样做的。我有一个单独的"SharedPreferencesManager"类,它将处理对共享prefs的所有读写操作,如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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对象。例如 :

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

希望这可以帮助!


这就是我实现它的方式。您可以使用一个界面来设计它,在该界面中您的应用程序和测试具有不同的实现。我使用了接口PersistentStorage,我从UI /测试中提供了depdencdy。这只是一个想法,随意修改它。

来自您的活动/片段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
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);
    }
}

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

由于共享首选项取决于上下文,因此它应该只知道它。为了把东西放在一个地方,我选择了一个Singleton来管理它。它由两个类组成:Manager(即SharePreferenceManager或ServiceManager或其他),以及注入Context的初始化程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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...

}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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之外,所有函数都是默认实现,它将所需的上下文注入到我们的管理器中。

要使其工作的最后一步是在清单中注册提供程序:

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

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


您可以在Repository层使用Application上下文而不通过Presenter,如此处所述。首先将Application类子类化,并将其实例保存在静态变量中。

1
2
3
4
5
6
7
8
9
10
11
12
public class MyApplication extends Application {
    private static context = null;

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

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

然后在AndroidManifest中提及您的Application类名称,

1
2
3
4
5
6
<application
    android:name=".MyApplication"
    ...
    >

</application>

现在,您可以使用MyApplication.context在Repository中使用Application上下文(对SharedPreferences,SQLite数据库,网络访问)。