[Huawei Developers] muraliameakula:
Introduction

In this article, I have explained to develop weather application for HarmonyOS using Huawei DevEco Studio and using HTML, JavaScript and Open Rest APIs. User can search the city name and fetch the information. Application will show current weather and weather prediction for next five days. The UI is developed with flexible rich HTML with JavaScript. Network calls are done using Java HttpClient.

Huawei Mobile Device

Requirements

1) DevEco IDE

2) Huawei phone running Harmony OS (Can use cloud emulator also)

New Project (Phone)

After installation of DevEco Studio, make new project.

Select Phone in Device and select Empty Feature Ability (JS) in Template.

After the project is created, its directory as shown in image.

hml files describe the page layout.

css files describe the page style.

js files process the interactions between pages and users.

The app.js file manages global JavaScript logics and application lifecycle.

The pages directory stores all component pages.

The java directory stores java files related to the projects.

Development process

Design the UI

We are designing a simple UI, with just single page which will display the current weather and predicted weather for next five days. We need three UI section in this page.

Search box for searching the city name

UI Section showing today’s weather

UI Section with carousel to display next five days

Step 1: Create the search box in the hml file.

As the first step, we can create an input component that will be holding the search area which will be used for searching the city.

index.html

<div class="container">
<div>
<div class="title">
<input class="comment" value="{{searchValue}}" placeholder ="Enter the city " onchange="updateSearchValue()"></input>
</div>
<image class="searchart" src="/common/search1.png" onclick="searchCity() "></image>
</div>
</div>

index.css

.container {
flex-direction: column;
background-color: #e8f6fe;
}.comment {
width: 550px;
height: 100px;
background-color: lightgrey;
}.searchart {
margin-top:40px;
width:70px;
height:68px;
margin-right:40px;
margin-left:-40px;
}

index.js

updateSearchValue(e) {
this.searchValue = e.text;
},searchCity() {
this.inProgress = true;
this.fetchWeather(this.searchValue).then()
},

Result

​

Step 2: Add UI section to display today’s weather.

Create Text fields for city name, todays date, temperature, precipitation and wind speed. Then we have section for description of today’s weather in few words like sunny, clear sky and so on. Finally we have image which depicts what type of weather it is.

index.hml

<div class="widget">
<div class="details">
<text class="city">{{currentCityInfo}}</text>
<text class="today">Today {{toDay}}</text>
<text class="temperature">{{currentTemperature}}</text>
<text class="precipitation">Precipitation: {{currentHumidity}}</text>
<text class="wind">Wind: {{currentWind}} km/hr</text>
<div>
<div class="summary">
<text class="summaryText">{{currentDesc}}</text>
</div>
<image class="weatherart" src="{{artImage}}"></image>
</div>
</div>
</div>

index.css

.temperature {
color: white;
font-weight: 300;
font-size: 150px;
}

.today {
color: white;
font-weight: 300;
font-size: 32px;
margin-top: 20px;
width: 420px;
padding-top: 10px;
border-top: 2px solid #9cd0ff;
}
.city {
color: white;
font-size: 70px;
margin-top: 0px;
}
.summary {
width: 660px;
margin-top: 16px;
padding-bottom: 16px;
border-top: 2px solid #9cd0ff;
}
.summaryText {
color: #d2e9fa;
font-size: 70px;
font-weight: 300;
margin: 0;
margin-left: 40px;
margin-top: 40px;
}
.precipitation, .wind {
color: #d2e9fa;
font-size: 32px;
font-weight: 300;
margin-left: 8px;
}
.precipitation {
margin-top: 16px;
}

Result

​

Step 3: Add UI section for next five days weather.

Now we have search box and today weather UI section. Below those add a carousel UI using swiper component. Each item in the swiper will have text fields for max and min temperature and an icon for weather indication.

index.html

<div class="daystitle"><text class="name">Next 4 days</text></div><swiper id="swiperImage" class="swiper-style">
<div class="daydetailscard" for="{{day in days}}">
<text class="daydetitle">{{day.dayName}}</text>
<div class="daydetailssubcard">
<text class="detailstemp">Hi : {{day.maxTemp}}°C</text>
<text class=" detailstemp ">Low : {{day.minTemp}}°C</text>
<image class="weatherarticon" src="{{day.artImageIcon}}"></image>
</div>
<text class="daydetails" >{{day.desc}}</text>
</div>
</swiper>

index.css

.daydetitle{
color: #626262;
text-align: center;
font-size: 40px;
padding-bottom: 10px;
border-bottom: 2px solid #626262;
font-family: Roboto, sans-serif;
display: flex;
flex-direction: column;
margin-top: 40px;
margin-bottom: 40px;

}

.daydetails{
color: white;
text-align: center;
font-size: 40px;
font-family: Roboto, sans-serif;
display: flex;
flex-direction: column;
margin-top: 40px;
margin-bottom: 40px;

}

.daydetailscard{
border-radius: 28px;
height: 300px;
width: 630px;
background: linear-gradient(to bottom right, #ffb20f 20%, #ecdebc);
font-family: Roboto, sans-serif;
display: flex;
flex-direction: column;
margin-top: 10px;
margin-left: 40px;
}

.daydetailssubcard{
height: 50px;
width: 630px;
font-family: Roboto, sans-serif;
display: flex;
flex-direction: row;
}
.deatilstemp {
color: white;
font-size: 32px;
font-weight: 300;
margin-left: 20px;
margin-top: 16px;
}

Result

​

Step 4: Add UI Screen for Loading.

We will use “inProgress” to control loading state of the network calls. When user clicks search icon, then loading screen will show until the network data is received.

<div if="{{inProgress}}" class="circleAnimation"></div>
<div if="{{inProgress}}" class="circleAnimation"></div>
<div if="{{inProgress}}" class="circleAnimation"></div>
<div if="{{inProgress}}" class="circleAnimation"></div>
<div if="{{inProgress}}" class="circleAnimation"></div>
<div if="{{inProgress}}" class="circleAnimation"></div>
</div>

index.css

.circleAnimation {
height: 20px;
width: 20px;
margin-left: 20px;
margin-top: 20px;
border-radius: 10;
background-color: red;
animation-name: Stretch;
animation-duration: 1.5s;
animation-timing-function: ease-out;
animation-delay: 0;
animation-iteration-count: infinite;
animation-fill-mode: none;
animation-play-state: running;
}

}
@keyframes spin {
0% { transform: rotate(0); }
100% { transform: rotate(360); }
}

Result

​

Consume REST APIs of openweather.org

We will use two APIs from openweather.org. One is to get the Current weather and other to get the prediction next five day weather. Before using these APIs, Create an account and obtain API Key.

Current weather data

Access current weather data for any location on Earth including over 200,000 cities! We collect and process weather data from different sources such as global and local weather models, satellites, radars and vast network of weather stations. Data is available in JSON, XML, or HTML format.

By city name

You can call by city name or city name, state code and country code.

api.openweathermap.org/data/2.5/weather?q={city name}&appid={API key}

Step 5: Create Model classes for Weather response.

CurrentWeatherResponse.java

public class CurrentWeatherResponse {

@SerializedName("dt")
private int dt;

@SerializedName("coord")
private Coord coord;

@SerializedName("weather")
private List<WeatherItem> weather;

@SerializedName("name")
private String name;

@SerializedName("cod")
private int cod;

@SerializedName("main")
private Main main;

@SerializedName("clouds")
private Clouds clouds;

@SerializedName("id")
private int id;

@SerializedName("sys")
private Sys sys;

@SerializedName("base")
private String base;

@SerializedName("wind")
private Wind wind;

public int getDt() {
return dt;
}

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

public Coord getCoord() {
return coord;
}

public void setCoord(Coord coord) {
this.coord = coord;
}

public List<WeatherItem> getWeather() {
return weather;
}

public void setWeather(List<WeatherItem> weather) {
this.weather = weather;
}

public String getName() {
return name;
}

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

public int getCod() {
return cod;
}

public void setCod(int cod) {
this.cod = cod;
}

public Main getMain() {
return main;
}

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

public Clouds getClouds() {
return clouds;
}

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

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public Sys getSys() {
return sys;
}

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

public String getBase() {
return base;
}

public void setBase(String base) {
this.base = base;
}

public Wind getWind() {
return wind;
}

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

Daily Forecast 5 Days

Daily Forecast 5 Days is available at any location or city. The forecast includes daily weather data and the response data is available in JSON or XML format

By city name

You can search 5 day weather forecast with daily average parameters by city name. All weather data can be obtained in JSON and XML formats.

api.openweathermap.org/data/2.5/forecast/daily?q={city name}&cnt={cnt}&appid={API key}

Step 6: Create Model classes for Weather response.

MultipleDaysWeatherResponse.java

public class MultipleDaysWeatherResponse {

@SerializedName("city")
private City city;

@SerializedName("cnt")
private int cnt;

@SerializedName("cod")
private String cod;

@SerializedName("message")
private double message;

@SerializedName("list")
private List<ListItem> list;

public City getCity() {
return city;
}

public void setCity(City city) {
this.city = city;
}

public int getCnt() {
return cnt;
}

public void setCnt(int cnt) {
this.cnt = cnt;
}

public String getCod() {
return cod;
}

public void setCod(String cod) {
this.cod = cod;
}

public double getMessage() {
return message;
}

public void setMessage(double message) {
this.message = message;
}

public List<ListItem> getList() {
return list;
}

public void setList(List<ListItem> list) {
this.list = list;
}
}

Step 7: Fetching network data.

We will use simple Java HttpURLConnection for fetching data from Rest APIs. We will have these network operations in Service ability.

WeatherServiceAbility.java

public class WeatherServiceAbility extends Ability {
private MyRemote remote = new MyRemote();

private static final String CODE = "CODE";
private static final String TEMP = "TEMP";
private static final String HUMIDITY = "HUMIDITY";
private static final String DESCRIPTION = "DESCRIPTION";
private static final String WIND = "WIND";
private static final String CITY_INFO = "CITY_INFO";
private static final String WEATHER_CODE = "WEATHER_CODE";
private static final String MAX_TEMP = "MAX_TEMP";
private static final String MIN_TEMP = "MIN_TEMP";

private static final String FORECAST_URL = "forecast/daily";
private static final String WEATHER_URL = "weather";
private static final String BASE_URI = "https://api.openweathermap.org/data/2.5/";
private static final String API_KEY ="appid={{Add your Key}}";
private static final String UNITS ="units=metric";

@Override
protected IRemoteObject onConnect(Intent intent) {
super.onConnect(intent);
return remote.asObject();
}
class MyRemote extends RemoteObject implements IRemoteBroker {
private static final int SUCCESS = 0;
private static final int CURRENT = 1001;
private static final int FORECAST = 1002;

MyRemote() {
super("MyService_MyRemote");
}

@Override
public boolean onRemoteRequest(int code, MessageParcel data, MessageParcel reply, MessageOption option) {
RequestParam param = getParamFromMessageParcel(data);
switch (code) {
case CURRENT: {
String output = startNetworkCall(BASE_URI + WEATHER_URL, new String[]{"q=" + param.getCity(), UNITS, API_KEY});
CurrentWeatherResponse countryObj = new Gson().fromJson(String.valueOf(output), CurrentWeatherResponse.class);
reply.writeString(bundleSuccessResult(countryObj));
}
break;
case FORECAST: {
String output = startNetworkCall(BASE_URI + FORECAST_URL, new String[]{"q=" + param.getCity(), "cnt=5", UNITS, API_KEY});
MultipleDaysWeatherResponse fiveHistoryObj = new Gson().fromJson(String.valueOf(output), MultipleDaysWeatherResponse.class);
reply.writeString(bundleforPredictedWeather(fiveHistoryObj));
}
break;
default: {
reply.writeString("service not defined");
return false;
}
}
return true;
}

@Override
public IRemoteObject asObject() {
return this;
}
}

private RequestParam getParamFromMessageParcel(MessageParcel message) {
String zsonStr = message.readString();
try {
return new Gson().fromJson(zsonStr, RequestParam.class);
} catch (RuntimeException e) {

}
return null;
}

private String bundleSuccessResult (CurrentWeatherResponse response) {
Map<String, Object> zsonResult = new HashMap<String, Object>();
zsonResult.put(CODE, MyRemote.SUCCESS);
zsonResult.put(TEMP , String.format(Locale.getDefault(), "%.0f°C", response.getMain().getTemp()) );
zsonResult.put(HUMIDITY , response.getMain().getHumidity());
zsonResult.put(WIND , response.getWind().getSpeed());
zsonResult.put(DESCRIPTION , response.getWeather().get(0).getDescription());
zsonResult.put(CITY_INFO , response.getName()+", "+response.getSys().getCountry());
zsonResult.put(WEATHER_CODE , response.getWeather().get(0).getId());
return ZSONObject.toZSONString(zsonResult);
}

private String bundleforPredictedWeather (MultipleDaysWeatherResponse response) {
List<ListItem> list = response.getList();
ZSONArray array = new ZSONArray();
for (ListItem item : list) {
Map<String, Object> zsonResult = new HashMap<String, Object>();
zsonResult.put(MAX_TEMP , item.getTemp().getMax());
zsonResult.put(MIN_TEMP ,item.getTemp().getMin());
zsonResult.put(WEATHER_CODE , (item.getWeather().get(0).getId()));
array.add(zsonResult);
}
return ZSONObject.toZSONString(array);
}

public String startNetworkCall(String link, String params[]) {
NetManager netManager = NetManager.getInstance(null);
if (!netManager.hasDefaultNet()) {
return null;
}
NetHandle netHandle = netManager.getDefaultNet();

// Listen to network state changes.
NetStatusCallback callback = new NetStatusCallback() {
// Override the callback for network state changes.
};
netManager.addDefaultNetStatusCallback(callback);

// Obtain a URLConnection using the openConnection method.
HttpURLConnection connection = null;
try {
StringBuilder urlFinal = new StringBuilder();
urlFinal.append(link);
urlFinal.append("?");
for (int i = 0; i < params.length; i++) {
urlFinal.append("&");
urlFinal.append(params[i]);
}
java.net.URL url = new URL(urlFinal.toString());

URLConnection urlConnection = netHandle.openConnection(url,
java.net.Proxy.NO_PROXY);
if (urlConnection instanceof HttpURLConnection) {
connection = (HttpURLConnection) urlConnection;
}
connection.setRequestMethod("GET");

connection.connect();

int responseCode = connection.getResponseCode();

if (responseCode == HttpURLConnection.HTTP_OK) {
BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream()));
StringBuilder sb = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
sb.append(line);
}
br.close();
return sb.toString();
}
return null;
} catch (IOException e) {
e.printStackTrace();
return "IOException";
} finally {
if (connection != null) {
connection.disconnect();
}
}
}
}

Step 8: Display fetched data in UI

Once the user clicks search icon, the city name passed as parameter to async call from JavaScript. Fetch weather method will send the feature ability call to the java layer.

fetchWeather: async function(city) {
var actionData = {};
actionData.city = ""+city;

var action = {};
action.bundleName = 'com.huawei.phonesample';
action.abilityName = 'com.huawei.phonesample.WeatherServiceAbility ';
action.messageCode = ACTION_MESSAGE_CODE_CURRENT;
action.data = actionData;
action.abilityType = ABILITY_TYPE_EXTERNAL;
action.syncOption = ACTION_SYNC;
var result = await FeatureAbility.callAbility(action);
var ret = JSON.parse(result);

this.inProgress = false;
this.currentTemperature = ret.TEMP;
this.currentDesc = ret.DESCRIPTION;
this.currentWind = ret.WIND;
this.currentHumidity = ret.HUMIDITY + "%"
this.currentCityInfo = ret.CITY_INFO
this.searchValue = ""
this.updateWeatherArt(ret.WEATHER_CODE)
this.toDay = new Date().getDate() + "-" + month_names[new Date().getMonth()];
if (ret.code == 0) {
console.info('plus result is:' + JSON.stringify(ret.abilityResult));
this.currentTemperature = ret.TEMP + "°";
this.currentDesc = ret.DESCRIPTION;
this.currentWind = ret.WIND;
this.currentHumidity = ret.HUMIDITY + "%"
} else {
console.error('plus error code:' + JSON.stringify(ret.code));
}
},

Once we have data after call ability parse the result to json object and retrieve the data required to display the UI.

Set the inProgress flag also to false to update the UI section with data.

Update weather icon checking the weather code.

updateWeatherArt(weatherCode) {
if (weatherCode / 100 == 2) {
this.artImage = "/common/art_storm.png";
} else if (weatherCode / 100 == 3) {
this.artImage = "/common/art_rain.png";
} else if (weatherCode / 100 == 5) {
this.artImage = "/common/art_light_rain.png";
} else if (weatherCode / 100 == 6) {
this.artImage = "/common/art_snow.png";
} else if (weatherCode / 100 == 7) {
this.artImage = "/common/art_clear.png";
} else if (weatherCode == 800) {
this.artImage = "/common/art_clear.png";
} else if (weatherCode == 801) {
this.artImage = "/common/art_light_clouds.png";
} else if (weatherCode == 803) {
this.artImage = "/common/art_light_clouds.png";
} else if (weatherCode / 100 == 8) {
this.artImage = "/common/art_clouds.png";
}

}

Similarly update the five day data for the second UI section.

Tips and Tricks

You can use Cloud emulator for development. I have explained UI updating for current weather, but similarly you can update the carousel UI with array object you got from second API response. There are few more options like fetching the current location and use that for getting the weather, which will be an added feature.

Conclusion

In this article, we have learnt how to create weather application using HarmonyOS UI components and Service Ability. We have explored Serviceability for fetching data from open source REST API.

References

JS API References

Weather API

Original Source