Android开发实战“基于环信的通信工具”——01.账号模块的实现

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.闪屏页面的实现

一般来说,我们应用的第一个界面都是以一个闪屏页面来实现,若用户第一次登陆则会弹出教程,否则就会直接加载到相关页面,这里我们来实现一下

  1. 在项目下新建一个view包,然后新建一个活动SplashActivity,作为闪屏页面的Activity,模板调用Empty Activity

  2. 修改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个元素:

  1. View:负责绘制UI元素,与用户进行交互(在Android中体现为activity(或fragment),activity(或fragment)只控制界面的数据展示和用户的交互)
  2. View Interface:需要View实现的接口,View通过View Interface与Presenter进行交互,降低耦合,方便进行单元测试
  3. Model:负责存储、检索、操作数据(有时也可以实现一个Model Interface用来降低耦合)
  4. Presenter:作为View与Model交互的中间纽带,处理与用户交互的负责逻辑

MVP模式的示意图如下:

在这里插入图片描述

使用MVP模式的好处如下:

  • 减轻activity的负担,实现业务逻辑和界面的解耦
  • 方便对业务逻辑进行单元测试

让我们开始简单构建一个MVP模式的模板

  1. 在包下创建一个presenter包,在该包中新建一个接口SplashPresenter,作为SplashActivity的一个功能接口,里面只有一个验证的方法,代码如下:
package com.dianbin.fastec.im95.presenter;

public interface SplashPresenter {
    /**
     * 联网检测当前设备是否已经登录了
     */
    void checkLogin();
}
  1. 同时,在view包下新建一个接口SplashView,用来获取当前设备的登录状态,代码如下:
package com.dianbin.fastec.im95.view;

public interface SplashView {
    /**
     * 获取当前设备的登录状态之后 要处理的界面跳转逻辑
     * @param isLogin 是否已经登录
     */
    void onGetLoginState(boolean isLogin);
}
  1. 在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),这里需要实现登录页面的逻辑。

  1. 在view包下新建LoginActivity,作为登录页面的Activity,模板选择Empty Activity即可

  2. 修改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,集成和初始化步骤可以参考官方文档,这里不再详述,大致的注册逻辑如图所示:

在这里插入图片描述

  1. 在view包下新建RegistActivity,作为注册页面的Activity,模板选择Empty Activity即可

  2. 修改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>
  1. 由于项目中采用MVP模式,需要给注册提供一个Presenter层接口,在presenter新建接口RegistPresenter,里面有一个用户注册方法,代码如下:
package com.dianbin.fastec.im95.presenter;

public interface RegistPresenter {
    void registUser(String username,String pwd);
}
  1. 在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);
}
  1. 在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() {}
        });
    }
}
  1. 接下来,完善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);
        }
    }
}
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

赈川

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值