Dziwne zachowanie SharedPreferences podczas wyciagania list

0

Cześć. W swojej aplikacji wysyłam requesta GET przy użyciu biblioteki Volley, a wynik requesta zapisuję w SharedPreferences w postaci listy. Dzieje się jednak coś dziwnego, podczas pierwszego naciśnięcia przycisku inicjalizującego wysłanie requesta, zapisanie listy w sharedpreferences, odczytanie listy i ustawienie napisu - wczytywana z SharedPreferences lista jest pusta. Natomiast po ponownym naciśnięciu tego samego przycisku i odpaleniu tej procedury poraz drugi lista jest już poprawnie wczytana z SharedPreferences.

Czy ktoś wie, o co tu może chodzić ? Gdzie jest błąd ?

MainActivity.class

package breakthecode.com.resttraining;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;

import com.android.volley.Request;
import com.android.volley.RequestQueue;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.JsonArrayRequest;
import com.android.volley.toolbox.Volley;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.util.ArrayList;
import java.util.List;

public class SecondActivity extends AppCompatActivity {

    private static String JSON_URL = "***";
    static List<City> listOfCities;
    TextView textView;
    Button btn;

    private AppPreferencesHelper sharedPrefs;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);

        textView = findViewById(R.id.textView);
        btn = findViewById(R.id.btn);
        sharedPrefs = new AppPreferencesHelper(this, "Shared_Preferences");

        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {

                sendRequest();

                }
            });
    }
    private void sendRequest(){
        extractCities();
        List<City> newListOfCities = sharedPrefs.getListOfCities();
        textView.setText("Lista ma dlugosc " + newListOfCities.size());
    }

    private void extractCities(){
        listOfCities = new ArrayList<>();
        RequestQueue queue = Volley.newRequestQueue(this);
        JsonArrayRequest jsonArrayRequest = new JsonArrayRequest(Request.Method.GET, JSON_URL, null, new Response.Listener<JSONArray>() {
            @Override
            public void onResponse(JSONArray response) {
                for (int i = 0; i < response.length(); i++) {
                    try {
                        JSONObject songObject = response.getJSONObject(i);
                        City city = new City();
                        city.setId(songObject.getInt("id"));
                        city.setCityName(songObject.getString("cityName"));
                        city.setBusStopName(songObject.getString("busStopName"));

                        listOfCities.add(city);

                    } catch (JSONException e) {
                        e.printStackTrace();
                    }
                }
                sharedPrefs.setListOfCities(listOfCities);
            }

        }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {

            }
        });
        queue.add(jsonArrayRequest);
    }
}

AppPreferencesHelper.class

package breakthecode.com.resttraining;

import android.content.Context;
import android.content.SharedPreferences;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;

import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;

public class AppPreferencesHelper {
    private static final String LIST_OF_CITIES = "LIST_OF_CITIES";

    private final SharedPreferences mPrefs;
    private SharedPreferences.Editor editor;

    public AppPreferencesHelper(Context context, String prefFileName){
        mPrefs = context.getSharedPreferences(prefFileName, Context.MODE_PRIVATE);
    }

    public List<City> getListOfCities() {
        Gson gson = new Gson();
        String json = mPrefs.getString(LIST_OF_CITIES, null);
        Type type = new TypeToken<List<City>>() {}.getType();
        List<City> listOfCities = gson.fromJson(json, type);
        if(listOfCities == null){
            listOfCities = new ArrayList<City>();
        }
        return listOfCities;
    }

    public void setListOfCities(List<City> listOfCities) {
        editor = mPrefs.edit();
        Gson gson = new Gson();
        String json = gson.toJson(listOfCities);
        editor.putString(LIST_OF_CITIES, json);
        editor.apply();
    }
}
1

Nie ma tu żadnego błędu i wszystko działa poprawnie. Zapytanie wykonane przez Volley jest wykonywane asynchronicznie i wynik zostanie zwrócony przez callback w przyszłości.

extractCities();
List<City> newListOfCities = sharedPrefs.getListOfCities();

getListOfCities() wykona się na 100% po sharedPrefs.setListOfCities(listOfCities); w onResponse() wewnątrz extarctCities(). Więc jak za pierwszym razem wywołujesz getListOfCities() to nie ma jeszcze niczego zapisanego. Dokładniej to zanim nie przyjdzie poprawna odpowiedź z Volley.

0
Michał Sikora napisał(a):

Nie ma tu żadnego błędu i wszystko działa poprawnie. Zapytanie wykonane przez Volley jest wykonywane asynchronicznie i wynik zostanie zwrócony przez callback w przyszłości.

extractCities();
List<City> newListOfCities = sharedPrefs.getListOfCities();

getListOfCities() wykona się na 100% po sharedPrefs.setListOfCities(listOfCities); w onResponse() wewnątrz extarctCities().

Czy mogę wpłynąć jakoś na to, by wyciągnąć listę dopiero wtedy, kiedy wynik zostanie zwrócony ? Doradziłbyś mi jak to obejść, by po jednym naciśnięciu przycisku miał pod ręką dobrą listę wyciągnięta z sharedpreferences ? A może inny sposób "przechwycenia" tego wyniku niż SharedPreferences ?

Właśnie to jest dla mnie dziwne, bo sprawdzając thebuggerem widziałem, że w momencie kiedy wywołuję instrukcję sharedPrefs.setListOfCities(listOfCities); lista listOfCities jest pełna i ma już odpowiedź z Volley, następnie wywołuje get i wyciągam pustą listę :/

1

Trochę dziwne pytanie. Wpłynąć nie możesz. Możesz natomiast użyć tej listy do tego, do czego chcesz w momencie kiedy wynik zostanie zwrócony. Masz dostęp do tej listy w onResponse(), więc tam możesz jej użyć (co nawet robisz podczas zapisu do SharedPreferences).

0
Michał Sikora napisał(a):

Trochę dziwne pytanie. Wpłynąć nie możesz. Możesz natomiast użyć tej listy do tego, do czego chcesz w momencie kiedy wynik zostanie zwrócony. Masz dostęp do tej listy w onResponse(), więc tam możesz jej użyć (co nawet robisz podczas zapisu do SharedPreferences).

Tak wiem, ale potrzebuje jej później do innych rzeczy, nie mogę wszystkiego zrobić w onResponse(). Dziwi mnie troche ta architektura, która nie pozwala w prosty sposób wyciągnąć tej listy z onResponse, tylko trzeba się gimnastykować.

Mówisz, że chodzi tu o to, że odpowiedź z Volley jeszcze nie doszła. Sensownie jest zatem napisanie tego tak, by instrukcja List<City> newListOfCities = sharedPrefs.getListOfCities(); wywołała się np 5 sekund później ? Chodzi tu o czas ?

1

Tak wiem, ale potrzebuje jej później do innych rzeczy, nie mogę wszystkiego zrobić w onResponse(). Dziwi mnie troche ta architektura, która nie pozwala w prosty sposób wyciągnąć tej listy z onResponse, tylko trzeba się gimnastykować.

To nie jest kwestia architektury tylko fizyki. Jeśli na odpowiedź trzeba poczekać np. pół sekundy, to musi te pół sekundy minąć, zanim zaczniesz używać wyniku odpowiedzi. W aplikacjach bez UI często ludzie pozwalają sobie na blokowanie wątku zanim przyjdzie odpowiedź. Jest to zła praktyka, ale wykorzystywana. W aplikacjach z UI, takich jak Android, nie możesz sobie na to pozwolić, bo musiałbyś zablokować główny wątek, co sprawiałoby, że użytkownik nie mógłby wchodzić w interakcję z aplikacją podczas wykonywanego zapytania.

Mówisz, że chodzi tu o to, że odpowiedź z Volley jeszcze nie doszła. Sensownie jest zatem napisanie tego tak, by instrukcja List<City> newListOfCities = sharedPrefs.getListOfCities(); wywołała się np 5 sekund później ? Chodzi tu o czas ?

Tak, o to chodzi. Callbacki, takie jak np. ten przekazywany do Volley służą dokładnie do tego. Zaznaczę tylko, że w średniej wielkości projektach już raczej się z nich dawno odeszło na rzecz takich bibliotek jak RxJava czy mechanizm korutyn wbudowany w Kotlina. Przy czym nie radziłbym Ci sprawdzać tych rozwiązań póki co, bo się prawdopodobnie jeszcze bardziej pogubisz.

0
Michał Sikora napisał(a):

Tak wiem, ale potrzebuje jej później do innych rzeczy, nie mogę wszystkiego zrobić w onResponse(). Dziwi mnie troche ta architektura, która nie pozwala w prosty sposób wyciągnąć tej listy z onResponse, tylko trzeba się gimnastykować.

To nie jest kwestia architektury tylko fizyki. Jeśli na odpowiedź trzeba poczekać np. pół sekundy, to musi te pół sekundy minąć, zanim zaczniesz używać wyniku odpowiedzi. W aplikacjach bez UI często ludzie pozwalają sobie na blokowanie wątku zanim przyjdzie odpowiedź. Jest to zła praktyka, ale wykorzystywana. W aplikacjach z UI, takich jak Android, nie możesz sobie na to pozwolić, bo musiałbyś zablokować główny wątek, co sprawiałoby, że użytkownik nie mógłby wchodzić w interakcję z aplikacją podczas wykonywanego zapytania.

Mówisz, że chodzi tu o to, że odpowiedź z Volley jeszcze nie doszła. Sensownie jest zatem napisanie tego tak, by instrukcja List<City> newListOfCities = sharedPrefs.getListOfCities(); wywołała się np 5 sekund później ? Chodzi tu o czas ?

Tak, o to chodzi. Callbacki, takie jak np. ten przekazywany do Volley służą dokładnie do tego. Zaznaczę tylko, że w średniej wielkości projektach już raczej się z nich dawno odeszło na rzecz takich bibliotek jak RxJava czy mechanizm korutyn wbudowany w Kotlina. Przy czym nie radziłbym Ci sprawdzać tych rozwiązań póki co, bo się prawdopodobnie jeszcze bardziej pogubisz.

Tzn dziwi mnie, że mam dostęp do listy w onResponse, mogę coś z tą listą robić, dodać do sharedpreferences, wyświetlić itp, a np nie mogę przypisać jej do innej, publicznej listy by później wykorzystywać. W dodatku mam listę by dodać ją do sharedpreferences, a instrukcja która wykonuje się później, czyli getList jeszcze nie ma do niej dostępu.
Oki, dzięki wielkie za pomoc ! :)

0
RezyserKinaAkcji napisał(a):

Tzn dziwi mnie, że mam dostęp do listy w onResponse, mogę coś z tą listą robić, dodać do sharedpreferences, wyświetlić itp, a np nie mogę przypisać jej do innej, publicznej listy by później wykorzystywać.

Ależ jak najbardziej możesz. Musisz tylko poczekać, aż wywoła się onResponse().

W dodatku mam listę by dodać ją do sharedpreferences, a instrukcja która wykonuje się później, czyli getList jeszcze nie ma do niej dostępu.

No i wracamy do mojej pierwszej odpowiedzi. getListOfCities() nie wywoła się później od onResponse() tylko wcześniej, bo wynik jest zwracany do onResponse() asynchronicznie i jest obarczony czasem komunikacji z serwerem.

0

Ciężko na to się patrzy co robisz. Nie dość, że Volley zamiast Retrofit, nie dość że Java a nie Kotlin, to nawet nie włączyłeś Javy 8. Nawet w Javie, z Retrofitem, Javą 8 i RxJava z lambdami ten kod by zajął 5 linijek max

0
Michał Sikora napisał(a):
RezyserKinaAkcji napisał(a):

Tzn dziwi mnie, że mam dostęp do listy w onResponse, mogę coś z tą listą robić, dodać do sharedpreferences, wyświetlić itp, a np nie mogę przypisać jej do innej, publicznej listy by później wykorzystywać.

Ależ jak najbardziej możesz. Musisz tylko poczekać, aż wywoła się onResponse().

W dodatku mam listę by dodać ją do sharedpreferences, a instrukcja która wykonuje się później, czyli getList jeszcze nie ma do niej dostępu.

No i wracamy do mojej pierwszej odpowiedzi. getListOfCities() nie wywoła się później od onResponse() tylko wcześniej, bo wynik jest zwracany do onResponse() asynchronicznie i jest obarczony czasem komunikacji z serwerem.

Ahh, ok. Zmyliła mnie trochę ta wypowiedź.

getListOfCities() wykona się na 100% po sharedPrefs.setListOfCities(listOfCities);

Faktycznie, wprowadziłem sobie drugi przycisk do wyciągania z sharedpreferences, który po naciśnięciu parę sekund później wyciąga od razu dobrą listę. Wiesz może, czy jest jakiś sposób by wiedzieć, czy callback nadszedł ? By od niego uzależnić wykonanie instrukcji getListOfCities() ?

0
Meini napisał(a):

Ciężko na to się patrzy co robisz. Nie dość, że Volley zamiast Retrofit, nie dość że Java a nie Kotlin, to nawet nie włączyłeś Javy 8. Nawet w Javie, z Retrofitem, Javą 8 i RxJava z lambdami ten kod by zajął 5 linijek max

No ja rozumiem, że ciężko się na to patrzy, ale mam trzy miesiące na napisanie aplikacji + 50-60 stron pracy inżynierskiej, więc wybacz, że nie będe uczył się teraz nowego języka programowania.

A Retroit działa od API 21, a ja na swoim sprzęcie demonstracyjnym mam API 19 :/

0
RezyserKinaAkcji napisał(a):

A Retroit działa od API 21

Nieprawda.

0
RezyserKinaAkcji napisał(a):

Faktycznie, wprowadziłem sobie drugi przycisk do wyciągania z sharedpreferences, który po naciśnięciu parę sekund później wyciąga od razu dobrą listę. Wiesz może, czy jest jakiś sposób by wiedzieć, czy callback nadszedł ? By od niego uzależnić wykonanie instrukcji getListOfCities() ?

Tak, w momencie kiedy się wywoła… przecież dokładnie do tego służy metoda onResponse(). Jej wykonania oznajmia, że zapytanie do serwera się zakończyło sukcesem i masz dostęp do odpowiedzi z serwera.

0
Michał Sikora napisał(a):
RezyserKinaAkcji napisał(a):

Faktycznie, wprowadziłem sobie drugi przycisk do wyciągania z sharedpreferences, który po naciśnięciu parę sekund później wyciąga od razu dobrą listę. Wiesz może, czy jest jakiś sposób by wiedzieć, czy callback nadszedł ? By od niego uzależnić wykonanie instrukcji getListOfCities() ?

Tak, w momencie kiedy się wywoła… przecież dokładnie do tego służy metoda onResponse(). Jej wykonania oznajmia, że zapytanie do serwera się zakończyło sukcesem i masz dostęp do odpowiedzi z serwera.

Oki, chyba zaczynam rozumieć. Czyli ogólnie nie musiałbym nawet jakoś specjalnie kombinować z sharedPreferences, mógłbym przypisać response jakiejś innej liście, sęk w tym, by nie próbować jej wykorzystać za szybko, bo na innym wątku może się jeszcze nie skończyć wykonywać onResponse() ?

0

W ogólności tak jak piszesz. W szczegółach chyba nie do końca, bo onResponse(), z tego co pamiętam, zwraca wynik już na głównym wątku aplikacji i tylko wykonuje pracę potrzebną na komunikację z serwerem na inny wątku w tle. Nie będzie to miało dla Ciebie znaczenia (a nawet pomoże), ale piszę już dla ścisłości.

0
Michał Sikora napisał(a):

W ogólności tak jak piszesz. W szczegółach chyba nie do końca, bo onResponse(), z tego co pamiętam, zwraca wynik już na głównym wątku aplikacji i tylko wykonuje pracę potrzebną na komunikację z serwerem na inny wątku w tle. Nie będzie to miało dla Ciebie znaczenia (a nawet pomoże), ale piszę już dla ścisłości.

Właśnie sprawdziłem i nie używałem nawet SharedPreferences, tylko po kilku sekundach wyświetliłem zawartość listy do której od początku w onResponse() pakowałem obiekty City i zgodnie z tym co gadamy, lista pełna.

A czy jest jakiś sposób by dowiedzieć się, że onResponse() został wywołany i już mogę się odwoływać do listy którą dostałem w responsie ? Bo używanie w metodzie jakiś flag mija się z celem, skoro onResponse wykonuje się na innym wątku, a ustawianie w programie sztywno "poczekaj 5 sekund zanim się odwołasz do listy" wygląda troche słabo

1

Gdy inicjujesz wywołanie requesta, pokaż jakiś progress, który ukryjesz w onResponse. Na dane i tak trzeba poczekać. Progress ma być z komunikatem typu "proszę czekać". Nie masz czekać 5 sekund, tylko masz czekać na wynik

1

Nie potrzebujesz wiedzieć czy wykonało się onResponse(), tylko czy już masz zapisaną gdzieś odpowiedź. Nie będę Cie prowadził aż tak za rękę, bo wpadnięcie na rozwiązanie tego powinno być trywialne*.

*Ignorując wszystkie problemy związane z wielowątkowością, które prawdopodobnie nie powinny Cię w tym momencie obchodzić. A w każdym razie nie aż tak.

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