1.内容介绍
IM,Instance message,QQ、微信都是属于这方面(指代通信App,下文一样)的应用
此外,直播类、电商类、通讯类都属于这类应用
开发这类项目时可以借用已经编写好的第三方平台,节省编写相关逻辑的花销,当前主要有融云、环信等SDK
在该项目中,主要使用环信sdk,可以查看官方网站环信,官方网站上有相关搭建教程,这里不再详述
该项目基本上就是简单实现一个类似于QQ的通讯工具,可以实现简单的消息发送,好友添加等一系列功能
大体的架构如图所示:
接下来,我们将通过三篇博客来整体地实现这个项目,现在让我们开始第二步吧
PS:该教程没有展示整个项目的所有代码,只展示了一些重点代码块的部分,若想获取该项目可以在该系列的最后一篇博客中获取
2.环信sdk集成以及初始化
环信sdk的集成过程可以参考官网上的文档,这里首先在环信的后台创建应用,创建好后的应用如图所示:
可以参照文档将SDK集成到项目中,这里不再赘述
建议使用手动下载SDK包的方式进行集成,因为经测试从Gradle远程链接的话会相当慢,而且大概率会失败
集成完毕后,进行初始化。在包下新建一个MyApplication继承Application,按照官方文档进行初始化,代码如下:
package com.dianbin.fastec.im95;
import android.app.ActivityManager;
import android.app.Application;
import android.content.pm.PackageManager;
import android.util.Log;
import com.hyphenate.chat.EMClient;
import com.hyphenate.chat.EMOptions;
import java.util.Iterator;
import java.util.List;
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
// 初始化环信
initEaseMobe();
}
/**
* 1.初始化环信的方法
*/
private void initEaseMobe() {
EMOptions options = new EMOptions();
// 默认添加好友时,是不需要验证的,改成需要验证
options.setAcceptInvitationAlways(false);
// 是否自动将消息附件上传到环信服务器,默认为True是使用环信服务器上传下载,如果设为 false,需要开发者自己处理附件消息的上传和下载
options.setAutoTransferMessageAttachments(true);
// 是否自动下载附件类消息的缩略图等,默认为 true 这里和上边这个参数相关联
options.setAutoDownloadThumbnail(true);
int pid = android.os.Process.myPid();
String processAppName = getAppName(pid);
// 如果APP启用了远程的service,此application:onCreate会被调用2次
// 为了防止环信SDK被初始化2次,加此判断会保证SDK被初始化1次
// 默认的APP会在以包名为默认的process name下运行,如果查到的process name不是APP的process name就立即返回
if (processAppName == null ||!processAppName.equalsIgnoreCase(this.getPackageName())) {
// Log.e(TAG, "enter the service process!");
// 则此application::onCreate 是被service 调用的,直接返回
return;
}
//初始化
EMClient.getInstance().init(this, options);
//在做打包混淆时,关闭debug模式,避免消耗不必要的资源
EMClient.getInstance().setDebugMode(true);
}
/**
* 2.获取当前App的名字
* @param pID
* @return
*/
private String getAppName(int pID) {
String processName = null;
ActivityManager am = (ActivityManager) this.getSystemService(ACTIVITY_SERVICE);
List l = am.getRunningAppProcesses();
Iterator i = l.iterator();
PackageManager pm = this.getPackageManager();
while (i.hasNext()) {
ActivityManager.RunningAppProcessInfo info = (ActivityManager.RunningAppProcessInfo) (i.next());
try {
if (info.pid == pID) {
processName = info.processName;
return processName;
}
} catch (Exception e) {
// Log.d("Process", "Error>> :"+ e.toString());
}
}
return processName;
}
}
3.闪屏页面的实现
一般来说,我们应用的第一个界面都是以一个闪屏页面来实现,若用户第一次登陆则会弹出教程,否则就会直接加载到相关页面,这里我们来实现一下
-
在项目下新建一个view包,然后新建一个活动SplashActivity,作为闪屏页面的Activity,模板调用Empty Activity
-
修改activity_splash.xml,使用线性布局和图片控件,代码如下:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout 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:orientation="vertical" tools:context=".view.SplashActivity"> <ImageView android:id="@+id/iv_splash" android:layout_width="match_parent" android:layout_height="match_parent" android:scaleType="centerCrop" android:src="@mipmap/splash"/> </LinearLayout>
4.MVP介绍
该项目采用了MVP架构模式,不同于传统的MVC模式,使用MVC设计模式时,大部分的情况下,View和Controller都由activity来进行控制,这样会造成Activity的代码过于臃肿,不好维护。
随着UI创建技术的功能日益增强,UI层也履行越来越多的职责。为了更好地细分视图(View)与模型(Model)的功能,让View专注于处理数据的可视化以及与用户的交互,同时让Model只关系数据的处理,基于MVC概念的MVP(Model-View-Presenter)模式应运而生。
在MVP模式里通常包含4个元素:
- View:负责绘制UI元素,与用户进行交互(在Android中体现为activity(或fragment),activity(或fragment)只控制界面的数据展示和用户的交互)
- View Interface:需要View实现的接口,View通过View Interface与Presenter进行交互,降低耦合,方便进行单元测试
- Model:负责存储、检索、操作数据(有时也可以实现一个Model Interface用来降低耦合)
- Presenter:作为View与Model交互的中间纽带,处理与用户交互的负责逻辑
MVP模式的示意图如下:
使用MVP模式的好处如下:
- 减轻activity的负担,实现业务逻辑和界面的解耦
- 方便对业务逻辑进行单元测试
让我们开始简单构建一个MVP模式的模板
- 在包下创建一个presenter包,在该包中新建一个接口SplashPresenter,作为SplashActivity的一个功能接口,里面只有一个验证的方法,代码如下:
package com.dianbin.fastec.im95.presenter;
public interface SplashPresenter {
/**
* 联网检测当前设备是否已经登录了
*/
void checkLogin();
}
- 同时,在view包下新建一个接口SplashView,用来获取当前设备的登录状态,代码如下:
package com.dianbin.fastec.im95.view;
public interface SplashView {
/**
* 获取当前设备的登录状态之后 要处理的界面跳转逻辑
* @param isLogin 是否已经登录
*/
void onGetLoginState(boolean isLogin);
}
- 在presenter包下新建Impl包,用于存储实现类,在该包下新建SplashPresenterImpl,作为SplashPresenter接口的实现类,代码如下:
package com.dianbin.fastec.im95.presenter.impl;
import com.dianbin.fastec.im95.presenter.SplashPresenter;
import com.dianbin.fastec.im95.view.SplashView;
import com.hyphenate.chat.EMClient;
public class SplashPresenterImpl implements SplashPresenter {
/**
* View层的接口
*/
private SplashView splashView;
/**
* 构造方法,构造的时候传入View接口的具体实现,通过这个实现,调用View层的业务逻辑
* @param splashView
*/
public SplashPresenterImpl(SplashView splashView) {this.splashView = splashView;
}
@Override
public void checkLogin() {
// 检测是否登录过(是否之前登录过,或者是否与环信的服务器进行连接)
if (EMClient.getInstance().isLoggedInBefore() && EMClient.getInstance().isConnected()){
splashView.onGetLoginState(true);
}else {
splashView.onGetLoginState(false);
}
}
}
在接下来的项目实践中,我们都将用到MVP模式进行架构,现在让我们来完善上一小节里编写的SplashActivity,但在这之前,我们先来导入一个View注入的经典框架:ButterKnife
5.ButterKnife集成以及使用
这里集成ButterKnife可以使用Android Studio的插件功能来进行下载和集成,比较简单,也可以采用远程链接的方式,这里不再赘述
集成了ButterKnife后,修改SplashActivity,同时在代码中实现MVP思想,注入相关的View控件,代码如下:
package com.dianbin.fastec.im95.view;
import androidx.appcompat.app.AppCompatActivity;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.content.Intent;
import android.os.Bundle;
import android.widget.ImageView;
import com.dianbin.fastec.im95.MainActivity;
import com.dianbin.fastec.im95.R;
import com.dianbin.fastec.im95.presenter.SplashPresenter;
import com.dianbin.fastec.im95.presenter.impl.SplashPresenterImpl;
import butterknife.BindView;
import butterknife.ButterKnife;
public class SplashActivity extends AppCompatActivity implements SplashView{
@BindView(R.id.iv_splash)
ImageView ivSplash;
// 声明presenter引用
private SplashPresenter splashPresenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_splash);
ButterKnife.bind(this);
// View层持有一个Presenter层引用
splashPresenter = new SplashPresenterImpl(this);
// 判断是否登陆过
splashPresenter.checkLogin();
}
@Override
public void onGetLoginState(boolean isLogin) {
if (isLogin){
// 如果登录过则跳转到主页面
startActivity(new Intent(SplashActivity.this,MainActivity.class));
}else {
// 如果没有登录则跳转到登录的界面
// 属性动画 alpha透明度,两秒之内从透明变成不透明
ObjectAnimator alpha = ObjectAnimator.ofFloat(ivSplash,"alpha",0,1).setDuration(2000);
// 开始动画
alpha.start();
// 给动画添加一个监听器
alpha.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
// 当动画执行结束开启登录的activity
startActivity(new Intent(SplashActivity.this,LoginActivity.class));
}
});
}
}
}
6.登录页面的实现
进入主页面后,会验证用户是否登录,如果登录则会直接跳转到主页面(MainActivity),如果没有则会跳转到登录页面(LoginActivity),这里需要实现登录页面的逻辑。
-
在view包下新建LoginActivity,作为登录页面的Activity,模板选择Empty Activity即可
-
修改activity_login.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="@mipmap/login_bk"
>
<ImageView
android:layout_gravity="center_horizontal"
android:id="@+id/iv_avatar"
android:layout_width="@dimen/avatar_size"
android:layout_height="@dimen/avatar_size"
android:src="@mipmap/avatar2"
android:layout_marginTop="20dp"
android:layout_marginBottom="20dp"
/>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/til_username"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:id="@+id/et_username"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:inputType="text"
android:hint="用户名"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/til_pwd"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:id="@+id/et_pwd"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:inputType="numberPassword"
android:imeOptions="actionDone"
android:hint="密码"/>
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/btn_login"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:text="登录"
android:textSize="22sp"
android:textColor="@android:color/white"
android:background="@drawable/login_btn_selector"/>
<TextView
android:id="@+id/tv_newuser"
android:layout_gravity="right"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:layout_marginRight="20dp"
android:text="新用户"
android:textColor="@color/btn_normal"/>
</LinearLayout>
7.注册页面的实现
一般来说,在登录之前,我们需要实现一个注册页面。而注册页面需要存储一些用户的相关数据,若没有自己搭建相关服务器的话这块的逻辑会比较麻烦,所以这里用到一个第三方的服务端存储sdk,即leancloud,集成和初始化步骤可以参考官方文档,这里不再详述,大致的注册逻辑如图所示:
-
在view包下新建RegistActivity,作为注册页面的Activity,模板选择Empty Activity即可
-
修改activity_regist.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="@mipmap/login_bk"
>
<ImageView
android:layout_gravity="center_horizontal"
android:id="@+id/iv_avatar"
android:layout_width="@dimen/avatar_size"
android:layout_height="@dimen/avatar_size"
android:src="@mipmap/avatar1"
android:layout_marginTop="20dp"
android:layout_marginBottom="20dp"
/>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/til_username"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:id="@+id/et_username"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:inputType="text"
android:hint="用户名"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/til_pwd"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:id="@+id/et_pwd"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:inputType="numberPassword"
android:imeOptions="actionDone"
android:hint="密码"/>
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/btn_regist"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:text="注册"
android:textSize="22sp"
android:textColor="@android:color/white"
android:background="@drawable/login_btn_selector"/>
</LinearLayout>
- 由于项目中采用MVP模式,需要给注册提供一个Presenter层接口,在presenter新建接口RegistPresenter,里面有一个用户注册方法,代码如下:
package com.dianbin.fastec.im95.presenter;
public interface RegistPresenter {
void registUser(String username,String pwd);
}
- 在view下新建RegistView接口,代码如下:
package com.dianbin.fastec.im95.view;
public interface RegistView {
/**
* 当获取到注册的状态之后,做进一步的操作
* 1.如果注册成功,就跳转到登录页面
* 2.如果注册失败,那么就弹出一个通知(Toast)
* @param username 用户名
* @param pwd 用户密码
* @param isSuccess 是否注册成功
* @param errorMsg 错误信息
*/
void onGetRegistState(String username,String pwd,boolean isSuccess,String errorMsg);
}
- 在presenter/impl下新建RegistPresenterImpl,作为RegistPresenter的实现类,注意这里需要开启线程进行一个异步的注册操作(需要同时注册到LeanCloud和环信上)代码如下:
package com.dianbin.fastec.im95.presenter.impl;
import com.dianbin.fastec.im95.presenter.RegistPresenter;
import com.dianbin.fastec.im95.utils.ThreadUtils;
import com.dianbin.fastec.im95.view.RegistView;
import com.hyphenate.chat.EMClient;
import com.hyphenate.exceptions.HyphenateException;
import cn.leancloud.AVUser;
import io.reactivex.Observer;
import io.reactivex.disposables.Disposable;
public class RegistPresenterImpl implements RegistPresenter {
private RegistView registView;
public RegistPresenterImpl(RegistView registView) {
this.registView = registView;
}
@Override
public void registUser(String username,String pwd) {
// 创建实例
AVUser user = new AVUser();
// 设置用户账号和密码
user.setUsername(username);
user.setPassword(pwd);
// 在子线程中注册用户
user.signUpInBackground().subscribe(new Observer<AVUser>() {
public void onSubscribe(Disposable disposable) {}
public void onNext(AVUser user) {
// 注册成功
// 注册到环信服务器上
ThreadUtils.runOnNonUIThread(new Runnable() {
@Override
public void run() {
//注册环信 需要注意 环信的api 联网的操作没有帮助开线程
try {
EMClient.getInstance().createAccount(username, pwd);
//说明注册成功
//通知界面跳转
ThreadUtils.runOnMainThread(() -> registView.onGetRegistState(username,pwd,true,null));
} catch (final HyphenateException e) {
e.printStackTrace();
ThreadUtils.runOnMainThread(() -> {
//如果环信注册失败 删除三方数据库的user
user.delete();
//通知界面显示注册失败
registView.onGetRegistState(username,pwd,false,e.getDescription());
});
}
}
});
}
public void onError(Throwable throwable) {
// 注册失败(通常是因为用户名已被使用)
//如果有异常说明注册失败 通知界面显示注册失败
registView.onGetRegistState(username,pwd,false,throwable.getMessage());
}
public void onComplete() {}
});
}
}
- 接下来,完善RegistActivity,让其实现RegistView接口,并且实现相应方法,注意这里有个沉浸式状态栏的代码实现,代码如下:
package com.dianbin.fastec.im95.view;
import android.app.ProgressDialog;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import com.dianbin.fastec.im95.R;
import com.dianbin.fastec.im95.presenter.RegistPresenter;
import com.dianbin.fastec.im95.presenter.impl.RegistPresenterImpl;
import com.dianbin.fastec.im95.utils.StringUtils;
import com.google.android.material.textfield.TextInputLayout;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
public class RegistActivity extends BaseActivity implements RegistView{
@BindView(R.id.iv_avatar)
ImageView ivAvatar;
@BindView(R.id.et_username)
EditText etUsername;
@BindView(R.id.til_username)
TextInputLayout tilUsername;
@BindView(R.id.et_pwd)
EditText etPwd;
@BindView(R.id.til_pwd)
TextInputLayout tilPwd;
@BindView(R.id.btn_regist)
Button btnRegist;
// Presenter层的注册接口
private RegistPresenter presenter;
// 进度条
private ProgressDialog progressDialog;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 沉浸式状态栏
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Window window = getWindow();
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
| WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
window.setStatusBarColor(Color.TRANSPARENT);
}
setContentView(R.layout.activity_regist);
ButterKnife.bind(this);
presenter = new RegistPresenterImpl(this);
}
@OnClick(R.id.btn_regist)
public void onClick() {
//获取用户名密码
String pwd = etPwd.getText().toString().trim();
String username = etUsername.getText().toString().trim();
//校验输入是否合法
if( !StringUtils.CheckUsername(username)){
tilUsername.setErrorEnabled(true);
tilUsername.setError("用户名不合法");
return;
}else{
tilUsername.setErrorEnabled(false);
}
//校验输入是否合法
if(!StringUtils.Checkpwd(pwd)){
tilPwd.setErrorEnabled(true);
tilPwd.setError("密码不合法");
return;
}else{
tilPwd.setErrorEnabled(false);
}
//创建进度条对话框 执行注册的逻辑
showProgressDialog("正在注册......");
presenter.registUser(username,pwd);
}
@Override
public void onGetRegistState(String username, String pwd, boolean isSuccess, String errorMsg) {
//走到这个方法中 说明已经获取到了注册的结果 隐藏进度对话框
cancelProgressDialog();
if(isSuccess){
//注册成功 可以跳转到登录页面
showToast("注册成功");
saveUsernamePwd(username,pwd);
startActivity(LoginActivity.class,true);
}else{
//通过吐司显示注册失败
showToast("注册失败:"+errorMsg);
}
}
}