我的Android项目架构进化论(四)

147 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第17天,点击查看活动详情

示例

目录结构

MVVM目录结构.png
一般我们一个功能包下只存在ui、viewmodel、repository,而source、di、entity、db、dao都是独立的包存放。

  • ui:存放activity、fragment、compose等,一个功能界面可能会存在多种形式的view
  • viewmodel:存放viewmodel,一个功能界面可能会存在多个viewmodel,根据view的业务设计,正常情况下都是同一个ViewModel。
  • repository:存放repository,对应viewmodel的业务一对一存在,正常情况下一个viewmodel就只对应一个repository。
  • source:存放source,对应一类业务数据,按照业务需求划分,自己觉得合理既可。
  • di:存放hilt依赖注入相关类,按照类含义进行分类,一般会有source、database、dao、service。
  • entity:存放数据实体,为了方便还是要简单分一下类存放。
  • db:存放数据库,一般情况下不多,都是按照业务需求进行划分。
  • dao:存放数据库接口类,为了方便还是要简单分一下类存放。

View层

这一层只与界面相关,也就是说要么是activity内部自己处理展示,要么是被观察者发生变化而改变视图展示。尽可能的不要让逻辑处理在这一层出现。就像示例中提到的,只提供了展示文本的控件,至于数据的变化是由观察者来驱动的,也就是数据驱动型界面,这么做的好处就是View层只需要处理展示逻辑,数据怎么来的,为何这么展示不关心。

@AndroidEntryPoint
@Route(path = "/activity/library_activity")
class LibraryActivity : AppCompatActivity() {
    private val viewModel by viewModels<LibraryViewModel>()
    private lateinit var bindView: ActivityLibraryBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ARouter.getInstance().inject(this)
       	// xml viewmbind方式
        bindView = ActivityLibraryBinding.inflate(layoutInflater)
        setContentView(bindView.root)
        
        // compose 展示
        bindView.cvInfo.setContent {
            recordInfoView()
        }

        // 获取UI展示信息
        bindView.btnRequest.setOnClickListener {
            viewModel.getUiInfo()
        }

        // 获取UI展示信息
        bindView.btnFlow.setOnClickListener {
            viewModel.getUiInfoByFlow()
        }

        // 数据观察
        viewModel.liveData.observe(this){
            bindView.tvInfo.text = "这里是xml控件的显示:$it"
        }
    }

    @Composable
    fun recordInfoView(){
    	// 如果是compose推荐还是使用MutableState,这里是图一个方便
        Text("这里是compose控件的显示:${viewModel.liveData.observeAsState("").value}")
    }
}

ViewModel层

这一层只与界面逻辑相关,也就是界面需要直接处理的数据全在这里处理,将需要处理的数据设置到被观察者的容器,让View层的观察者观察。一但数据发生变化View层就可以处理对应的展示逻辑。这么做的好处就是ViewModel层只做一种操作,那就是更改将被观察者的数据。就像示例中提到的,只提供界面展示需要的数据,它并不关心数据是如何来的,也不关心界面显示是什么样。

// 采用依赖注入的方式
@HiltViewModel
class LibraryViewModel @Inject constructor(val repository: LibraryRepository) : ViewModel() {

	// 被观察者,双向绑定的纽带
    var liveData = MutableLiveData<String>()

    // 获取仓库层组拼的数据
    // 为view层准备数据
    fun getUiInfo() {
        viewModelScope.launch(Dispatchers.IO) {
            liveData.postValue(repository.getRecordInfo())
        }
    }

    // 获取仓库层组拼的数据
    // 为view层准备数据
    fun getUiInfoByFlow() {
        viewModelScope.launch {
            repository.getRecordInfoByFlow().catch { }.flowOn(Dispatchers.IO)
                .collect {
                    liveData.postValue(it)
                }
        }

    }
}

Repository层

这一层只与界面展示的数据相关,也就是界面上组拼的数据都来源于这里,将组拼的数据通过一定的方式返回给上一层的调用者。就像示例中提到的,只为ViewModel提供可处理的数据,并不关心ViewModel是如何传达指令给View的。当ViewModel所需的数据是一个复杂数据,数据来源有多个的时候,repository层则需要从多个地方获取数据源来进行组拼,目的就是为界面提供可直接处理的数据。

// 采用依赖注入的方式
class LibraryRepository @Inject constructor(
    private val bookSource: BookSource,
    private val orderSource: OrderSource,
    private val consumerSource: ConsumerSource
) {


    // 获取数据源层数据
    // 为ViewModel层准备数据
    suspend fun getRecordInfo(): String {
        val book = bookSource.getBookInfo()
        val order = orderSource.getOrderInfo()
        val consumerLocal = consumerSource.getConsumerInfoByLocal().dec
        val consumerRemote = consumerSource.getConsumerInfoByRemote().dec
        return "$book\n$order\n$consumerLocal\n$consumerRemote"
    }

     // flow 方式
     // 一般在repository层用flow处理数据,viewmodel层去收集
     // source层是否使用flow看自己的想法,从room返回的数据也可以用flow接收,远端返回的数据也可以
     suspend fun getRecordInfoByFlow(): Flow<String> = flow {
         val book = bookSource.getBookInfo()
         val order = orderSource.getOrderInfo()
         val consumerLocal = consumerSource.getConsumerInfoByLocal().dec
         val consumerRemote = consumerSource.getConsumerInfoByRemote().dec
         emit("$book\n$order\n$consumerLocal\n$consumerRemote")
    }

}

Source层

这一层只与一类数据源相关,也就是只提供同一种类型的数据,不会出现多种类型数据。就行示例提到的,同一种类型的数据有的数据来源网络,有的数据来源本地数据库,最终对外提供的都是一类数据。

// 书本数据源
class BookSource {

    // 获取书本相关信息
    suspend fun getBookInfo(): String {
        // 模拟网络请求耗时
        delay(1000)
        return "我是一本书"
    }

}
// 消费者数据源
class ConsumerSource @Inject constructor(val dao: ConsumerDao) {

    suspend fun getConsumerInfoByLocal(): ConsumerEntity {
        // 模拟数据已存入db,也可以在db创建时预置
        dao.insertAll(ConsumerEntity(name = "本地数据", dec = "这是一条本地数据"))

        return dao.getConsumerInRoom()
    }

    suspend fun getConsumerInfoByRemote(): ConsumerEntity {
        // 模拟网络请求耗时
        delay(1000)
        // 假设网络请求获取的数据
        return ConsumerEntity(name = "远端数据", dec = "这是一条网络数据")
    }
}
// 订单数据源
class OrderSource {

    // 获取订单相关信息
    suspend fun getOrderInfo(): String {
        // 模拟网络请求耗时
        delay(1000)
        return "我是一笔订单";
    }
}

总结

以上的示例是我们的一个业务架构分层策略,涉及到每一层的职能、类型,它仅作为一个架构思想的参考意义。但是个人认为业务架构需要秉承一个适合公司、适合团队的目的去设计。对公司的业务是否有益,团队成员是否认同。当然也需要去考虑架构的可实施性、可扩展性,不用一味的追求最新的架构演进,毕竟你永远卷不过哪些想要把你卷死的人。就像我们的项目中依旧是存在MVC类型的,不是不优化,而是有些简单且变动概率极低的业务需求根本不需要考虑复杂的设计,因为MVC就是最适合它的。所以对业务架构而言适合才是最重要的,我们希望的是业务编码清晰明了,而不是复杂难读。