一、前言
最近其实有一点“不务正业”,快两个月了都在学网络相关的后端开发,安卓方面很久没去研究了,这次带来的demo是大项目中的一个小小的一块,由于编程语言渐渐转向kotlin,所以原本的项目需要进行重构,不过还不是非常熟练,所以这次写了这个注册功能的demo,百分百kotlin就是它了,验证码是自己搭建的服务器那边处理的,所以还是一个非常值得自己做的一个功能,起初是想采用短信实现的,后面看到腾讯短信业务要企业级用户才能使用,就放弃了,自己造轮子显然不是一个明智的选择,不过鉴于这个功能在思路上非常的简单,所以简单实现了一下,不过不要小看这个demo,”麻雀虽小五脏俱全”就是它了,设计后端即springboot的开发,app处理网络请求的开发,appUI界面的设计(虽然只有一步,但也还是吧),数据库查询相关,app搭建相关架构的实现等等,值得学习一波。下面是制作过程的思维导图
最终的成品也展示一下
二、spring boot端相关接口开发
在设计之前还需要设计两个表,建议在本地开发完成之后再上线服务器,所以最好本地也建个表,访问更快,表的设计比较简单,这里就展示一下结构图
注册表
验证表
还有这里面用到的比较多的就是MyStatus这个数据类,因为注册最终的展现形式都差不多,所以采用统一的状态方式进行返回内容,下面展示一下类的结构
data class MyStatus(var account:String, var status: String, var message:String, var time:String)
用到的json转化工具是hutool,参考一下之前写的博客hutool的使用
1.开发发送验证码接口
首先确定一下,接口的形式
http://域名:端口号/verify/{邮箱}
只需要一个参数就可以了,确认完参数,我们开始进行下一步,设计一下发送验证码的流程
//1.首先进行查询最近的验证码的发送时间,与目前的做比较
//可能会有三种情况:查询为空,间隔时间大于5分钟,间隔时间小于5分钟
//小于5分钟直接返回提示,验证频繁
//查询为空和大于5分钟继续
//下面展示核心代码,具体实现源码文末领取
//查找是否存在上一次验证码记录,并进行比较,5分钟以内就不重复发送
val isExist = "select * from verify WHERE mail = ? ORDER BY sendTime desc LIMIT 1"
val psExist = con.prepareStatement(isExist)
psExist.setString(1, mail)
val resultSet:ResultSet = psExist.executeQuery()
//这里的if语句就相当于在做判空操作
if (resultSet.next()){
//计算时间差
val t = System.currentTimeMillis() - MyDate.timeToLong(resultSet.getString("sendTime"))
//判断超过时间就不发送
if(t < 5*60*1000){
val status = MyStatus(mail,"404","请勿重复发送验证码",timeVerify)
return JSONUtil.toJsonStr(status)
}
}
//2.生成验证码
//这里采用随机数的方法随机生成了100000~999999的数字
val random = Random()
val code = random.nextInt(999999 - 100000 + 1) + 100000
val message = "【Dream】您的验证码$code,该验证码5分钟内有效,请勿泄漏于他人,时间${timeVerify}"
//3.发送邮件
//首先对邮箱进行一个检查
//无效返回提示,有效则继续
try{
SendMail(mail).sendTextEmail(message, "【Dream】注册验证码")
}catch (e:Exception){
val status = MyStatus(mail,"404","邮箱无效",timeVerify)
return JSONUtil.toJsonStr(status)
}
//4.插入验证码发送的记录
//这里是为后面的校验做准备
val insertCode = "insert into verify(mail,myCode,sendTime) values(?,?,?)"
val psCode = con.prepareStatement(insertCode)
return try {
psCode.setString(1, mail)
psCode.setString(2, code.toString())
psCode.setString(3, timeVerify)
psCode.executeUpdate()
val status = MyStatus(mail,"200","发送验证码成功",timeVerify)
JSONUtil.toJsonStr(status)
}catch (e:Exception){
//插入时异常
val status = MyStatus(mail,"404","系统故障",timeVerify)
JSONUtil.toJsonStr(status)
}
2.开发注册接口
接口格式确定一下,这里本来应该可以采用post进行开发的,由于参数也不是太多,所以采用仍然采用get进行开发
http://域名:端口号/register/{邮箱}/{密码}/{验证码}
三个参数,不算太多,可能就是浏览器手动请求有点累
//1.判断用户是否已经存在
//这里采用了主键约束,所以插入的时候根据数据库的返回结果即可判断是否已经存在
//存在,返回已经存在的提示,反之则继续
//2.判断验证码是否过期
//查询最近一次的验证码发送时间
//若查询为空,则说明用户还没发送验证码,返回提示,不为空继续
//若时间与当前的时间间隔大于5分钟就返回验证码已经过期的提示,反之继续
//判断是否发送过验证码
if(!resultSet.next()){
val status = MyStatus(mail,"404","暂未发送验证码",timeRegister)
return JSONUtil.toJsonStr(status)
}
//判断是否验证码是否过期
val t = System.currentTimeMillis() - MyDate.timeToLong(resultSet.getString("sendTime"))
if (t > 5*60*1000){
val status = MyStatus(mail,"404","验证码过期",timeRegister)
return JSONUtil.toJsonStr(status)
}
//3.判断验证码是否正确
//正确则进行下一步操作,错误返回提示
//判断验证码是否正确
if (code != resultSet.getString("myCode")){
val status = MyStatus(mail,"404","验证码错误",timeRegister)
return JSONUtil.toJsonStr(status)
}
//4.检查sql语句是否出错,即判断用户是否已经存在
//错误,返回用户已存在的提示,否则继续
//插入数据库成功,发送邮箱给用户提示已经发送成功
//检查数据库查询是否有错误发生
return try{
val register = "insert into register(mail,myPassword)values(?,?)"
val psRegister = con.prepareStatement(register)
psRegister.setString(1,mail)
psRegister.setString(2,password)
psRegister.executeUpdate()
val status = MyStatus(mail,"200","注册成功",timeRegister)
//发送注册成功通知
val message = "【Dream】尊敬的用户:恭喜你已成功注册Dream,后续软件使用问题关注公众号:android 踩坑小天才 进行咨询,感谢您的支持"
SendMail(mail).sendTextEmail(message, "【Dream】注册成功通知")
JSONUtil.toJsonStr(status)
}catch (e:Exception){
val status = MyStatus(mail,"404","用户已存在",timeRegister)
JSONUtil.toJsonStr(status)
}
三、app客户端界面UI相关开发
这方面说实话,审美不是很好,甚至这个颜色还是从某某平台扣的,过程比较简单,如果真的要开发一款比较好的软件,UI设计非常重要
//1.确定布局
//这个最简单的就是使用线性布局
//2.确定主要控件
//这里邮箱,密码,验证码就是主要的控件
//都是EditText,需要设置一下输入的数据类型
//这里以邮箱为例
//主要用到了hint即还未输入前界面显示的提示性文字
//inputType,控制输入的数据格式
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="2"
android:text="注册邮箱:"
android:gravity="center"
android:textColor="#000000" />
<EditText
android:id="@+id/re_mail"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="6"
android:hint="请输入合法的邮箱"
android:inputType="textEmailAddress" />
</LinearLayout>
//3.界面优化
//这部分主要是利用了Google提供的cardView进行圆角化处理
//还有就是控件直接的间隔调控即layout_margin
//这里利用线性布局中的权重配置,以适配不同分辨率的手机
//这里简单展示一下
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_margin="10dp"
app:cardCornerRadius="5dp"
app:elevation="15dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="2"
android:gravity="center"
android:text="hello"
android:textColor="#000000" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
//4.图标
//众所周知,图标是相当重要的,能不能给一个良好的第一印象就靠它了
//这里是上阿里云矢量图库里面找的
//是不是相当的帅气
四、app网络请求处理相关开发
基于retrofit开发的,下面展示一下基本流程
//1.创建接收json数据的数据模型
data class RegisterData(val account:String,val password:String,val verifyCode:String)
data class StatusResponse(val account:String, val status: String, val message:String, val time:String )
//2.创建服务接口
interface LoginService {
//发送验证码接口
@GET("verify/{mail}")
fun getVerifyStatus(@Path("mail")mail:String): Call<StatusResponse>
//注册接口
@GET("register/{mail}/{password}/{code}")
fun getRegisterStatus(@Path("mail")mail:String,@Path("password")password:String,@Path("code")code:String) :Call<StatusResponse>
}
//3.服务创建的类
//这里面进一步完成retrofit的封装
object ServiceCreator {
//如果是本地测试的话,用自己电脑的ip地址即可
//cmd ipconfig即可获取
private const val BASE_URL = "http://IP地址:8080/"
private val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
fun <T> create (serviceClass:Class<T>):T {
return retrofit.create(serviceClass)
}
//泛型实化,用到reified关键字
inline fun <reified T>create():T = create(T::class.java)
}
//4.统一处理网络请求
//这里进行网络请求的处理,用到协程使得网络请求可以异步执行
object LoginDemoNetwork {
private val loginService = ServiceCreator.create<LoginService>()
//suspend kotlin中协程的关键字
suspend fun getVerifyStatus(query: String): StatusResponse = loginService.getVerifyStatus(query).await()
suspend fun getRegisterStatus(query1: String,query2: String,query3: String) = loginService.getRegisterStatus(query1,query2,query3).await()
//网络回调的处理
private suspend fun <T> Call<T>.await():T{
return suspendCoroutine { continuation ->
enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
val body = response.body()
if (body != null)continuation.resume(body)
else continuation.resumeWithException(RuntimeException("response body is null"))
}
override fun onFailure(call: Call<T>, t: Throwable) {
continuation.resumeWithException(t)
}
})
}
}
}
//5.respository里面进行网络请求的最终处理并且返回回调结果
object Repository {
fun getVerifyStatus(query: String) = fire(Dispatchers.IO){
val verifyResponse = LoginDemoNetwork.getVerifyStatus(query)
Result.success(verifyResponse)
}
fun getRegisterStatus(query1: String,query2: String,query3: String) = fire(Dispatchers.IO){
val registerResponse = LoginDemoNetwork.getRegisterStatus(query1,query2, query3)
Result.success(registerResponse)
}
/**
* 对于结果处理进行一个高阶函数的封装
*/
private fun <T> fire(context: CoroutineContext,block:suspend () -> Result<T>) = liveData<Result<T>>(context) {
val result = try {
block()
}catch (e:Exception){
Result.failure<T>(e)
}
emit(result)
}
}
五、基于MVVM架构的模块组装
这一块主要是对fragment和viewModel进行设计,由于我们的需求比较简单,所以这一块也实现的比较简单
//1.viewModel层设计
class LoginViewModel : ViewModel() {
//创建了两个网络请求的liveData
private val verifyLiveData = MutableLiveData<String>()
private val registerLiveData = MutableLiveData<RegisterData>()
//对于liveData进行转换
val verifyStatusLiveData = Transformations.switchMap(verifyLiveData) { query ->
Repository.getVerifyStatus(query)
}
val registerStatusLiveData = Transformations.switchMap(registerLiveData){register->
Repository.getRegisterStatus(register.account,register.password,register.verifyCode)
}
//赋值
fun getVerifyStatus(query:String){
verifyLiveData.value = query
}
fun getRegisterStatus(query1: String,query2: String,query3: String){
registerLiveData.value = RegisterData(query1,query2,query3)
}
}
//2.fragment设计
//这里主要就是创建了两个liveData的监听和两个listener
//还有一些简单的网络判断
class LoginFragment : Fragment() {
private val viewModel by lazy { ViewModelProvider(this).get(LoginViewModel::class.java) }
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.login_fragment, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
sent.setOnClickListener {
viewModel.getVerifyStatus(re_mail.text.toString())
}
register.setOnClickListener {
if(re_password.text.toString() == check_password.text.toString()){
viewModel.getRegisterStatus(re_mail.text.toString(),re_password.text.toString(),check_code.text.toString())
}else{
"两次密码输入不一致".showToast()
}
}
viewModel.verifyStatusLiveData.observe(viewLifecycleOwner) { result ->
val status = result.getOrNull()
if (status != null){
if(status.status == "200"){
status.message.showToast()
}else{
status.message.showToast()
}
}else{
"网络错误".showToast()
}
}
viewModel.registerStatusLiveData.observe(viewLifecycleOwner) { result ->
val status = result.getOrNull()
if (status != null){
if(status.status == "200"){
status.message.showToast()
}else{
status.message.showToast()
}
}else{
"网络错误".showToast()
}
}
}
}
最后,如果有感兴趣的,可以发送关键字:验证码 到公众号:android 踩坑小天才 获取源码和签名好的app