Dialog Fragment - przywracanie stanu UI

0

Od pewnego czasu walczę z "przywracaniem" stanu UI w oknie dialogowym i niestety z uwagi na mój krótki staż, wszelkie moje próby są nieskuteczne. Poniżej opiszę dwa sposoby, którymi próbowałem rozwiązać mój problem.

Założenia: po kliknięciu przycisku zarządzaj otwiera nam się okno dialogowe z kilkoma widgetami, jednak zarówno górny przycisk zarządzaj jak i dolny przycisk zarządzaj powinny operować na "oddzielnych" dialogach tzn jeśli zaznaczę coś w dialogu numer jeden (z górnego przycisku) to nie chciałbym, żeby zaznaczało się to również w dialogu numer dwa (z dolnego przycisku). Co więcej - chciałbym żeby stan tych okien nie "przepadał" po przejściu do innego activity.
screenshot-20190807164440.png

Pierwszy sposób:
Stworzenie dwóch oddzielnych obiektów DialogFragment inicjalizowanych pojedyńczym parametrem. W ten sposób udaje się rozróżnić okna dialogowe, tak że każde z nich stanowi oddzielny obiekt i nie wpływa na UI drugiego z nich, niestety po zamknięciu dialogu i przejściu do innego activity, a następnie powrót i otwarcie dialogu powoduje, że jego wartość "przepada" i naszym oczom ukazuje się dialog ze stanem początkowym.

Drugi sposób
Do odtworzenia stanu UI w danym oknie dialogowym wykorzystuje Shared Preferences, niestety skutkuje to tym, że stan widgetów zostaje "przywrócony" dla oby dwóch okien dialogowych tzn jeśli w pierwszym z nich (górnym) zaznacze checkbox, a nastepnie otworze dolny to checkbox bedzie zaznaczony, mimo że fizycznie zrobiłem to tylko na pierwszym oknie.

Nie chciałbym wklejać tutaj całego kodu bezmyślnie, dlatego jeśli ktoś zasugeruje mi, którą jego część powinienem umieścić w celu łatwiejszej analizy to bardzo chętnie to zrobię.
Bardzo chciałbym, żeby ktoś bardziej doświadczony "przeprowadził mnie za rękę" przez mój problem, ponieważ nie mam gdzie szukać pomocy - z góry dziękuję.

1

Shared preferences jest ok. Z Twojego opisu wydaje się że do dwóch dialogów podpinasz ten sam obiekt shared preferences z takim samym polem.

Mówiąc bardzo ogólnie: dla każdego dialogu zrób albo inną instację shared prefernces albo w jednym shared preferences zrób różne pola dla różnych dialogów.

1

Koncepcyjnie możesz to zrobić tak. Powinieneś móc osiągnąć to, co chcesz. Do nauki wystarczy, ale niestety taki kod ma ogromny problem. Nie da się go łatwo przetestować, a to znaczy, że wartość tego kodu jest bliska 0.

public final class MainActivity extends AppCompatActivity {
  @Override protected void onCreate(Bundle inState) {
    super.onCreate(inState);
    setContentView(R.layout.activity_main);
    FragmentManager manager = getSupportFragmentManager();
    findViewById(R.id.showFirstDialog).setOnClickListener(new MyDialogFragmentDisplayer(1, manager));
    findViewById(R.id.showSecondDialog).setOnClickListener(new MyDialogFragmentDisplayer(2, manager));
  }

  private static final class MyDialogFragmentDisplayer implements View.OnClickListener {
    private final long id;
    private final FragmentManager manager;

    MyDialogFragmentShower(long id, FragmentManager manager) {
      this.id = id;
      this.manager = manager;
    }

    @Override public void onClick(View view) {
      FragmentTransaction transaction = manager.beginTransaction();
      Fragment fragment = manager.findFragmentByTag(tag);
      if (fragment != null) {
        transaction.remove(fragment);
      }
      transaction.addToBackStack(null);
      MyDialogFragment.create(id).show(transaction, tag);
    }

    private static final String tag = "MyDialogFragment";
  }
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    >

  <Button
      android:id="@+id/showFirstDialog"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:text="Show dialog"
      />

  <Button
      android:id="@+id/showSecondDialog"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:text="Show dialog"
      />

</LinearLayout>
public final class MyDialogFragment extends DialogFragment {
  private UiModelStore store;
  private UiModel uiModel;

  public static MyDialogFragment create(long id) {
    MyDialogFragment fragment = new MyDialogFragment();

    Bundle arguments = new Bundle();
    arguments.putLong(idKey, id);
    fragment.setArguments(arguments);

    return fragment;
  }

  @Override public void onCreate(@Nullable Bundle inState) {
    super.onCreate(inState);
    long id = extractId(getArguments());
    SharedPreferences preferences = requireContext().getSharedPreferences(
        "UiModelPreferences",
        MODE_PRIVATE
    );
    store = new UiModelStore(preferences);
    uiModel = store.read(id);
  }

  @NonNull @Override public View onCreateView(
      @NonNull LayoutInflater inflater,
      @Nullable ViewGroup container,
      @Nullable Bundle inState
  ) {
    View view = inflater.inflate(R.layout.my_dialog_fragment, container, false);

    EditText name = view.findViewById(R.id.name);
    name.setText(uiModel.name);
    name.addTextChangedListener(new AfterTextChangedWatcher() {
      @Override public void afterTextChanged(Editable editable) {
        uiModel = uiModel.withName(editable.toString());
      }
    });

    EditText description = view.findViewById(R.id.description);
    description.setText(uiModel.description);
    description.addTextChangedListener(new AfterTextChangedWatcher() {
      @Override public void afterTextChanged(Editable editable) {
        uiModel = uiModel.withDescription(editable.toString());
      }
    });

    view.findViewById(R.id.cancel).setOnClickListener(new View.OnClickListener() {
      @Override public void onClick(View view) {
        dismiss();
      }
    });

    view.findViewById(R.id.save).setOnClickListener(new View.OnClickListener() {
      @Override public void onClick(View view) {
        store.save(uiModel);
        dismiss();
      }
    });

    return view;
  }

  private long extractId(Bundle arguments) {
    Objects.requireNonNull(arguments, "Missing input arguments.");
    if (!arguments.containsKey(idKey)) {
      throw new IllegalArgumentException("Input arguments must contain ID.");
    }
    return arguments.getLong(idKey);
  }

  private static final String idKey = "MyDialogFragment.Id";

  private static abstract class AfterTextChangedWatcher implements TextWatcher {
    @Override public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
      // No-op
    }

    @Override public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
      // No-op
    }
  }
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    >

  <EditText
      android:id="@+id/name"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:hint="Name"
      />

  <EditText
      android:id="@+id/description"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:hint="Description"
      />

  <Button
      android:id="@+id/cancel"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:text="Cancel"
      />

  <Button
      android:id="@+id/save"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:text="Save"
      />

</LinearLayout>
public final class UiModel {
  public final long id;
  public final String name;
  public final String description;

  public UiModel(long id, String name, String description) {
    this.id = id;
    this.name = name;
    this.description = description;
  }

  public UiModel withName(String name) {
    return new UiModel(id, name, description);
  }

  public UiModel withDescription(String description) {
    return new UiModel(id, name, description);
  }
}
public final class UiModelStore {
  private final SharedPreferences preferences;

  public UiModelStore(SharedPreferences preferences) {
    this.preferences = preferences;
  }

  public UiModel read(long id) {
    long modelId = preferences.getLong(idKey(id), id);
    String modelName = preferences.getString(nameKey(id), "");
    String modelDescription = preferences.getString(descriptionKey(id), "");
    return new UiModel(modelId, modelName, modelDescription);
  }

  public UiModel save(UiModel uiModel) {
    preferences.edit()
        .putLong(idKey(uiModel.id), uiModel.id)
        .putString(nameKey(uiModel.id), uiModel.name)
        .putString(descriptionKey(uiModel.id), uiModel.description)
        .apply();
    return uiModel;
  }

  private static String idKey(long id) {
    return "UiModel.Id." + id;
  }

  private static String nameKey(long id) {
    return "UiModel.Name." + id;
  }

  private static String descriptionKey(long id) {
    return "UiModel.Description." + id;
  }
}
0
lubie_programowac napisał(a):

Shared preferences jest ok. Z Twojego opisu wydaje się że do dwóch dialogów podpinasz ten sam obiekt shared preferences z takim samym polem.

Mówiąc bardzo ogólnie: dla każdego dialogu zrób albo inną instację shared prefernces albo w jednym shared preferences zrób różne pola dla różnych dialogów.

W takim razie spróbuje może coś jeszcze podziałać z tematem w celach edukacyjnych. Z perspektywy laika - czy shared preferences wykorzystywane jest powzsechnie w aplikacjach, czy raczej stosuje się inne rozwiązania?

Michał Sikora napisał(a):

Koncepcyjnie możesz to zrobić tak. Powinieneś móc osiągnąć to, co chcesz. Do nauki wystarczy, ale niestety taki kod ma ogromny problem. Nie da się go łatwo przetestować, a to znaczy, że wartość tego kodu jest bliska 0.

Bardzo dziękuje za kod, niedługo zabiorę się za jego przejrzenie. Chciałbym jednak dopytać, co masz na myśli mówiąc, że takiego kodu nie da się łatwo przetestować?

1
kzkZBK napisał(a):

Chciałbym jednak dopytać, co masz na myśli mówiąc, że takiego kodu nie da się łatwo przetestować?

Kiedy pisze się aplikację, to pisze się też do niej testy, czyli funkcje, które sprawdzają jej poprawność i zabezpieczają nas przed wprowadzaniem nieporządanych zmian w istniejącym już kodzie. Do napisanego przeze mnie kodu takich testów nie da się łatwo napisać. A nawet jakby testy się napisało, to wykonywałyby się nieporównywalnie dłużej od typowych testów.

0
Michał Sikora napisał(a):

Kiedy pisze się aplikację, to pisze się też do niej testy, czyli funkcje, które sprawdzają jej poprawność i zabezpieczają nas przed wprowadzaniem nieporządanych zmian w istniejącym już kodzie. Do napisanego przeze mnie kodu takich testów nie da się łatwo napisać. A nawet jakby testy się napisało, to wykonywałyby się nieporównywalnie dłużej od typowych testów.

Przerobiłem Twój kod pod mój dotychczasowy i działa dokładnie tak jak chicałem, żeby działało! Wielkie dzięki!
Tak jak wspomniałeś, taki kod jest "niepraktyczny", więc z punktu widzenia developera jak rozwiązuje się podobne zagadnienia?
Mógłbyś podać jakieś hasła klucze, pod którymi powinienem szukać? Chętnie doczytam na ten temat

0

Takie ogólne hasła to unit tests, clean architecture, SOLID, design patterns, dependency injection.

Tematy w tym klimacie na forum https://4programmers.net/Forum/Mobilne/306266-architektura_mvp_z_interactorem_i_repository, https://4programmers.net/Forum/Mobilne/315637-androidowe_biblioteki_i_wzorce_projektowe_programistyczne_rozwolnienie

1 użytkowników online, w tym zalogowanych: 0, gości: 1