Создаём приложение “прогноз погоды” на Android
Russian (Pусский) translation by Alex Grigorovich (you can also view the original English article)



Многие популярные приложения прогноза погоды в Google Play либо содержат много рекламы, либо требуют слишком большого количества разрешений, либо содержат функционал, который большинство из нас никогда не используют. Было бы здорово, если бы мы могли создать собственное приложение погоды с нуля?
В этом уроке я покажу вам, как это сделать. Наше приложение будет иметь простой и минималистичный пользовательский интерфейс, показывающий пользователю именно то, что ему нужно знать о текущих погодных условиях. Давайте начнем.
1. Подготовка
Прежде чем продолжить, дважды проверьте, что у вас установлены следующие компоненты:
- Eclipse ADT Bundle: вы можете скачать его на веб-сайте разработчика Android.
- Ключ API OpenWeatherMap : это не обязательно для выполнения нашей задачи, но это бесплатно. Вы можете получить его, зарегистрировавшись на сайте OpenWeatherMap.
- Иконки: Я рекомендую вам скачать шрифт weather icons font, созданный Эриком Флорсом. Вам нужно загрузить файл TTF, потому что мы будем использовать его в
нативном приложении. Мы будем использовать этот шрифт чтобы показывать
различные значки в зависимости от погодных условий.
2. Создадим новый проект
Я собираюсь назвать это приложение SimpleWeather, но вы можете дать ему любое имя, которое вам понравится. Введите уникальное имя пакета, установите минимальный необходимый SDK
на Android 2.2 и установите целевой SDK на Android 4.4. Вы можете оставить тему Holo Dark.



Это приложение будет иметь только одно Activity, и оно будет основано на шаблоне Blank Activity, как показано ниже.



Назовите Activity WeatherActivity. Мы будем использовать Fragment внутри этого Activity. Макет, связанный с Activity называется activity_weather.xml. Макет, связанный с Fragment называется fragment _weather.xml.



3. Добавим пользовательский шрифт
Скопируйте weathericons-regular-webfont.ttf в каталог assets/fonts вашего проекта и переименуйте его в weather.ttf.
4. Отредактируем файл манифеста
Единственное разрешение, которое требуется этому приложению - это android.permission.INTERNET.
1 |
<uses-permission android:name="android.permission.INTERNET"/> |
Чтобы упростить этот урок, мы рассмотрим только портретный режим. Узел activity манифеста должен выглядеть так:
1 |
<activity
|
2 |
android:name="ah.hathi.simpleweather.WeatherActivity" |
3 |
android:label="@string/app_name" |
4 |
android:screenOrientation="portrait" |
5 |
>
|
6 |
<intent-filter>
|
7 |
<action android:name="android.intent.action.MAIN" /> |
8 |
<category android:name="android.intent.category.LAUNCHER" /> |
9 |
</intent-filter>
|
10 |
</activity>
|
5. Отредактируем макет Activity
В activity_weather.xml изменений не так много. Он должен уже включать в себя FrameLayout. Добавьте дополнительное свойство, чтобы изменить цвет фона на #FF0099CC.
1 |
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" |
2 |
xmlns:tools="http://schemas.android.com/tools" |
3 |
android:id="@+id/container" |
4 |
android:layout_width="match_parent" |
5 |
android:layout_height="match_parent" |
6 |
tools:context="ah.hathi.simpleweather.WeatherActivity" |
7 |
tools:ignore="MergeRootFrame" |
8 |
android:background="#FF0099CC" /> |
6. Отредактируем макет Fragment
Измените файл fragment_weather.xml, добавив пять тегов TextView, чтобы отобразить следующую информацию:
- город и страна
- текущая температура
- значок, показывающий текущее погодное условие
- отметка времени, указывающая пользователю, когда было получено последнее обновление информация о погоде
- более подробная информация о текущей погоде, например, описание и влажность
Используйте RelativeLayout для размещения текстовых представлений. Вы можете настроить textSize для различных устройств.
1 |
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" |
2 |
xmlns:tools="http://schemas.android.com/tools" |
3 |
android:layout_width="match_parent" |
4 |
android:layout_height="match_parent" |
5 |
android:paddingBottom="@dimen/activity_vertical_margin" |
6 |
android:paddingLeft="@dimen/activity_horizontal_margin" |
7 |
android:paddingRight="@dimen/activity_horizontal_margin" |
8 |
android:paddingTop="@dimen/activity_vertical_margin" |
9 |
tools:context="ah.hathi.simpleweather.WeatherActivity$PlaceholderFragment" > |
10 |
|
11 |
<TextView
|
12 |
android:id="@+id/city_field" |
13 |
android:layout_width="wrap_content" |
14 |
android:layout_height="wrap_content" |
15 |
android:layout_alignParentTop="true" |
16 |
android:layout_centerHorizontal="true" |
17 |
android:textAppearance="?android:attr/textAppearanceLarge" /> |
18 |
|
19 |
<TextView
|
20 |
android:id="@+id/updated_field" |
21 |
android:layout_width="wrap_content" |
22 |
android:layout_height="wrap_content" |
23 |
android:layout_below="@+id/city_field" |
24 |
android:layout_centerHorizontal="true" |
25 |
android:textAppearance="?android:attr/textAppearanceMedium" |
26 |
android:textSize="13sp" /> |
27 |
|
28 |
<TextView
|
29 |
android:id="@+id/weather_icon" |
30 |
android:layout_width="wrap_content" |
31 |
android:layout_height="wrap_content" |
32 |
android:layout_centerVertical="true" |
33 |
android:layout_centerHorizontal="true" |
34 |
android:textAppearance="?android:attr/textAppearanceLarge" |
35 |
android:textSize="70sp" |
36 |
/>
|
37 |
|
38 |
<TextView
|
39 |
android:id="@+id/current_temperature_field" |
40 |
android:layout_width="wrap_content" |
41 |
android:layout_height="wrap_content" |
42 |
android:layout_alignParentBottom="true" |
43 |
android:layout_centerHorizontal="true" |
44 |
android:textAppearance="?android:attr/textAppearanceLarge" |
45 |
android:textSize="40sp" /> |
46 |
|
47 |
<TextView
|
48 |
android:id="@+id/details_field" |
49 |
android:layout_width="wrap_content" |
50 |
android:layout_height="wrap_content" |
51 |
android:layout_below="@+id/weather_icon" |
52 |
android:layout_centerHorizontal="true" |
53 |
android:textAppearance="?android:attr/textAppearanceMedium" |
54 |
/>
|
55 |
|
56 |
</RelativeLayout>
|
7. Изменим strings.xml
Этот файл содержит строки, используемые в нашем приложении, а также коды символов Юникода, которые мы будем использовать для отображения значков погоды. Приложение сможет отображать восемь различных типов погодных условий. Если вам нужно больше, примените следующую хитрость. Добавьте следующие значения в файл values/strings.xml:
1 |
<?xml version="1.0" encoding="utf-8"?>
|
2 |
<resources>
|
3 |
|
4 |
<string name="app_name">Simple Weather</string> |
5 |
<string name="change_city">Change city</string> |
6 |
|
7 |
<!-- Put your own APP ID here -->
|
8 |
<string name="open_weather_maps_app_id">11111</string> |
9 |
|
10 |
<string name="weather_sunny"></string> |
11 |
<string name="weather_clear_night"></string> |
12 |
|
13 |
<string name="weather_foggy"></string> |
14 |
<string name="weather_cloudy"></string> |
15 |
<string name="weather_rainy"></string> |
16 |
<string name="weather_snowy"></string> |
17 |
<string name="weather_thunder"></string> |
18 |
<string name="weather_drizzle"></string> |
19 |
|
20 |
<string name="place_not_found">Sorry, no weather data found.</string> |
21 |
|
22 |
</resources>
|
8. Добавим пункт меню
Пользователь должен иметь возможность выбирать город, чью погоду они хотят видеть. Измените файл menu/weather.xml и добавьте элемент для этой опции.
1 |
<menu xmlns:android="http://schemas.android.com/apk/res/android" |
2 |
xmlns:app="http://schemas.android.com/apk/res-auto" |
3 |
xmlns:tools="http://schemas.android.com/tools" |
4 |
tools:context="ah.hathi.simpleweather.WeatherActivity" > |
5 |
|
6 |
<item
|
7 |
android:id="@+id/change_city" |
8 |
android:orderInCategory="1" |
9 |
android:title="@string/change_city" |
10 |
app:showAsAction="never"/> |
11 |
|
12 |
</menu>
|
Теперь, когда все XML-файлы готовы к использованию, давайте перейдем к запросу на API OpenWeatherMap для получения данных о погоде.
9. Получаем данные из OpenWeatherMap
Мы можем получить текущие данные о погоде в любом городе, в формате JSON, с помощью API OpenWeatherMap. В строке запроса мы передаем имя города и систему измерения, в которой должны быть результаты.
Например, чтобы получить текущую информацию о погоде в Канберре, используя метрическую систему, мы отправляем запрос на http://api.openweathermap.org/data/2.5/weather?q=Canberra&units=metric
Ответ, который мы получаем через API, выглядит так:
1 |
{
|
2 |
"base": "cmc stations", |
3 |
"clouds": { |
4 |
"all": 90 |
5 |
},
|
6 |
"cod": 200, |
7 |
"coord": { |
8 |
"lat": -35.28, |
9 |
"lon": 149.13 |
10 |
},
|
11 |
"dt": 1404390600, |
12 |
"id": 2172517, |
13 |
"main": { |
14 |
"humidity": 100, |
15 |
"pressure": 1023, |
16 |
"temp": -1, |
17 |
"temp_max": -1, |
18 |
"temp_min": -1 |
19 |
},
|
20 |
"name": "Canberra", |
21 |
"sys": { |
22 |
"country": "AU", |
23 |
"message": 0.313, |
24 |
"sunrise": 1404335563, |
25 |
"sunset": 1404370965 |
26 |
},
|
27 |
"weather": [ |
28 |
{
|
29 |
"description": "overcast clouds", |
30 |
"icon": "04n", |
31 |
"id": 804, |
32 |
"main": "Clouds" |
33 |
}
|
34 |
],
|
35 |
"wind": { |
36 |
"deg": 305.004, |
37 |
"speed": 1.07 |
38 |
}
|
39 |
}
|
Создайте новый класс Java и назовите его RemoteFetch.java. Этот класс отвечает за получение данных о погоде через API OpenWeatherMap.
Мы используем класс HttpURLConnection для выполнения удаленного запроса. API OpenWeatherMap ожидает ключ API в HTTP-заголовке с именем x-api-key. Это определено в нашем запросе с использованием метода setRequestProperty.
Мы используем BufferedReader для чтения ответа API в StringBuffer. Когда мы получим полный ответ, мы преобразуем его в объект JSONObject.
Как видно из приведенного выше ответа, данные JSON содержат поле с именем cod. Его значение равно 200, если запрос был успешным. Мы используем это значение, чтобы проверить, имеет ли ответ JSON текущую информацию о погоде или нет.
Класс RemoteFetch.java должен выглядеть следующим образом:
1 |
package ah.hathi.simpleweather; |
2 |
|
3 |
import java.io.BufferedReader; |
4 |
import java.io.InputStreamReader; |
5 |
import java.net.HttpURLConnection; |
6 |
import java.net.URL; |
7 |
|
8 |
import org.json.JSONObject; |
9 |
|
10 |
import android.content.Context; |
11 |
import android.util.Log; |
12 |
|
13 |
public class RemoteFetch { |
14 |
|
15 |
private static final String OPEN_WEATHER_MAP_API = |
16 |
"http://api.openweathermap.org/data/2.5/weather?q=%s&units=metric"; |
17 |
|
18 |
public static JSONObject getJSON(Context context, String city){ |
19 |
try { |
20 |
URL url = new URL(String.format(OPEN_WEATHER_MAP_API, city)); |
21 |
HttpURLConnection connection = |
22 |
(HttpURLConnection)url.openConnection(); |
23 |
|
24 |
connection.addRequestProperty("x-api-key", |
25 |
context.getString(R.string.open_weather_maps_app_id)); |
26 |
|
27 |
BufferedReader reader = new BufferedReader( |
28 |
new InputStreamReader(connection.getInputStream())); |
29 |
|
30 |
StringBuffer json = new StringBuffer(1024); |
31 |
String tmp=""; |
32 |
while((tmp=reader.readLine())!=null) |
33 |
json.append(tmp).append("\n"); |
34 |
reader.close(); |
35 |
|
36 |
JSONObject data = new JSONObject(json.toString()); |
37 |
|
38 |
// This value will be 404 if the request was not
|
39 |
// successful
|
40 |
if(data.getInt("cod") != 200){ |
41 |
return null; |
42 |
}
|
43 |
|
44 |
return data; |
45 |
}catch(Exception e){ |
46 |
return null; |
47 |
}
|
48 |
}
|
49 |
}
|
10. Сохраним город в настройках
Пользователь не должен указывать имя города каждый раз, когда захочет использовать приложение. Приложение должно запомнить последний город, который искал пользователь. Мы делаем это, используя SharedPreferences. Однако вместо прямого доступа к этим настройкам из нашего класса Activity, для этого лучше создать отдельный класс.
Создайте новый класс Java и назовите его CityPreference.java. Чтобы сохранить и получить имя города, создайте два метода setCity и getCity. Объект SharedPreferences инициализируется в конструкторе. Класс CityPreference.java должен выглядеть следующим образом:
1 |
package ah.hathi.simpleweather; |
2 |
|
3 |
import android.app.Activity; |
4 |
import android.content.SharedPreferences; |
5 |
|
6 |
public class CityPreference { |
7 |
|
8 |
SharedPreferences prefs; |
9 |
|
10 |
public CityPreference(Activity activity){ |
11 |
prefs = activity.getPreferences(Activity.MODE_PRIVATE); |
12 |
}
|
13 |
|
14 |
// If the user has not chosen a city yet, return
|
15 |
// Sydney as the default city
|
16 |
String getCity(){ |
17 |
return prefs.getString("city", "Sydney, AU"); |
18 |
}
|
19 |
|
20 |
void setCity(String city){ |
21 |
prefs.edit().putString("city", city).commit(); |
22 |
}
|
23 |
|
24 |
}
|
11. Создадим Fragment
Создайте новый класс Java и назовите его WeatherFragment.java. В этом фрагменте в качестве макета используется frag_weather.xml. Объявите пять объектов TextView и инициализируйте их в методе onCreateView. Объявите новый объект Typeface с именем weatherFont. Объект TypeFace указывает на веб-шрифт, который вы загрузили и сохранили в папке assets/fonts.
Мы будем использовать отдельный Thread для асинхронного получения данных через API OpenWeatherMap. Мы не можем обновить пользовательский интерфейс из подобного фонового потока. Поэтому нам нужен объект Handler, который мы инициализируем в конструкторе класса WeatherFragment.
1 |
public class WeatherFragment extends Fragment { |
2 |
Typeface weatherFont; |
3 |
|
4 |
TextView cityField; |
5 |
TextView updatedField; |
6 |
TextView detailsField; |
7 |
TextView currentTemperatureField; |
8 |
TextView weatherIcon; |
9 |
|
10 |
Handler handler; |
11 |
|
12 |
public WeatherFragment(){ |
13 |
handler = new Handler(); |
14 |
}
|
15 |
|
16 |
@Override
|
17 |
public View onCreateView(LayoutInflater inflater, ViewGroup container, |
18 |
Bundle savedInstanceState) { |
19 |
View rootView = inflater.inflate(R.layout.fragment_weather, container, false); |
20 |
cityField = (TextView)rootView.findViewById(R.id.city_field); |
21 |
updatedField = (TextView)rootView.findViewById(R.id.updated_field); |
22 |
detailsField = (TextView)rootView.findViewById(R.id.details_field); |
23 |
currentTemperatureField = (TextView)rootView.findViewById(R.id.current_temperature_field); |
24 |
weatherIcon = (TextView)rootView.findViewById(R.id.weather_icon); |
25 |
|
26 |
weatherIcon.setTypeface(weatherFont); |
27 |
return rootView; |
28 |
}
|
29 |
}
|
Инициализируем объект weatherFont, вызывая createFromAsset в классе Typeface. Мы также вызываем метод updateWeatherData в onCreate.
1 |
@Override
|
2 |
public void onCreate(Bundle savedInstanceState) { |
3 |
super.onCreate(savedInstanceState); |
4 |
weatherFont = Typeface.createFromAsset(getActivity().getAssets(), "fonts/weather.ttf"); |
5 |
updateWeatherData(new CityPreference(getActivity()).getCity()); |
6 |
}
|
В updateWeatherData, мы запускаем новый поток и вызываем getJSON в классе RemoteFetch. Если значение, возвращаемое getJSON, равно null, мы выводим сообщение об ошибке пользователю. Если это не так, мы вызываем метод renderWeather.
Только основной поток разрешает обновлять пользовательский интерфейс приложения для Android. Вызов Toast или renderWeather прямо из фонового потока приведет к ошибке выполнения. Вот почему мы вызываем эти методы с использованием метода post-обработчика.
1 |
private void updateWeatherData(final String city){ |
2 |
new Thread(){ |
3 |
public void run(){ |
4 |
final JSONObject json = RemoteFetch.getJSON(getActivity(), city); |
5 |
if(json == null){ |
6 |
handler.post(new Runnable(){ |
7 |
public void run(){ |
8 |
Toast.makeText(getActivity(), |
9 |
getActivity().getString(R.string.place_not_found), |
10 |
Toast.LENGTH_LONG).show(); |
11 |
}
|
12 |
});
|
13 |
} else { |
14 |
handler.post(new Runnable(){ |
15 |
public void run(){ |
16 |
renderWeather(json); |
17 |
}
|
18 |
});
|
19 |
}
|
20 |
}
|
21 |
}.start(); |
22 |
}
|
Метод renderWeather использует данные JSON для обновления объектов TextView. Узел weather ответа JSON представляет собой массив данных. В этом уроке мы будем использовать только первый элемент массива метеорологических данных.
1 |
private void renderWeather(JSONObject json){ |
2 |
try { |
3 |
cityField.setText(json.getString("name").toUpperCase(Locale.US) + |
4 |
", " + |
5 |
json.getJSONObject("sys").getString("country")); |
6 |
|
7 |
JSONObject details = json.getJSONArray("weather").getJSONObject(0); |
8 |
JSONObject main = json.getJSONObject("main"); |
9 |
detailsField.setText( |
10 |
details.getString("description").toUpperCase(Locale.US) + |
11 |
"\n" + "Humidity: " + main.getString("humidity") + "%" + |
12 |
"\n" + "Pressure: " + main.getString("pressure") + " hPa"); |
13 |
|
14 |
currentTemperatureField.setText( |
15 |
String.format("%.2f", main.getDouble("temp"))+ " ℃"); |
16 |
|
17 |
DateFormat df = DateFormat.getDateTimeInstance(); |
18 |
String updatedOn = df.format(new Date(json.getLong("dt")*1000)); |
19 |
updatedField.setText("Last update: " + updatedOn); |
20 |
|
21 |
setWeatherIcon(details.getInt("id"), |
22 |
json.getJSONObject("sys").getLong("sunrise") * 1000, |
23 |
json.getJSONObject("sys").getLong("sunset") * 1000); |
24 |
|
25 |
}catch(Exception e){ |
26 |
Log.e("SimpleWeather", "One or more fields not found in the JSON data"); |
27 |
}
|
28 |
}
|
В конце метода renderWeather мы вызываем setWeatherIcon с идентификатором текущей погоды, а также временем восхода и захода солнца. Настройка значка погоды немного сложна, потому что API OpenWeatherMap поддерживает больше погодных условий, чем мы можем отобразить с помощью используемого веб-шрифта. К счастью, идентификаторы погоды следуют шаблону, о котором вы можете узнать больше на веб-сайте OpenWeatherMap.
Так мы будем сопоставлять идентификатор погоды с иконкой:
- коды погоды в диапазоне 200 обозначают грозу, что означает, что мы можем использовать
R .string.weather_thunderдля них - коды погоды в диапазоне 300 связаны с моросями, и мы используем
R.string.weather_drizzle - коды погоды в диапазоне 500 означают дождь, и мы используем
R.string.weather_rain - и так далее ...
Мы используем время восхода и захода солнца, чтобы отобразить солнце или луну, в зависимости от текущего времени суток и только в том случае, если погода ясная.
1 |
private void setWeatherIcon(int actualId, long sunrise, long sunset){ |
2 |
int id = actualId / 100; |
3 |
String icon = ""; |
4 |
if(actualId == 800){ |
5 |
long currentTime = new Date().getTime(); |
6 |
if(currentTime>=sunrise && currentTime<sunset) { |
7 |
icon = getActivity().getString(R.string.weather_sunny); |
8 |
} else { |
9 |
icon = getActivity().getString(R.string.weather_clear_night); |
10 |
}
|
11 |
} else { |
12 |
switch(id) { |
13 |
case 2 : icon = getActivity().getString(R.string.weather_thunder); |
14 |
break; |
15 |
case 3 : icon = getActivity().getString(R.string.weather_drizzle); |
16 |
break; |
17 |
case 7 : icon = getActivity().getString(R.string.weather_foggy); |
18 |
break; |
19 |
case 8 : icon = getActivity().getString(R.string.weather_cloudy); |
20 |
break; |
21 |
case 6 : icon = getActivity().getString(R.string.weather_snowy); |
22 |
break; |
23 |
case 5 : icon = getActivity().getString(R.string.weather_rainy); |
24 |
break; |
25 |
}
|
26 |
}
|
27 |
weatherIcon.setText(icon); |
28 |
}
|
Конечно, вы можете обрабатывать больше погодных условий, соответственно изменяя метод setWeatherIcon.
Наконец, добавим метод changeCity к фрагменту, чтобы пользователь мог обновить текущий город. Метод changeCity будет вызываться только из основного класса Activity.
1 |
public void changeCity(String city){ |
2 |
updateWeatherData(city); |
3 |
}
|
12. Редактируем Activity
В течении настройки проекта, Eclipse заполняет WeatherActivity.java некоторым шаблонным кодом. Замените стандартную реализацию метода onCreate на приведенную ниже, в которой мы используем WeatherFragment. Метод onCreate должен выглядеть следующим образом:
1 |
@Override
|
2 |
protected void onCreate(Bundle savedInstanceState) { |
3 |
super.onCreate(savedInstanceState); |
4 |
setContentView(R.layout.activity_weather); |
5 |
|
6 |
if (savedInstanceState == null) { |
7 |
getSupportFragmentManager().beginTransaction() |
8 |
.add(R.id.container, new WeatherFragment()) |
9 |
.commit(); |
10 |
}
|
11 |
}
|
Затем отредактируйте метод onOptionsItemSelected и обработайте единственный параметр меню, который у нас есть. Все, что вам нужно сделать, это вызвать метод showInputDialog.
В методе showInputDialog мы используем AlertDialog.Builder для создания объекта Dialog, который предлагает пользователю ввести имя города. Эта информация передается методу changeCity, который хранит имя города с использованием класса CityPreference и вызывает метод changeCity во фрагменте.
1 |
@Override
|
2 |
public boolean onOptionsItemSelected(MenuItem item) { |
3 |
if(item.getItemId() == R.id.change_city){ |
4 |
showInputDialog(); |
5 |
}
|
6 |
return false; |
7 |
}
|
8 |
|
9 |
private void showInputDialog(){ |
10 |
AlertDialog.Builder builder = new AlertDialog.Builder(this); |
11 |
builder.setTitle("Change city"); |
12 |
final EditText input = new EditText(this); |
13 |
input.setInputType(InputType.TYPE_CLASS_TEXT); |
14 |
builder.setView(input); |
15 |
builder.setPositiveButton("Go", new DialogInterface.OnClickListener() { |
16 |
@Override
|
17 |
public void onClick(DialogInterface dialog, int which) { |
18 |
changeCity(input.getText().toString()); |
19 |
}
|
20 |
});
|
21 |
builder.show(); |
22 |
}
|
23 |
|
24 |
public void changeCity(String city){ |
25 |
WeatherFragment wf = (WeatherFragment)getSupportFragmentManager() |
26 |
.findFragmentById(R.id.container); |
27 |
wf.changeCity(city); |
28 |
new CityPreference(this).setCity(city); |
29 |
}
|
Теперь ваше приложение для погоды готово. Соберите проект и установите его на Android-устройстве для тестирования.



Заключение
Теперь у вас есть полнофункциональное приложение прогноза погоды. Изучайте API OpenWeatherMap для дальнейшего развития вашего приложения. Вы также можете использовать больше значков погоды, так как в примере мы используем только немногие из них.



