Android 组件化开发实例技巧
- 技术交流
- 2024-09-25 20:31:01
首先对项目核心业务进行整体的评估,先对大的业务功能进行拆分,比如 App 核心业务有商城、社区分享、消息中心、用户信息管理等。之后再根据每一个业务的流程,对单一职责的功能进行细分。
比如一个商城的大功能,想买东西当然先要有商品,所以要展示商品列表,能点进去看详情,还有要能对商品进行价格降序、类品等筛选,这是一个查询商品的业务(增删改在商家端)。然后看完东西要下单,需要填地址,要能增删查改地址,设置默认地址,智能识别输入内容,这就是一个地址管理业务。下单后能查看待付款、待发货、待签收、待评价等订单列表,查看订单详情,涉及到订单的增删查,这就是一个订单业务。买的东西可能不满意,会有一套退货或换货流程,这就是一个售后业务。
这样我们就从一个完整的购物流程里,拆分出了商品、地址管理、订单组件、售后等业务功能。如果这几个业务都各自有人负责,那么就都写成组件,每人负责一个组件减少代码冲突。如果是一个人负责整个购物功能的开发,那么可以只写一个购物组件减少模块数量。假设地址管理还会在签到领礼品里用到,那么再抽出一个地址组件给购物组件和签到组件调用。
这是比较理想的情况,而实际开发中会有很多业务交叉依赖的情况。通常会有两种业务依赖关系:
业务强依赖,一个组件对于另一个组件是必要的。最常见的场景是登录后才能做某事,比如下单功能,即使能独立调试也必须登录后才能下单,因为这是个前提,该组件脱离了账户组件是用不了的,不知道是谁的话怎么下单。
业务弱依赖,一个组件对于另一个组件是非必要的。比如首页就聚合了各种业务的信息,在视频板块有消息中心的入口,在聊天的个人资料页面可能有朋友圈入口等,有一些页面整合了多种不相关的业务。
处理强依赖关系就直接对所需组件进行依赖或者合并组件代码。比如下单不仅仅需要登录,还得要先有商品。可以让订单组件直接包含查询商品的业务代码,而登录功能一般是会有个账户组件进行管理,我们直接依赖即可,这样独立调试时也能有完整的登录功能。
dependencies {
// ...
implementation project(':module-account-api')
runtimeOnly project(':module-moment')
}
复制代码
弱依赖关系会有很多种情况,最常见的是一个页面含有多种业务的信息,这里个人给出两种解决方案。
第一种方案是直接依赖 api 模块,最终运行有没组件的代码是让 App 壳来决定。
比如我们要做一个类似微信的 App,有 IM 功能和朋友圈功能,在聊天页面和朋友圈页面点击头像进入的个人资料,会有朋友圈入口和发消息入口。不过在另外一个 App 只需要 IM 功能,个人资料不会有朋友圈入口。
我们可以让 IM 组件依赖朋友圈的 api 模块。
dependencies {
// ...
implementation project(':module-moment-api')
}
复制代码
然后我们在 IM 组件的个人资料页面,获取朋友圈模块的路由服务接口 MomentService,如果能获取到实例对象,就查询最近三张朋友圈图片并在页面上增加朋友圈入口。
val momentService = ARouter.getInstance().navigation(MomentService::class.java)
if (momentService != null) {
val momentImages = momentService.getRecentMomentImages()
// 在个人资料页面展示朋友圈入口
}
复制代码
这样开发完后,后续复用就变得简单了。我们要开发类似微信的 App,就会同时依赖 IM 组件和朋友圈组件,那么在 IM 的个人资料页面能获取到 MomentService 的接口实例,从而按照需求添加入口。
dependencies {
// ...
implementation project(':module-im-api')
runtimeOnly project(':module-im')
implementation project(':module-moment-api')
runtimeOnly project(':module-moment')
}
复制代码
如果还有个 App 只有 IM 功能不需要朋友圈功能,那就不会依赖朋友圈组件,MomentService 接口就没法实例化,在 IM 的个人资料也就不会出现朋友圈入口。
dependencies {
// ...
implementation project(':module-im-api')
runtimeOnly project(':module-im')
}
复制代码
另一个方案是组件不依赖,业务交叉的部分在 App 壳里实现。
还用上面例子,依赖 IM 组件就只会有 IM 的功能,当 IM 组件的个人资料页面需要额外有朋友圈入口,那就在 App 壳里实现一个同时具有聊天入口和朋友圈入口的个人资料页面。
@Route(path = AppPaths.ACTIVITY_PROFILE)
class ProfileActivity : AppCompatActivity() {
private lateinit var accountService: AccountService
private lateinit var imService: IMService
private lateinit var momentService: MomentService
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_profile)
accountService = ARouter.getInstance().navigation(AccountService::class.java)!!
imService = ARouter.getInstance().navigation(IMService::class.java)!!
momentService = ARouter.getInstance().navigation(MomentService::class.java)!!
// 展示个人资料,添加聊天入口和朋友圈入口
}
}
复制代码
给原本点击聊天头像和点击朋友圈头像跳转的页面添加路由,这样我们就能用路由的拦截器对路由进行拦截,改成跳转一个新的个人资料页面。
@Interceptor(priority = 1)
class ProfileInterceptor: IInterceptor {
override fun init(context: Context) = Unit
override fun process(postcard: Postcard, callback: InterceptorCallback) {
if (postcard.path == IMPaths.ACTIVITY_PROFILE || postcard.path == MomentPaths.ACTIVITY_PROFILE) {
callback.onInterrupt(null)
ARouter.getInstance().build(AppPaths.ACTIVITY_PROFILE)
.with(postcard.extras)
.navigation()
} else {
callback.onContinue(postcard)
}
}
}
复制代码
那在这个 App 壳里,个人资料页面就会有同时有聊天入口和朋友圈入口。
既然 Activity 能拦截,那么 Fragment 能拦截吗?很可惜路由框架一般是不支持的,但是我们自己能另外实现。我们可以用接口服务提供一个 xxxFragmentFactory 的配置,用于创建某个 Fragment。
interface AccountService : IProvider {
// ...
var profileFragmentFactory: (Bundle) -> Fragment
}
复制代码
在组件的服务接口实现类返回一个默认的 Fragment。
@Route(path = AccountPaths.SERVICE)
class AccountServiceProvider : AccountService {
// ...
override var profileFragmentFactory: (Bundle) -> Fragment = {
ProfileFragment().apply { arguments = it }
}
}
复制代码
并且在组件内的某个页面通过组件服务的 xxxFragmentFactory 来创建出所需的 Fragment。
val fragment = accountService?.profileFragmentFactory(bundle)
复制代码
如果想拦截该组件的 Fragment,就设置对应的 xxxFragmentFactory 创建出新的 Fragment 对象来替换掉原本的 Fragment,这就实现了拦截 Fragment 的效果。
accountService?.profileFragmentFactory = {
FullProfileFragment().apply { arguments = it }
}
复制代码
另外说一下首页会有各种业务的 Fragment,可能有的人会抽出个首页组件来管理,但是这个首页组件基本只会用在一个特定的 App 里使用,其实写在 App 壳里更合适。有些人觉得 App 壳里应该是没多少代码的,但是个人认为套壳很经常是需要定制化的,是需要写些定制化的代码的。
以上两种方案各有优缺点,第一种方案的优点是可根据有无某个组件的实现模块去动态添加入口,开发起来比较方便,但缺点是组件之间的耦合度高。第二种方案的优点是组件之间的耦合度低,但缺点是需要根据需求额外写个新页面整合两个模块的信息,要得到信息又得增加路由接口方法,可能还要拦截路由跳到新页面,会在 APP 壳里写不少代码。具体选哪个方案还是要根据实际情况来决定。
多套 UI
组件化的优势是耦合度低能独立调试提高编译效率,还有个优势是更加容易复用,那就不可避免要让同一套业务在多个 app 里使用。而 app 的风格通常不一样,这就需要组件支持多套 UI,该怎么配置 UI 是个问题。
个人提供三个配置 UI 的方案,第一个是配置自定义 style 属性。首先在 api 模块添加 attrs.xml 文件,声明所需的主题属性及其类型。
<resources>
<attr name="account_sign_in_bg" type="color"/>
<attr name="account_sign_in_logo" type="reference"/>
</resources>
复制代码
然后在组件的布局里通过 ?attr/xxxxx 的方式使用已声明的主题属性,比如:
<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"
android:background="?attr/account_sign_in_bg">
<ImageView
android:id="@+id/iv_logo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="48dp"
app:layout_constraintBottom_toTopOf="@+id/et_username"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:srcCompat="?attr/account_sign_in_logo" />
...
</androidx.constraintlayout.widget.ConstraintLayout>
复制代码
在 App 壳的 Application 主题里配置颜色、图片、大小等主题属性,更改对应样式。
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.ComponentizationSample" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<item name="android:statusBarColor">@color/white</item>
<item name="android:windowLightStatusBar">true</item>
<!-- Customize your theme here. -->
<item name="account_sign_in_bg">@color/sign_in_bg</item>
<item name="account_sign_in_logo">@drawable/ic_sign_in_logo</item>
</style>
</resources>
复制代码
第二个方案是通过变量来控制展示怎么样的 UI。在服务接口提供一些样式的配置,比如主题、排布方式、图标、颜色等。
interface AccountService : IProvider {
// ...
var theme: AccountTheme
}
enum AccountTheme {
BLUE, GREEN, ORANGE
}
复制代码
之后在服务接口的实现类返回默认的样式。
@Route(path = AccountPaths.SERVICE)
class AccountServiceProvider : AccountService {
// ...
var theme = AccountTheme.GREEN
}
复制代码
需要修改组件样式就通过服务接口来配置。
accountService?.theme = AccountTheme.ORANGE
复制代码
第三个方案是多渠道打包,不同的渠道会打包运行出不同的样式。
可能不少人只是做过友盟多渠道,并没在多个模块用过多渠道。而网上有很多讲解多渠道的文章都比较老了,照着敲可能都编译不过。现在做多渠道必须要先声明 flavorDimensions,表示有哪些渠道的维度 ,比如我们在组件的 build.gradle 里声明一个 ui 维度,并在 productFlavors 里定义几个 ui 维度的渠道。
android {
// ...
flavorDimensions "ui"
productFlavors {
blue {
dimension "ui"
}
green {
dimension "ui"
}
orange {
dimension "ui"
}
}
}
复制代码
这里的渠道名是用主题色,还可以用 ui1、ui2 代表第几套 ui,或者直接用 app 名作为渠道名也可以。在该模块的 src 文件夹下创建渠道名的文件夹,添加 drawable、color、layout 等同名资源,打包该渠道时就会用渠道文件夹下的资源去替换的 main 目录的同名资源。
在 App 壳里可能要做友盟多渠道,这些都定义为 appstore 的维度,我们再声明一个 ui 维度的渠道,选择用哪套样式,这样打包出来就是对应的样式了。
android {
// ...
flavorDimensions "appstore", "ui"
productFlavors {
orange {
dimension "ui"
}
xiaomi {
dimension "appstore"
}
huawei {
dimension "appstore"
}
oppo {
dimension "appstore"
}
vivo {
dimension "appstore"
}
}
}
复制代码
以上三种配置方案可以根据需要选择使用,或者也可以混着用。
Android 组件化开发实例技巧由讯客互联技术交流栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“Android 组件化开发实例技巧”