Skip to content

使用Jetpack全家桶+Kotlin实现的Android社区App加音乐播放器。不写晦涩难懂的代码,尽量标清每一行注释,严格遵守六大基本原则,大量运用设计模式,此项目可快速帮你入手Kotlin、Jetpack。如果觉得对你有帮助,右上角点个star,事先谢过🍉🍉🍉

Notifications You must be signed in to change notification settings

fiytodie/Jetpack-Mvvm

 
 

Repository files navigation

注意注意

as升级到4.2后jdk location默认是11,而本项目jdk 1.8所以有冲突,会报java.lang.NoClassDefFoundError: javax/xml/bind/JAXBException错误。具体解决方案 https://stackoverflow.com/questions/67410927/java-lang-noclassdeffounderror-javax-xml-bind-jaxbexception-after-updating-to-a

建议结合本人两篇关于架构的文章去理解项目

引入Jetpack架构后,你的App会发生哪些变化?

关于Android架构,你是否还在生搬硬套?

1. 背景

为了更深入的理解Jetpack中各个组件,在前段时间基于Jetpack MVVM实现了一版 WanAndroid。相比上一版的MVP增加了夜间模式音乐播放器,播放器界面仿照网易云音乐App中也大量的使用属性动画让界面简约而不简陋。先上图look一波

2. 应用技术

基础框架选用MVVM,选用的Jetpack组件包括Lifecycle、ViewModel、LiveData、DataBinDing、Navigation、Room

项目基于Navigation由单ActivityFragment实现,使用这种模式给我最直观的感受就是,比如点击搜索进入搜索界面的衔接动画,在多Activity之间是不可能这么连贯的。

整个项目全部使用Kotlin语言,广泛应用了协程编写了大量的扩展函数

关于每个模块的职责我是这样定义的:

Model

对应项目中Repository,做数据请求以及业务逻辑。

ViewModel

基于Jetpack中的ViewModel进行封装(友情提示:Jetpack ViewModelMVVM ViewModel没有半毛钱关系,切勿将两个概念混淆)。在项目中VM层职责很简单,通过内部通过LiveData做数据存储,以及结合DataBinding做数据绑定。

View

尽量只做UI渲染。与MVP中不同,View是通过DataBinding与数据进行绑定,ActivityFragment非常轻盈只专注于生命周期的管理,数据渲染基本全部由DataBinding+BindAdapter实现。

关于MVVM模版类的封装可至package com.zs.base_library.base(包名)下查看。

网络层

关于网络层继续使用OkHttp Retrofit,并对Retrofit多ApiService以及多域名进行了封装。

数据库

项目中历史记录是在本地数据库进行维护的,关于数据库使用了Jetpack中的Room

主题切换

Android原生提供的夜间切换好像又API版本限制,所以就没有用。我个人在本地维护了两套主题,可动态切换。当前包含白天、夜间两套主题

3. 关于注释

我个人是非常喜欢写注释的,基本每一个复杂的功能都有对应的文字描述

项目中运用了大量的设计模式,每用到一种设计模式我都会结合当时场景进行解释,比如播放器中某个接口,我会这样写注释:


/**
 * des 所有的具体Player必须实现该接口,目的是为了让PlayManager不依赖任何
 *     具体的音频播放实现,原因大概有两点
 *
 *     1.PlayManager包含业务信息,Player不应该与业务信息进行耦合,否则每次改动都会对业务造成影响
 *
 *     2.符合开闭原则,如果需要对Player进行替换势必会牵连到PlayManager中的业务,因而造成不必要的麻烦
 *       如果基于IPlayer接口编程,扩展出一个Player即可,正所谓对扩展开放、修改关闭
 *
 * @author zs
 * @date 2020-06-23
 */
interface IPlayer {
    ....
    ....
}

/**
 * des 音频管理
 *     通过单例模式实现,托管音频状态与信息,并且作为唯一的可信源
 *     通过观察者模式统一对状态进行分发
 *     实则是一个代理,将目标对象Player与调用者隔离,并且在内部实现了对观察者的注册与通知
 * @author zs
 * @date 2020/6/25
 */
class PlayerManager private constructor() : IPlayerStatus {
     ....
     ....
}

写在最后

此项目中你很难看到不明不白的代码。JetpackKotlin是大势所趋,既然拒绝不了那何不开心的拥抱。功能目前已完成90%,代码也在持续优化,欢迎大家关注、下载源代码,让我们共同学习、共同进步。


highlight: a11y-dark

前言

不久前我写了两篇关于架构的文章,真没想到能有这么高的关注度,后续也不断有小伙伴私信向我请教设计/架构的学习方式,从他们提问得问题以及表现出的困惑我能感觉到 想学又无从下手,学了又不会用

其实好多小伙伴都认为学习设计就是学习设计模式,这是一个误区,没有底层思想的支持写出来的设计模式无非就是生搬硬套罢了,这里的底层思想其实就是设计原则,而设计原则则是面向对象编程基于现实背景衍生出来的一套规则,用来解决开发中的痛点。

好的架构需要反复进行思考以及设计,今天我将从面向对象为出发点 来分享自己对设计/架构衍变过程的理解,尽量帮你理清背景 抓住本质

文章目录看起来有些枯燥,但有别于其他八股文,跟着流程去学大概率可以短时间内见成效,所以请耐心阅读

前置知识:需要对 面向对象、设计原则、设计模式 有基本了解

1. 面向对象

什么是面向对象? 估计这个问题能难倒一大片同学,相信读完本文你心里应该会有一个合适的答案。先来看下基本定义:

 面向对象是一种风格,会以类作为代码的基本单位,通过对象访问,并拥有封装、继承、多肽、抽象四种特性作为基石,可让其更为智能。代表语言Java

1.1 四大特性

封装

封装也也可称之为信息隐藏。类通过暴露有限的访问接口,授权外部仅能通过类提供的方式(或者叫函数)来访问内部信息或者数据。举个例子解释下:

合理运用封装可以降低模块间依赖关系(松耦合)

继承

多肽

抽象

面向对象的四大特性相信大家都很熟悉,本小结只是帮大家做一次简单的回忆,关于其背景职责下半问会详细描述

1.2 诞生背景

谈及面向对象必定磨不开面向过程,毕竟它就是由面向过程衍变而来,吸收其大部分优点并解决其痛点。那什么是面向过程呢?基本定义如下:

分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了,更侧重于功能的设计。代表语言C 用代码体现就是下面这样:

#java版面向过程

public class Wallet {
    /**
     * 余额
     */
    int balance;
    /**
     * 存钱
     */
    void saveMoney(int money){
        balance += money;
    }
    /**
     * 花钱
     */
    void spendMoney(int money){
        balance -= money;
    }
}

无权限修饰符将内部信息全部暴露,简单粗暴很符合初级程序员的思维,但带来的问题很明显,外部可直接访问balance修改钱包内余额,现象就是"我钱包都没掏出来但里面钱却变少/多了"面向过程在开发中带来的问题远不止这些,所以在此背景下诞生了面向对象 通过面向对象封装特性将面向过程代码做个改进,如下:

#java版面向对象

public class Wallet {
    /**
     * 余额
     */
    private int balance;
    /**
     * 存钱
     */
    void saveMoney(int money){
        balance += money;
    }
    /**
     * 花钱
     */
    void spendMoney(int money){
        balance -= money;
    }
}

通过封装特性将balance通过private修饰,这样外部就没有权限直接修改金额,避免误操作带来的未知风险,满足松耦合特性 面向过程编程偏向于功能的开发,简单粗暴难以维护。而面向对象在编程之前需要基于四大特性对功能做建模设计,可以提高代码安全性、复用性、扩展性,更易于维护 既然面向对象这么智能为什么面向过程语言还没有被淘汰?其实面向对象语言的智能是针对我们开发者的,为了能让我们能写出易于维护的代码会多做一步设计,虽然离开发者更了 但离机器确了,毕竟机器只认识0和1而已。C语言规则简单易于形成机器码,所以执行效率高,这也是其没有被淘汰的原因。

小提示:

不要以为用了面向对象语言写出的就是面向对象代码,如果没有利用其特性那可能还是面向过程,比如没有利用权限修饰符、一个类一把梭等等....

2. 六大设计原则才是一切设计的基石

设计原则是基于面向对象思想衍变出来的一些规则,用来解决实际开发中的一些痛点,是所有设计的底层思想,也是我个人认为是设计/架构领域最重要的知识,所以请大家务必掌握好

2.1 单一设计原则

单一原则很好理解,指一个函数或者一个类再或者一个模块,职责越单一复用性就越强,同时能够间接降低耦合性。

案例:本地获取用户信息,提交到网络

fun post(){
        //创建数据库访问对象Dao
        val userDao = ...(这一过程很复杂)

        //从本地获取
        val age = dao.getAge()
        val name = dao.getName()
        //....省略大量字段

        //将个人信息提交至网络
        http.request(age,name,....)
}

以上案例将创建、获取、提交三步操作写到同一个函数中,很显然违背了单一设计原则,面临的问题也很明显,当修改创建、获取、提交任一过程时都会影响到其他二者,千万不要说"我注意一点就不会出错"这种话,因为人不是机器改动就可能出错,此时可以通过单一设计原则做一次重构,代码如下:

fun getUserDao():UserDao{
        ...
        return dao
}

fun getUserInfo():UserInfo{
        val dao = getUserDao()
        val userInfo = UserInfo()
        userInfo.age = dao.getAge()
        userInfo.name = dao.getName()
        ...
        return userInfo
}

fun post(){
        val userInfo = getUserInfo()
        //将个人信息提交至网络
        http.request(userInfo.age,userInfo.name,....)
}

三步操作被拆至三个函数 互不影响,从根本上杜绝因改动带来的一系列问题。所以使用面向对象语言开发时,不要急着写代码,要优先考虑下模块、类、函数...的设计是否足够单一

2.2 开闭原则

一句话概括开闭原则:对扩展开放,修改关闭。它即充分诠释抽象、多肽特性,又是多数行为型设计模式的基础,遍布于各大优秀框架之中,是最重要的一条设计原则,仅这一条原则就能把你的设计能力提高40%

举个例子让大家感受一下:

需求:通过SQLite做CRUD操作

class SQLiteDao{
    public void insert() {
         //通过SQLite做insert
    }
    public void delete() {
        //通过SQLite做insert
    }
}

SQLiteDao dao = new SQLiteDao();
dao.insert();
...

以上是最简单粗暴的写法,但存在一个致命问题,如果某一天想替换SQLite业务层基本要动一遍,改动就存在出错的可能,并且需要做大量的重复操作

面对以上问题可以利用抽象、多肽特性基于开闭原则做出重构,代码如下:

interface IDao{
    void insert();
    void delete();
}

class SQLiteDao implements IDao{
    @Override
    public void insert() {
         //通过SQLite做insert
    }
    @Override
    public void delete() {
        //通过SQLite做insert
    }
}

class RoomDao implements IDao{
    @Override
    public void insert() {
        //通过Room做insert
    }
    @Override
    public void delete() {
        //通过Room做delete
    }
}

//扩展点
IDao dao = new SQLiteDao();
dao.insert();
  • 定义功能接口IDao
  • 定义类SQLiteDao、RoomDao并实现IDao的功能
  • 业务层基于接口IDao进行编程

重构后,当需要将SQLite替换至Room时,只需将注释扩展点SQLiteDao替换成RoomDao即可,其他地方完全不用改动。这就是所谓的扩展开放,修改关闭

业务不断迭代情况下,唯一不变的就是改变,这种背景下我们能做的只有在代码中基于开闭原则多留扩展点以不变应万变。

2.3 迪米特法则

基本概念:不该有直接依赖关系的模块不要有依赖。有依赖关系的模块之间,尽量只依赖必要的接口。

迪米特法则很好理解并且非常实用,违背迪米特法则会产生什么问题?还以2.1面向过程代码举例:

class Wallet{
    /**
     * 余额
     */
    int balance;

    /**
     * 存钱
     */
    void saveMoney(int money){
        balance += money;
    }

    /**
     * 花钱
     */
    void spendMoney(int money){
        balance -= money;
    }
}

Wallet的设计违背了迪米特法则,毕竟外部只需要savespend功能,将balance暴漏使用者就有权限直接修改其值,可能会对整个Wallet功能造成影响。此时应基于迪米特法则Wallet进行改造,将balance通过封装特性增加private修饰符

迪米特法则单一设计原则很像,前者符合松耦合 后者符合高内聚`

2.4 接口隔离原则

基本概念:接口的调用者不应该依赖它不需要的接口。

乍一看与迪米特法则很相似。先来看下什么样的接口违背接口隔离原则

interface Callback{
    /**
     * 点击事件回调方法
     */
    void clickCallback();
    /**
     * 滚动事件回调方法
     */
    void scrollCallback();
}

接口Callback包含点击、滚动两个回调方法,面临的问题有两个:

  • 某些特定场景使用者只需要依赖点击回调,那滚动回调便成了多余,把外部不需要的功能暴露出来就存在误操作的可能。
  • 点击滚动本来就是两种特性,强行揉到一块只能让接口更臃肿,进而降低其复用性

根据接口隔离原则改造后如下:

interface ClickCallback{
    /**
     * 点击事件回调方法
     */
    void clickCallback();
}

interface ScrollCallback{
    /**
     * 滚动事件回调方法
     */
    void scrollCallback();
}

基于单一设计原则点击滚动拆分成两个接口,将模块间隔离的更彻底。并且由于粒度更细,所以复用性也更高

接口隔离原则迪米特法则目的很相似,都可以降低模块间依赖关系。但接口隔离更侧重于设计单一接口,提升复用性并间接降低模块间依赖关系,而迪米特法则是直接降低模块间依赖关

2.5 里氏替换原则

基本概念:

设计子类的时候,要遵守父类的行为约定。父类定义了函数的行为约定,子类可以改变函数的内部实现逻辑,但不能改变函数原有的行为约定。

里氏替换非常简单并且很容易遵守,

2.6 依赖倒置原则

控制反转: 提及依赖倒置便不得不提控制反转,一句话概括:将复杂的程序操作控制权由程序员交给成熟的框架处理,程序员->成熟的框架为反转,框架应暴露出扩展点由程序员实现 想详细了解可至 关于Android架构,你是否还在生搬硬套? 2.1章节查看

什么是依赖倒置?

高层模块(使用者)不应依赖低层模块(被使用者),它们共同依赖同一个抽象,抽象不要依赖具体实现细节,具体实现细节依赖抽象。

其实核心点就是基于接口而非实现编程2.2数据库案例也符合依赖倒置原则,高层模块(业务层)不依赖于低层模块(SQLiteDao/RoomDao),而是依赖于抽象(IDao),可见依赖倒置也是开闭原则扩展而来。 区别是依赖倒置更侧重于指导框架的设计,框架层应该尽量将更多的细节隐藏在内部,对外只暴露抽象(抽象类/接口),指导框架设计这方面核心就是控制反转

3. 设计模式只是设计原则的产物而已

设计模式共有23种,详细描述都能出一本书出来。本小结仅会分享一些通用的思路,个人认为还是比较硬核的,毕竟设计主要还是思想,而非生搬硬套

3.1 设计模式该怎么去学?

本小节会分析几个常见的设计模式核心思想以及设计背景,用于抛砖引玉

工厂模式

基本概念:用于创建复杂对象

创建复杂对象常规写法如下:

class B{
    ...
}

class D{
    void test(){
        B b = ....(创建B的过程很复杂)
        ...
    }
}

在使用的地方直接创建,如果直接new倒也没啥问题,但如果创建过程过于复杂,当修改创建过程时就会影响到test(),进而存在一些未知的隐患。

这一问题可通过迪米特法则进行改造:

class FactoryB{
    ...
    static B createB(){
        .....(B创建过程)
        return b;
    }
}
class D{
    void test1(){
        B b = FactoryB.create();
        ...
    }
}

B的创建本身就于调用者无关,将创建过程转移到类FactoryB中,根本上避免了创建过程对调用者的影响。改造后就是一个标准的简单工厂模式,所以简单工厂模式的核心思想就是迪米特法则

模版模式 通过开闭原则和里氏替换描述模板 设计背景:

观察者模式 通过接口隔离和开闭原则描述观察者 基本概念:当一个对象发生改变时需要通知到另一个对象

粗暴写法:

/**
 * 观察者
 */
class Observer{
    /**
     * 接收通知
     */
    void receive(){
        //具体逻辑
    }
}

/**
 * 被观察者
 */
class Observable{
    /**
     * 发送通知
     */
    void send(){
        Observer observer = new Observer();
        observer.receive();
    }
}

Observable(被观察者)内部直接持有Observer(观察者),在合适的时机发出通知,但这种写法有两个很明显的问题:

  • 扩展性差:当存在多个观察者Observer1,Observer2...时,Observable需要逐个手动创建发出通知
  • 耦合性强:Observable直接持有Observer对象,而Observer可能暴露出一些Observable不需要的属性/方法,存在误操作的风险

面对以上两个问题可以利用开闭原则接口隔离原则进行改造:

interface IObserver{
    /**
     * 接收通知
     */
    void receive();
}

class Observable{
    /**
     * 观察者集合
     */
    private final List<IObserver> observers = new ArrayList<>();

    /**
     * 发送通知
     */
    void send(){
        for (IObserver observer : observers){
            observer.receive();
        }
    }
}

以上是一个标准的观察者模式。通过接口隔离原则设计IObserver接口保证其单一性,避免模块之间依赖关系过强造成的安全隐患,解决了耦合性强问题。通过开闭原则维护一个observers,当新增观察者时只需添加到observers即可,符合扩展开放、修改关闭,解决类扩展性差问题。

所以开闭原则,接口隔离原则是观察者模式扩展性强,耦合性低的根本原因呐

以上三个案例足以表明设计模式的核心就是设计原则呐,所以学会设计模式的窍门就是先掌握设计原则

3.2 生搬硬套为大忌 

3.3  "盐加少许" 只可意会

  描述如何避免过度设计

3.4 自创设计模式

  描述控制反转思想扩招出来的数据驱动ui

4. 如何做好架构?

掌握设计原则可以写出扩展性强、复用性高...的代码 掌握设计模式可以设计出易用性强、安全性高...成熟的框架 掌握设计原则、设计模式,可以设计出容错率更高的架构

那什么是架构?

架构是一个很笼统的概念,上至框架选型下至业务代码都能称为架构的一部分,比喻到盖房子 设计图,打地基,选料…都能称之为架构,总之能够提升项目稳定性以及开发效率就是好架构。好的架构不是一蹴而就,而是根据面临的问题不断添砖加瓦

架构是如何衍进的?

  • 远古时代,基于Activity和XML开发,XML这种结构可以天然的将视图与Activity隔离,看起来很美妙,我也很开心..
  • 随着业务的发展,Activity代码不断壮大,各种逻辑全都揉到一块,常常改一处崩多处。我觉得不能再拖了,得赶紧基于单一设计原则将代码进行模块化。模块化后效果很明显,莫名其妙的bug少了很多
  • 每次写Activity时都总觉的有好多重复代码啊,而且一不留神容易错写、忘写,这让我想到了模版设计模式,将通用功能封装在内部并暴露一些抽象方法(钩子方法),新来的同事也变得开心的了,基于这套模板他可以无障碍开发
  • 某一天网络请求时发现参数一直对不上,各种排查才发现原来是修改某个View时对应的数据却忘记改了,这个问题真的很头痛。偶然间发现LiveData、DataBinding,这玩意基于控制反转+观察者设计 改变数据就能修改UI,那我肯定毫不犹豫引入到项目中啊。从此我再也不用担心数据UI一致性问题了
  • 数年后,项目工程逐渐庞大,编译一次都要好几分钟,找个文件找半天还容易改错,令大家苦不堪言。听说Android可以依据单一原则将代码拆分至多个module中并可以单独运行,试了试果然可以
  • 未完待续...

以上是一个简单架构的衍变过程,选用的每一个库都是基于设计原则,设计模式拓展出来用来解决开发痛点的。但是团队开发人员水平可能参次不齐,不一定能领悟到架构的含义,仅从口头上约束可能作用不大,此时一般会通过模板模式将通用信息做封装,在内部协调好各模块间关系,并暴露出对应的泛型、抽象方法(钩子),这样开发人员在使用模板类的时候就会被强制遵守现有的规则。

做架构需要考虑描述组件化拆分、livedata、viewbinding、diff等等,通过项目的实际现状做框架选型以及解决的问题,主要从思想层面 通过合理运用设计模式、设计原则封装框架、模版类,从范式上约束开发者,用于提高复用性、扩展性、安全性,

tips

关于面向对象、设计原则、设计模式如果详细讲解能写三大本书出来。本文主要描述其基本概念、设计背景以及三者之间的关系,起到抛砖引玉的作用。想真的学好设计、做好架构 需要不断从实践中体会、思考。

About

使用Jetpack全家桶+Kotlin实现的Android社区App加音乐播放器。不写晦涩难懂的代码,尽量标清每一行注释,严格遵守六大基本原则,大量运用设计模式,此项目可快速帮你入手Kotlin、Jetpack。如果觉得对你有帮助,右上角点个star,事先谢过🍉🍉🍉

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages

  • Kotlin 69.0%
  • Java 31.0%