第十四章:进入实战,开发酷欧天气
           实现一个功能较为完整的天气预报程序。中文:酷欧天气;英文:Cool weather
14.1功能需求和技术可行性分析。
(1)应具备以下功能:
       可罗列所有省市县、可查全国任意城市天气信息、自由切换城市、手动更新和后台自动更新天气的功能。需要用到UI、网络、数据存储和服务等等。
(2)技术可行性分析:
       天气数据问题:免费天气接口越来越少,彩云天气和和和风天气提供接口,彩云精确到了分钟,实时专业,一天1000次免费请求;和风每天3000次。我们这里使用和风天气。
全国省市县数据问题:网上没有稳定接口去使用,郭专门架设一台服务器提供全国所有省市县数据。链接:http://guolin.tech/api/china(注意不是https),返回的是JSON数据类型。若要访问城市,只需要将省份id添加到url最后地址就可以了。每个县或区都有一个weather_id,再拿这个区访问和风天气的接口。
注册和风天气:https://console.heweather.com/register。注册后使用这个账号登录。注册一个应用,包名和KEY名称自行指定。
14.2Git时间-将会代码托管到Gtithub上。
        第一步,注册。
        第二步,创建新项目,勾选.gitgnore文件和开源协议,命名版本库为CoolWeather。此时版本库主页地址为:https://github.com/hzka/coolweather.git。系统自动为我们建立了三个文件。

第三步,到Cool Weather目录下,右键git bash

git clone https://github.com/hzka/coolweather.git

第四步,ls -al显示所有目录文件
          第五步,

git add .
git commit -m “First Commit”
git push origin master

 14.3创建数据库和表
        1.和MainActiviy.java同级的目录下建立用于存放数据模型相关代码的db目录,存放GSON模型相关代码的gson包,用于存放服务相关的service包,最后是存放工具相关的util包。
        2.增加依赖库,okhttp用于进行网络请求,Litepal用于对数据库进行操作;GSON用于解析JSON数据,Glide用于加载和显示图片。

dependencies {compile fileTree(dir: 'libs', include: ['*.jar'])testCompile 'junit:junit:4.12'compile 'com.android.support:appcompat-v7:23.3.0'compile 'org.litepal.android:core:1.3.2'compile 'com.squareup.okhttp3:okhttp:3.4.1'compile 'com.google.code.gson:gson:2.7'compile 'com.github.bumptech.glide:glide:3.7.0'
}

3.设计数据库表结构,建立三张表,province、city和country,存放省市县数据信息,对应三个实体类。id是每个实体类都应该有的字段,provinceName记录省的名字,provincecode是指省的代号。另外Litpal每一个实体类必须继承自DataSupport类。其他两个理论一样。

public class Province extends DataSupport{private int id;private String provincename;private int provinceCode;public int getId() {return id;}public void setId(int id) {this.id = id;}public String getProvincename() {return provincename;}public void setProvincename(String provincename) {this.provincename = provincename;}public int getProvinceCode() {return provinceCode;}public void setProvinceCode(int provinceCode) {this.provinceCode = provinceCode;}
}

4.在main目录下新建assets目录,下再建立litepal.xml文件,编辑,将数据库名称指定为cool_weather,版本为1,并将三个实体类添加到映射列表中。

<litepal><dbname value="cool_weather"/><version value="1"/><list><mapping class= "com.example.hzk.myapplication.db.Province"/><mapping class = "com.example.hzk.myapplication.db.City"/><mapping class= "com.example.hzk.myapplication.db.Country"/></list>
</litepal>

5.修改AndroidManifest.xml以配置LitepalApplication。将数据库所有配置完成。

<applicationandroid:name="org.litepal.LitePalApplication"....><activity android:name=".MainActivity">....</activity>
</application>

14.3遍历省市县数据
       1.所有信息从服务器端获取,因此交互必不可少,所有我们在util下建立HttpUtil类。与服务器交互较为简单。发起一条http请求,传入请求地址,注册一个回调来处理服务器响应即可。

public class HttpUtil {public static void senOkHttpRequest(String address, okhttp3.Callback callback) {OkHttpClient client = new OkHttpClient();Request request = new Request.Builder().url(address).build();client.newCall(request).enqueue(callback);}
}

2.我们在Utils实体类中提供了handleProvinceResponse、handleCityResponse、handleCountryResponse去解析和处理服务器返回的省市县数据。先使用JSONARRAY和JSONObject将数据解析出来,然后组装成实体对象,最后使用save方法存放到数据库当中。

 /*** 解析和处理服务器返回的省级数据*/public static boolean handleProvinceResponse(String response){if(!TextUtils.isEmpty(response)){try {JSONArray allProvinces = new JSONArray(response);for(int i = 0;i<allProvinces.length();i++){JSONObject provinceobjext = allProvinces.getJSONObject(i);Province province = new Province();province.setProvincename(provinceobjext.getString("name"));province.setProvinceCode(provinceobjext.getInt("id"));province.save();}return true;} catch (JSONException e) {e.printStackTrace();}}return false;
}

3.由于我们的遍历市县的功能后面还要服用,因此写在碎片而非活动中,为头布局定义标题栏,高度设置为ActionBar的高度,背景色设为colorPrimary,头文件设置TextView用于设置内容。Button按钮用于执行返回操作。我们使用自定义图片,碎片中最好不能直接使用Toolbar或者Actionbar。否则会出现一些不好的效果。在choose_area.xml中:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"android:background="#fff"><RelativeLayoutandroid:layout_width="match_parent"android:layout_height="?attr/actionBarSize"android:background="?attr/colorPrimary"><TextViewandroid:id="@+id/title_text"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_centerInParent="true"android:textColor="#fff"android:textSize="20sp"/><Buttonandroid:id="@+id/back_button"android:layout_width="25dp"android:layout_height="25dp"android:layout_marginLeft="10dp"android:layout_alignParentLeft="true"android:layout_centerVertical="true"android:background="@drawable/ic_back"/></RelativeLayout>
</LinearLayout>

4.定义好投布局之后,使用Listview来遍历省市县数据的碎片了。新建ChoeAreaFragment继承自Fragment。onCreateView用于获取控件实例,初始化ArrayAdapter,设置为ListView的适配器。在onActivityCreated中完成ListView和Button的点击事件,初始化完成。
        5.在onActivityCreated最后,调用queryProvinces();以加载省级数据,先设置标题栏文字,隐藏返回按钮,调用LitePal读取数据库内的省级数据,若读取到,显示于界面,否则拼装请求地址,调用queryFromServer方法即查询省市县到服务器上查询。第一遍时肯定没有请求到,因此必须使用该方法请求,然后存放至LitePal数据库中,以后查询就不用从数据库访问了。
        6.queryFromServer主要是调用HttpUtil的sendOkhttpRequest来向服务器发送请求,相应数据会回调至onResponse方法中,然后我们调用Utilty的handleProvinceRespnse来解析和处理返回数据,存储至数据库中。接下来调用的queryProvinces来重新加载省级数据,由于牵扯到UI操作,必须切换至主线程,runOnUIThread实现该过程,这样queryProvinces直接将数据显示于主界面上。

package com.example.hzk.myapplication.util;import android.annotation.TargetApi;
...
import okhttp3.Response;/*** Created by HZK on 2018/12/19.*/
public class ChooseAreaFragment extends Fragment {public static final int LEVEL_PROVINCE = 0;public static final int LEVEL_CITY = 1;public static final int LEVEL_COUNTRY = 2;private ProgressDialog progressDialog;private TextView title_text;private Button back_button;private ListView listView;private ArrayAdapter<String> adapter;private List<String> datalist = new ArrayList<>();/*** 省列表*/private List<Province> provinceList;/*** 市列表*/private List<City> cityList;/*** 县列表*/private List<Country> countryList;/*** 选中的省*/private Province selecetProvince;/*** 选中的城市*/private City selectCity;/*** 当前选中的级别*/private int currentlevel;@TargetApi(Build.VERSION_CODES.M)@Overridepublic View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {View view = inflater.inflate(R.layout.choose_area, container, false);title_text = (TextView) view.findViewById(R.id.title_text);back_button = (Button) view.findViewById(R.id.back_button);listView = (ListView) view.findViewById(R.id.list_view);adapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_list_item_1, datalist);listView.setAdapter(adapter);return view;}@Overridepublic void onActivityCreated(Bundle savedInstanceState) {super.onActivityCreated(savedInstanceState);listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {@Overridepublic void onItemClick(AdapterView<?> parent, View view, int position, long id) {if (currentlevel == LEVEL_PROVINCE) {selecetProvince = provinceList.get(position);queeryCities();} else if (currentlevel == LEVEL_CITY) {selectCity = cityList.get(position);queryCounties();}}});back_button.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {if (currentlevel == LEVEL_COUNTRY) {queeryCities();} else if (currentlevel == LEVEL_CITY) {queryProvinces();}}});queryProvinces();}/*** 查询所有省,优先从数据库中查询,若是没有再到服务器上查询*/private void queryProvinces() {title_text.setText("中国");back_button.setVisibility(View.GONE);provinceList = DataSupport.findAll(Province.class);if (provinceList.size() > 0) {datalist.clear();for (Province province : provinceList) {datalist.add(province.getProvincename());}adapter.notifyDataSetChanged();listView.setSelection(0);currentlevel = LEVEL_PROVINCE;} else {String address = "http://guolin.tech/api/china";queryFromServer(address, "province");}}/*** 查询省内的市,优先从数据库中查询,若是没有再到服务器上查询*/private void queeryCities() {title_text.setText(selecetProvince.getProvincename());back_button.setVisibility(View.VISIBLE);cityList = DataSupport.where("provinceid = ?", String.valueOf(selecetProvince.getId())).find(City.class);if (cityList.size() > 0) {datalist.clear();for (City city : cityList) {datalist.add(city.getCityname());}adapter.notifyDataSetChanged();listView.setSelection(0);currentlevel = LEVEL_CITY;} else {int provinceCode = selecetProvince.getProvinceCode();String address = "http://guolin.tech/api/china/" + provinceCode;queryFromServer(address, "city");}}/*** 查询市内的县,优先从数据库中查询,若是没有再到服务器上查询*/private void queryCounties() {title_text.setText(selectCity.getCityname());back_button.setVisibility(View.VISIBLE);countryList = DataSupport.where("cityid=?", String.valueOf(selectCity.getId())).find(Country.class);if (countryList.size() > 0) {datalist.clear();for (Country country : countryList) {datalist.add(country.getCountryname());}adapter.notifyDataSetChanged();listView.setSelection(0);currentlevel = LEVEL_COUNTRY;} else {int provinceCode = selecetProvince.getProvinceCode();int cityCode = selectCity.getCitycode();String address = "http://guolin.tech/api/china/" + provinceCode + "/" + cityCode;queryFromServer(address, "country");}}/*** 根据传入的地址在服务器上查询省市县数据** @param address* @param type*/private void queryFromServer(String address, final String type) {showProgressDialog();HttpUtil.senOkHttpRequest(address, new Callback() {@Overridepublic void onFailure(Call call, IOException e) {getActivity().runOnUiThread(new Runnable() {@TargetApi(Build.VERSION_CODES.M)@Overridepublic void run() {closeProgressDialog();Toast.makeText(getContext(),"加载失败",Toast.LENGTH_SHORT).show();}});}@Overridepublic void onResponse(Call call, Response response) throws IOException {String responsetext = response.body().string();boolean result = false;if ("province".equals(type)) {result = Utility.handleProvinceResponse(responsetext);} else if ("city".equals(type)) {result = Utility.handleCityResponse(responsetext, selecetProvince.getId());} else if ("country".equals(type)) {result = Utility.handleCountryResponse(responsetext, selectCity.getId());}if(result){getActivity().runOnUiThread(new Runnable() {@Overridepublic void run() {closeProgressDialog();if ("province".equals(type)) {queryProvinces();} else if ("city".equals(type)) {queeryCities();} else if ("country".equals(type)) {queryCounties();}}});}}});}private void closeProgressDialog() {if(progressDialog!=null){progressDialog.dismiss();}}private void showProgressDialog() {if(progressDialog==null){progressDialog = new ProgressDialog(getActivity());progressDialog.setMessage("正在加载...");progressDialog.setCanceledOnTouchOutside(false);}progressDialog.show();}
}

7.碎片不能直接显示于界面,将其添加到activity_main的主活动中,

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"><fragmentandroid:id="@+id/choose_area_fragment"android:name="com.example.hzk.myapplication.util.ChooseAreaFragment"android:layout_width="match_parent"android:layout_height="match_parent"android:text="Hello World!" />
</FrameLayout>

8.增加网络访问权限和去掉Actionbar。

Styles.xml中:
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
增加权限:
<uses-permission android:name="android.permission.INTERNET"/>

这样数据就可以显示出来了。
14.5 显示天气信息
         开始到查询天气的环节了,将天气信息显示出来,由于和风天气返回JSON信息复杂,所以我们借助于GSON来解析。
1.定义GSON实体类。
        在GSON包下建立一个Basic类,JSON中的一些字段不太适合直接作为Java字段命名,使用注解让JSON字段和JAva字段之间建立应映射关系。相同方法建立basic、aqi、now、suggestion和daily_forecast五个类。

返回数据格式:                                                                                    basic中具体内容:

public class Basic {@SerializedName("city")public String cityname;@SerializedName("id")public String weatherId;public Update update;private class Update {@SerializedName("loc")public String updateTime;}
}

2.最后创建一个总的实例类来引用刚刚创建的每个实体类。

public class Weather {public String status;public Basic basic;public AQI aqi;public Now now;public Suggestion suggestion;@SerializedName("daily_forecast")public List<Forecast> forecastList;
}

3.编写天气界面:
         创建显示天气信息的活动,创建Activity和相应的xml,我们将界面的不同部分写在不同的布局文件中,最后通过引入布局的形式集成到刚才建立的xml中,新建title.xml作为头布局。放两个TextView,一个居中显示城市名,一个居右显示更新时间。新建now.xml作为显示当前天气信息的布局。天气布局中也是放置两个TextView,一个用于显示当前温度,一个用于显示天气概况。新建forecast.xml作为未来几天天气信息的布局。最外层的LinearLayout定义半透明背景,在使用TextView定义一个标题,接着使用LinearLayout定义显示未来几天信息的布局,根据服务器数据在代码中动态添加。在此基础之上建立未来天气信息的子项布局forecast_item.xml,4个TextView分别是天气预报日期、天气概况、最高和最低温度。新建AQI.xml作为空气质量信息的布局。定义半透明背景,使用TextView指定标题,然后使用LinearLayout定义一个半透明的背景,使用TextView指定一个标题,最后使用LinearLayout和Relativelayout嵌套的方式实现左右评分居中对齐的布局,分别用于显示AQI和PM25指数。新建suggestion.xml作为生活建议的布局。定义半透明背景和标题,显示舒适度、洗车指数和运动建议。最后建立的是将所有文件引入activity_weather.xml中,最外层布局使用一个Framelayout,将它的背景色设置为colorPrimary,ScorllView允许我们通过滚动的方式查看屏幕以外的内容。由于Scorllview只允许一个子布局,因此嵌套一个垂直方向的LinearLayout。再将所有布局引入。

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:background="@color/colorPrimary"><ScrollViewandroid:id="@+id/weather_layout"android:layout_width="match_parent"android:layout_height="match_parent"android:scrollbars="none"android:overScrollMode="never"><LinearLayoutandroid:orientation="vertical"android:layout_width="match_parent"android:layout_height="wrap_content"><include layout="@layout/title"/><include layout="@layout/now"/><include layout="@layout/forecast"/><include layout="@layout/aqi" /><include layout="@layout/suggestion"/></LinearLayout></ScrollView>
</FrameLayout>

 4.将天气显示到界面上。
       在Utility中显示用于解析天气JSON数据的方法,具体是通过JSONObject和JSONArray将天气数据中的主题内容解析出来。由于我们之前已经定义了相应的GSON实体类,所以我们可以通过fromJson方法将JSON数据转换成Weather对象。

public static Weather handleWeatherResponse(String reponse){try {JSONObject jsonObject = new JSONObject(reponse);JSONArray jsonArray = jsonObject.getJSONArray("HeWeather");String weathercontent = jsonArray.getJSONObject(0).toString();return  new Gson().fromJson(weathercontent,Weather.class);} catch (JSONException e) {e.printStackTrace();}return null;}

5.如何在活动中请求数据并将数据显示于界面上。在WeatherActivity中先获取实例,尝试从本地缓存中读取天气数据,第一次肯定没有,所以会调用requestWeather去服务器端请求天气数据。这是首先要将ScrollView隐藏。
       6.requestWeather中首先使用参数中传入的天气id和我们之前申请好的API key拼装一个接口地址。接着调用Okhttp的senOkHttpRequest方法向该地址发送请求,服务器会返回JSON格式对象。将其转为String类型数据,并将当前线程切换至主线程。如果返回的stauts为ok,则说明请求天气成功,将数据缓存至SharedPreferces中,并调用showWeatherInfo中显示。
       7.showWeatherInfo从Weather对象获取数据,并显示到相应控件中,注意未来天气使用for循环来处理每天天气信息,动态加载forecast_item.xml并设置相应的数据。添加至父布局中,最后显示ScroollView,这样就处理完该Activity中的逻辑。

public class WeatherActivity extends AppCompatActivity {private ScrollView weatherlayout;private TextView titlecity;private TextView tilteUpdatedtime;private TextView degreetext;private TextView weatherinfotext;private LinearLayout forecastlayout;private TextView aqitext;private TextView pm25text;private TextView comforttext;private TextView carwashtext;private TextView sporttext;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_weather);//初始化各控件。weatherlayout = (ScrollView) findViewById(R.id.weather_layout);titlecity = (TextView) findViewById(R.id.title_city);tilteUpdatedtime = (TextView) findViewById(R.id.title_update_time);degreetext = (TextView) findViewById(R.id.degree_text);weatherinfotext = (TextView) findViewById(R.id.weather_info_text);forecastlayout = (LinearLayout) findViewById(R.id.forecast_layout);aqitext = (TextView) findViewById(R.id.aqi_text);pm25text = (TextView) findViewById(R.id.pm25_text);comforttext = (TextView) findViewById(R.id.comfort_text);carwashtext = (TextView) findViewById(R.id.car_wash_text);sporttext = (TextView) findViewById(R.id.sport_text);SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);String weatherStrings = prefs.getString("weather", null);if (weatherStrings != null) {//有缓存时直接显示数据。Weather weather = Utility.handleWeatherResponse(weatherStrings);showeaherInfo(weather);} else {//无缓存时去服务器查询天气String weatherId = getIntent().getStringExtra("weather_id");weatherlayout.setVisibility(View.INVISIBLE);requestWeather(weatherId);}}/*** 根据天气ID请求城市天气信息** @param weatherId*/private void requestWeather(final String weatherId) {String weatherUrl = "http://guolin.tech/api/weather?cityid=" + weatherId +"&key=484d9d657d9e4c7eab7d50493c3cdb59";HttpUtil.senOkHttpRequest(weatherUrl, new Callback() {@Overridepublic void onFailure(Call call, IOException e) {runOnUiThread(new Runnable() {@Overridepublic void run() {Toast.makeText(WeatherActivity.this, "获取天气新信息失败", Toast.LENGTH_SHORT).show();}});}@Overridepublic void onResponse(Call call, Response response) throws IOException {final String responsetext = response.body().string();final Weather weather = Utility.handleWeatherResponse(responsetext);runOnUiThread(new Runnable() {@Overridepublic void run() {if (weather != null && "ok".equals(weather.status)) {SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(WeatherActivity.this).edit();editor.putString("weather", responsetext);editor.apply();showeaherInfo(weather);} else {Toast.makeText(WeatherActivity.this, "获取天气新信息失败", Toast.LENGTH_SHORT).show();}}});}});}private void showeaherInfo(Weather weather) {String city_name = weather.basic.cityname;String update_name = weather.basic.update.updateTime;String degree = weather.now.temperature+"摄氏度";String weatherinfo = weather.now.more.info;titlecity.setText(city_name);tilteUpdatedtime.setText(update_name);degreetext.setText(degree);weatherinfotext.setText(weatherinfo);forecastlayout.removeAllViews();for(Forecast forecast: weather.forecastList){View view = LayoutInflater.from(this).inflate(R.layout.forecast_item,forecastlayout,false);TextView datatext = (TextView) view.findViewById(R.id.data_text);TextView infotext = (TextView) view.findViewById(R.id.info_text);TextView maxText = (TextView) view.findViewById(R.id.max_text);TextView minText = (TextView) view.findViewById(R.id.min_text);datatext.setText(forecast.data);datatext.setText(forecast.more.info);datatext.setText(forecast.temperature.max);datatext.setText(forecast.temperature.min);forecastlayout.addView(view);}if(weather.aqi!=null){aqitext.setText(weather.aqi.city.aqi);pm25text.setText(weather.aqi.city.pm25);}String comfoort = "舒适度:"+weather.suggestion.comft.info;String carwash = "洗车指数:"+weather.suggestion.carWash.info;String sport = "运动建议"+weather.suggestion.sport.info;comforttext.setText(comfoort);carwashtext.setText(carwash);sporttext.setText(sport);weatherlayout.setVisibility(View.VISIBLE);}
}

8.修改ChooseAreaFragment中的代码,即如何从省市县列表界面跳转至天气界面。比较简单,若当前级别为LEVEL_COUNTRY,则将当前选中的天气和id传过去。

public void onActivityCreated(Bundle savedInstanceState) {super.onActivityCreated(savedInstanceState);listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {@Overridepublic void onItemClick(AdapterView<?> parent, View view, int position, long id) {...queryCounties();} else if (currentlevel == LEVEL_COUNTRY) {String weatherId = countryList.get(position).getWeatherId();Intent intent = new Intent(getActivity(), WeatherActivity.class);intent.putExtra("weather_id",weatherId);startActivity(intent);getActivity().finish();}}});
....
}

因为目前和风天气的接口已经发生了变化,可在官网查看其最新使用方法在这里不作过多赘述。
14.8修改图标或名称。
        (1)在AndroidManifest.xml中将ic_back.png放入所有以mipmap开头的目录下,(2)在application中引入该ic_back的调用,(3)修改string类型下的app_name字符串为酷欧天气。

<applicationandroid:name="org.litepal.LitePalApplication"android:allowBackup="true"android:icon="@mipmap/ic_back"android:label="@string/app_name"android:supportsRtl="true"android:theme="@style/AppTheme">

14.9改进
     增加设置选项、优化软件界面、允许选择多个城市、提供更加完整天气信息等。

第十五章——将应用发布于360应用中
       谷歌官方Google Play,国内360、豌豆荚(小公司)、百度、应用宝(腾讯)等等。在这里我们将主要说明如何将应用发布于360中。
15.1生成签名后的APK文件
      自己测试时没有进行签名为什么可以安装,这是因为有一个默认的keystore文件帮我们自动进行了签名。

这是最简单的,只适用于开发阶段,等正式发布了需要正式的keystore进行签名才行。
15.1.1使用AS生成
1.按如下操作

2.由于我们没有正式的Keystore文件,点击Create new以创建keyStore必须的信息。

这样就可以生成了。
15.1.2使用Gradle生成
        Gradle使用Groovy语言编写,我们只使用期来构建项目而已。
        1.在app的build.gradle目录下增加signingConfigs,在buildTypes目录下增加该配置:

signingConfigs{config{storeFile file('C:/Users/HZK/kevinhe.jks')storePassword '你的密码'keyAlias 'kevinhe'keyPassword '你的密码'}
}
buildTypes {release {minifyEnabled falseproguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'signingConfig signingConfigs.config}
}

2.生成APK之前,先双击Clean一下,点击Assemble release,此为生成正式版APK文件,assemble同时生成正式版和测试版。

3.为了防止隐私信息泄露,我们配置全局键值对的方式来使用:在gradle.properties中增加如下内容:

KEY_PATH=C:/Users/HZK/kevinhe.jks
KEY_PASS=你的密码
ALIAS_NAME=kevinhe
ALIAS_PASS=你的密码
修改之前的signingConfigs为:
signingConfigs{config{storeFile file(KEY_PATH)storePassword KEY_PASSkeyAlias ALIAS_NAMEkeyPassword ALIAS_PASS}
}

15.2 申请360开发者账号
         网址:http://dev.360.cn/;点击,然后注册开发者账号:

注册结束后,按照下一步发布应用程序,上传后加固,然后进行发布,一步步按照步骤来,傻瓜式操作。
15.4嵌入广告进行盈利
       目前没有这个需求,因此可以跳过。
       至此,第一行代码这本书就结束了。正如书的结语中所说的:我挥舞着键盘和本子,发誓要把世界写个明明白白。希望大家在移动开发的浪潮中屹立长青。

《第一行代码》总结之实战酷欧天气、发布应用(九)相关推荐

  1. 学习《第一行代码》之实战项目-搜杰天气

    前言 书上提及中国天气网提供的API接口早已经无法调用,初期费了些周折,项目搁浅了一段时间,现重新接触项目,学会了调用聚合数据,功能得以实现.本篇系学习酷欧天气项目,做出小项目-搜杰天气,后续依情况或 ...

  2. 第一行代码 开发酷欧天气DataSupport,ProgressDialog,加载失败,PreferenceManager.getDefaultSharedPreferences()方法

    第一行代码学到开发酷欧天气时,在继承DataSupport类时发现DataSupport过时,于是发现LitePalSupport可以替代DataSupport.后面会用到一个DataSupport. ...

  3. 用kotlin方式打开《第一行代码:Android》之开发酷欧天气(2)

    参考:<第一行代码:Android>第2版--郭霖 注1:本文为原创,例子可参考郭前辈著作:<第一行代码:Android>第2版 注2:本文不赘述android开发的基本理论, ...

  4. 用kotlin方式打开《第一行代码:Android》之开发酷欧天气(1)

    参考:<第一行代码:Android>第2版--郭霖 注1:本文为原创,例子可参考郭前辈著作:<第一行代码:Android>第2版,转载请注明出处! 注2:本文不赘述androi ...

  5. Android实战:CoolWeather酷欧天气(加强版数据接口)代码详解(上)

    -----------------------------------该文章代码已停更,可参考浩比天气(更新于2019/6/25)----------------------------------- ...

  6. 《第二行代码》—— 酷欧天气的开发

    使用的教材是国@郭霖写的<第二行代码> 应用名:Cool Weather 一.功能需求及技术可行性分析 1.具备的功能 可以罗列出全国所有的省市县 可以查看全国任意城市的天气信息 可以自由 ...

  7. Android 学习之《第一行代码》第二版 笔记(二十三)Material Design 实战 —— 卡片式布局

    实现基础: Android 学习之<第一行代码>第二版 笔记(二十二)Material Design 实战 -- 悬浮按钮和可交互提示 卡片式布局 卡片式布局是 Materials Des ...

  8. 安卓学习专栏——实战项目酷欧天气(4)给天气页面加上背景图片

    步骤 系列文章 前言 实现效果 项目结构 1.获取必应每日一图 1.1修改修改activity_weather.xml 1.2修改WeatherActivity 2.背景图和状态栏效果修改 2.1修改 ...

  9. 安卓学习专栏——实战项目酷欧天气(2)遍历全国省市县数据

    步骤 系列文章 前言 1.实现效果 2.项目结构 util包 util包下新建HttpUtil util包下新建Utility 3.新建choose_area.xml布局 4.新建ChooseArea ...

最新文章

  1. python中filenotfounderror_Python3 报错 FileNotFoundError: [WinError 2]
  2. 信息管理代码分析二读取二进制文件数据
  3. 西卡 你要浮出水面啦
  4. Unable to find remote helper for 'https'解决办法
  5. 如何使用C#自带的GDI+双缓冲类BufferedGraphics实现双缓冲功能
  6. WSDM 2022 | 合约广告自适应统一分配框架
  7. SLS机器学习最佳实战:日志聚类+异常告警
  8. Golang——包引入和闭包
  9. Jmeter-Maven-Plugin高级应用:Selecting Tests To Run
  10. JWPlayer快速入门指南(中文)
  11. Windows平台调查网络程序的2个系统工具
  12. V模型、W模型、测试工具的介绍
  13. Altium Designer画PCB详细教程
  14. 小程序解析富文本(支持视频,支持微信编辑器,支持135编辑器富文本样式)
  15. 服务器部署dble全流程
  16. 阿里云数据工厂DataWorks
  17. Java原始数据类型
  18. unity,射手游戏
  19. 0316复利和单利计算更新4
  20. kafka的安装使用

热门文章

  1. 大数据毕设选题 - 深度学习股票预测系统(python Django)
  2. html5中将doctype分为几种,HTML!DOCTYPE是什么
  3. 【金融学】耶鲁大学 Financial Markets
  4. 爬取招聘网站信息,并使用pyecharts和matplotlib进行简单的可视化测试
  5. html全屏枫叶飘落,jQuery飘落的枫叶
  6. closeEvent
  7. 条码管理系统,助力企业打造轻量级数字化车间
  8. LOJ#538. 「LibreOJ NOIP Round #1」数列递推
  9. 一个暴风雨的夜晚——一个普通而又伟大老师的一天
  10. Eclipse常规设置(我的Style我做主)