Статьи

Разработка Android-приложения: прогноз погоды с прогнозом

В этом руководстве будет описано, как разработать приложение для Android . В этом уроке мы рассмотрим, что нам нужно для создания приложения для Android и какие кирпичики мы можем использовать для создания нашего приложения. В качестве примера мы создадим приложение погоды. Мы говорили об этом несколько месяцев назад , мы разработаем приложение с погодой, текущим состоянием и прогнозом погоды. Для разработки приложения для Android нам нужно как минимум:

  • Деятельность
  • макет

Это основной компонент, который мы можем использовать. Конечно, мы хотим создать что-то более сложное, потому что мы должны получать информацию с удаленного сервера (в нашем случае openweathermap), и мы должны проанализировать данные результата. Поэтому нам нужно добавить к основным компонентам:

  • HTTP соединение
  • AsyncTask (чтобы не было проблем с ANR)
  • JSON-разбор
  • Модель данных (которая содержит данные JSON)

В конце мы получим:

android_weather_app [4]

Макет приложения

Первый шаг — создание макета. Как видно из изображения ниже, наш макет разделен на две части: одна содержит текущее состояние погоды, а другая содержит прогноз погоды. В прогнозе погоды мы должны перемещаться между разными днями, чтобы мы могли использовать ViewPager. В этом случае мы можем использовать LinearLayout:

android_linearlayout [4]

Теперь у нас есть наш макет, мы можем начать работать над каждым разделом. В первом разделе (текущая погода) мы можем использовать RelativeLayout, чтобы разместить каждый виджет в нужном месте:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity"
    android:layout_weight="1" >
 
<TextView
    android:id="@+id/cityText"
    style="?android:attr/textAppearanceMedium"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignParentLeft="true">       
</TextView>
 
<TextView
    android:id="@+id/temp"
    style="@style/tempStyle"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_below="@id/cityText"
    android:layout_centerHorizontal="true">       
</TextView>
<TextView
    android:id="@+id/unittemp"       
    android:layout_width="wrap_content"
    style="?android:attr/textAppearanceMedium"
    android:layout_height="wrap_content"
    android:layout_below="@id/cityText"
    android:layout_toRightOf="@id/temp"
    android:layout_alignBaseline="@id/temp">       
</TextView>
<TextView
    android:id="@+id/skydesc"       
    android:layout_width="wrap_content"
    style="?android:attr/textAppearanceMedium"
    android:layout_height="wrap_content"
    android:layout_below="@id/temp"       
    android:layout_alignStart="@id/temp"
    android:layout_toRightOf="@id/temp">
</TextView>
 
<!--  Image weather condition -->
<ImageView android:id="@+id/condIcon"
            android:layout_height="wrap_content"
            android:layout_width="wrap_content"
            android:layout_alignTop="@id/temp"
            android:layout_toRightOf="@id/temp"/>
 
</RelativeLayout>

Во втором разделе, как мы уже сказали, мы должны показать прогноз на несколько дней. В этом случае мы можем использовать ViewPager, который помогает нам перемещаться между страницами, поэтому мы имеем:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
<android.support.v4.view.ViewPager
        android:id="@+id/pager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_weight="6" >
 
      <android.support.v4.view.PagerTitleStrip
            android:id="@+id/pager_title_strip"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="top"
            android:background="#E6E6E6"
            android:paddingBottom="4dp"
            android:paddingTop="4dp"
            android:textColor="#fff" />
 
</android.support.v4.view.ViewPager>

Это наш макет. Теперь мы должны заполнить его данными о погоде.

Разбор данных HTTP, AsyncTask и json

Следующим шагом является получение данных с использованием HTTP-соединения с асинхронной задачей, чтобы избежать проблемы ANR. Получив данные, мы проанализируем их с помощью анализатора JSON. Давайте спросим у нашего браузера прогноз погоды по этой ссылке:

http://api.openweathermap.org/data/2.5/forecast/daily?q=Rome,IT&mode=json&units=metric&cnt=3

где cnt — количество дней, в которые мы хотим иметь погодные условия. Результат показан ниже:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
{
   "cod": "200",
   "message": 0.0192,
   "city": {
      "id": "3169070",
      "name": "Rome",
      "coord": {
         "lon": 12.4958,
         "lat": 41.903
      },
      "country": "Italy",
      "population": 0
   },
   "cnt": 7,
   "list": [
      {
         "dt": 1377774000,
         "temp": {
            "day": 26.83,
            "min": 16.41,
            "max": 29.12,
            "night": 16.41,
            "eve": 24.81,
            "morn": 26.83
         },
         "pressure": 1007.2,
         "humidity": 72,
         "weather": [
            {
               "id": 800,
               "main": "Clear",
               "description": "sky is clear",
               "icon": "01d"
            }
         ],
         "speed": 0.71,
         "deg": 146,
         "clouds": 0
      },
      {
         "dt": 1377860400,
         "temp": {
            "day": 29.84,
            "min": 16.8,
            "max": 29.84,
            "night": 16.8,
            "eve": 24.81,
            "morn": 17.93
         },
         "pressure": 1006.68,
         "humidity": 50,
         "weather": [
            {
               "id": 800,
               "main": "Clear",
               "description": "sky is clear",
               "icon": "02d"
            }
         ],
         "speed": 2.2,
         "deg": 235,
         "clouds": 8
      },
      {
         "dt": 1377946800,
         "temp": {
            "day": 29.14,
            "min": 14.11,
            "max": 29.14,
            "night": 14.11,
            "eve": 24.08,
            "morn": 18.32
         },
         "pressure": 1005.17,
         "humidity": 45,
         "weather": [
            {
               "id": 801,
               "main": "Clouds",
               "description": "few clouds",
               "icon": "02d"
            }
         ],
         "speed": 1.56,
         "deg": 320,
         "clouds": 20
      },
      {
         "dt": 1378033200,
         "temp": {
            "day": 28.43,
            "min": 12.11,
            "max": 28.95,
            "night": 14.28,
            "eve": 23.89,
            "morn": 12.11
         },
         "pressure": 1005.23,
         "humidity": 39,
         "weather": [
            {
               "id": 800,
               "main": "Clear",
               "description": "sky is clear",
               "icon": "01d"
            }
         ],
         "speed": 1.55,
         "deg": 289,
         "clouds": 0
      },
      {
         "dt": 1378119600,
         "temp": {
            "day": 29.83,
            "min": 16.02,
            "max": 29.83,
            "night": 18.86,
            "eve": 25.35,
            "morn": 16.02
         },
         "pressure": 1006.57,
         "humidity": 32,
         "weather": [
            {
               "id": 800,
               "main": "Clear",
               "description": "sky is clear",
               "icon": "01d"
            }
         ],
         "speed": 3.3,
         "deg": 292,
         "clouds": 0
      },
      {
         "dt": 1378206000,
         "temp": {
            "day": 27.76,
            "min": 19.68,
            "max": 27.76,
            "night": 19.68,
            "eve": 25,
            "morn": 20.25
         },
         "pressure": 1014.89,
         "humidity": 0,
         "weather": [
            {
               "id": 800,
               "main": "Clear",
               "description": "sky is clear",
               "icon": "01d"
            }
         ],
         "speed": 4.05,
         "deg": 298,
         "clouds": 0
      },
      {
         "dt": 1378292400,
         "temp": {
            "day": 27.85,
            "min": 19.91,
            "max": 27.85,
            "night": 19.91,
            "eve": 24.78,
            "morn": 20.13
         },
         "pressure": 1017.2,
         "humidity": 0,
         "weather": [
            {
               "id": 800,
               "main": "Clear",
               "description": "sky is clear",
               "icon": "01d"
            }
         ],
         "speed": 1.91,
         "deg": 252,
         "clouds": 0
      }
   ]
}

Анализируя результат, мы обнаруживаем, что искомая информация находится внутри тега list, а это — массив. Чтобы хранить эту информацию, мы можем создать два класса, которые содержат ежедневный прогноз, и другой, который содержит классы ежедневного прогноза:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
public class DayForecast {
 
    private static SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy");
    public Weather weather = new Weather();
    public ForecastTemp forecastTemp = new ForecastTemp();
    public long timestamp;
 
    public class ForecastTemp {
        public float day;
        public float min;
        public float max;
        public float night;
        public float eve;
        public float morning;
    }
 
    public String getStringDate() {
        return sdf.format(new Date(timestamp));
    }
}

а другой:

01
02
03
04
05
06
07
08
09
10
11
12
13
public class WeatherForecast {
 
    private List<DayForecast> daysForecast = new ArrayList<DayForecast>();
 
    public void addForecast(DayForecast forecast) {
        daysForecast.add(forecast);
        System.out.println("Add forecast ["+forecast+"]");
    }
 
    public DayForecast getForecast(int dayNum) {
        return daysForecast.get(dayNum);
    }
}

Теперь у нас есть модель данных, и мы просто анализируем данные JSON, используя простой класс, подобный тому, который использовался в предыдущем посте (JSONWeatherParser).

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public static WeatherForecast getForecastWeather(String data) throws JSONException  {
 
    WeatherForecast forecast = new WeatherForecast();
 
    // We create out JSONObject from the data
    JSONObject jObj = new JSONObject(data);
 
    JSONArray jArr = jObj.getJSONArray("list"); // Here we have the forecast for every day
 
    // We traverse all the array and parse the data
    for (int i=0; i < jArr.length(); i++) {
        JSONObject jDayForecast = jArr.getJSONObject(i);
 
        // Now we have the json object so we can extract the data
        DayForecast df = new DayForecast();
 
        // We retrieve the timestamp (dt)
        df.timestamp = jDayForecast.getLong("dt");
 
        // Temp is an object
        JSONObject jTempObj = jDayForecast.getJSONObject("temp");
 
        df.forecastTemp.day = (float) jTempObj.getDouble("day");
        df.forecastTemp.min = (float) jTempObj.getDouble("min");
        df.forecastTemp.max = (float) jTempObj.getDouble("max");
        df.forecastTemp.night = (float) jTempObj.getDouble("night");
        df.forecastTemp.eve = (float) jTempObj.getDouble("eve");
        df.forecastTemp.morning = (float) jTempObj.getDouble("morn");
 
        // Pressure & Humidity
        df.weather.currentCondition.setPressure((float) jDayForecast.getDouble("pressure"));
        df.weather.currentCondition.setHumidity((float) jDayForecast.getDouble("humidity"));
 
        // ...and now the weather
        JSONArray jWeatherArr = jDayForecast.getJSONArray("weather");
        JSONObject jWeatherObj = jWeatherArr.getJSONObject(0);
        df.weather.currentCondition.setWeatherId(getInt("id", jWeatherObj));
        df.weather.currentCondition.setDescr(getString("description", jWeatherObj));
        df.weather.currentCondition.setCondition(getString("main", jWeatherObj));
        df.weather.currentCondition.setIcon(getString("icon", jWeatherObj));
 
        forecast.addForecast(df);
    }
 
    return forecast;
}

Отлично сработано! Что нам еще нужно? … Что ж, нам нужно обработать ViewPager и создать адаптер для ежедневного прогноза.

ViewPager, фрагменты и адаптер

Первым делом мы должны создать наш адаптер, который обрабатывает каждый фрагмент, который отображает ежедневную информацию о погоде. В этом случае, чтобы создать наш адаптер, мы должны расширить FragmentPagerAdapter:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class DailyForecastPageAdapter extends FragmentPagerAdapter {
 
private int numDays;
private FragmentManager fm;
private WeatherForecast forecast;
private final static SimpleDateFormat sdf = new SimpleDateFormat("E, dd-MM");
 
public DailyForecastPageAdapter(int numDays, FragmentManager fm, WeatherForecast forecast) {
    super(fm);
    this.numDays = numDays;
    this.fm = fm;
    this.forecast = forecast;   
}
 
// Page title
@Override
public CharSequence getPageTitle(int position) {
    // We calculate the next days adding position to the current date
    Date d = new Date();
    Calendar gc =  new GregorianCalendar();
    gc.setTime(d);
    gc.add(GregorianCalendar.DAY_OF_MONTH, position);
 
    return sdf.format(gc.getTime());   
}
 
@Override
public Fragment getItem(int num) {
    DayForecast dayForecast = (DayForecast) forecast.getForecast(num);
    DayForecastFragment f = new DayForecastFragment();
    f.setForecast(dayForecast);
    return f;
}
 
/*
 * Number of the days we have the forecast
 */
@Override
public int getCount() {
 
    return numDays;
}

Важны два метода: один «создает» фрагмент, а другой «создает» заголовок страницы. Первый экземпляр создает фрагмент, который показывает ежедневный forecat, передающий данные dayForecast, а другой создает плитку страницы с помощью GregorianCalendar.

Последний шаг — это кодирование фрагмента:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
public class DayForecastFragment extends Fragment {
 
...
 
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        View v = inflater.inflate(R.layout.dayforecast_fragment, container, false);
 
        TextView tempView = (TextView) v.findViewById(R.id.tempForecast);
        TextView descView = (TextView) v.findViewById(R.id.skydescForecast);
        tempView.setText( (int) (dayForecast.forecastTemp.min - 275.15) + "-" + (int) (dayForecast.forecastTemp.max - 275.15) );
        descView.setText(dayForecast.weather.currentCondition.getDescr());
        iconWeather = (ImageView) v.findViewById(R.id.forCondIcon);
        // Now we retrieve the weather icon
        JSONIconWeatherTask task = new JSONIconWeatherTask();
        task.execute(new String[]{dayForecast.weather.currentCondition.getIcon()});
 
        return v;
    }
...
}

В onCreateView мы раздуваем наш макет и получаем (AsyncTask) значок погоды.

Доступен исходный код @ github

Справка: разработка Android-приложения: прогноз погоды от нашего партнера JCG Франческо Аццолы в блоге Surviving с Android .