推荐一部关于Ai的系列漫画,叫做代码的深渊
相关代码已经上传到Github的仓库kotlinRobot
先来看一下实现的效果图
文章思路参考自刘桂林前辈的带领新手快速开发APP ,聊天界面代码参考于郭神的第一行代码第二版第三章3.7节,由衷感谢。自己也写过一篇讲解kotlin几个基础小知识点的文章,kotlin初体验,文中不足之处,希望能得到您的指正,十分感谢。
布局文件
创建我们的项目,勾选kotlin支持,并在build.gradle下填入依赖
implementation 'com.android.support:design:26.1.0'
然后我们进去res-values-styles下,把主题修改成没有标题的样式
在activity_main布局里写一个recyclerView,用来显示对话聊天消息,以及一个EditText输入框,和一个发送数据的button按钮
先来写一个Log工具类,方便测试,创建util包,在util下创建L类
/**
* Created by 舍长 on 2018/4/27.
* 在kotlin中,加了object后,L类就成为了一个单例模式的类,相当于帮我们省略掉了以前Java实现单例的代码
* 最后我们可以直接L.d调用类中的方法
*/
object L {
// TAG
public var TAG: String = "tonJies"
fun d(test: String) {
Log.d(TAG, test)
}
}
我们回到Activity,在activity的导包中添加这一行,我们就不需要再写findViewByid了
import kotlinx.android.synthetic.main.activity_main.*
我们给发送按钮设置点击事件,回调监听,在点击事件里面进行输入框内容的处理,依次是:
1,获取输入框的内容
2,判断是否为空
3,点击发送按钮后清空当前输入框
//加了这一行后我们就不需要再findViewById了
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity(), View.OnClickListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 设置控件的回调监听
init()
L.d("Hello")
}
/**
* 设置控件的回调监听
*/
private fun init() {
btn_send.setOnClickListener(this)
}
/**
* 重写OnClickListener接口的onClick方法
*/
override fun onClick(v: View?) {
when (v!!.id) {
R.id.btn_send -> {
/**
* 1,获取输入框的内容
* 2,判断是否为空
* 4,发送后清空当前的输入框
*/
// 1,获取输入框的内容
val text: String = et_text.text.toString()
// 2,判断是否为空
if (!TextUtils.isEmpty(text)) {
L.d("不为空")
} else {
L.d("为空")
}
}
}
}
}
聊天界面的书写
思路是使用recyclerView作为聊天对话列表的显示控件。让我们先在build.gradle下添加圆形化图片的框架CircleImageView,用来对头像的圆形处理
//CircleImageView
compile 'de.hdodenhof:circleimageview:2.1.0'
创建bean包,在文件夹类创建实体类Chat,用于储存后面的聊天数据
/**
* data 数据类,默认帮我们实现了实体类的几个方法,例如toString,赋值
* text 聊天文本数据
* type 用来标示此对话类型是属于左边机器人发来的文本类型还是我们向机器人发送的文本类型,后面我们就根据这个属性来
*/
data class Chat(var text: String,var type: Int) {
}
这里的实体类和我们在Java中的实体类写法有些不同,data关键字在kotlin是数据类的意思,它的作用是帮我们默认生成了几个常用方法,如toString()方法,而且加了data后,括号里面的参数就可直接作为属性值使用了,不需要再像Java一样,在类中声明,然后再到构造方法中进行赋值。
然后我们开始写recyclerview的item布局,命名为chat_item,图片素材参考源代码
我们先把左右的消息都写出来,在后面Adapter加载每一个item的时候我们再根据Chat的type类型来决定要隐藏那边的布局
接下来开始写RecyclerView的适配器
/**
* Created by 舍长 on 2018/5/7.
* 描述: 聊天布局适配器
*/
class RecyclerViewAdapter : RecyclerView.Adapter {
// 上下文
private var context: Context? = null
// 用户聊天消息列表
private var mlist = ArrayList()
/**
* 空参构造方法
*/
constructor() {
}
/**
* context 上下文
* list 对话列表聊天数据
*/
constructor(context: Context, list: ArrayList) {
this.context = context
this.mlist = list
}
/**
* 加载item布局
*/
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.chat_item, parent, false)
return ViewHolder(view)
}
/**
* 在onBindViewHolder()方法中判断是要显示对应position位置的item布局要隐藏哪边的布局
* 我们用0来表示机器人的文本类型,用1来表示用户的文本类型。
*/
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val chat = mlist[position]
//
if (chat.type == 0) {
// 如果数据是机器人的文本类型,就显示左边的布局,隐藏右边的布局
holder.leftLayout.visibility = View.VISIBLE
holder.rightLayout.visibility = View.GONE
// 把文本设置到机器人对话框内
holder.leftChat.text = chat.text
//
} else if (chat.type == 1) {
// 如果数据是用户的文本类型,就隐藏左边布局,显示右边的布局
holder.rightLayout.visibility = View.VISIBLE
holder.leftLayout.visibility = View.GONE
// 把文本设置到用户对话框内
holder.rightChat.text = chat.text
}
}
override fun getItemCount(): Int {
return mlist.size
}
/**
* 声明控件
*
*/
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
// 左边的机器人布局
var leftLayout: LinearLayout
// 右边的用户布局
var rightLayout: LinearLayout
// 左边的机器人文本
var leftChat: TextView
// 右边的用户发送文本
var rightChat: TextView
/**
*
*/
init {
leftLayout = itemView.findViewById(R.id.left_layout)
rightLayout = itemView.findViewById(R.id.right_layout)
leftChat = itemView.findViewById(R.id.tv_left_text)
rightChat = itemView.findViewById(R.id.tv_right_text)
}
}
}
我们声明一个mlist集合来进行数据的传递,在onBindViewHolder方法中使用0作为机器人文本的标识,使用1作为用户文本的标识,根据标识的不同来决定显示item哪边的布局,再把Chat类对象的text属性传入要显示布局的TextView上
Adapter写好后我们在Activity中进行数据的填充,并设置RecyclerView的布局管理器,以及适配器
//加了这一行后我们就不需要再findViewById了
import kotlinx.android.synthetic.main.activity_main.*
/**
* 继承于AppCompatActivity()
* 继承于View.OnClickListener接口,复写onClick点击事件
*/
class MainActivity : AppCompatActivity(), View.OnClickListener {
// 对话列表集合
private var list = ArrayList()
// recyclerView适配器
private var recyclerViewAdapter = RecyclerViewAdapter(this, list)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 设置控件的回调监听
init()
// 加载数据
initData();
// 设置recyclerView布局管理
val linearLayoutManager = LinearLayoutManager(this)
// 把布局管理器添加到recyclerView中
recycler.setLayoutManager(linearLayoutManager)
// 把适配器添加到recyclerView中
recycler.setAdapter(recyclerViewAdapter)
}
/**
* 设置控件的回调监听
*/
private fun init() {
btn_send.setOnClickListener(this)
}
/**
* 重写OnClickListener接口的onClick方法
*/
override fun onClick(p0: View?) {
when (p0!!.id) {
R.id.btn_send -> {
/**
* 1,获取输入框的内容
* 2,判断是否为空
* 4,发送后清空当前的输入框
*/
// 1,获取输入框的内容
val text: String = et_text.text.toString()
// 2,判断是否为空
if (!TextUtils.isEmpty(text)) {
L.d(text + "")
addData(text, 1)
} else {
L.d("你的输入为空")
}
}
}
}
/**
* 通过传递进来的test和type创建数据实体类,添加到聊天数据集合list中
* @param text 文本信息
* @param type 标示类型
*/
private fun addData(mtext: String, mtype: Int) {
L.d(mtext + "" + mtype)
var c = Chat(mtext, mtype)
list.add(c)
// 更新适配器,插入新数据
recyclerViewAdapter.notifyItemInserted(list.size - 1)
// 把显示的位置定位到最后一行
recycler.scrollToPosition(list.size - 1)
}
/**
* 模拟加载数据
*/
private fun initData() {
// 传入0到c1的type属性来表示该数据是接受到的数据,然后把实体类c1添加到对话列表集合中
var c1: Chat = Chat("你好,我叫阿紫", 0)
list.add(c1)
// 使用1到c2的type属性来表示该数据是输入框的发送数据,然后把实体类c2添加到对话列表集合中
var c2: Chat = Chat("你好,你现在会些什么呢?", 1)
list.add(c2)
// 接受数据
var c3: Chat = Chat("我还在成长中,很多东西还不懂,但是你可以考考我\"", 0)
list.add(c3)
// 发送数据
var c4: Chat = Chat("1+1等于几?\"", 1)
list.add(c4)
}
}
我们声明了一个集合list来存储我们的聊天数据,在initData()方法中加载数据,然后设置RecyclerView的布局管理器,并将集合数据填充到适配器中。
在按钮点击事件里获取数据框的输入内容,设置标识为1,填充到list集合中,刷新适配器并把显示的地方定位到最后一行,清空输入框。
至此,我们的聊天界面就写好了,让我们来看看目前的效果
后端接口的调试
后端的接口我们使用图灵机器人,打开网站,注册好账号,创建一个机器人,我把机器人命名为阿紫,然后我们先用调试工具进行接口的调试,这里我用的调试工具是PostMan,参照图灵机器人官方文档
格式是发送Json数据的Post请求,在Json数据中带着两个参数apikey和userId:
{
"reqType":0,
"perception": {
"inputText": {
"text": "你叫什么"
}
},
"userInfo": {
"apiKey": "c00282de107144fb940adab994d9ff98",
"userId": "225167"
}
}
1,apikey(在机器人管理页),比如这里我的是:c00282de107144fb940adab994d9ff98,
2,userId(右上角用户头像右边的数字),这里我的是:225167
3,text就是表示我们想要和机器人聊天的具体文字
进行调试
不得已截了大图,缩略图又有点模糊,点击图片就可以放大查看了
{
"emotion": {
"robotEmotion": {
"a": 0,
"d": 0,
"emotionId": 0,
"p": 0
},
"userEmotion": {
"a": 0,
"d": 0,
"emotionId": 0,
"p": 0
}
},
"intent": {
"actionName": "",
"code": 10004,
"intentName": ""
},
"results": [{
"groupType": 0,
"resultType": "text",
"values": {
"text": "叫我阿紫就可以了"
}
}]
}
数据比较多,但是我们关心得的,仅仅只是Json数组result里面的test:我叫阿紫,不要被我的名字所迷倒哦而已,我们可以试试发送不同的文本,看看机器人会怎么回答,比如:
什么是Android? -- Android是一种基于Linux的自由及开放源代码的操作系统,主要使用于移...
你喜欢我吗? -- 你喜欢我,我就喜欢你。
至此,我们确定接口调试成功了
对接网络接口:
我们采用Retrofit作为我们网络框架,还没接触过Retrofit的伙伴可以参考这篇文章。
添加依赖:
// retrofit
compile 'com.squareup.okhttp3:okhttp:3.1.2'
compile 'com.squareup.retrofit2:retrofit:2.0.1'
compile 'com.squareup.retrofit2:converter-gson:2.0.1'
别忘了在AndroidManifest.xml文件中添加网络权限
让我们来看看要作为body发送给服务端是Json数据
{
"reqType":0,
"perception": {
"inputText": {
"text": "你叫什么"
}
},
"userInfo": {
"apiKey": "c00282de107144fb940adab994d9ff98",
"userId": "225167"
}
}
我们在bean包创建实体类Ask,作为发送请求的请求体。
使用Java代码时,生成JavaBean实体类一般使用JsonFormat插件进行生成,但是JsonFormat在Kotlin中是使用不了的,所以我们要使用kotlin的生成JavaBean插件,JSON To Kotlin。
打开File-Setting-Plugins-Browse respositories 搜索 JSON To Kotlin Class 安装,然后使用它生成Ask请求体实体类
打开Setting-Other-勾选Enable Inner Class Model 的作用是使得生成的数据都以内部类的形式出现,不勾选的话会默认把所有的类都生成在包内
/**
* 请求数据请求体实体类
* reqType 传0就行
*/
data class Ask(val reqType: Int,
val perception: Perception,
val userInfo: UserInfo
) {
data class Perception(
val inputText: InputText
) {
// 要发送的文本消息
data class InputText(
val text: String
)
}
data class UserInfo(
// 机器人apiKey
val apiKey: String,
// 用户id
val userId: String
)
}
上面提到加了data关键字,会帮我们默认生成几个方法,比如toString(),格式是例如"User(name=John, age=42)"
;但是要注意的一点是加了data,类就不能有无参构造方法了,如果我们输入这样的代码,var ask=Ask(),那就会报错了。
再看看响应体的Json数据
{
"emotion": {
"robotEmotion": {
"a": 0,
"d": 0,
"emotionId": 0,
"p": 0
},
"userEmotion": {
"a": 0,
"d": 0,
"emotionId": 0,
"p": 0
}
},
"intent": {
"actionName": "",
"code": 10004,
"intentName": ""
},
"results": [{
"groupType": 0,
"resultType": "text",
"values": {
"text": "叫我阿紫就可以了"
}
}]
}
一样,在bean目录下创建接受数据实体类Take(响应体)
/**
* 返回数据响应体实体类
*/
data class Take(
val emotion: Emotion,
val intent: Intent,
val results: List
) {
data class Emotion(
val robotEmotion: RobotEmotion,
val userEmotion: UserEmotion
) {
data class UserEmotion(
val a: Int,
val d: Int,
val emotionId: Int,
val p: Int
)
data class RobotEmotion(
val a: Int,
val d: Int,
val emotionId: Int,
val p: Int
)
}
data class Result(
val groupType: Int,
val resultType: String,
val values: Values
) {
// 返回文本 "叫我阿紫就可以了"
data class Values(
val text: String
)
}
data class Intent(
val actionName: String,
val code: Int,
val intentName: String
)
}
响应体实体类中,我们关注的只有text返回的文本数据
接下来我们创建net包,在net包内写一个Retrofit的实现接口
/**
* Created by 舍长 on 2018/5/10.
* 描述: Retrofit接口
*/
public interface Api {
//发送json数据形式的post请求,把网络请求接口的后半部分openapi/api/v写在里面
//Get是请求数据实体类,Take接受数据实体类
@POST("openapi/api/v2")
Call request(@Body Ask ask);
}
之后就直接在MainActivity写我们的数据请求方法了
//加了这一行后我们就不需要再findViewById了
import kotlinx.android.synthetic.main.activity_main.*
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
/**
* 继承于AppCompatActivity()
* 继承于View.OnClickListener接口,复写onClick点击事件
*/
class MainActivity : AppCompatActivity(), View.OnClickListener {
// 对话列表集合
private var list = ArrayList()
// recyclerView适配器
private var recyclerViewAdapter = RecyclerViewAdapter(this, list)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 设置控件的回调监听
init()
// 加载数据
initData()
// 设置recyclerView布局管理
val linearLayoutManager = LinearLayoutManager(this)
// 把布局管理器添加到recyclerView中
recycler.setLayoutManager(linearLayoutManager)
// 把适配器添加到recyclerView中
recycler.setAdapter(recyclerViewAdapter)
}
/**
* 设置控件的回调监听
*/
private fun init() {
btn_send.setOnClickListener(this)
}
/**
* 重写OnClickListener接口的onClick方法
*/
override fun onClick(p0: View?) {
when (p0!!.id) {
R.id.btn_send -> {
/**
* 1,获取输入框的内容
* 2,判断是否为空
* 4,发送后清空当前的输入框
*/
// 1,获取输入框的内容
val text: String = et_text.text.toString()
// 2,判断是否为空
if (!TextUtils.isEmpty(text)) {
addData(text, 1)
request(text)
} else {
L.d("你的输入为空")
}
}
}
}
/**
* 通过传递进来的test和type创建数据实体类,添加到聊天数据集合list中
* @param text 文本信息
* @param type 标示类型
*/
private fun addData(mtext: String, mtype: Int) {
L.d("addData mtext:" + mtext + " mtype" + mtype)
var c = Chat(mtext, mtype)
list.add(c)
// 更新适配器,插入新数据
recyclerViewAdapter.notifyItemInserted(list.size - 1)
// 把显示的位置定位到最后一行
recycler.scrollToPosition(list.size - 1)
}
/**
* 模拟加载数据
*/
private fun initData() {
// 传入0到c1的type属性来表示该数据是接受到的数据,然后把实体类c1添加到对话列表集合中
var c1: Chat = Chat("你好,我叫阿紫", 0)
list.add(c1)
// 使用1到c2的type属性来表示该数据是输入框的发送数据,然后把实体类c2添加到对话列表集合中
var c2: Chat = Chat("你好,你现在会些什么呢?", 1)
list.add(c2)
// 接受数据
var c3: Chat = Chat("我还在成长中,很多东西还不懂,但是你可以考考我\"", 0)
list.add(c3)
// 发送数据
var c4: Chat = Chat("1+1等于几?\"", 1)
list.add(c4)
// 接受数据
var c5: Chat = Chat("1+1=2", 0)
list.add(c5)
}
/**
* 请求数据
*
*/
private fun request(mText: String) {
// 存储要发送的的文本
var perceotion = Ask.Perception(Ask.Perception.InputText(mText))
// 设置用户id和ApidKey
val userInfo = Ask.UserInfo("c00282de107144fb940adab994d9ff98", "225167")
// 填充到请求体Ask中
var ask = Ask(0, perceotion, userInfo)
// 使用retiofit进行请求
var retrofit = Retrofit.Builder()
.baseUrl("http://openapi.tuling123.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
// 创建网络请求接口的实例
val api = retrofit.create(Api::class.java)
//
val call = api.request(ask)
//
call.enqueue(object : retrofit2.Callback {
// 请求成功
override fun onResponse(call: retrofit2.Call, response: Response) {
// 接受到的机器人回复的数据
L.d("返回的全部信息:" + response.body().toString())
var text = response.body().results.get(0).values.text
//在这里进行处理,防止接口没有返回数据时抛出异常
if (text == null) {
text = "我还小,不知道这句话的意思"
//把接受到的数据传入addData方法中,类型是TYPE_RECEIVED接受数据
addData(text, 0)
} else {
//把接受到的数据传入addData方法中,类型是TYPE_RECEIVED接受数据
addData(text, 0)
}
L.d("接受到的机器人回复的数据: " + text)
}
// 请求失败
override fun onFailure(call: retrofit2.Call, t: Throwable) {
L.d("请求失败: " + t.toString())
}
})
}
}
在方法中我们创建了一个Ask请求体对象,把机器人key,userId和要发送的文本传入,注意reqType我们传入0就可以了。使用Retrofit进行网络请求,在回调里接收返回的文本,把返回的文本传入addData中,标识为0,机器人文本。
当然request方法是在点击按钮时调用的,Activty完整代码
//加了这一行后我们就不需要再findViewById了
import kotlinx.android.synthetic.main.activity_main.*
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
/**
* 继承于AppCompatActivity()
* 继承于View.OnClickListener接口,复写onClick点击事件
*/
class MainActivity : AppCompatActivity(), View.OnClickListener {
// 对话列表集合
private var list = ArrayList()
// recyclerView适配器
private var recyclerViewAdapter = RecyclerViewAdapter(this, list)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 设置控件的回调监听
init()
// 加载数据
initData()
// 设置recyclerView布局管理
val linearLayoutManager = LinearLayoutManager(this)
// 把布局管理器添加到recyclerView中
recycler.setLayoutManager(linearLayoutManager)
// 把适配器添加到recyclerView中
recycler.setAdapter(recyclerViewAdapter)
}
/**
* 设置控件的回调监听
*/
private fun init() {
btn_send.setOnClickListener(this)
}
/**
* 重写OnClickListener接口的onClick方法
*/
override fun onClick(p0: View?) {
when (p0!!.id) {
R.id.btn_send -> {
/**
* 1,获取输入框的内容
* 2,判断是否为空
* 4,发送后清空当前的输入框
*/
// 1,获取输入框的内容
val text: String = et_text.text.toString()
// 2,判断是否为空
if (!TextUtils.isEmpty(text)) {
addData(text, 1)
request(text)
} else {
L.d("你的输入为空")
}
}
}
}
/**
* 通过传递进来的test和type创建数据实体类,添加到聊天数据集合list中
* @param text 文本信息
* @param type 标示类型
*/
private fun addData(mtext: String, mtype: Int) {
L.d("addData mtext:" + mtext + " mtype" + mtype)
var c = Chat(mtext, mtype)
list.add(c)
// 更新适配器,插入新数据
recyclerViewAdapter.notifyItemInserted(list.size - 1)
// 把显示的位置定位到最后一行
recycler.scrollToPosition(list.size - 1)
}
/**
* 模拟加载数据
*/
private fun initData() {
// 传入0到c1的type属性来表示该数据是接受到的数据,然后把实体类c1添加到对话列表集合中
var c1: Chat = Chat("你好,我叫阿紫", 0)
list.add(c1)
// 使用1到c2的type属性来表示该数据是输入框的发送数据,然后把实体类c2添加到对话列表集合中
var c2: Chat = Chat("你好,你现在会些什么呢?", 1)
list.add(c2)
// 接受数据
var c3: Chat = Chat("我还在成长中,很多东西还不懂,但是你可以考考我\"", 0)
list.add(c3)
// 发送数据
var c4: Chat = Chat("1+1等于几?\"", 1)
list.add(c4)
// 接受数据
var c5: Chat = Chat("1+1=2", 0)
list.add(c5)
}
/**
* 请求数据
*
*/
private fun request(mText: String) {
// 存储要发送的的文本
var perceotion = Ask.Perception(Ask.Perception.InputText(mText))
// 设置用户id和ApidKey
val userInfo = Ask.UserInfo("c00282de107144fb940adab994d9ff98", "225167")
// 填充到请求体Ask中
var ask = Ask(0, perceotion, userInfo)
// 使用retiofit进行请求
var retrofit = Retrofit.Builder()
.baseUrl("http://openapi.tuling123.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
// 创建网络请求接口的实例
val api = retrofit.create(Api::class.java)
//
val call = api.request(ask)
//
call.enqueue(object : retrofit2.Callback {
// 请求成功
override fun onResponse(call: retrofit2.Call, response: Response) {
// 接受到的机器人回复的数据
L.d("返回的全部信息:" + response.body().toString())
var text = response.body().results.get(0).values.text
//在这里进行处理,防止接口没有返回数据时抛出异常
if (text == null) {
text = "我还小,不知道这句话的意思"
//把接受到的数据传入addData方法中,类型是TYPE_RECEIVED接受数据
addData(text, 0)
} else {
//把接受到的数据传入addData方法中,类型是TYPE_RECEIVED接受数据
addData(text, 0)
}
L.d("接受到的机器人回复的数据: " + text)
}
// 请求失败
override fun onFailure(call: retrofit2.Call, t: Throwable) {
L.d("请求失败: " + t.toString())
}
})
}
}
这一小节就到这里啦,谢谢您的观看,文中不足之处,希望能得到您的指正,期待您的留言