简体   繁体   中英

NullPointerException using Retrofit library - Android

(Scroll at the end of question to see the final solution)

Playing around with the Retrofit Android library. I am trying to push a POST request into a web server that is supposed to return 3 fields after a successful call to a "/login" method through POST. Information:

  1. End point: http://example.us.es:3456
  2. Server-side method to perform the login: /login
  3. Required parameters to the server-side method: "user" and "password"
  4. HTTP method allowed by server administrator: POST

No matter what values for "user" and "password" the app client enters, the web server should send a single JSONObject containing three fields:

  1. ok : string with values "true" or "false" (false would mean that the credentials were not valid).
  2. msg : would carry a string message in case there was an error (for example, invalid credentials or database error)
  3. data : another JSONObject that in this method contains one single name/value pair, the id_session (string containing the session identifier). Other methods contain several name/value pairs; this is not the case for the login method.

Basing myself in this information, the first thing I did is create a POJO Java method that looks like this:

POJO Method ( LoginInfo_POJO.java )

package com.example.joselopez.prueba1;

import java.util.List;

public class LoginInfo_POJO {
    public String _ok;
    public String _msg;
    public Dataset _dataset;

    class Dataset {
        String _idsession;
    }
}

The next thing I did is create an interface containing the login method (I'd add other methods here after I can successfully log in):

API METHODS IN INTERFACE ( IApiMethods.java )

package com.example.joselopez.prueba1;

import retrofit.http.POST;
import retrofit.http.Query;

public interface IApiMethods {

    // Log-in method
    @FormUrlEncoded
    @POST("/login")
    LoginInfo_POJO logIn(@Query("user") String user,
                    @Query("password") String password);
}

Almost there. Now I create a class that extends from AsyncTask that will perform the networking operations in a separate thread. This class is inside the "MainActivity.java" file.

Main Activity ( MainActivity.java )

...
...


private class BackgroundTask_LogIn extends AsyncTask<Void, Void, LoginInfo_POJO> {
    RestAdapter restAdapter;

    @Override
    protected LoginInfo_POJO doInBackground(Void... params) {
        IApiMethods methods = restAdapter.create(IApiMethods.class);
        LoginInfo_POJO loginInfo = methods.logIn(mUser, mPassword);
        return loginInfo;
    }

    @Override
    protected void onPreExecute() {
        restAdapter = new RestAdapter.Builder()
                .setEndpoint("http://example.us.es:3456")
                .build();
    }

    @Override
    protected void onPostExecute(LoginInfo_POJO loginInfo_pojo) {
        tv.setText("onPostExecute()");
        tv.setText(loginInfo_pojo._ok + "\n\n");
        tv.setText(tv.getText() + loginInfo_pojo._msg + "\n\n");
        tv.setText(tv.getText() + loginInfo_pojo.data.id_sesion);
        }
    }
}

The MainActivity layout contains a single TextView (inside a RelativeLayout) whose id is "textView", and is instantiated in code as "tv" as you will see next. The complete code for MainActivity is:

Main Activity ( MainActivity.java )

package com.example.joselopez.prueba1;

import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.widget.TextView;
import retrofit.RestAdapter;

public class MainActivity extends AppCompatActivity {

TextView tv;
String mUser, mPassword;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_main);
    tv = (TextView) findViewById(R.id.textView);
    mUser = "test_user";
    mPassword = "test_password";

    BackgroundTask_LogIn tryLogin = new BackgroundTask_LogIn();
    tryLogin.execute();
}

private class BackgroundTask_LogIn extends AsyncTask<Void, Void, LoginInfo_POJO> { ... }

Everything should be working. But it is not, and after a bit of debugging I found that the onPostExecute() method inside class BackgroundTask_LogIn stops in line:

for (LoginInfo_POJO.Dataset dataset : loginInfo_pojo._dataset) {

The error thrown is:

com.example.joselopez.prueba1 E/AndroidRuntime﹕ FATAL EXCEPTION: main java.lang.NullPointerException

So I set a breakpoint at this line and guess what? My LoginInfo_POJO instance is holding these values for its internal variables:

  • _ok = null
  • _msg = null
  • _dataset = null

This means my variables aren't being populated from the server response, BUT the connection seems to be successful as the doInBackground method runs entirely and onPostExecute is being called.

So what do you think? Maybe I am not carrying out the POST request the right way?


UPDATE

As @Gaëtan said, I made a huge error in my POJO class; local variable names there MUST be EQUAL to those in the resulting JSON. I said that I was expecting fields "ok", "msg", "data" and "id_session" from the JSON, but the local variables inside my LoginInfo_POJO have names "_ok", "_msg", "_dataset", "_idsession" (notice the leading underscores). This is a huge error from a Retrofit perspective, so rewriting the POJO method accounting for this will eventually solve the problem.

A couple of information about how to use Retrofit:

  1. The name of the fields in your POJO must match the first in the JSON response. Here, your fields are named _ok , _msg and _dataset while the JSON response contains ok , msg and data . You have two options here: either rename the fields to match the JSON response, or use the @SerializedName annotation on each field to give the name of the JSON field.
public class LoginInfo_POJO {
    // If you rename the fields
    public String ok;
    public String msg;
    public List<Dataset> data;

    // If you use annotation
    @SerializedName("ok")
    public String _ok;
    @SerializedName("msg")
    public String _msg;
    @SerializedName("data")
    public String _dataset;
}
  1. Retrofit provide a way to not use AsyncTask . Instead of using a return type for your API method (here LoginInfo_POJO logIn(String, String ), use a last parameter of type Callback<LoginInfo_POJO> . The request will be executed on a background thread by retrofit, and the callback will be called on the main thread when the request is complete (or failed if something went wrong).

See the documentation for more information, in the SYNCHRONOUS VS. ASYNCHRONOUS VS. OBSERVABLE section.

You could try to set the logLevel option to RestAdapter.LogLevel.FULL when constructing your adapter inside your onPreExecute to get perhaps more information on what's actually going on.

From Square Retrofit API Declaration :

If you need to take a closer look at the requests and responses you can easily add logging levels to the RestAdapter with the LogLevel property.

Example with logLevel set to FULL:

    RestAdapter restAdapter = new RestAdapter.Builder()
.setLogLevel(RestAdapter.LogLevel.FULL)
.setEndpoint("https://api.github.com")
.build();

That said, it's true that you don't need AsyncTask with Retrofit.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM