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

参考:《第一行代码:Android》第2版——郭霖

注1:本文为原创,例子可参考郭前辈著作:《第一行代码:Android》第2版,转载请注明出处!

注2:本文不赘述android开发的基本理论,不介绍入门知识,不介绍Android Studio基本安装,开门见山,直接使用kotlin改写郭前辈的《第一行代码:Android》中的部分例子,有机会的话自己做一些新例子出来!

注3:本文基本以kotlin语言作为Android开发,偶尔涉及java作为对比

注4:开发基于Android Studio 3.0,并且新建项目时勾选“support kotlin”

进入实战——开发酷欧天气(1)

本次博文,我将尝试使用kotlin语言对郭前辈的《第一行代码》中的最后那个实战项目“酷欧天气”进行重写

我将跳过需求分析阶段,开门见山,进入正题(代码),详见书本:p486

14.4 遍历全国省市县数据(原书p499)

原书中在遍历了省市县数据后,郭神使用了litepal将数据写入到了sqlite表中,由于时间问题,跳过此步,采用每次都重新访问网址获取数据!

省市县网址:http://guolin.tech/api/china/

配置gradle

gradle.build(Module: app)

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
    compile 'com.android.support:appcompat-v7:25.3.1'
    testCompile 'junit:junit:4.12'
    compile 'org.jetbrains.anko:anko-sdk15:0.9'
    compile 'org.jetbrains.anko:anko-support-v4:0.9'
    compile 'org.jetbrains.anko:anko-appcompat-v7:0.9'
    compile	"com.google.code.gson:gson:2.7"
}

可以看到我们依然使用了anko类库,这个类库简直就是android上的jquery!

另外添加了Google的Gson类库,用于对在线获取的json数据转换

manifest

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="cn.cslg.weatherkotlin">

    <application android:allowBackup="true" android:icon="@mipmap/ic_launcher"
	......
    </application>
    
    <uses-permission android:name="android.permission.INTERNET" />
</manifest>

打开网络权限!

layout布局文件结构

这里写图片描述

红色框出来的表示本次博文中必须用到的布局,当然其他的xml后面也要用到

注:详细xml布局文件和代码参考原书即可,布局文件我是照搬的!或者跳到本文最后!

最后打开res>valus>styles.xml

<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">

由于我们使用的是自己做的头部布局,所以去掉Android默认的actionBar,改为NoActionBar即可!

class文件布局(kotlin文件)

这里写图片描述

数据类

数据类是一种非常强大的类,它可以让你避免创建Java中的用于保存状态但又操作 非常简单的POJO的模版代码。

数据类往往和数据库或者某个实体模型属性一一对应的
比如例子中要用到的省,市,县,天气都是带有自身的属性,set,get方法,使用java你必须一一的定义他们,而kotlin的data class可以让你轻松构建这些Model,并且自带提供了用于访问它们属性的简单的getter 和setter!

这次我们需要三个数据类:省,市,县

根据json格式,例如:

[{"id":1,"name":"北京"},{"id":2,"name":"上海"},{"id":3,"name":"天津"},{"id":4,"name":"重庆"},{"id":5,"name":"香港"},{"id":6,"name":"澳门"},{"id":7,"name":"台湾"},{"id":8,"name":"黑龙江"},{"id":9,"name":"吉林"},{"id":10,"name":"辽宁"},{"id":11,"name":"内蒙古"},{"id":12,"name":"河北"},{"id":13,"name":"河南"},{"id":14,"name":"山西"},{"id":15,"name":"山东"},{"id":16,"name":"江苏"},{"id":17,"name":"浙江"},{"id":18,"name":"福建"},{"id":19,"name":"江西"},{"id":20,"name":"安徽"},{"id":21,"name":"湖北"},{"id":22,"name":"湖南"},{"id":23,"name":"广东"},{"id":24,"name":"广西"},{"id":25,"name":"海南"},{"id":26,"name":"贵州"},{"id":27,"name":"云南"},{"id":28,"name":"四川"},{"id":29,"name":"西藏"},{"id":30,"name":"陕西"},{"id":31,"name":"宁夏"},{"id":32,"name":"甘肃"},{"id":33,"name":"青海"},{"id":34,"name":"新疆"}]

建立:

data class Province(val id: Int, val name: String)

data class City(val id: Int, val name: String)

data class County(val id: Int, val name: String,val weather_id:String)

看着十分简单,这是根据返回的json数据定义的,json中有哪些键,就需要几个属性,这里json返回的是id,name,还有县的weather_id

这些类放在哪里?

你可以单独放在一个datas.kt文件中,注意kotlin可以将多个类放在同一个文件中,不像java那样一个文件只能有一个public类!

我将这些数据类放在了adapters.kt当中,因为布局适配器adapter当中要用到他们的List泛型

另外Gson转换服务器端的json时也需要用这三个数据类的Litst泛型做映射!生成数据类实体对象,这个后面再说

Adapter

做过安卓开发的,都知道ListView需要适配器Adapter将不同类型的数据适配到其中,做成列表样式布局,我们也可以通过继承已有的Adapter做自己的apdater

注:详细请翻阅原书p116作者的FruitAdapter例子

“酷欧天气”项目在原书中,作者对省市县ListView使用的是ArrayAdapter适配器,这个适配器系统自带,非常简易,只传入了一个dataList,dataList是一个List String类,这里我不想每次都将上面那三种数据类一一放到一个dataList,因为这样每次都要遍历他们的List,再一个个add到dataList当中(原书p504),故而我封装了三个数据类List泛型相应的Adapter(肯能有人觉这样更加繁琐,但我觉得这样看着少了很多循环语句,比较美观)

Adapters.kt

package cn.cslg.weatherkotlin

import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.TextView
import org.jetbrains.anko.find

/**
 * Created by devil on 2017/5/20.
 */

//数据类
data class Province(val id: Int, val name: String)
data class City(val id: Int, val name: String)
data class County(val id: Int, val name: String,val weather_id:String)

class ProvinceAdapter(context: Context?, resource: Int, objects: List<Province>) : ArrayAdapter<Province>(context, resource, objects) {

    val resId: Int = resource

    override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
        val item: Province = getItem(position)
        val view = LayoutInflater.from(context).inflate(resId, parent, false)
        val itemName = view.find<TextView>(R.id.item_name)
        itemName.text = item.name
        return view
    }
}

class CityAdapter(context: Context?, resource: Int, objects: List<City>) : ArrayAdapter<City>(context, resource, objects) {

    val resId: Int = resource

    override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
        val item: City = getItem(position)
        val view = LayoutInflater.from(context).inflate(resId, parent, false)
        val itemName = view.find<TextView>(R.id.item_name)
        itemName.text = item.name
        return view
    }
}

class CountyAdapter(context: Context?, resource: Int, objects: List<County>) : ArrayAdapter<County>(context, resource, objects) {

    val resId: Int = resource

    override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
        val item: County = getItem(position)
        val view = LayoutInflater.from(context).inflate(resId, parent, false)
        val itemName = view.find<TextView>(R.id.item_name)
        itemName.text = item.name
        return view
    }
}

同样的我将三个Adapter放在同一个文件中!如果使用的是java,就需要三个文件,或者用内部类!

注意开头是刚刚说的三个数据类Province,City,County,也一起放在了Adapters.kt中了。

ChooseAreaFragment

这是本文中最复杂的一个代码文件了,修改自原书的java代码:P502

ChooseAreaFragment.kt:

package cn.cslg.weatherkotlin

import android.app.ProgressDialog
import android.os.Bundle
import android.support.v4.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ListView
import android.widget.TextView
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import org.jetbrains.anko.custom.async
import org.jetbrains.anko.find
import org.jetbrains.anko.uiThread
import java.net.URL

/**
 * Created by devil on 2017/5/20.
 */
class ChooseAreaFragment : Fragment() {
    private val LEVEL_PROVINCE = 0
    private val LEVEL_CITY = 1
    private val LEVEL_COUNTY = 2
    private var current_level = 0

    private val URL = "http://guolin.tech/api/china/"

    private var provinceList = ArrayList<Province>()
    private var cityList = ArrayList<City>()
    private var countyList = ArrayList<County>()

    private var selectedProvince: Province? = null
    private var selectedCity: City? = null
    private var selectedCounty: County? = null

    private var backBtn: Button? = null
    private var titleText: TextView? = null
    private var listView: ListView? = null

    override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View {
        val view = inflater!!.inflate(R.layout.choose_area, container, false)

        titleText = view.find<TextView>(R.id.title_text)
        listView = view.find<ListView>(R.id.list_view)
        backBtn = view.find<Button>(R.id.back_button)


        return view
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)

        //列表点击监听事件
        listView!!.setOnItemClickListener {
            _, _, position, _ ->
            when (current_level) {
                LEVEL_PROVINCE -> {
                    selectedProvince = provinceList[position]
                    queryCity()
                }
                LEVEL_CITY -> {
                    selectedCity = cityList[position]
                    queryCounty()
                }
                LEVEL_COUNTY -> {
                    selectedCounty = countyList[position]
                }
            }
        }

        //返回按钮监听事件
        backBtn!!.setOnClickListener {
            if (current_level == LEVEL_CITY)
                queryProvince()
            else if (current_level == LEVEL_COUNTY)
                queryCity()
        }
        queryProvince()
    }

    private fun queryProvince() {
        titleText!!.text = "中国"
        backBtn!!.visibility = View.INVISIBLE     //隐藏返回键
        showProgress()

        async {
            val s = URL(URL).readText()

            uiThread {
                closeProgress()
                val t = object : TypeToken<List<Province>>() {}.type
                provinceList = Gson().fromJson<List<Province>>(s, t) as ArrayList<Province>
                val adapter = ProvinceAdapter(context, R.layout.list_city_item, provinceList)
                listView!!.adapter = adapter
                listView!!.setSelection(0)
                current_level = LEVEL_PROVINCE
            }
        }

    }

    private fun queryCity() {
        titleText!!.text = selectedProvince!!.name       //标题为当前省份
        backBtn!!.visibility = View.VISIBLE   //显示返回键
        showProgress()

        async {
            val s = URL(URL + "/" + selectedProvince!!.id).readText()

            uiThread {
                closeProgress()
                val t = object : TypeToken<List<City>>() {}.type
                cityList = Gson().fromJson<List<City>>(s, t) as ArrayList<City>
                val adapter = CityAdapter(context, R.layout.list_city_item, cityList)
                listView!!.adapter = adapter
                listView!!.setSelection(0)
                current_level = LEVEL_CITY
            }
        }
    }

    private fun queryCounty() {
        titleText!!.text = selectedCity!!.name       //标题为当前市
        backBtn!!.visibility = View.VISIBLE   //显示返回键
        showProgress()

        async {
            val s = URL(URL + "/" + selectedProvince!!.id + "/" + selectedCity!!.id).readText()

            uiThread {
                closeProgress()
                val t = object : TypeToken<List<County>>() {}.type
                countyList = Gson().fromJson<List<County>>(s, t) as ArrayList<County>
                val adapter = CountyAdapter(context, R.layout.list_city_item, countyList)
                listView!!.adapter = adapter
                listView!!.setSelection(0)
                current_level = LEVEL_COUNTY
            }
        }
    }


    //进度条
    private var progress: ProgressDialog? = null

    private fun showProgress(message: String = "加载中") {
        if (progress == null) {
            progress = ProgressDialog(activity)
            progress!!.setMessage(message)
            progress!!.setCancelable(false)
        }
        progress!!.show()
    }

    private fun closeProgress() {
        if (progress != null)
            progress!!.dismiss()
    }

}

先看到类开头的一些类属性,他们的作用域是整个ChooseAreaFragment类

kotlin的属性自带get,set方法,你不需要单独写

注:在Kotlin中,一切都是对象。没有像Java中那样的原始基本类型。

和java不一样,kotlin可以自动推断数据类型,使用var(可变)和val(不可变)声明一个变量或属性,给他赋值同时就会在编译期间推断出数据类型,那如果我想像java一样开头声明几个类属性但不赋值呢?

不可以!

Kotlin不允许声明变量但不初始化
http://www.cnblogs.com/sw926/p/5870326.html

java可以这么做:

Province selectedProvince;

而kotlin需要使用如下形式:

var selectedProvince: Province? = null

随后overwrite了Fragment的两个方法:
“onCreateView”
“onActivityCreated”
在Fragment的生命周期中,onCreateView会更早执行,我将属性的赋值过程放在里面,让他获得控件

onActivityCreated方法中为返回按钮和ListView中的Item添加了点击事件,并根据点击时的情况分别执行不同的操作,当用户点击ListView的其中一个Item时会先获得用户点击的position(是哪一个Item),然后存储在selectedXXX属性中,接下来会在query中用到他,根据选定的数据对象的id请求他的子数据,比如根据选中的Province的id属性就可以获得Province下的所有CIty。

注意:由于ListView的Item的position是个编号(Int),刚好又和三个数据类的List的索引一一对应,故而可以根据这个position取出List里的一个Province对象,City和County同理,如下:

//列表点击监听事件
listView!!.setOnItemClickListener {
    _, _, position, _ ->
        when (current_level) {
            LEVEL_PROVINCE -> {
                selectedProvince = provinceList[position]
                    queryCity()
            }
            LEVEL_CITY -> {
                selectedCity = cityList[position]
                    queryCounty()
            }
            LEVEL_COUNTY -> {
                selectedCounty = countyList[position]
            }
        }
}

position使用kotlin的lambada表达式传入后面的代码块方法,用户点击时就会触发
注:when 就相当于java中的switch语句!

三个query中都有简单async异步请求,并在请求结束后回到主线程执行UI操作,我没有参照原书中单独将请求放在一个方法中,因为kotlin的Net扩展相当强大,访问相当简单,只有短短几行代码而已。

在uiThread中使用了Gson().fromJson把获取到的String(json)映射成为数据类的实体对象的List泛型,在这之前还需要TypeToken方法,如果不是List类型,可以在fromJson的最后一个参数直接使用xxx.class的形式映射,但如果使用List泛型则必须用这个方法进行List类型映射,注意代码形式:

val t = object : TypeToken<List<Province>>() {}.type

provinceList = Gson().fromJson<List<Province>>(s, t) as ArrayList<Province>

TypeToken前面添加“object :”后面还有“{}”

布局文件

基本与原书一致:

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<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"
    >
    <fragment
        android:id="@+id/choose_area_fragment"
        android:name="cn.cslg.weatherkotlin.ChooseAreaFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:ignore="InvalidId" />

</FrameLayout>

choose_area.xml

<?xml version="1.0" encoding="utf-8"?>
<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"
    >
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="@color/colorPrimary"
        >

        <Button
            android:id="@+id/back_button"
            android:layout_width="25dp"
            android:layout_height="25dp"
            android:layout_alignParentLeft="true"
            android:layout_centerVertical="true"
            android:layout_marginLeft="10dp"
            android:background="@android:drawable/ic_menu_revert" />
        <TextView
            android:id="@+id/title_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:textColor="#fff"
            android:textSize="20sp"
            />

    </RelativeLayout>

    <ListView
        android:id="@+id/list_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"></ListView>
</LinearLayout>

list_city_item.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal">

    <TextView
        android:id="@+id/item_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical"
        android:padding="10dp"
        android:layout_marginLeft="10dp" />

</LinearLayout>

效果

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

结语

好了今天就写到此处,进入下一个阶段,编写天气和空气质量显示功能!

转载注明:出自:http://www.cnblogs.com/devilyouwei/p/6885052.html

posted @ 2017-05-21 15:42  devilyouwei  阅读(2657)  评论(4编辑  收藏  举报