Search code examples
javaandroidnullpointerexceptionretrofit2simple-framework

NullPointerException in Retrofit2 after converting response


I'm trying to get list of categories from the XML. I'm using Retrofit2 with SimpleXmlConverter and RxJavaCallAdapter. Getting response is successful, but it contains null object reference. So, when I'm trying to add my list to the RecyclerView's adapter, I get NPE. I think, the problem is in POJO classes.

Here is my observable and corresponding methods:

    restartableLatestCache(GET_CATEGORY_REQUEST,  //method from Nucleus MVP library,
            () -> api.getCategories("ukAXxeJYZN") //API method, returns Observable<Categories> (see below)
                    .subscribeOn(Schedulers.io())
                    .map(Categories::getCategories)
                    .observeOn(mainThread()),
            CategoriesFragment::onItems,
            CategoriesFragment::onNetworkError);

void onItems(List<Category> items) {
    adapter.add(items); //<- exception raises in this method
}

void onNetworkError(Throwable throwable) {
    adapter.hideProgress();
    if (throwable instanceof HttpException) {
        try {
            Log.e(TAG, "Something wrong with your query: " + ((HttpException) throwable).response().errorBody().string() );
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    if (throwable instanceof IOException) {
        Log.e(TAG, "Network Error or bad conversion: ", throwable.getCause());
    }

    Log.e(TAG, "Something goes wrong: ", throwable.getCause());
    Toast.makeText(getActivity(), throwable.getMessage(), Toast.LENGTH_LONG).show();
}

//adapter.add(items);
public void add(List<T> items) {
    int prevSize = this.items.size();
    List<T> list = new ArrayList<>(prevSize + items.size()); <- NPE raises exactly here. items is null, as debugger shows
    list.addAll(this.items);
    list.addAll(items);
    this.items = Collections.unmodifiableList(list);
    notifyItemRangeInserted(prevSize, items.size());
}

There is an exception:

FATAL EXCEPTION: main
              Process: com.example.testapplication, PID: 26290
              java.lang.IllegalStateException: Exception thrown on Scheduler.Worker thread. Add `onError` handling.
                  at rx.android.schedulers.LooperScheduler$ScheduledAction.run(LooperScheduler.java:112)
                  at android.os.Handler.handleCallback(Handler.java:739)
                  at android.os.Handler.dispatchMessage(Handler.java:95)
                  at android.os.Looper.loop(Looper.java:148)
                  at android.app.ActivityThread.main(ActivityThread.java:5438)
                  at java.lang.reflect.Method.invoke(Native Method)
                  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:739)
                  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:629)
               Caused by: rx.exceptions.OnErrorNotImplementedException: Attempt to invoke interface method 'int java.util.List.size()' on a null object reference
                  at rx.internal.util.InternalObservableUtils$ErrorNotImplementedAction.call(InternalObservableUtils.java:386)
                  at rx.internal.util.InternalObservableUtils$ErrorNotImplementedAction.call(InternalObservableUtils.java:383)
                  at rx.internal.util.ActionSubscriber.onError(ActionSubscriber.java:44)
                  at rx.observers.SafeSubscriber._onError(SafeSubscriber.java:157)
                  at rx.observers.SafeSubscriber.onError(SafeSubscriber.java:120)
                  at rx.exceptions.Exceptions.throwOrReport(Exceptions.java:204)
                  at rx.observers.SafeSubscriber.onNext(SafeSubscriber.java:144)
                  at rx.internal.operators.OperatorFilter$FilterSubscriber.onNext(OperatorFilter.java:73)
                  at rx.internal.operators.OnSubscribeCombineLatest$LatestCoordinator.drain(OnSubscribeCombineLatest.java:286)
                  at rx.internal.operators.OnSubscribeCombineLatest$LatestCoordinator.combine(OnSubscribeCombineLatest.java:228)
                  at rx.internal.operators.OnSubscribeCombineLatest$CombinerSubscriber.onNext(OnSubscribeCombineLatest.java:383)
                  at rx.observers.SafeSubscriber.onNext(SafeSubscriber.java:139)
                  at rx.internal.operators.OperatorFilter$FilterSubscriber.onNext(OperatorFilter.java:73)
                  at rx.internal.operators.OperatorMaterialize$ParentSubscriber.onNext(OperatorMaterialize.java:113)
                  at rx.internal.operators.OperatorObserveOn$ObserveOnSubscriber.call(OperatorObserveOn.java:227)
                  at rx.android.schedulers.LooperScheduler$ScheduledAction.run(LooperScheduler.java:107)
                  at android.os.Handler.handleCallback(Handler.java:739) 
                  at android.os.Handler.dispatchMessage(Handler.java:95) 
                  at android.os.Looper.loop(Looper.java:148) 
                  at android.app.ActivityThread.main(ActivityThread.java:5438) 
                  at java.lang.reflect.Method.invoke(Native Method) 
                  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:739) 
                  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:629) 
               Caused by: java.lang.NullPointerException: Attempt to invoke interface method 'int java.util.List.size()' on a null object reference
                  at com.example.testapplication.util.adapters.SimpleListAdapter.add(SimpleListAdapter.java:29)
                  at com.example.testapplication.ui.cat.CategoriesFragment.onItems(CategoriesFragment.java:74)
                  at com.example.testapplication.ui.cat.CategoriesPresenter.-com_example_testapplication_ui_cat_CategoriesPresenter-mthref-1(CategoriesPresenter.java:43)
                  at com.example.testapplication.ui.cat.CategoriesPresenter$-void_onCreate_android_os_Bundle_savedState_LambdaImpl1.call(CategoriesPresenter.java)
                  at nucleus.presenter.delivery.Delivery.split(Delivery.java:26)
                  at nucleus.presenter.RxPresenter$4.call(RxPresenter.java:266)
                  at nucleus.presenter.RxPresenter$4.call(RxPresenter.java:263)
                  at rx.internal.util.ActionSubscriber.onNext(ActionSubscriber.java:39)
                  at rx.observers.SafeSubscriber.onNext(SafeSubscriber.java:139)
                  at rx.internal.operators.OperatorFilter$FilterSubscriber.onNext(OperatorFilter.java:73) 
                  at rx.internal.operators.OnSubscribeCombineLatest$LatestCoordinator.drain(OnSubscribeCombineLatest.java:286) 
                  at rx.internal.operators.OnSubscribeCombineLatest$LatestCoordinator.combine(OnSubscribeCombineLatest.java:228) 
                  at rx.internal.operators.OnSubscribeCombineLatest$CombinerSubscriber.onNext(OnSubscribeCombineLatest.java:383) 
                  at rx.observers.SafeSubscriber.onNext(SafeSubscriber.java:139) 
                  at rx.internal.operators.OperatorFilter$FilterSubscriber.onNext(OperatorFilter.java:73) 
                  at rx.internal.operators.OperatorMaterialize$ParentSubscriber.onNext(OperatorMaterialize.java:113) 
                  at rx.internal.operators.OperatorObserveOn$ObserveOnSubscriber.call(OperatorObserveOn.java:227) 
                  at rx.android.schedulers.LooperScheduler$ScheduledAction.run(LooperScheduler.java:107) 
                  at android.os.Handler.handleCallback(Handler.java:739) 
                  at android.os.Handler.dispatchMessage(Handler.java:95) 
                  at android.os.Looper.loop(Looper.java:148) 
                  at android.app.ActivityThread.main(ActivityThread.java:5438) 
                  at java.lang.reflect.Method.invoke(Native Method) 
                  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:739) 
                  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:629) 

You can take a look at my XML here.

My POJOs:

@Root(strict = false, name = "categories")
public class Categories {

  @Path("yml_catalog/shop")
  @ElementList(inline = true, entry="category", type = Category.class)
  private List<Category> categories;
  //getters, setters
}

@Root(strict = false)
@Path("yml_catalog/shop/categories")
public class Category {
  @Attribute
  private int id;

  @Text
  private String categoryName;
  //getters, setters
}

So, how can I fix my POJOs to get properly converted object?


Solution

  • I've found the solution for my problem, but here I try to explain this problem more widely.

    First of all, when you have, like me, huge XML file which contains separated models you want to extract from it, you must create custom converter. By default SimpleXML works bad with huge XML.

    It should look like this:

    public class CategoriesConverter implements Converter<Categories> {
    
    @Override
    public Categories read(InputNode node) throws Exception {
        Categories categories = new Categories(); 
        InputNode childNode = node.getNext(); //InputNode passed there points to the document root, so we're starting from the next one. 
        List<Category> categoriesList = new ArrayList<>(); 
        while( childNode != null ) { // while we're in the document tree
            if (childNode.getName().equals("categories")) {
                InputNode innerChild = childNode.getNext();
    
                while (innerChild != null) {
                    Category category = new Category();
                    if (innerChild.getName().equals("category")) {
                        category.setId(Integer.parseInt(innerChild.getAttribute("id").getValue()));
                        category.setText(innerChild.getValue());
                        categoriesList.add(category);
                    }
    
                    innerChild = childNode.getNext();
                }
            }
    
            childNode = node.getNext();
        }
    
        categories.setCategories(categoriesList);
        return categories;
    }
    
        @Override
        public void write(OutputNode node, Categories value) throws Exception {
            //we don't need to serialize objects now, so it does nothing
        }
    }
    

    In short, we need to go through the document tree and get attributes and values of the tags we need by hand. When needed, we can fill the list or map also. This approach helps only when you know the structure of the document exactly and repeat it in your models.

    All annotated fields in your model class(@Attribute, @Element, etc) should have param required be set to false, and name param explicitly specified, e.g.:

      @Element(required = false, name = "text")
      private String text;
    

    And model class should be annotated with @Root(strict = false) and with @Convert(MyConverter.class), where MyConverter is the converter you write for this model. Each model should have its own converter.

    To make this working, you need to specify AnnotationStrategy for the SimpeXML. It pays attention to the @Convert annotation. You can do it like this:

    Serializer serializer = new Persister(new AnnotationStrategy());
    

    If you use Retrofit2 with the SimpleXML converter and Dagger, you can write the following:

    @Provides
    @Singleton
    ServerAPI provideServerAPI() {
        return new Retrofit.Builder()
                .baseUrl(ServerAPI.ENDPOINT)
                .client(client)
                .addConverterFactory(SimpleXmlConverterFactory.createNonStrict(new Persister(new AnnotationStrategy())))
                .build().create(ServerAPI.class);
    }
    

    That's it! Hope this helps.