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)
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With