本篇先记录下当前项目中涉及的主要技术要点。也算是对所作项目的一次总结。如果这个过程能对你有些许的帮助,那可能就显得有意义点了。
一个完整的Android项目会涉及后台和前端。我们只关注于前端,也就是我们的app本身。
下面列出项目架构需要具备的技术点。(以当前所作项目为例)
1.项目结构(MVP设计模式)
2.屏幕适配
3.程序启动页
4.运行权限获取
5.基类(BaseActivity/BaseFragment/BaseApplication)
6.Retrofit(最流行的网络请求框架)+RxJava(链式编程风格+异步)
7.程序崩溃界面处理
项目采用MVP设计架构。
关于MVC,MVP,MVVM设计模式的升级改造做如下说明:
MVC模式:
M 指模型层(网络IO、文件IO等操作)
V 指视图层(对应Android中的Layout和Activity/Fragment)
C 指控制层(对应Android中的Activity/Fragment)
在Android中,Activity/Fragment既充当控制层又充当视图层,这就导致了V和C这两层耦合在一起,当业务比较复杂时,Activity/Fragment文件就很庞大,导致难以维护和测试,于是MVP模式便应用而生。
MVP模式:
M 指模型层(同MVC)
V 指视图层(同MVC)
P 指业务层(业务逻辑)
Activity/Fragment只充当视图层,不做任何的业务逻辑,将业务逻辑全部放在业务层,由Presenter和Model进行交互,避免Model直接操作View。MVP的优点:将业务从Activity/Fragment分离,便于后期维护和测试。MVP使用特点是面向接口编程(View/Presenter/Model都定义一套接口)。可以说MVP模式的出现主要是将MVC中Control控制层进行解耦,View-视图层中需要展示数据则通过Present-业务层调用Model-模型层去获取,Present-业务层拿到数据后以接口形式回传给View-视图层,View-视图层只需要注册监听Present-业务层用于更新View-视图层的接口就行了。
关于MVC和MVP的区别,大家可以参考 Android中MVC/MVP模式区别 ,里面介绍的很清楚,简单易懂。
有了MVP就够了,为什么又出来了一个MVVM呢?
关于MVP和MVVM模式的区别,网上资料多而杂,看过一圈之后,你可能会感觉更晕了。这个时候你可以继续往下看我的总结。
MVC和MVVM的唯一区别就是MVVM中多了一个DataBinding,其他基本无差别。
DataBinding 是谷歌官方发布的一个框架,顾名思义即为数据绑定,是 MVVM 模式在 Android 上的一种实现,用于降低布局和逻辑的耦合性,使代码逻辑更加清晰。MVVM 相对于 MVP,其实就是将 Presenter 层替换成了 ViewModel 层。DataBinding 能够省去我们一直以来的 findViewById() 步骤,大量减少 Activity 内的代码,数据能够单向或双向绑定到 layout 文件中。
关于DataBinding框架你可以参考 Android DataBinding 从入门到进阶,但是我没有在项目中使用。尝试用了一下,发现不好用。
原因如下:
第一条,DataBindding优势在于支持数据绑定(单向和双向),这意味着MVP模式中Present-业务层通过接口回调的方式来通知View-视图层来进行界面更新操作流程不存在了,取而代之的是通过标签绑定的形式来进行更新。如此,layout.xml中可能会进行一些简单的业务逻辑处理。虽然是简单的业务逻辑,我还是感觉和我们一致提倡的视图&&业务分离的理念相悖。
第二条,.使用DataBinding不方便维护。我曾尝试将MVP模式的项目代码改为DataBinding+MVVM的实现方式。结果很是不爽。举个例子,在使用DataBinding设置监听事件的时候,对点击事件的引用是不会进行错误提示的,一旦你定义的名字和实际引用的存在大小写不匹配的情况,很难排查出错点。Android IDE对代码补全的支持还不是很友好。
3.使用DataBinding的使用率不是很高。别的我不知道,我身边的人也仅限于了解过活着写过简单的Demo去学习其用法。但是实际项目中没有用到。因此使用DataBinding的话还需谨慎,如果多人维护同一个项目的话,那么大家都会DataBinding的使用,不然日后维护绝对是个坑。
4.网上介绍的关于DataBinding的用法的介绍,我不想吐槽。好多都是直接复制粘贴过来的,都是些基本的不能再基本的用法,高级用法介绍的很少。有些连基本用法都没讲清楚。比如,我们经常在layout.xml中来使用include来进行布局的复用(虽然不能减少层级嵌套),但是在include中的Layout中使用clickListener的时候,需要由父布局层层传递下去,才能起作用。但是这种很重要的用法,有些文章连提都没提。
算了,不吐槽了。不然该有小伙伴拿“存在即合理”来反驳我了。
当项目界面比较复杂控件比较多的时候,可能会需要写大量的findById(R.id.xxx),如果你看着别扭的话,可以使用开源库Butterknife(黄油刀)来进行替换。不一定要使用DataBindding。算了,你们还是自己回头在实际项目中用一下吧,如果你们觉得好用,麻烦告诉我以下,我们再交流交流。
如果对于MVC/MVP设计架构不清楚的话,推荐大家阅读:
Android App的设计架构:MVC,MVP,MVVM与架构经验谈,文章中有段话说的特别好:
刚开始理解这些概念的时候认为这几种模式虽然都是要将view和model解耦,但是非此即彼,没有关系,一个应用只会用一种模式。后来慢慢发现世界绝对不是只有黑白两面,中间最大的一块其实是灰色地带,同样,这几种模式的边界并非那么明显,可能你在自己的应用中都会用到。实际上也根本没必要去纠结自己到底用的是MVC、MVP还是MVVP,不管黑猫白猫,捉住老鼠就是好猫。
通俗表述就是,不要刻意的是自己的代码故意去迎合某种设计模式,而是通过这种"视图&&业务逻辑解耦"的思想去引贯穿和引导自己去写代码。
前面受限于截图,特此将values/dimens.xml文件展开如上图。android项目开发,屏幕适配是首先要考虑的问题关于屏幕适配,大家可参照另一篇文章:
Android 屏幕适配方案
AndroidMantifest.xml
...
<activity
android:name=".ui.activity.HRSplashActivity"
android:theme="@style/SplashTheme"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
...
<style name="SplashTheme" parent="Theme.AppCompat.Light.NoActionBar">
- "colorPrimary"
>#00FFFF
- "colorPrimaryDark">#FFFFFF
- "colorAccent">#FFFF00
- "android:windowBackground">@mipmap/startup
- "android:windowActionBar">false
- "android:windowNoTitle">true
style>
上述中的
<item name="android:windowBackground">@mipmap/startupitem>
就是启动页的图。
HRSplashActivity.java
public class HRSplashActivity extends FragmentActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//todo:可在此处添加关于启动页面的延时操作eg(延时2s):
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
startActivity(new Intent(HRSplashActivity.this, HRLoginActivity.class));
finish();
}
}
正常情况下程序运行的流程图下:
启动页----->登录页----->程序首页
没错,但是在从启动页跳转到登录页的时候,涉及到权限授予,主要是针对Android 6.0权限的授予。也许你会问,我在用到权限的时候再申请不行么? 答案是:可以,但是不建议。如果如果只有少量界面需要申请权限,即用即申请是可以的。但是如果需要权限的界面很多,这就比较麻烦了-------我们需要在每个界面都要判断所需权限是否已经授予了,如果没有的话就继续申请。权限申请逻辑比较分散,不便于后续维护。
因此最好的办法就是在程序开启时就要进行权限申请,如果所需权限没有全部被授予**(记住:所需权限必须全部被授予)**,就不允许用户登录。事实上QQ就是这么做的,当权限被用户手动拒绝之后,会弹出提示框,从而引导用户去设置中手动设置。
废话不说,直接贴出权限工具类,这个工具类其实还有些别的权限相关的方法,怕影响阅读,我把它阉割掉了,如果想看完整的,也可移步
Android常用的工具类汇总(方便日后使用)
PermissionUtils .java
public final class PermissionUtils {
/**
* Return whether you have granted the permissions.
*
* @param permissions The permissions.
* @return {@code true}: yes
{@code false}: no
*/
public static boolean isGranted(final String... permissions) {
for (String permission : permissions) {
if (!isGranted(permission)) {
return false;
}
}
return true;
}
private static boolean isGranted(final String permission) {
return Build.VERSION.SDK_INT < Build.VERSION_CODES.M
|| PackageManager.PERMISSION_GRANTED
== ContextCompat.checkSelfPermission(Utils.getApp(), permission);
}
/**
* Launch the application's details settings.
*/
public static void launchAppDetailsSettings() {
Intent intent = new Intent("android.settings.APPLICATION_DETAILS_SETTINGS");
intent.setData(Uri.parse("package:" + Utils.getApp().getPackageName()));
Utils.getApp().startActivity(intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
}
/**
* 批量申请权限(如果当前没有权限的话)。授权结果在onRequestPermissionsResult中处理
*/
public static void requestPermissionsIfNeed(Activity activity, String[] perms, int requestCode) {
if (perms.length == 0) {
return;
}
HashSet<String> needPerms = new HashSet<>();
for (String perm : perms) {
if (!isGranted(perm)) {
needPerms.add(perm);
}
}
if (needPerms.size() == 0) {
return;
} else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
activity.requestPermissions(needPerms.toArray(new String[needPerms.size()]), requestCode);
}
}
}
}
上述保留了最基本的权限申请逻辑,完全能满足需求。
至于怎么在程序入口处调用,很简单,看程序入口。
HRLoginActivity.java
public class HRLoginActivity extends HRBaseActivity implements View.OnClickListener {
private final int PERMISSION_REQUEST_CODE = 0x183;
private final String perms[] = {
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.MODIFY_AUDIO_SETTINGS,
Manifest.permission.INTERNET,
Manifest.permission.ACCESS_WIFI_STATE,
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO,
Manifest.permission.CHANGE_NETWORK_STATE
};
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestPermissions();
....
....
....
}
private void requestPermissions() {
PermissionUtils.requestPermissionsIfNeed(this, perms, PERMISSION_REQUEST_CODE);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.login_confirm:
if (PermissionUtils.isGranted(perms)) {
login();
} else {
requestPermissions();
}
break;
default:
break;
}
}
/**
* 某一权限没有被授予,弹出提示框
*/
private void showPermissionDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(this)
.setTitle("温馨提示")
.setMessage("请在设置中开启所需权限,以正常使用xxx功能")
.setNeutralButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
finish();
}
})
.setNegativeButton("去设置", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
PermissionUtils.launchAppDetailsSettings();
finish();
}
});
final AlertDialog dialog = builder.show();
dialog.setCanceledOnTouchOutside(false);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode != PERMISSION_REQUEST_CODE) {
return;
}
for (int i = 0; i < grantResults.length; i++) {
if (grantResults[i] != PackageManager.PERMISSION_GRANTED) {
showPermissionDialog();
return;
}
}
}
}
逻辑很简单:如果权限没授予,就弹出提示框,引导用户去“设置”中手动授予权限,如果在弹框中点击“取消”,则退出程序。下次进来,依旧如此,直至所有权限全部被授予。
好了,先到这吧,下面的3条将会在下一篇 Android项目框架搭建(二) 中进行介绍。
5.基类(BaseActivity/BaseFragment/BaseApplication)
6.Retrofit(最流行的网络请求框架)+RxJava(链式编程风格+异步)
7.程序崩溃界面处理
请先让我酝酿一下。
附上链接:
Android项目框架搭建(二)