Подтвердить что ты не робот

Как использовать общие настройки в MVP без кинжала и не заставлять Presenter быть зависимым от контекста?

Я пытаюсь реализовать MVP без кинжала (для обучения). Но я столкнулся с проблемой - я использую PEST-репозиторий для получения необработанных данных либо из кеша (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;
    }

    //...
}

Как использовать общие настройки в MVP без кинжала и не заставлять Presenter быть зависимым от контекста?

4b9b3361

Ответ 1

Ваш докладчик не должен быть зависимым от Context в первую очередь. Если вашему докладчику нужны SharedPreferences вы должны передать их в конструктор.
Если вашему докладчику нужен Repository, снова поместите его в конструктор. Я настоятельно рекомендую посмотреть доклады о чистом коде Google, так как они действительно хорошо объясняют, почему вы должны использовать правильный API.

Это правильное управление зависимостями, которое поможет вам написать чистый, поддерживаемый и тестируемый код. И не важно, используете ли вы кинжал, какой-либо другой инструмент 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);
    }
}

Создание этого объекта может быть упрощено с помощью фабричного шаблона или некоторой DI-инфраструктуры, такой как dagger, но, как вы можете видеть выше, ни Repository ни докладчик не зависят от Context. Если вы хотите предоставить ваши фактические SharedPreferences только их создание будет зависеть от контекста.

Ваш репозиторий зависит от некоторого API-клиента и SharedPreferences, ваш докладчик зависит от Repository. Оба класса можно легко протестировать, просто поставив им надуманные объекты.

Без какого-либо статического кода. Без каких-либо побочных эффектов.

Ответ 2

Вот как я это делаю. У меня есть одноэлементный класс 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
}

Затем, когда требуется доступ к общим предпочтениям, я передаю объект SharedPreferencesManager в соответствующем конструкторе Presenter. Например:

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

Надеюсь, это поможет!

Ответ 3

Вы можете использовать контекст Application на уровне Repository, не проходя через Presenter как описано здесь. Первый подкласс класса Application и сохраните его экземпляр в статической переменной.

public class MyApplication extends Application {
    private static context = null;

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

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

Затем укажите название своего приложения в AndroidManifest,

<application
    android:name=".MyApplication"
    ...
    >

</application>

Теперь вы можете использовать контекст приложения внутри репозитория (либо в SharedPreferences, базу данных SQLite, доступ к сети), используя MyApplication.context.

Ответ 4

Другой подход также можно найти в библиотеках архитектуры Android:

Поскольку общие настройки зависят от контекста, он должен знать об этом исключительно. Чтобы все было в одном месте, я выбираю Singleton для управления этим. Он состоит из двух классов: Manager (т.е. SharePreferenceManager или ServiceManager или чего-либо еще) и инициализатор, который внедряет контекст.

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" />

Таким образом, ваш менеджер сервисов отделен от любой внешней инициализации контекста. Теперь его можно полностью заменить другой реализацией, которая не зависит от контекста.

Ответ 5

Вот как я это реализую. Вы можете создать его с помощью интерфейса, в котором у вас будет другая реализация для вашего приложения и теста. Я использовал интерфейс PersistentStorage, который я предоставляю depdencdy из UI/tests. Это просто идея, не стесняйтесь ее изменять.

Из вашей деятельности/фрагмента

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