Android项目框架搭建(一)

本篇先记录下当前项目中涉及的主要技术要点。也算是对所作项目的一次总结。如果这个过程能对你有些许的帮助,那可能就显得有意义点了。

一个完整的Android项目会涉及后台和前端。我们只关注于前端,也就是我们的app本身。

下面列出项目架构需要具备的技术点。(以当前所作项目为例)
1.项目结构(MVP设计模式)
2.屏幕适配
3.程序启动页
4.运行权限获取
5.基类(BaseActivity/BaseFragment/BaseApplication)
6.Retrofit(最流行的网络请求框架)+RxJava(链式编程风格+异步)
7.程序崩溃界面处理

1.项目结构

在这里插入图片描述
项目采用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,不管黑猫白猫,捉住老鼠就是好猫。

通俗表述就是,不要刻意的是自己的代码故意去迎合某种设计模式,而是通过这种"视图&&业务逻辑解耦"的思想去引贯穿和引导自己去写代码。

 

2.屏幕适配

在这里插入图片描述
前面受限于截图,特此将values/dimens.xml文件展开如上图。android项目开发,屏幕适配是首先要考虑的问题关于屏幕适配,大家可参照另一篇文章:
Android 屏幕适配方案
 

3.程序启动页

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>
...
  <!-- Base application theme. -->
    <style name="SplashTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">#00FFFF</item>
        <item name="colorPrimaryDark">#FFFFFF</item>
        <item name="colorAccent">#FFFF00</item>
        <item name="android:windowBackground">@mipmap/startup</item>
        <item name="android:windowActionBar">false</item>
        <item name="android:windowNoTitle">true</item>
    </style>

上述中的

 <item name="android:windowBackground">@mipmap/startup</item>

就是启动页的图。
 
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();
    }

}

 

4.运行权限获取

正常情况下程序运行的流程图下:
启动页----->登录页----->程序首页
没错,但是在从启动页跳转到登录页的时候,涉及到权限授予,主要是针对Android 6.0权限的授予。也许你会问,我在用到权限的时候再申请不行么? 答案是:可以,但是不建议。如果如果只有少量界面需要申请权限,即用即申请是可以的。但是如果需要权限的界面很多,这就比较麻烦了-------我们需要在每个界面都要判断所需权限是否已经授予了,如果没有的话就继续申请。权限申请逻辑比较分散,不便于后续维护。

因此最好的办法就是在程序开启时就要进行权限申请,如果所需权限没有全部被授予**(记住:所需权限必须全部被授予)**,就不允许用户登录。事实上QQ就是这么做的,当权限被用户手动拒绝之后,会弹出提示框,从而引导用户去设置中手动设置。

废话不说,直接贴出权限工具类,这个工具类其实还有些别的权限相关的方法,怕影响阅读,我把它阉割掉了,如果想看完整的,也可移步
Android常用的工具类汇总(方便日后使用)
PermissionUtils .java

public final class PermissionUtils {

    /**
     * Return whether <em>you</em> have granted the permissions.
     *
     * @param permissions The permissions.
     * @return {@code true}: yes<br>{@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项目框架搭建(二)

  • 12
    点赞
  • 148
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值