Android|Android 进阶 Gradle讲解

1.MVC
MVC,Model View Controller,是软件架构中最常见的一种框架,简单来说就是通过controller的控制去操作model层的数据,并且返回给view层展示,具体见下图
Android|Android 进阶 Gradle讲解
文章图片

当用户触发事件的时候,view层会发送指令到controller层,接着controller去通知model层更新数据,model层更新完数据以后直接显示在view层上,这就是MVC的工作原理。


Android MVC
【Android|Android 进阶 Gradle讲解】一个Android工程有Java的class文件,有res文件夹,里面是各种资源,还有类似manifest文件等等。对于原生的Android项目来说。

layout.xml里面的xml文件就对应于MVC的view层,里面都是一些view的布局代码。

各种Java bean,还有一些类似repository类以及工具类就对应于model层。

各种activity就对应controller层。

举例:比如你的界面有一个按钮,按下这个按钮去网络上下载一个文件。这个按钮是view层的,是使用xml来写的,而那些和网络连接相关的代码写在其他类里,比如你可以写一个专门的networkHelper类,这个就是model层,那怎么连接这两层呢?是通过button.setOnClickListener()这个函数,这个函数就写在了activity中,对应于controller层。是不是很清晰。
大家想过这样会有什么问题吗?显然是有的,不然为什么会有MVP和MVVM的诞生呢,是吧。问题就在于xml作为view层,控制能力实在太弱了,你想去动态的改变一个页面的背景,或者动态的隐藏/显示一个按钮,这些都没办法在xml中做,只能把代码写在activity中,造成了activity既是controller层,又是view层的这样一个窘境。如果是一个逻辑很复杂的页面,activity或者fragment是不是动辄上千行呢?这样不仅写起来麻烦,维护起来更是噩梦。
MVC还有一个重要的缺陷,大家看上面那幅图,view层和model层是相互可知的,这意味着两层之间存在耦合,耦合对于一个大型程序来说是非常致命的,因为这表示开发,测试,维护都需要花大量的精力。
正因为MVC有这样那样的缺点,所以才演化出了MVP和MVVM这两种框架。




2.MVP
MVP作为MVC的演化,解决了MVC不少的缺点,对于Android来说,MVP的model层相对于MVC是一样的,而activity和fragment不再是controller层,而是纯粹的view层,所有关于用户事件的转发全部交由presenter层处理。下面还是让我们看图
Android|Android 进阶 Gradle讲解
文章图片

从图中就可以看出,最明显的差别就是view层和model层不再相互可知,完全的解耦,取而代之的presenter层充当了桥梁的作用,用于操作view层发出的事件传递到presenter层中,presenter层去操作model层,并且将数据返回给view层,整个过程中view层和model层完全没有联系。看到这里大家可能会问,虽然view层和model层解耦了,但是view层和presenter层不是耦合在一起了吗?其实不是的,对于view层和presenter层的通信,我们是可以通过接口实现的。
具体的意思就是说我们的activity,fragment可以去实现实现定义好的接口,而在对应的presenter中通过接口调用方法。不仅如此,我们还可以编写测试用的View,模拟用户的各种操作,从而实现对Presenter的测试。这就解决了MVC模式中测试,维护难的问题。
当然,其实最好的方式是使用fragment作为view层,而activity则是用于创建view层(fragment)和presenter层(presenter)的一个控制器。
和MVC最大的不同,MVP把activity作为了view层,通过代码也可以看到,整个activity没有任何和model层相关的逻辑代码,取而代之的是把代码放到了presenter层中,presenter获取了model层的数据之后,通过接口的形式将view层需要的数据返回给它就OK了。
这样的好处是。首先,activity的代码逻辑减少了,其次,view层和model层完全解耦,具体来说,如果你需要测试一个http请求是否顺利,你不需要写一个activity,只需要写一个java类,实现对应的接口,presenter获取了数据自然会调用相应的方法,相应的,你也可以自己在presenter中mock数据,分发给view层,用来测试布局是否正确。




3.MVVM
MVVM最早是由微软提出的
Android|Android 进阶 Gradle讲解
文章图片

从图中看出,它和MVP的区别貌似不大,只不过是presenter层换成了viewmodel层,还有一点就是view层和viewmodel层是相互绑定的关系,这意味着当你更新viewmodel层的数据的时候,view层会相应的变动ui。
我们很难去说MVP和MVVM这两个MVC的变种孰优孰劣,还是要具体情况具体分析。



MVC代码举例:

需求描述:在一个页面完成上传附件的功能。(上传手机本地多张图片,上传之前要检查手机存储卡问题,是否有赋予该APP相关权限问题...)。

View层 Xml文件(布局)
Text格式:



Design格式:
Android|Android 进阶 Gradle讲解
文章图片


很简单,一个ImageView点击上传图片,一个ListView用来显示选择的图片。




Controller层 activity类(窗口)

部分核心代码:
case R.id.activity_attachmentadd_chooselayout://上传图片 if(!BooleanUtils.isEmpty(resultid)&&!BooleanUtils.isEmpty(resultname)){ requestAndroidPermission(); }else{ toast.showToast(StringConstant.attachmentststatus10); } break;

private void requestAndroidPermission(){ if(FileHelper.isSdCardExist()){ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){//6.0 boolean has= AndroidPermissionUtils.hasAndroidPermission(AttachmentAddActivity.this, DataConstant.permission); if(!has){//6.0及以上 没有权限 AndroidPermissionUtils.requestAndroidPermission(AttachmentAddActivity.this,0,DataConstant.permission); }else{//6.0及以上 有权限 请求相册图片 pickImage(); } }else{//6.0以下 请求相册图片 pickImage(); } }else{ toast.showToast(StringConstant.Filestatus2); } }





Model层 工具类

操作文件的工具类
public class FileHelper {private static FileHelper mInstance = new FileHelper(); /** * 获取对象的静态方法 * */public static FileHelper getInstance() { return mInstance; }/** * 判断当前设备是否有外部存储(SD卡) * */public static boolean isSdCardExist() { return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED); }/** * 获取sd卡根路径 * */public static String getTheRootDirectory(){ String result=""; if(isSdCardExist()){ result=Environment.getExternalStorageDirectory()+ "/"; } return result; }/** * 判断某个文件是否已存在于文件夹内 * */public static boolean isExistFile(File file,String name){ boolean result=false; if((null==file)||(name==null)){ return false; }else{ List fileList = getFile(file); if(fileList.size()>0){ for (int i = 0; i < fileList.size(); i++) { File filename = fileList.get(i); if(name.equals(filename.getName())){ result=true; break; } } }else{ result=false; } } returnresult; }/** * 获取某个文件夹下所有文件集合 * */public static List getFile(File file) { List mFileList = new ArrayList(); File[] fileArray = file.listFiles(); for (File f : fileArray) { if (f.isFile()) { mFileList.add(f); } else { getFile(f); } } return mFileList; }public static String createAvatarPath(String userName) { String dir = StringConstant.PICTURE_DIR; File destDir = new File(dir); if (!destDir.exists()) { destDir.mkdirs(); } File file; if (userName != null) { file = new File(dir, userName + ".png"); } else { file = new File(dir, new DateFormat().format("yyyy_MMdd_hhmmss", Calendar.getInstance(Locale.CHINA)) + ".png"); } return file.getAbsolutePath(); }public static String createAvatarPathPicture(String userName) { String dir = FileHelper.getTheRootDirectory() + StringConstant.PICTURTemporary_Path; File destDir = new File(dir); if (!destDir.exists()) { destDir.mkdirs(); } File file; if (userName != null) { file = new File(dir, userName + ".png"); } else { file = new File(dir, new DateFormat().format("yyyy_MMdd_hhmmss", Calendar.getInstance(Locale.CHINA)) + ".png"); } return file.getAbsolutePath(); }public static String getUserAvatarPath(String userName) { return StringConstant.PICTURE_DIR + userName + ".png"; }public interface CopyFileCallback { public void copyCallback(Uri uri); }/** * 复制后裁剪文件 聊天发送图片 * * @param file 要复制的文件 */ public void copyAndCrop(final File file, final Activity context, final CopyFileCallback callback) { if (isSdCardExist()) { Thread thread = new Thread(new Runnable() { @Override public void run() { try { FileInputStream fis = new FileInputStream(file); //String path = createAvatarPath(JMessageClient.getMyInfo().getUserName()); String path = ""; final File tempFile = new File(path); FileOutputStream fos = new FileOutputStream(tempFile); byte[] bt = new byte[1024]; int c; while ((c = fis.read(bt)) > 0) { fos.write(bt, 0, c); } //关闭输入、输出流 fis.close(); fos.close(); context.runOnUiThread(new Runnable() { @Override public void run() { callback.copyCallback(Uri.fromFile(tempFile)); } }); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { context.runOnUiThread(new Runnable() { @Override public void run() {} }); } } }); thread.start(); } else { Toast.makeText(context, StringConstant.Filestatus2, Toast.LENGTH_SHORT).show(); } }/** * 复制后裁剪文件 聊天发送图片 * * @param file 要复制的文件 */ public void copyAndCropPicture(final File file, final Activity context, final CopyFileCallback callback) { if (isSdCardExist()) { Thread thread = new Thread(new Runnable() { @Override public void run() { try { FileInputStream fis = new FileInputStream(file); String name = String.valueOf(System.currentTimeMillis()); String path = createAvatarPathPicture(name); final File tempFile = new File(path); FileOutputStream fos = new FileOutputStream(tempFile); byte[] bt = new byte[1024]; int c; while ((c = fis.read(bt)) > 0) { fos.write(bt, 0, c); } //关闭输入、输出流 fis.close(); fos.close(); context.runOnUiThread(new Runnable() { @Override public void run() { callback.copyCallback(Uri.fromFile(tempFile)); } }); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { context.runOnUiThread(new Runnable() { @Override public void run() {} }); } } }); thread.start(); } else { Toast.makeText(context, StringConstant.Filestatus2, Toast.LENGTH_SHORT).show(); } }}


操作权限的工具类
public class AndroidPermissionUtils {/** * Android 6.0及以上 检测是否具有某些权限 * */public static boolean hasAndroidPermission(Context context, String [] permission){ boolean has=true; for(String per:permission){ if(ContextCompat.checkSelfPermission(context,per) != PackageManager.PERMISSION_GRANTED){ has=false; break; } } return has; }/** * Android 6.0及以上 申请某些权限 * */public static void requestAndroidPermission(Activity activity, int code, String []permission){ ActivityCompat.requestPermissions(activity,permission,code); }}



图解:
Android|Android 进阶 Gradle讲解
文章图片



Android|Android 进阶 Gradle讲解
文章图片




Android|Android 进阶 Gradle讲解
文章图片



MVC在Android上的应用,一个具体的问题就是activity的责任过重,既是controller又是view。比如有一个progressDialog,在加载数据的时候显示,加载完了以后取消,逻辑其实是view层的逻辑,但是这个在xml里面操作比较困难,包括TextView.setTextView(),这个也一样。我们只能把这些逻辑写到activity中,这就造成了activity的臃肿。



MVP代码举例


需求描述:实现简单的登录功能
Android|Android 进阶 Gradle讲解
文章图片





View层 Xml文件(布局)以及Activity文件(或Fragment文件)

Activity类
public class LoginActivity extends AppCompatActivity implements ILoginActivity, View.OnClickListener{private EditText useredittext; private EditText pwdedittext; private TextView logintextview; private TextView resettextview; private ProgressDialog procDialog; private ILoginInPresenter iLoginInPresenter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_login); initView(); }/** * 初始化各种View * */public void initView(){ useredittext= (EditText) findViewById(R.id.activity_login_usernameedittext); pwdedittext= (EditText) findViewById(R.id.activity_login_pwdedittext); logintextview= (TextView) findViewById(R.id.activity_login_logintextview); resettextview= (TextView) findViewById(R.id.activity_login_resettextview); logintextview.setOnClickListener(this); resettextview.setOnClickListener(this); iLoginInPresenter = new LoginInPresenter(this); }@Override public void onClick(View view) { switch (view.getId()){ case R.id.activity_login_logintextview://登录 logintextview.setEnabled(false); iLoginInPresenter.submitLogin(useredittext.getText().toString(), pwdedittext.getText().toString()); break; case R.id.activity_login_resettextview://重置 iLoginInPresenter.clear(); break; } }@Override public void clearEdittext() { useredittext.setText(""); pwdedittext.setText(""); }@Override public void loginStart() { procDialog = new ProgressDialog(this); procDialog.setMessage("登录中..."); procDialog.setCancelable(false); procDialog.setIndeterminate(false); procDialog.show(); }@Override public void loginFinish(boolean result, int code) { if(null!=procDialog){ if (procDialog.isShowing()) { procDialog.dismiss(); } } if (result) { Toast.makeText(this, "登录成功!", Toast.LENGTH_SHORT).show(); //模拟MVP的model层 所以创建一个Java Bean Student student=new Student(); student.setName("张三"); student.setPwd("123"); String name=student.getName(); String pwd=student.getPwd(); Log.d("TAG","name----:"+name); Log.d("TAG","pwd----:"+pwd); } else { Toast.makeText(this, "登录失败,错误码:" + code, Toast.LENGTH_SHORT).show(); } }@Override protected void onDestroy() { super.onDestroy(); if(null!=procDialog){ if (procDialog.isShowing()) { procDialog.dismiss(); } } } }


xml布局文件

Text格式:

Design格式:
Android|Android 进阶 Gradle讲解
文章图片





Mode层 Java Bean
public class Student {private String name; private String pwd; public String getName() { return name; }public void setName(String name) { this.name = name; }public String getPwd() { return pwd; }public void setPwd(String pwd) { this.pwd = pwd; } }


Presenter层 各种接口和其实现类

ILoginActivity接口
public interface ILoginActivity {void clearEdittext(); void loginStart(); void loginFinish(boolean result,int code); }


ILoginInPresenter接口

public interface ILoginInPresenter {void clear(); void submitLogin(String name,String pwd); }


ILoginInPresenter接口实现类
public class LoginInPresenter implements ILoginInPresenter{private boolean result=false; private int code=-1; private ILoginActivity iLoginActivity; public LoginInPresenter(ILoginActivity iLoginActivity){ this.iLoginActivity=iLoginActivity; }@Override public void clear() { iLoginActivity.clearEdittext(); }@Override public void submitLogin(String name, String pwd) { if(null!=name&&null!=pwd&&name.equals(pwd)){ result=true; code=0; }else{ result=false; code=-1; }/** * 提交后直接给开始的回调 * */iLoginActivity.loginStart(); /** * 模拟耗时操作 5秒后给完成的回调 * */Handler handler=new Handler(); handler.postDelayed(new Runnable() { @Override public void run() { iLoginActivity.loginFinish(result,code); } }, 5000); }}



结果:
name----:张三pwd----:123




MVVM代码举例

Android 依赖注入库之Data Binding Library(MVVM设计模式)详解





    推荐阅读