观心静

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

前言

 版权声明

       本文来自博客园,作者:观心静 ,转载请注明原文链接:https://www.cnblogs.com/guanxinjing/p/17107974.html

       本文版权归作者和博客园共有,欢迎转载,但必须给出原文链接,并保留此段声明,否则保留追究法律责任的权利。

 最新情况

  注意!注意!注意!,基于现在Android的技术方向都是compose来实现UI了。ARouter(阿里已经不更新了)已经开始不太支持新版本的gradle了,请谨慎选择ARouter与组件化。此外,个人觉得中小型项目都没必要组件化,组件化其实用空间换取复杂度降低的方式。但是大部分情况下,我们都不需要依靠组件化来降低技术复杂度。而是采取严格的文件目录,精准的分类,标准化的代码格式。

 介绍

  ARouter进行组件化项目我已经使用了很久很久了,因为涉及的内容实在太多,如果深究组件化甚至能讨论到哲学层面,比如深究分类与信息传递从而引发出还原论、整体论、系统论的思考。组件化其实一个没用最终答案的事情,所以一直不太想写博客,以前怕思考不够深误人子弟。但是现在我觉得哪怕我的观点可能是错误的,但是也要总结分享给其他人思考。回到正题总的来说组件化体验还是不错的,项目组件化后的优缺点:

  • 解耦项目,独立业务模块
  • 开发模块隔离后,团队协助也更容易,可以单独的运行测试单个模块
  • 模块插拔也更容易,单个模块容易搬迁到别的项目中使用
  • 单元测试也更加轻便

缺点也有:

  • 工作量增大,模板文件名增多
  • 重复一样的资源文件增多,apk体积难免冗余(因为你要考虑轻松搬迁模块到其他项目中,所以整个项目某个资源可能重复了,但是也是必须有一份当前模块)
  • 整个项目的编译时间可能会变长一点

  此篇博客其实涉及了2个方面的内容一个是组件化结构与思维,另一个是ARouter框架的使用。 可能博客会有一些些内容上的跳跃,个人认为如果单独讲其中一个内容其实都是无法立刻实际体验组件化的,也无法看到实际项目里是怎么使用的。

组件化的思想

  在立即开始学习ARouter前,我先输出我理解的组件化的构思,你可以不认同我的构思。甚至觉得我在创造大量冗余代码。这里只是给你一个参考。因为如果组件化构思不正确,组件化出来的东西就是怪物。 组件化最重要的思想就是如何准确分割业务模块。如何将一个app或者将一个设备的若干功能区分。这里我推荐刚接触组件化的新人,最好尽量将粒度提高,让组件化碎一些。辛苦一些多写重复代码,完成尽可能创建多个模块。 因为组件化的前期只是辛苦多写一些重复代码,可能很麻烦。但是后期是真的舒心。否则你后期想对一个模块剥离2个业务模块是真的麻烦,简直是藕断丝连。但是合并很简单,如果你后期发现2个模块不需要独立可以轻轻松松的合并。最后切记,想偷懒就别组件化,想组件化就别偷懒。否则又偷懒又组件化,你写的东西与架构没人能明白。

  思考一个功能是否需要独立成一个模块,可以用以下思想判断:

  1.它是不是以现有任何一个模块类型一致,功能一致?(比如音量调节相关功能一般属于设置功能下,它们类型功能一致)

  2.它是不是只服务一个业务模块? (比如人工验证码、短信验证码功能就不属于只服务一个业务,它可以服务多个业务)

  3..它以后是不是不会有子类业务与子功能延申?(比如音量调节、亮度调节功能,以后就很难延申出其他内容了,它就可以归类到设置模块中)

如果上面3个问题结果都是,这个业务就可以进入现存的某个模块中,否则就是需要独立出一个模块。

个人推荐的组件化目录结构

 实际项目截图图片:

壳工程

顾名思义它只是一个外壳,基本上不涉及任何代码。

common模块

  是整个项目的通用库,所以推荐将 全局通用业务数据、工具类、第三方的库、通用的Bean类、Base类、通用第三方模块的初始化逻辑 等等,都放到common模块里,不推荐放任何Activity相关代码。其实common模块的身份更像一个lib库,它也不需要跳转页面,但是common也存放一些最基础的通用的业务数据(例如:Token、用户的手机号码)与通用Bean类与Base类。所以我的建议是将它看成module。最后强调common模块的任何代码添加都要仔细考虑是否有必要。因为common不是共享垃圾桶,它涵盖的东西越精简越好,否则非常混乱难以管理。

业务模块 

  只放与当前模块业务相关的任何代码,包括只在当前模块下使用的UI、自定义View、工具类,广播、Bean数据、BaseActivity、BaseFragment、网络请求、SharedPreferences存储等等。是的,哪怕是网络请求这么麻烦的东西,我都建议网络请求都在各自模块里单独写一份(例如请求短信验证码,它们业务小代码量小不适合独立成模块,但是它们也不适合放到common,因为有些模块并不需要短信验证码,这种情况就需要在当前模块重复写一份短信验证码的网络请求)。而第三方框架,如果其他模块使用不到,那就应该放到对应需要的模块里。

  业务模块为什么每一个会有这么多重复冗余代码?这里就是上面强调的 想偷懒就别组件化,想组件化就别偷懒,否则又偷懒又组件化就是灾难,把大量共用网络请求业务、数据库、广播、SP存储数据堆到common模块那就不叫组件化,这叫割裂业务代码位置与强化模块之间的业务耦合。你只面对common模块写代码好了,写来写去你会发现你只需要common模块了。 这是大部分程序员的通病,不想写重复代码,时时刻刻想封装一些共用功能。

图片资源与xml资源

  我建议是放到各自的业务模块中,哪怕他们有重复的图片和xml我也强烈建议在各自模块放一份不同名称的复制品。为什么要干这么重复的工作?原因很简单,方便模块的插拔与资源清理与合作开发。特别是当项目庞大到一定程度,虽然重复资源会让app体积多多少少增大,但是会让资源管理不这么混乱,你在某个子业务模块里修改、删除某些资源的时候可以不用顾虑到其他子模块的调用。合作开发也不用查找与担心其他开发有没有添加资源或者修改资源,而可以随心所欲的在自己开发的子模块里添加资源。最后这里还是强调想偷懒就别组件化了。

其他建议

有一些人会直接在根目录下创建多应用的壳工程,然后也会创建多个各色common与分类多个module文件夹放多个应用的业务模块,并且让libs可以随便组合,这样看起来每天工作只要打开一个工程就能解决公司的多个应用同时开发。 个人建议是不要这么干:

第一是  后期容易乱,不小心的一个错误操作就能连锁反应影响好几个应用。

第二是  打开这么多项目对电脑要求极大,容易让Android Studio的内存占用飙升。

第三是  git管理,你这么多应用项目必须同时用一个git管理,到后期git的分支、回滚、合并操作来说这简直是灾难。

组件化的消息传递

  组件化的消息传递包括了路由跳转与发送与接受事件,路由跳转在下面ARouter会说明这点我没有建议。组件化的消息事件传递下面的ARouter是用服务完成的。而其他消息事件总线框架也能进行跨模块的消息事件传递,比如EventBus或者LiveDataBus。 但是,我不建议你使用各种消息事件总线进行跨模块的消息事件传递,而是坚持使用ARouter服务来完成消息事件传递。讨论这些问题涉及一个信息传递的思考。我先说一下事件总线的优缺点

EventBus,LiveDataBus: 

优点:

1.解耦

2.多对多通信(大喇叭)

3.粘性消息机制

缺点:

1.无法溯源

2.可以任意发送消息

3.任何操作都是异步的,没办法进行同步获得返回值,代码量上去了更加复杂。

4.代码上的强入侵性(需要一些共用的消息实体类)

总结思考:

  总结下我的想法。我认为事件总线可以用,但是最多限于模块内部。模块与模块之间是不需要这种事件总线这种大喇叭。如果你需要这种事件总线这种大喇叭在模块与模块之间传递消息,那么只有几种情况:

  1.你的模块分的太细碎了,让一个功能出现了需要多个模块配合才能实现。需要事件总线进行多模块的流程上的配合。

  2.某个模块太臃肿了,需要你让它通过大喇叭一次性解决共享性的消息传递。 (比如那种计时器事件、心跳事件、全局token失效事件)

软件开发经典名句高内聚低耦合。但是个人认为用事件总线其实并不是真正意义上的低耦合,这里举几个原因:

  1.写同步代码永远比异步代码强!否则这么多语言支持的协程特性是为了什么? 如果你认为EventBus依然是最强, 写异步能解耦,我建议你关闭这个博客, 回到你java8用接口和EventBus的世界观里。

  2.当发送者修改了代码逻辑发送了一个消息,让一群接收者接收信息,但是某一个接受者可能并不需要在这个情况下接收信息出现了错误。这个时候发送者就需要负责搜索那个接收者(你还无法溯源,必须一个一个的查找),将它抓出来为它独立一个消息总线。

  3.使用EventBus事件总线,你需要大量的实体类进行分类,这需要开发人员大量的互相交流,否则很容易出现混乱导致事件总线的循环。容易出现错误的原因是事件总线所有操作都是异步的,发送然后等待数据回调。

  所以事件总线真是低耦合吗?某种程度上事件总线框架成为了耦合怪物。而且人员互相交流是需要时间成本的。所以我认为,事件总线可以用在高内聚的模块内部,让事件总线累积后造成的混乱封锁在模块内部(话虽如此其实本人在模块里不需要使用事件总线,这当然涉及一些其他方面的知识内容了)。

  而模块之间信息传递我是推荐使用ARouter服务的方式。 你可以通过代码的方式去决定消息是观察者模式(大喇叭),还是一对一模式(单一接口)。而且有些时候你不需要这么多异步操作,而ARouter服务用调用方法可以立即获得返回值,你不需要发送事件后等待接收返回消息数据,这减轻了接口调用的复杂度。另外模块的接口设计其实是需要部门讨论后就敲定的,服务的接口是不可能你随意增加减少修改的,这不像事件总线这么随意。此外,如果你在使用kotlin我建议你尽量减少接口,多用协程写同步代码。

  这里将一个场景故事作为我思维的总结,你去网购一个家庭这个月都想要阅读的书籍,当厂家发货通过快递公司把快递送到你家。你取得快递,然后拆开快递分发各个家庭成员需要的书籍。比如老婆的烹饪书,你立刻给了厨房里的老婆,老婆快速阅读后问你明天想吃书上的那道菜。你然后随手把三国演义与童话绘本给了大女儿,并且告知大女儿把童话绘本递给房间里的妹妹。

  厂家发货通过快递公司把快递送到你家,这就是点线点的消息传递,A点是厂家,线是快递公司,B点是你家。他们可以互相溯源,快递包裹是点对点的传递。这就是厂家与你家都是模块的概念,你需要目标单一、过程简单、变化少的事件传递。

  而家庭内部里分发书籍,这就是事件总线的概念,你们需要迅速、方便、异步流程配合的进行事件的互相传递。

ARouter配置流程

根目录settings.gradle配置

ARouter是阿里的第三方项目,需要在settings.gradle添加阿里的镜像网址

pluginManagement {
    repositories {
        gradlePluginPortal()
        google()
        mavenCentral()
    }
}
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        //这里添加阿里的镜像网址
        maven { url 'https://maven.aliyun.com/repository/central' }
        maven { url 'https://maven.aliyun.com/repository/public' }
        maven { url 'https://maven.aliyun.com/repository/google' }
        maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
        maven { url "https://jitpack.io"}
        mavenCentral()
        google()
    }
}
rootProject.name = "ARouterDemo"
//这里是我们配置的所有模块
include ':app'
include ':module:common'
include ':module:main'
include ':module:settings'

壳工程的配置

build配置

plugins {
    //注意com.android.application, 壳工程一定是这个属性,这表明这是模块是应用程序,不是模块库
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    id 'kotlin-kapt'
}

android {
    namespace 'com.example.arouterdemo'
    compileSdk 32

    defaultConfig {
        //略...

        //阿里AROUTER依赖添加,这是每一个模块都要添加的,kapt对应的是kotlin
        kapt {
            arguments {
                arg("AROUTER_MODULE_NAME", project.getName())
            }
        }
        //阿里AROUTER依赖添加,这是每一个模块都要添加的,javaComileOptions对应的是java
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [AROUTER_MODULE_NAME: project.getName()]
            }
        }
    }

    //略...
}

dependencies {
    //略...
    
    //这里添加所有的模块
    implementation project(path: ":module:common")
    implementation project(path: ":module:main")
    implementation project(path: ":module:settings")
}

AndroidManifest

壳工程的AndroidManifest很干净,只有应用程序的配置

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:name=".app.App"
        android:theme="@style/Theme.ARouterDemo"
        tools:targetApi="31">
    </application>

</manifest>

Common的配置

Build的配置

plugins {
    //common模块与其他页面模块都是com.android.library, 表明这是一个模块库
    id 'com.android.library'
    id 'org.jetbrains.kotlin.android'
    id 'kotlin-kapt'
}

android {
    namespace 'com.example.common'
    compileSdk 32

    defaultConfig {
        //略...

        //阿里AROUTER依赖添加
        kapt {
            arguments {
                arg("AROUTER_MODULE_NAME", project.getName())
            }
        }
        //阿里AROUTER依赖添加
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [AROUTER_MODULE_NAME: project.getName()]
            }
        }
    }

    //略...
}

dependencies {
    //略...

    //阿里路由主库,其实这个库每一个模块都要依赖,包括了壳工程。但是,在我的项目结构下,全部业务模块都会导入common模块。所以我这里用的是api导入,这样其他业务库都会共享阿里的这个主库。
    api 'com.alibaba:arouter-api:1.5.0'
}

业务模块配置

Build的配置

plugins {
    id 'com.android.library'
    id 'org.jetbrains.kotlin.android'
    id 'kotlin-kapt'
}

android {
    namespace 'com.example.main'
    compileSdk 32

    defaultConfig {
        //略...

        //阿里AROUTER依赖添加,这是每一个模块都要添加的,kapt对应的是kotlin
        kapt {
            arguments {
                arg("AROUTER_MODULE_NAME", project.getName())
            }
        }
        //阿里AROUTER依赖添加,这是每一个模块都要添加的,javaComileOptions对应的是java
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [AROUTER_MODULE_NAME: project.getName()]
            }
        }
    }
    //略...
}

dependencies {
    //略...

    //导入通用模块
    implementation project(path: ":module:common")
    //阿里路由arouter的编译器,注意这个是每一个模块都要单独添加的,添加这个库代表你这个模块是有Activity或者服务需要进行路由的
    kapt 'com.alibaba:arouter-compiler:1.2.2'

}

组件化后各个模块的res资源的取名注意

这个很重要,在各个模块组件化后,其实同名的资源也是会冲突的,比如A图片叫ic_logo.png ,B图片也叫ic_logo.png。这个时候你会发现你怎么都无法显示B图片的内容,会默认选择为A图片的内容。 为了避免这个bug,我们一般需要给xml文件名称、颜色名称、字符串名称等等都需要添加项目的前缀。

我们可以在各个模块的build添加资源前缀限制,以提醒我们给资源文件创建名称时需要添加前缀,如下:

android {
   //略...
    resourcePrefix "settings_" //给 Module内的资源名增加前缀, 避免资源名冲突
}

ARouter的初始化

        if (CommonBuild.IS_TEST_VERSION){
            //debug下新增需要调用,才能跳转新增的路径
            ARouter.openLog()     // 打印日志
            ARouter.openDebug()   // 开启调试模式(如果在InstantRun模式下运行,必须开启调试模式!线上版本需要关闭,否则有安全风险)
        }
        ARouter.init(mApp) // 尽可能早,推荐在Application中初始化

ARouter初始化一般在Application的onCreate生命周期里完成,并且我们要尽快初始化。

特别注意!如果你的app是debug版本那么你一定要在初始化前先调用openDebug,否则任何新增或者修改的路径都不会有功能执行。而正式上线的版本请一定关闭openDebug。

ARouter的Activity的模块跳转

创建路径:

个人习惯在common模块里创建一个路径类,来管理项目组件化的全局的路径,请注意!要求是必须有两层路径。

public interface ARouterPath {
     String PTAH_SETTING = "/settings/main";
}

需要跳转的设置模块Activity:

//增加如下注解并且添加路径
@Route(path = ARouterPath.PTAH_SETTING)
class SettingsActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_settings)
        val settingsText = findViewById<TextView>(R.id.settingsText)
        settingsText.setOnClickListener {
            finish()
        }
    }
}

执行跳转代码:

从其他模块activity跳转到设置模块的Activity

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val button = findViewById<Button>(R.id.button)
        button.setOnClickListener {
            //跳转设置模块
            ARouter.getInstance().build(ARouterPath.PTAH_SETTING).navigation()
        }
    }
}

ARouter传参跳转

手动获取传参

需要带参跳转的设置模块Activity:

//增加如下注解并且添加路径
@Route(path = ARouterPath.PTAH_SETTING)
class SettingsActivity : AppCompatActivity() {
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_settings)

        val data = intent.extras?.getString("data")
        Log.e("zh", "onCreate:导航携带的数据 = ${data}"  )

        val settingsText = findViewById<TextView>(R.id.settingsText)
        settingsText.setOnClickListener {
            finish()
        }
    }
}

执行带参跳转代码:

        button.setOnClickListener {
            //跳转设置模块
            ARouter.getInstance().build(ARouterPath.PTAH_SETTING)
                .withString("data","这是数据")
                .navigation()
        }

依赖注入的方式获取传参

需要带参跳转的设置模块Activity:

//增加如下注解并且添加路径
@Route(path = ARouterPath.PTAH_SETTING)
class SettingsActivity : AppCompatActivity() {

    //JvmField是在kotlin下需要添加的注解,让编译器不要生成getter和setter,而是生成Java中成员.如果你是java类就可以不需要添加
    @JvmField
    //增加Autowired告诉ARouter这字段需要注入数据
    @Autowired
    public var data: String = ""

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_settings)

        //带参跳转的一定要尽快执行此方法,让携带的数据自动注入全局变量
        ARouter.getInstance().inject(this)
        Log.e("zh", "onCreate:导航携带的数据 = ${data}"  )

        val settingsText = findViewById<TextView>(R.id.settingsText)
        settingsText.setOnClickListener {
            finish()
        }
    }
}

执行带参跳转代码:

        button.setOnClickListener {
            //跳转设置模块
            ARouter.getInstance().build(ARouterPath.PTAH_SETTING)
                .withString("data","这是数据")
                .navigation()
        }

ARouter带参返回

执行跳转代码,携带请求code:

class MainActivity : AppCompatActivity() {
    private val mRequestCode = 100
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val btn = findViewById<Button>(R.id.btn)
        btn.setOnClickListener {
            ARouter.getInstance().build(ARouterPath.PTAH_SETTING).navigation(this, mRequestCode)
        }
    }

    //返回数据的处理跟原生的一致
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == mRequestCode && resultCode == RESULT_OK) {
            val content = data?.getStringExtra("content")
            Log.e("zh", "数据 = ${content}")
        }
    }
}

返回数据:

@Route(path = ARouterPath.PTAH_SETTING)
class SettingsActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_settings)

        val settingsText = findViewById<TextView>(R.id.settingsText)
        settingsText.setOnClickListener {
            intent.putExtra("content","12345678")
            setResult(RESULT_OK, intent)
            finish()
        }
    }
}

ARouter以Uri方式跳转

应用内部通过Uri跳转

路径:

public interface ARouterPath {
     String PTAH_SETTING = "/settings/main";
}

跳转目标模块的Activity:

//增加如下注解并且添加路径
@Route(path = ARouterPath.PTAH_SETTING)
class SettingsActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_settings)

        val settingsText = findViewById<TextView>(R.id.settingsText)
        settingsText.setOnClickListener {
            finish()
        }
    }
}

从其他模块执行跳转代码:

com.example.arouterdemo 是应用包名

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val btn = findViewById<Button>(R.id.btn)
        btn.setOnClickListener {
            //在 arouter://com.example.arouterdemo 后面拼接路径
            val uri = Uri.parse("arouter://com.example.arouterdemo/settings/main")
            ARouter.getInstance().build(uri).navigation()
        }
    }
}

通过scheme协议实现Uri跳转 

scheme知识点,请参考博客:Android开发 Scheme协议跳转、资源分享与被分享

路径:

public interface ARouterPath {
     String PTAH_SETTING = "/settings/main";
}

SettingsActivity:

//增加如下注解并且添加路径
@Route(path = ARouterPath.PTAH_SETTING)
class SettingsActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_settings)

        val settingsText = findViewById<TextView>(R.id.settingsText)
        settingsText.setOnClickListener {
            finish()
        }
    }
}

进行处理scheme的SchemeActivity:

其实就是用来对接外部Scheme协议内容分发的Activity

class SchemeActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_scheme)
        getUriData()
    }

    fun getUriData(){
        val uri: Uri? = intent.data
        uri?.let {
            //这里调用了ARouter跳转到目标页面
            ARouter.getInstance().build(it).navigation(this, object : NavCallback() {
                override fun onArrival(postcard: Postcard?) {
                    //已经到达
                    finish()
                }
            })
        }
    }
}

SchemeActivity在AndroidManifest.xml 需要添加的注册:

注意,android:path="/settings/main" 路径需要与ARouter的路径需要保持一致

         <activity
            android:name=".ui.SchemeActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.VIEW"/>
                <category android:name="android.intent.category.DEFAULT"/>
                <category android:name="android.intent.category.BROWSABLE"/>
                <data android:scheme="zh"
                    android:host="com.example.settings"
                    android:port="9000"
                    android:path="/settings/main" />
            </intent-filter>
        </activity> 

在其他App里执行的Uri跳转代码:

            //注意,后面拼接的路径/settings/main需要与ARouter的路径需要保持一致
            val uri = Uri.parse("zh://com.example.settings:9000/settings/main")
            val intent = Intent(ACTION_VIEW, uri)
            startActivity(intent)

通过scheme协议实现带参Uri跳转 

其他部分与上面一样,只说一说带参部分的代码

在其他app里通过uri带参跳转:

            val uri = Uri.parse("zh://com.example.settings:9000/settings/main?id=123456&time=${System.currentTimeMillis()}")
            val intent = Intent(ACTION_VIEW, uri)
            startActivity(intent)

SchemeActivity跟上面一样没有变化,在SettingsActivity解析参数:

//增加如下注解并且添加路径
@Route(path = ARouterPath.PTAH_SETTING)
class SettingsActivity : AppCompatActivity() {

    @JvmField
    @Autowired
    public var id: String = ""

    @JvmField
    @Autowired
    public var time: String = ""

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_settings)
        //自动注入
        ARouter.getInstance().inject(this)
        Log.e("zh", "onCreate:Uri参数解析 id = ${id} time = ${time}")
        //手动解析
        Log.e("zh", "onCreate:extras = ${intent.extras?.toString()}")
        Log.e("zh", "onCreate:Uri id = ${intent.extras?.getString("id")}")
        Log.e("zh", "onCreate:Uri time = ${intent.extras?.getString("time")}")
    }
}

结果:

  onCreate:Uri参数解析 id = 123456 time = 1678956472561
 onCreate:extras = Bundle[{NTeRQWvye18AkPd6G=zh://com.example.settings:9000/settings/main?id=123456&time=1678956472561, id=123456, time=1678956472561, wmHzgD4lOj5o4241=[id, time]}]
 onCreate:Uri id = 123456
 onCreate:Uri time = 1678956472561 

ARouter跳转动画

ARouter.getInstance()
        .build("/path/bactivity")
        //参数1为打开的Activity的进入动画,参数2为当前的Activity的退出动画
        .withTransition(R.anim.slide_in_bottom, R.anim.slide_out_bottom)
        .navigation(this);

ARouter的Fragment的跳转

ARouter的fragment跳转,很多人以为会直接跳转到目标Activity的某个fragment下。其实不是,而是将其他模块的fragment实例返回。 感觉有点鸡肋? 可能使用场景的确不多,但是并不鸡肋。因为并不是全部模块的入口都是Activity,有时候我们的应用是Tab翻页型的主页进行组件化的时候。每一个ItemTab其实都是fragment。 这个时候我们的每一个模块入口可能就是fragment了。

MainActivity的xml布局

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ui.MainActivity">

    <TextView
        android:id="@+id/jump"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="跳转Fragment"
        android:textColor="@android:color/black"
        android:textSize="30sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <FrameLayout
        android:id="@+id/fragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

fragment代码

@Route(path = "/main/my")
class MyFragment : Fragment() {
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.fragment_my, container, false)
    }
}

执行跳转fragment的代码:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val jump = findViewById<TextView>(R.id.jump)
        jump.setOnClickListener {
            val myFragment = ARouter.getInstance().build(ARouterPath.PTAH_MY).navigation() as Fragment
            val transaction: FragmentTransaction = supportFragmentManager.beginTransaction() //注意!每次要执行commit()方法的时候都需要重新获取一次FragmentTransaction,否则用已经commit过的FragmentTransaction再次commit会报错!
            transaction.replace(R.id.fragment, myFragment)
            transaction.commit()
        }
    }
}

ARouterService获取

 首先在common通用模块里创建服务接口类

 SettingsService代码:

//注意接口类需要继承IProvider
interface SettingsService  : IProvider {

    fun getCurrentVolume(): Int

    fun setCurrentVolume(num: Int)
}

在设置业务模块里实现SettingsService的逻辑功能:

//注意这里添加了路径
@Route(path = ARouterPath.PTAH_SETTINGS_SERVICE)
class SettingsServiceImpl : SettingsService {

    override fun init(context: Context?) {

    }

    override fun getCurrentVolume(): Int {
        Log.e("zh", "getCurrentVolume")
        return 0
    }

    override fun setCurrentVolume(num: Int) {
        //假装这里有逻辑
        Log.e("zh", "setCurrentVolume:${num}")
    }
}

在其他模块里调用SettingsService:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        //因为当前这个调用设置服务的模块也导入了common通用模块,所以是直接能获取到SettingsService接口类的。这里就可以强制转型
        val service = ARouter.getInstance().build(ARouterPath.PTAH_SETTINGS_SERVICE).navigation() as SettingsService
        val setBtn = findViewById<Button>(R.id.setBtn)
        setBtn.setOnClickListener {
            service.setCurrentVolume(15)
        }

        val getBtn = findViewById<Button>(R.id.getBtn)
        getBtn.setOnClickListener {
            service.getCurrentVolume()
        }
    }
}

ARouter拦截器功能

 拦截器功能与跳转路径一样,只需要添加注解就行了。ARouter会自动注册拦截器,但是需要注意,使用的项目也是要添加:

    //阿里路由arouter
    kapt 'com.alibaba:arouter-compiler:1.2.2'

代码:

//priority为拦截器的优先级,ARouter将按照优先级执行它们。
@Interceptor(priority = 1, name = "自定义拦截器")
public class MyInterceptor implements IInterceptor {
    private Context mContext;
    private boolean mIsTokenFailure = true;

    @Override
    public void process(Postcard postcard, InterceptorCallback callback) {
        Log.e("zh", "拦截器process " + postcard.toString());
        Log.e("zh", "路径 " + postcard.getPath());
        Log.e("zh", "目的地 " + postcard.getDestination());
        Log.e("zh", "跳转时携带的数据 " + postcard.getExtras());
        if (postcard.getPath().equals(ARouterPath.PTAH_SETTING) && mIsTokenFailure){
            //模拟token失效,拦截跳转
            callback.onInterrupt(null); //中断跳转
            ARouter.getInstance().build(ARouterPath.PTAH_LOGIN).navigation(mContext);
        } else {
            callback.onContinue(postcard); //继续跳转
        }
    }

    @Override
    public void init(Context context) {
        mContext = context;
        Log.e("zh", "拦截器初始化init");
    }
}

关于组件化后每个模块获得Application

 在组件化后,子模块会无法获得Application,因为Application只在壳工程继承里实现。 网上有各种各样的方式将Application分配给子模块。 但是多多少少其实都很复杂,有一些甚至学习ARouter的注解,自己也实现这种注解反射注册的方式。但是我认为这些方式都过于为了组件化而复杂了。甚至是为了模仿而模仿。 这里我给一个简单的方式,只要在子模块里实现一个单例,让壳工程里的Application初始化并且分配。 这样还有一个好处,因为每一个模块都有各自需要初始化的数据,而你可以轻松分配每一个模块初始化的顺序。

子模块Application单例:

import android.app.Application

object SettingsApp {
    private lateinit var mApplication: Application

    fun init(application: Application){
        mApplication = application
    }

    fun getApp() = mApplication
}

壳工程初始化:

public class App extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        CommonApp.INSTANCE.init(this);
        MainApp.INSTANCE.init(this);
        SettingsApp.INSTANCE.init(this);

    }
}

关于ARouter在设备项目中系统内置应用中的大坑

如果你是设备平台项目,比如开发的是launcher、内置系统应用等等,并且也打算用ARouter来组件化。我不建议你使用ARouter。 这里的大坑在系统OTA升级(OTA的同时更新应用)、强制静默安装上。在正常情况ARouter在正常安装应用后会主动更新路由的数据,会将它最新的路由数据到一份SP中。但是OTA升级、强制静默安装并不属于正常安装应用,如果你提高了应用版本或者修改了路由代码。ARouter在初始化的时候的路由检查会出现错误,从而抛出异常:

但是你可能已经使用了阿里ARouter进行了组件化,或者你还是不打算放弃组件化。 这里可以给你另一个思路。用原生的方式实现组件化。

在跨模块启动Activity的情况上,你可以选择隐性启动Activity。

在AndroidManifest给要隐性启动的Activity添加Action

<activity
    android:name="com.xxx.ui.logo.LogoActivity"
    android:exported="false" >
    <intent-filter>
        <action android:name = "com.xxx.ui.logo.LogoActivity"/>
        <category android:name = "android.intent.category.DEFAULT"/>
    </intent-filter>
</activity>

隐性启动代码

startActivity(Intent("com.xxx.ui.logo.LogoActivity"))

在跨模块通信上,你可以用接口与反射。

在common 模块中添加服务接口类

interface IMyService  {

    fun isTest(): Boolean
}

在其他模块里去实现上面的接口服务

class MyServiceImpl : IMyService {

     suspend fun isTest(): Boolean {
        return true
    }
}

然后再common模块中反射MyServiceImpl,将接口暴露给其他模块调用

class MyService {
    private lateinit var mIMyService: IMyService
    
    companion object {
        val instance by lazy { MyService() }
    }

    fun getService(): IMyService {
        if (!::mIMyService.isInitialized){
            val cls = Class.forName("com.xxx.MyServiceImpl")
            mIMyService = cls.newInstance() as IMyService
            return mIMyService
        }
        return mIMyService
    }
}

 

end

posted on 2023-02-10 13:53  观心静  阅读(2389)  评论(0)    收藏  举报