webentwicklung-frage-antwort-db.com.de

Wie verwende ich gemeinsam genutzte Voreinstellungen in MVP ohne Dolch und dass Presenter nicht kontextabhängig ist?

Ich versuche, MVP ohne Dolch (zu Lernzwecken) zu implementieren. Aber ich bin zu dem Problem gekommen - ich verwende das Repository-Muster, um Rohdaten entweder aus dem Cache (Shared Preferences) oder aus dem Netzwerk zu erhalten:

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

Aber um Shared Preferences in die Finger zu bekommen, muss ich irgendwo eine Zeile setzen

presenter = new Presenter(getApplicationContext());

Ich verwende onRetainCustomNonConfigurationInstance/getLastCustomNonConfigurationInstance Paar, um den Presenter "beizubehalten".

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

    //...
}

Wie verwende ich also die gemeinsamen Einstellungen in MVP ohne Dolch und nicht, dass Presenter kontextabhängig ist?

31

Ihr Presenter sollte in erster Linie nicht Context abhängig sein. Wenn Ihr Präsentator SharedPreferences benötigt, sollten Sie in im -Konstruktor übergeben.
Wenn Ihr Präsentator ein Repository benötigt, setzen Sie das erneut in den -Konstruktor. Ich empfehle dringend, Google Clean Code Talks anzusehen, da sie wirklich gute Arbeit leisten und warum erklären, dass Sie eine geeignete API verwenden sollten.

Dies ist das richtige Abhängigkeitsmanagement, mit dessen Hilfe Sie sauberen, wartbaren und testbaren Code schreiben können .. Und ob Sie Dolch oder ein anderes DI-Tool verwenden oder die Objekte selbst bereitstellen, ist unerheblich.

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

Diese Objekterstellung kann durch Verwendung eines Factory-Patterns oder eines DI-Frameworks wie Dolch vereinfacht werden. Wie Sie jedoch oben sehen können, ist weder Repository noch Ihr Präsentator von einem Context abhängig. Wenn Sie Ihre tatsächliche SharedPreferences angeben möchten, hängen nur deren Erstellung von ihnen vom Kontext ab.

Ihr Repository hängt von einem API-Client und SharedPreferences ab, Ihr Präsentator von Repository. Beide Klassen können einfach getestet werden, indem ihnen nur Scheinobjekte zugeführt werden.

Ohne statischen Code. Ohne Nebenwirkungen.

67
David Medenjak

So mache ich es. Ich habe eine Singleton-Klasse "SharedPreferencesManager", die alle Lese- und Lesevorgänge für gemeinsam genutzte Einstellungen wie unten behandelt

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
}

Wenn der Zugriff auf die gemeinsamen Einstellungen erforderlich ist, übergebe ich das SharedPreferencesManager-Objekt im Konstruktor des betreffenden Presenter. Zum Beispiel :

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

Hoffe das hilft!

2
Much Overflow

So setze ich es um. Sie können es mit einer Oberfläche entwerfen, in der Sie unterschiedliche Implementierungen für Ihre App und Ihren Test haben. Ich habe die Schnittstelle PersistentStorage verwendet, die ich abhängig von der Benutzeroberfläche/den Tests zur Verfügung stelle. Dies ist nur eine Idee, zögern Sie nicht, sie zu ändern.

Aus Ihrer Aktivität/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);
    }
}
0
Mudassar

Sie können Application context auf der Repository-Ebene verwenden, ohne Presenterwie hier erläutert zu durchlaufen. Unterklassen Sie zunächst Ihre Application-Klasse ein und speichern Sie ihre Instanz in einer statischen Variablen.

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

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

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

Erwähnen Sie dann den Namen Ihrer Anwendungsklasse bei AndroidManifest.

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

</application>

Jetzt können Sie den Anwendungskontext im Repository verwenden (entweder für SharedPreferences, SQLite-Datenbank, Netzwerkzugriff), indem Sie MyApplication.context verwenden.

0
Bob

Ein anderer Ansatz kann auch in den Bibliotheken von Android Architecture gefunden werden:

Da die gemeinsamen Einstellungen von einem Kontext abhängen, sollte er nur darüber Bescheid wissen. Um Dinge an einem Ort zu haben, wähle ich ein Singleton, um dies zu verwalten. Es besteht aus zwei Klassen: dem Manager (d. H. Dem SharePreferenceManager oder ServiceManager oder was auch immer) und einem Initialisierer, der den Kontext einfügt.

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

}

Der Initialisierer ist im Grunde eine leere Provider ( https://developer.Android.com/guide/topics/providers/content-providers.html ), die im AndroidManifest.xml registriert und beim Start der App geladen wird:

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

Alle Funktionen sind Standardimplementierungen mit Ausnahme von onCreate, die den erforderlichen Kontext in unseren Manager einfügt.

Um dies zu erreichen, müssen Sie den Anbieter im Manifest registrieren:

<provider
            Android:authorities="com.example.service-trojan"
            Android:name=".interactor.impl.ServiceManagerInitializer"
            Android:exported="false" />

Auf diese Weise ist Ihr Service Manager von der externen Kontextinitialisierung entkoppelt. Sie kann jetzt vollständig durch eine andere kontextunabhängige Implementierung ersetzt werden. 

0
Denis Loh