Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Migrate from SharedPreferences to Jetpack DataStore "Java"

According to this question, I am trying to migrate the current project from SharedPreferences to the dataStore to store the value of layout chosen by the user, the problem is with reading the value, I got NPE, first this my code

The inner DataStoreRepository class

public class Utils {

/*
* Some unrelated codes
*/

public static class DataStoreRepository {
        RxDataStore<Preferences> dataStore;

        public DataStoreRepository(Context context) {
            dataStore =
                    new RxPreferenceDataStoreBuilder(Objects.requireNonNull(context), /*name=*/ "settings").build();
        }


        public Preferences.Key<String> RECYCLER_VIEW_LAYOUT_KEY;


        public void saveValue(String keyName, String value) {

            RECYCLER_VIEW_LAYOUT_KEY = PreferencesKeys.stringKey(keyName);

            dataStore.updateDataAsync(prefsIn -> {
                MutablePreferences mutablePreferences = prefsIn.toMutablePreferences();
                String currentKey = prefsIn.get(RECYCLER_VIEW_LAYOUT_KEY);

                if (currentKey == null) {
                    saveValue(keyName, value);
                }

                mutablePreferences.set(RECYCLER_VIEW_LAYOUT_KEY,
                        currentKey != null ? value : "cardLayout");
                return Single.just(mutablePreferences);
            }).subscribe();
           // The update is completed once updateResult is completed.
        }

        public Flowable<String> readLayoutFlow =
                dataStore.data().map(prefs -> prefs.get(RECYCLER_VIEW_LAYOUT_KEY));

    }
}

and I use it in the fragment like this

first I read the flowable

private Utils.DataStoreRepository dataStoreRepository;
private String layout2;


public View onCreateView(@NonNull LayoutInflater inflater,
                             ViewGroup container, Bundle savedInstanceState) {


        dataStoreRepository = new Utils.DataStoreRepository(requireContext());

            dataStoreRepository.readLayoutFlow.subscribeOn(Schedulers.io())
                    .observeOn(AndroidSchedulers.mainThread())
                    .subscribe(new FlowableSubscriber<String>() {
                        @Override
                        public void onSubscribe(@io.reactivex.rxjava3.annotations.NonNull Subscription s) {

                        }

                        @Override
                        public void onNext(String layout) {
                            layout2 = layout;
                        }

                        @Override
                        public void onError(Throwable t) {
                            Log.e(TAG, "onError: " + t.getMessage());
                        }

                        @Override
                        public void onComplete() {

                        }
                    });

then in the same class I store the value on method changeAndSaveLayout()

private void changeAndSaveLayout() {
        android.app.AlertDialog.Builder builder
                = new android.app.AlertDialog.Builder(getContext());

        builder.setTitle(getString(R.string.choose_layout));

        String[] recyclerViewLayouts = getResources().getStringArray(R.array.RecyclerViewLayouts);
//        SharedPreferences.Editor editor = sharedPreferences.edit();


        builder.setItems(recyclerViewLayouts, (dialog, index) -> {
            switch (index) {
                case 0: // Card List Layout
                    adapter.setViewType(0);
                    binding.homeRecyclerView.setLayoutManager(layoutManager);
                    binding.homeRecyclerView.setAdapter(adapter);
//                    editor.putString("recyclerViewLayout", "cardLayout");
//                    editor.apply();

                    dataStoreRepository.saveValue("recyclerViewLayout","cardLayout");

                    break;
                case 1: // Cards Magazine Layout
                    adapter.setViewType(1);
                    binding.homeRecyclerView.setLayoutManager(layoutManager);
                    binding.homeRecyclerView.setAdapter(adapter);
//                    editor.putString("recyclerViewLayout", "cardMagazineLayout");
//                    editor.apply();
                    dataStoreRepository.saveValue("recyclerViewLayout","cardMagazineLayout");
                    break;
                case 2: // PostTitle Layout
                    adapter.setViewType(2);
                    binding.homeRecyclerView.setLayoutManager(titleLayoutManager);
                    binding.homeRecyclerView.setAdapter(adapter);
//                    editor.putString("recyclerViewLayout", "titleLayout");
//                    editor.apply();
                    dataStoreRepository.saveValue("recyclerViewLayout","titleLayout");
                    break;
                case 3: //Grid Layout
                    adapter.setViewType(3);
                    binding.homeRecyclerView.setLayoutManager(gridLayoutManager);
                    binding.homeRecyclerView.setAdapter(adapter);
//                    editor.putString("recyclerViewLayout", "gridLayout");
//                    editor.apply();
                    dataStoreRepository.saveValue("recyclerViewLayout","gridLayout");

            }
        });

        android.app.AlertDialog alertDialog = builder.create();
        alertDialog.show();
    }

and when I run I got this NPE, and it about reading value readLayoutFlow , I tried to make the creation of the key outside the method like this

public Preferences.Key<String> RECYCLER_VIEW_LAYOUT_KEY = PreferencesKeys.stringKey("recyclerViewLayout");

and in the saveValue() change it's value like this, but it also doesn't work

RECYCLER_VIEW_LAYOUT_KEY.to(keyName);

the output of NPE

 Process: com.blogspot.abtallaldigital, PID: 9184
    java.lang.NullPointerException: Attempt to invoke virtual method 'io.reactivex.rxjava3.core.Flowable androidx.datastore.rxjava3.RxDataStore.data()' on a null object reference
        at com.blogspot.abtallaldigital.utils.Utils$DataStoreRepository.<init>(Utils.java:4)
        at com.blogspot.abtallaldigital.ui.home.HomeFragment.onCreateView(HomeFragment.java:10)
        at androidx.fragment.app.Fragment.performCreateView(Fragment.java:4)
        at androidx.fragment.app.FragmentStateManager.f(FragmentStateManager.java:15)
        at androidx.fragment.app.FragmentStateManager.l(FragmentStateManager.java:20)
        at androidx.fragment.app.FragmentStore.s(FragmentStore.java:3)
        at androidx.fragment.app.FragmentManager.h0(FragmentManager.java:6)
        at androidx.fragment.app.FragmentManager.dispatchStateChange(FragmentManager.java:3)
        at androidx.fragment.app.FragmentManager.G(FragmentManager.java:1)
        at androidx.fragment.app.Fragment.performViewCreated(Fragment.java:2)
        at androidx.fragment.app.FragmentStateManager.f(FragmentStateManager.java:26)
        at androidx.fragment.app.FragmentStateManager.l(FragmentStateManager.java:20)
        at androidx.fragment.app.FragmentStore.s(FragmentStore.java:3)
        at androidx.fragment.app.FragmentManager.h0(FragmentManager.java:6)
        at androidx.fragment.app.FragmentManager.dispatchStateChange(FragmentManager.java:3)
        at androidx.fragment.app.FragmentManager.m(FragmentManager.java:4)
        at androidx.fragment.app.FragmentController.dispatchActivityCreated(FragmentController.java:1)
        at androidx.fragment.app.FragmentActivity.onStart(FragmentActivity.java:6)
        at androidx.appcompat.app.AppCompatActivity.onStart(AppCompatActivity.java:1)
        at android.app.Instrumentation.callActivityOnStart(Instrumentation.java:1435)
        at android.app.Activity.performStart(Activity.java:8018)
        at android.app.ActivityThread.handleStartActivity(ActivityThread.java:3475)
        at android.app.servertransaction.TransactionExecutor.performLifecycleSequence(TransactionExecutor.java:221)
        at android.app.servertransaction.TransactionExecutor.cycleToPath(TransactionExecutor.java:201)
        at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:173)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:97)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2066)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:223)
        at android.app.ActivityThread.main(ActivityThread.java:7656)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)

like image 345
Dr Mido Avatar asked Oct 26 '22 11:10

Dr Mido


1 Answers

The reason for the NullPointerException is the initialization of readLayoutFlow before dataStore.
The code

public Flowable<String> readLayoutFlow =
                dataStore.data().map(prefs -> prefs.get(RECYCLER_VIEW_LAYOUT_KEY));

is evaluated before the constructors' code is executed. Therefore dataStore is null and the NullPointerException is thrown.

An easy solution would be to move the initialization of readLayoutFlow into the constructor like this:

public static class DataStoreRepository {
    RxDataStore<Preferences> dataStore;
    
    public final Flowable<String> readLayoutFlow;

    public DataStoreRepository(Context context) {
        dataStore = new RxPreferenceDataStoreBuilder(
                            Objects.requireNonNull(context),
                            /*name=*/ "settings")
                        .build();

        readLayoutFlow = dataStore.data()
            .map(prefs -> prefs.get(RECYCLER_VIEW_LAYOUT_KEY));
    }

    \\ other code
}

Edit: Blank screen on first launch

There are two different cases that you could mean. I'm going to answer for both:

First start after installing the app

To set the layout on the very first launch I would recommend setting a default value for the preference that is saved at the first start (set a booleanPreferencesKey to check if it is the first start).

Restart of the app

If the blank screen occurs at every new start of the app, you probably have no check for the currently set preference in the fragments onViewCreated. Personally I would use a MutableLiveData<String> recyclerViewLayoutPreference in the Fragment to observe the preference like this:

private MutableLiveData<String> recyclerViewLayoutPreference;

@Override
public void onCreate(/*params*/) {
    super.onCreate(/*params*/);
    recyclerViewLayoutPreferences = new MutableLiveData<>();
}

@Override
public void onViewCreated(/*params*/) {
    super.onViewCreated(/*params*/),

    dataStoreRepository.readLayoutFlow()
        .subscribeOn(Schedulers.io())
        .doOnNext(recyclerViewLayoutPreference::postValue)
        .subscribe();

    recyclerViewLayoutPreference.observe(getViewLifecycleOwner(), layoutString -> {
        if (layoutString == null) return;

        switch (layoutString) {
            case "cardLayout":
                adapter.setViewType(0);
                binding.homeRecyclerView.setLayoutManager(layoutManager);
                    binding.homeRecyclerView.setAdapter(adapter);
            /* other cases */
        }
    });
}

This way you only have to call dataStoreRepository.saveValue(...) in the cases in changeAndSaveLayout and the usage of LiveData keeps the necessary management of Lifecycle Events to a minimum.

like image 67
D. Exter Avatar answered Nov 15 '22 06:11

D. Exter