1

I'm working on a weather app, and currently on the part of developing a search city button. My major aim is to enable the button search and display any city data typed on the edittext.

I have created an edittext and search button, I have also as well connected them with my retrofit parsed classes.

I followed this youtube tutorial for some help https://www.youtube.com/watch?v=SrVY2la7lCI and also got a little help from this post How to get "weather" object data from OpenWeatherMap API Using Retrofit. They were all able to display their weather data for any searched city.

But if i use this address(that they used) on my ApiInterface weather?appid=9c547bfc852923c3b30d0d62a5ae35e8&units=metric, it returns the following error:

java.lang.NullPointerException: Attempt to invoke virtual method 'com.viz.realtimeweather.Retrofit.Main com.com.viz.realtimeweather.Retrofit.Example.getMain()' on a null object reference
at com.viz.realtimeweather.FirstFragment$1.onResponse(FirstFragment.java:109)
at retrofit2.DefaultCallAdapterFactory$ExecutorCallbackCall$1.lambda$onResponse$0$DefaultCallAdapterFactory$ExecutorCallbackCall$1(DefaultCallAdapterFactory.java:89)
at retrofit2.-$$Lambda$DefaultCallAdapterFactory$ExecutorCallbackCall$1$3wC8FyV4pyjrzrYL5U0mlYiviZw.run(Unknown Source:6)
at android.os.Handler.handleCallback(Handler.java:873)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:193)
at android.app.ActivityThread.main(ActivityThread.java:6819)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:497)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:912)

and displays no data.

Then when I added a query to it. i.e q=london, it returned no error but still displays no data. I feel the problem lies somewhere else but I don't know exactly where. Also, that cannot be a solution because I need to enable the app to search any location, not just a particular city.

So far, I have:

  • Checked in and out of all my parsed networking classes for mistakes but found none

  • Checked both my ApiClient and ApiInterface class for any wrong network address but found none

  • Added q=london on my ApiInterface address, but it doesn't display any data nor search another city data

  • Checked this site for any related issue on the weather but found none.

Using https://stackoverflow.com/help/minimal-reproducible-example, I will share my code for help.

I am using OpenWeatherMap API Format:

{
   "coord":{
      "lon":-122.08,
      "lat":37.39
   },
   "weather":[
      {
         "id":800,
         "main":"Clear",
         "description":"clear sky",
         "icon":"01d"
      }
   ],
   "base":"stations",
   "main":{
      "temp":282.55,
      "feels_like":281.86,
      "temp_min":280.37,
      "temp_max":284.26,
      "pressure":1023,
      "humidity":100
   },
   "visibility":16093,
   "wind":{
      "speed":1.5,
      "deg":350
   },
   "clouds":{
      "all":1
   },
   "dt":1560350645,
   "sys":{
      "type":1,
      "id":5122,
      "message":0.0139,
      "country":"US",
      "sunrise":1560343627,
      "sunset":1560396563
   },
   "timezone":-25200,
   "id":420006353,
   "name":"Mountain View",
   "cod":200
}

My JSON response(when I don't add a query):

    {
   "cod":"400",
   "message":"Nothing to geocode"
}

HomeActivity.java:

public class HomeActivity extends AppCompatActivity {
    // User current time
    TextView time_field;
    ImageView Search;
    EditText textfield;
    ConstraintLayout constraintLayout;
    // For scheduling background image change
    public static int count=0;
    int[] drawable =new int[]{R.drawable.dubai,R.drawable.central_bank_of_nigeria,R.drawable.eiffel_tower,R.drawable.hong_kong,R.drawable.statue_of_liberty};
    Timer _t;

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

        time_field = findViewById(R.id.textView9);
        Search = findViewById(R.id.imageView4);
        textfield = findViewById(R.id.textfield);

        BottomNavigationView bottomNavigationView = findViewById(R.id.bottomNavigationView);
        final NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.fragment);
        assert navHostFragment != null;
        final NavController navController = navHostFragment.getNavController();
        NavigationUI.setupWithNavController(bottomNavigationView, navController);

        Search.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {


                getWeatherData(textfield.getText().toString().trim());

                int id = Objects.requireNonNull(navController.getCurrentDestination()).getId();
                navController.popBackStack();
                navController.navigate(id);


                            constraintLayout = findViewById(R.id.layout);
                            constraintLayout.setBackgroundResource(R.drawable.dubai);
                            _t = new Timer();
                            _t.scheduleAtFixedRate(new TimerTask() {
                                @Override
                                public void run() {
                                    // run on ui thread
                                    runOnUiThread(() -> {
                                        if (count < drawable.length) {

                                            constraintLayout.setBackgroundResource(drawable[count]);
                                            count = (count + 1) % drawable.length;
                                        }
                                    });
                                }
                            }, 5000, 5000);
                        }

            private void getWeatherData(String name) {

                ApiInterface apiInterface = ApiClient.getClient().create(ApiInterface.class);

                Call<Example> call = apiInterface.getWeatherData(name);

                call.enqueue(new Callback<Example>() {
                    @Override
                    public void onResponse(@NotNull Call<Example> call, @NotNull Response<Example> response) {

                        assert response.body() != null;
                        time_field.setText(String.valueOf(response.body().getDt()));



                    }

                    @Override
                    public void onFailure(@NotNull Call<Example> call, @NotNull Throwable t) {
                        t.printStackTrace();
                    }


                });
            }




        });
    }
}

First Fragment.java:

public class FirstFragment extends Fragment {
    // User current time, current temperature, current condition, sunrise, sunset, temperature, pressure, humidity, wind_speed, visibility, clouds
    TextView current_temp, current_output, rise_time, set_time, temp_out, Press_out, Humid_out, Ws_out, Visi_out, Cloud_out;
    // TODO: Rename parameter arguments, choose names that match
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
    private static final String ARG_PARAM1 = "param1";
    private static final String ARG_PARAM2 = "param2";

    // TODO: Rename and change types of parameters
    private String mParam1;
    private String mParam2;

    public FirstFragment() {
        // Required empty public constructor
    }

    /**
     * Use this factory method to create a new instance of
     * this fragment using the provided parameters.
     *
     * @param param1 Parameter 1.
     * @param param2 Parameter 2.
     * @return A new instance of fragment SecondFragment.
     */
// TODO: Rename and change types and number of parameters
    public static FirstFragment newInstance(String param1, String param2) {
        FirstFragment fragment = new FirstFragment();
        Bundle args = new Bundle();
        args.putString(ARG_PARAM1, param1);
        args.putString(ARG_PARAM2, param2);
        fragment.setArguments(args);
        return fragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (getArguments() != null) {
            mParam1 = getArguments().getString(ARG_PARAM1);
            mParam2 = getArguments().getString(ARG_PARAM2);

        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        View rootView = inflater.inflate(R.layout.fragment_first, container, false);
        // For displaying weather data
        current_temp = rootView.findViewById(R.id.textView10);
        current_output = rootView.findViewById(R.id.textView11);
        rise_time = rootView.findViewById(R.id.textView25);
        set_time = rootView.findViewById(R.id.textView26);
        temp_out = rootView.findViewById(R.id.textView28);
        Press_out = rootView.findViewById(R.id.textView29);
        Humid_out = rootView.findViewById(R.id.textView30);
        Ws_out = rootView.findViewById(R.id.textView33);
        Visi_out = rootView.findViewById(R.id.textView34);
        Cloud_out = rootView.findViewById(R.id.textView35);

        // Use activity data
        FragmentActivity fa = getActivity();
        assert fa != null;
        EditText textfield = fa.findViewById(R.id.textfield);
        getWeatherData(textfield.getText().toString().trim());
        return rootView;
    }

    private void getWeatherData(String name) {

        ApiInterface apiInterface = ApiClient.getClient().create(ApiInterface.class);

        Call<Example> call = apiInterface.getWeatherData(name);

        call.enqueue(new Callback<Example>() {
            @Override
            public void onResponse(@NotNull Call<Example> call, @NotNull Response<Example> response) {

                assert response.body() !=null;
                current_temp.setText(response.body().getMain().getTemp() + " ℃");
                current_output.setText(response.body().getWeatherList().get(0).getDescription());
                rise_time.setText(response.body().getSys().getSunrise() + " ");
                set_time.setText(response.body().getSys().getSunset() + " ");
                temp_out.setText(response.body().getMain().getTemp() + " ℃");
                Press_out.setText(response.body().getMain().getPressure() + " hpa");
                Humid_out.setText(response.body().getMain().getHumidity() + " %");
                Ws_out.setText(response.body().getWind().getSpeed() + " Km/h");
                Visi_out.setText(response.body().getVisibility() + " m");
                Cloud_out.setText(response.body().getClouds().getAll()+ " %");
            }

            @Override
            public void onFailure(@NotNull Call<Example> call, @NotNull Throwable t) {
                t.printStackTrace();
            }
        });
    }
}

ApiClient.java:

public class ApiClient {

    private static Retrofit retrofit = null;

    public static  Retrofit getClient(){ //creating object

        if (retrofit == null) {

            retrofit = new Retrofit.Builder()
                    .baseUrl("http://api.openweathermap.org/data/2.5/")
                    .addConverterFactory(GsonConverterFactory.create())
                    .build();
        }

        return retrofit;

    }
}

ApiInterface.java:

public interface ApiInterface {

    @GET("weather?appid=9c547bfc852923c3b30d0d62a5ae35e8&units=metric")
    Call<Example> getWeatherData(@Query("q") String name);
}

Example.java:

public class Example {
    @SerializedName("main")
    private Main main;
    @SerializedName("weather")
    private List<Weather> weatherList;
    @SerializedName("visibility")
    private Visibility visibility;
    @SerializedName("wind")
    private Wind wind;
    @SerializedName("clouds")
    private Clouds clouds;
    @SerializedName("dt")
    private Dt dt;
    @SerializedName("sys")
    private Sys sys;
    @SerializedName("name")
    private Name name;

    public Main getMain() {
        return main;
    }

    public void setMain(Main main) {
        this.main = main;
    }

    public List<Weather> getWeatherList() {
        return weatherList;
    }

    public void setWeatherList(List<Weather> weatherList) {
        this.weatherList = weatherList;
    }

    public Visibility getVisibility() {
        return visibility;
    }

    public void setVisibility(Visibility visibility) {
        this.visibility = visibility;
    }

    public Wind getWind() {
        return wind;
    }

    public void setWind(Wind wind) {
        this.wind = wind;
    }

    public Clouds getClouds() {
        return clouds;
    }

    public void setClouds(Clouds clouds) {
        this.clouds = clouds;
    }

    public Dt getDt() {
        return dt;
    }

    public void setDt(Dt dt) {
        this.dt = dt;
    }

    public Sys getSys() {
        return sys;
    }

    public void setSys(Sys sys) {
        this.sys = sys;
    }

    public Name getName() {
        return name;
    }

    public void setName(Name name) {
        this.name = name;
    }
}

Main.java:

public class Main {

    @SerializedName("temp")
    String temp;

    @SerializedName("pressure")
    String pressure;

    @SerializedName("humidity")
    String humidity;

    public String getTemp() {
        return temp;
    }

    public void setTemp(String temp) {
        this.temp = temp;
    }
    public String getPressure() {
        return pressure;
    }

    public void setPressure(String pressure) {
        this.pressure = pressure;
    }

    public String getHumidity() {
        return humidity;
    }

    public void setHumidity(String humidity) {
        this.humidity = humidity;
    }
}

EDIT

dt.java:

public class Dt {

    @SerializedName("dt")
    @Expose
    private PrettyTime dt;

    public PrettyTime getDt() {
        return dt;
    }

    public void setDt(PrettyTime dt) {
        this.dt = dt;
    }
}
Chinez
  • 551
  • 2
  • 6
  • 29
  • 1
    I'm not familiar with Retrofit, but I see that the Response class has an `.isSuccessful()` method and an `.errorBody()` method. Did you check if the response is indeed successful and whether there is an error body? – Ivar Apr 30 '21 at 23:34
  • 1
    ... and what the response code is, and what is >in< the error body. – Stephen C Apr 30 '21 at 23:36
  • How can I check it please? @Ivar I don't know if it is or not – Chinez May 01 '21 at 18:57
  • The response code is the example class and I can't detect any fault there @stephenC – Chinez May 01 '21 at 18:58
  • Please read https://square.github.io/retrofit/2.x/retrofit/retrofit2/Response.html. Particularly the `code()` and `errorBody()` methods. The response code is NOT an object. It is a number. – Stephen C May 02 '21 at 02:24
  • irrelevant to your question but perhaps interesting to know, you don't have to use `@SerializedName("temp")` if the variable name is the same as the api response :) – a_local_nobody May 02 '21 at 18:05
  • okay, please what can I use in place? @a_local_nobody – Chinez May 02 '21 at 18:16
  • Just to verify, the variables in the Main class(temp, pressure, humidity) are declared as String but the API returns a Float/Double. Can you change the type to see if it casts properly? Also, can you post the Dt class? – Droid May 08 '21 at 18:25
  • @Droid please how do I change them? Sure I've posted the Dt class, I used pretty time intentionally in order to convert the time response from i.e 37828929 formats to hh:mm format following https://stackoverflow.com/a/66146060/15804753 but I don't know If I did it rightly – Chinez May 08 '21 at 19:53
  • @BenDreg I cannot see any custom deserializer in your post which is present in the answer you have linked. All I can see is GsonConverterFactory.create() which simply creates a Gson instance without any deserializer. You can change the type from PrettyTime to long just to check if that works or implement the deserializer and be sure to register the type adapter to the gson instance that you would pass to the converter factory. – Droid May 08 '21 at 19:59
  • @droid ok I'll work on that, but let's deal with the most important one first which is to change the type as you suggested, how can it be achieved please? – Chinez May 08 '21 at 20:02
  • @BenDreg it's as simple as changing String temp; to Float temp; and the same for its getter and setter. Repeat this for pressure and humidity properties too. – Droid May 08 '21 at 20:04
  • @droid okay, done it still didn't display any results. I'm working on the dt now – Chinez May 08 '21 at 20:17
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/232127/discussion-between-ben-dreg-and-droid). – Chinez May 08 '21 at 20:26

2 Answers2

2

The problem is that you're receiving a 400 Bad Request from calling http://api.openweathermap.org/data/2.5/weather?appid=9c547bfc852923c3b30d0d62a5ae35e8&units=metric

or even

http://api.openweathermap.org/data/2.5/weather?appid=9c547bfc852923c3b30d0d62a5ae35e8&units=metric&q=

Anything that doesn't fall into the range [200; 300[ retrofit considers an error and will not give you a body(), hence the null pointer because body() is null. On the other hand, errorBody() will have the string you want.

To consume the error body you can simply do errorBody().string() but be careful because it behaves as a stream and can only be consumed once.

As to why your request is falling, that seems to be because you're lacking some query parameters to allow the open weather api to return weather for a given coordinate. Adding a simple q=lisbon seems to solve the issue:

http://api.openweathermap.org/data/2.5/weather?appid=9c547bfc852923c3b30d0d62a5ae35e8&units=metric&q=lisbon

will return 200 OK and retrofit body() method will return something. Maybe you're sending it empty?

Fred
  • 16,367
  • 6
  • 50
  • 65
  • I understand very much how you explained it, but my main purpose is to be able to search for any city and get the data, not just one city. I.e using q=Lisbon will not give an error but will not even display the data for that city talk less of another city, I tried it before. That's why I gave an example with this guy https://stackoverflow.com/questions/65005292/how-to-get-weather-object-data-from-openweathermap-api-using-retrofit he used the exact apiclient and interface as mine without q yet he was able to get results. I believe there's a way to get results without using q – Chinez May 03 '21 at 08:23
  • I'm not sure I understand... according to the post you linked the same apiclient uses `q`. If you look at the official [docs](https://openweathermap.org/current) there are indeed multiple ways of getting it geolocated - city id, city name, coordinates, zip code, etc. The point here is that you're using the `q` parameter according to your apiclient and this might be empty, hence the error you get. Also, using `q=lisbon` returns the json for the weather in Lisbon. If you're not displaying it maybe you have another issue somewhere else? I tried this before too and it works. – Fred May 03 '21 at 09:28
  • Yes, using q=lisbon doesn't display anything and I won't be able to search for another city even if it displays, that's why I need a similar setup with the post I linked otherwise I won't be able to achieve my main aim. Maybe I should share my activity class too – Chinez May 03 '21 at 14:19
  • Not sure, in the end I think it goes a bit beyond the question you asked. Maybe open a new one. This was about why you're getting a null response body. – Fred May 03 '21 at 15:55
  • Totally not. Because I used q=London on my setup even before I asked this question. I knew about using q since but that doesn't solve my problem – Chinez May 03 '21 at 19:58
  • What I meant is that your question was about why you're getting no response. Now that you do get a response but nothing is displayed it sounds like the problem lies somewhere else. Of course you can edit your question with all this extra info, but all the comments and answers you've got so far won't really fit and might be confusing to others. Your question might help others, so keeping it understandable is often the best. Up to you. – Fred May 04 '21 at 01:55
  • Okay I understand, I'll edit it in my possible best, thanks – Chinez May 04 '21 at 17:53
1

After a discussion and also from my comments to the question the model mapping the JSON response was not correct and all that was required was to map the response correctly to the Java model.

Droid
  • 528
  • 1
  • 5
  • 12
  • After following your instructions, it has been able to display data on the app. I appreciate that a lot. Don't get angry, but I boldly emphasized my main aim: to search and display any city data typed on the edittext but as of now, it can only display any city I write on the q= in the ApiInterface i.e q=london. I need to enable it to display any city data as he did here https://stackoverflow.com/questions/65005292/how-to-get-weather-object-data-from-openweathermap-api-using-retrofit, then I'll tick this answer as correct – Chinez May 08 '21 at 22:07
  • You need to remove "q=London&" from your ApiInterface.java class, refer to the answer you have linked – Droid May 08 '21 at 22:15
  • when i remove it, it crashes with this error: java.lang.NullPointerException: Attempt to invoke virtual method 'com.texra.preciseweatherforecast.Retrofit.Example$Main com.texra.preciseweatherforecast.Retrofit.Example.getMain()' on a null object reference. The problem is somewhere else – Chinez May 08 '21 at 22:19
  • That is because in firstfragment.java you are calling getWeatherData and passing the contents of the search field on launch which is empty so the request has "q=" and you get an error. Also, you have the search button with a click listener in HomeActivity.java and that calls a different getWeatherData which only update the timeField as opposed to how you update fields in firstfragment.java using its getWeatherData() – Droid May 08 '21 at 22:26
  • Okay I understand, is there any solution to this?. I'd rather remove the timefield than not pass the data to the fragment because my fragment is my first bottom nav bar that displays the data and swipes on current tab – Chinez May 08 '21 at 22:31
  • That would out of scope of this particular question in my opinion. It would be knowledge of basic Android elements and Java. All that is required is to ensure that the search click listener calls getWeatherData() with the content of the text field and then in the following onResponse of the call the relevant UI fields are updated from the response object. Hope that helps. – Droid May 08 '21 at 22:36
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/232129/discussion-between-ben-dreg-and-droid). – Chinez May 08 '21 at 22:38
  • Since you have the solution to the issue, won't it just be fair to just help me out? I mean I've been dealing with this error for weeks now, not that I'm lazy but I just have no idea how it can be fixed and I can't just stick with using a particular city because my friends over the world will like to try it out too and the bounty expires in 1 day. I would accept the moment your able to help me make it work – Chinez May 09 '21 at 05:39
  • @BenDreg I have explained the steps in the discussion – Droid May 09 '21 at 06:39
  • After implementing the setup, it runs with no error but is only able to display the dt data(which is on the homeActivity, it's unable to display any data from the fragment – Chinez May 09 '21 at 07:59
  • Please the getPrettyTime method only works for dt but not Sunrise and sunset – Chinez May 11 '21 at 20:34
  • @BenDreg you have to add it to the relevant class to support sunrise and sunset and then call that function – Droid May 11 '21 at 20:57