如何在没有 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...
}
初始化器基本上是一个空的Provider
(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" />
这样,您的服务管理器就与任何外部上下文初始化分离了。它现在可以完全替换为另一个实现 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);
}
}
我正在尝试在没有 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...
}
初始化器基本上是一个空的Provider
(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" />
这样,您的服务管理器就与任何外部上下文初始化分离了。它现在可以完全替换为另一个实现 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);
}
}