范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文
国学影视

JetpackCompose架构如何选?MVPMVVM还是MVI?

  本次 I/O 大会上曝出了 Compose 1.0 即将发布的消息,虽然 API 层面已趋于稳定,但真正要在项目中落地还少不了一套合理的应用架构。传统 Android 开发中的 MVP、MVVM 等架构在声明式UI这一新事物中是否还依旧可用呢?
  本文以一个简单的业务场景为例,试图找出一种与 Compose 最契合的架构模式  Sample : Wanandroid Search
  App基本功能:用户输入关键字,在 wanandroid 网站中搜索出相关内容并展示
  功能虽然简单,但是集合了数据请求、UI展示等常见业务场景,可用来做UI层与逻辑层的解耦实验。  前期准备:Model层
  其实无论 MVX 中 X 如何变化, Model 都可以用同一套实现。我们先定义一个  DataRepository  ,用于从 wanandroid 获取搜索结果。后文Sample中的 Model 层都基于此 Repo 实现 @ViewModelScoped class DataRepository @Inject constructor(){      private val okhttpClient by lazy {         OkHttpClient.Builder().build()     }      private val apiService by lazy {         Retrofit.Builder()             .baseUrl("https://www.wanandroid.com/")             .client(okhttpClient)             .addConverterFactory(GsonConverterFactory.create())             .build().create(ApiService::class.java)     }       suspend fun getArticlesList(key: String) =         apiService.getArticlesList(key) }Compose为什么需要架构?
  首先,先看看不借助任何架构的 Compose 代码是怎样的?
  不使用架构的情况下,逻辑代码将与UI代码耦合在一起,在Compose中这种弊端显得尤为明显。常规 Android 开发默认引入了 MVC 思想,XML的布局方式使得UI层与逻辑层有了初步的解耦。但是 Compose 中,布局和逻辑同样都使用Kotlin实现,当布局中夹了杂逻辑,界限变得更加模糊。
  此外,Compose UI中混入逻辑代码会带来更多的潜在隐患。由于 Composable 会频繁重组,逻辑代码中如果涉及I/O 就必须当做  SideEffect{}  处理、一些不能随重组频繁创建的对象也必须使用 remember{}  保存,当这些逻辑散落在UI中时,无形中增加了开发者的心智负担,很容易发生遗漏。
  Sample 的业务场景特别简单,UI中出现少许  remember{}  、LaunchedEffect{}  似乎也没什么不妥,对于一些相对简单的业务场景出现下面这样的代码没有问题: @Composable fun NoArchitectureResultScreen(     answer: String ) {      val isLoading = remember { mutableStateOf(false) }      val dataRepository = remember { DataRepository() }      var result: List by remember { mutableStateOf(emptyList()) }          LaunchedEffect(Unit) {         isLoading.value = true         result = withContext(Dispatchers.IO) { dataRepository.getArticlesList(answer).data.datas }         isLoading.value = false     }      SearchResultScreen(result, isLoading.value , answer)  }
  但是,当业务足够复杂时,你会发现这样的代码是难以忍受的。这正如在 React 前端开发中,虽然 Hooks 提供了处理逻辑的能力,但却依然无法取代 Redux。  Android中的常见架构模式
  MVP 、 MVVM 、 MVI  是 Android中的而一些常见架构模式,它们的目的都是服务于UI层与逻辑层的解耦,只是在解耦方式上有所不同,如何选择取决于使用者的喜好以及项目的特点
  "没有最好的架构,只有最合适的架构。"
  那么在 Compose 项目中何种架构最合适呢?  MVP
  MVP 主要特点是  Presenter  与 View  之间通过接口通信, Presenter 通过调用 View 的方法实现UI的更新。
  这要求 Presenter 需要持有一个 View 层对象的引用,但是 Compose 显然无法获得这种引用,因为用来创建 UI 的 Composable 必须要求返回 Unit,如下:  @Composable fun HomeScreen() {     Column {         Text("Hello World!")     } }
  官方文档中对无返回值的要求也进行了明确约束:
  The function doesn’t return anything. Compose functions that emit UI do not need to return anything, because they describe the desired screen state instead of constructing UI widgets. https://developer.android.com/jetpack/compose/mental-model
  Compose UI 既然存在于 Android 体系中,必定需要有一个与 Android 世界连接的起点,起点处可能是一个  Activity  或者 Fragment ,用他们做UI层的引用句柄不可以吗?
  理论上可以,但是当 Activity 接收 Presenter 通知后,仍然无法在内部获取局部引用,只能设法触发整体Recomposition,这完全丧失了 MVP 的优势,即通过获取局部引用进行精准刷新。
  通过分析可以得到结论:"MVP 这种依赖接口通信的解耦方式无法在 Compose 项目中使用"MVVM(Without Jetpack)
  相对于 MVP 的接口通信 ,MVVM 基于观察者模式进行通信,当 UI 观察到来自 ViewModle 的数据变化时自我更新。UI层是否能返回引用句柄已不再重要,这与 Compose 的工作方式非常契合。
  自从 Android 用 ViewModel 命名了某 Jetpack 组件后,在很多人心里,Jetpack 似乎就与 MVVM 画上了等号。这确实客观推动了 MVVM 的普及,但是 Jetpack 的 ViewModel 并非只能用在 MVVM 中(比如如后文介绍的 MVI 也可以使用 );反之,没有 Jetpack ,照样可以实现 MVVM。
  先来看看不借助 Jetpack 的情况下,MVVM 如何实现?  Activity 中创建 ViewModel
  首先 View 层创建 ViewModel 用于订阅  class MvvmActivity : AppCompatActivity() {      private val mvvmViewModel = MvvmViewModel(DataRepository())      override fun onCreate(savedInstanceState: Bundle?) {         super.onCreate(savedInstanceState)         setContent {             ComposePlaygroundTheme {                 MvvmApp(mvvmViewModel) //将vm传给Composable             }         }     } }
  Compose 项目一般使用单 Activity 结构, Activity 作为全局入口非常适合创建全局 ViewModel。子 Compoable 之间需要基于 ViewModel 通信,所以构建 Composable 时将 ViewModel 作为参数传入。
  Sample 中我们在 Activity 中创建的 ViewModel 仅仅是为了传递给  MvvmApp  使用,这种情况下也可以通过传递 Lazy ,将创建延迟到真正需要使用的时候以提高性能。 定义 NavGraph
  当涉及到 Compose 页面切换时, navigation-compose  是一个不错选择,Sample中也特意设计了SearchBarScreen  和 SearchResultScreen  的切换场景 // build.gradle implementation "androidx.navigation:navigation-compose:$latest_version" @Composable fun MvvmApp(     mvvmViewModel: MvvmViewModel ) {     val navController = rememberNavController()      LaunchedEffect(Unit) {         mvvmViewModel.navigateToResults             .collect {                  navController.navigate("result") //订阅VM路由事件通知,处理路由跳转             }      }      NavHost(navController, startDestination = "searchBar") {         composable("searchBar") {             MvvmSearchBarScreen(                 mvvmViewModel,             )         }         composable("result") {             MvvmSearchResultScreen(                 mvvmViewModel,             )         }     } }在 root-level 的 MvvmApp 中定义  NavGraph , composable("$dest_id"){}  中构造路由节点的各个子 Screen,构造时传入 ViewModel 用于 Screen 之间的通信 每个 Composable 都有一个  CoroutineScope  与其 Lifecycle 绑定,LaunchedEffect{}  可以在这个 Scope 中启动协程处理副作用。代码中使用了一个只执行一次的 Effect 订阅 ViewModel 的路由事件通知 当然我们可以将 navConroller 也传给  MvvmSearchBarScreen  ,在其内部直接发起路由跳转。但在较复杂的项目中,跳转逻辑与页面定义应该尽量保持解耦,这更利于页面的复用和测试。 我们也可以在 Composeable 中直接  mutableStateOf()  创建 state 来处理路由跳转,但是既然选择使用 ViewModel 了,那就应该尽可能将所有 state 集中到 ViewModle 管理。
  注意: 上面例子中的处理路由跳转的 navigateToResults 是一个"事件"而非"状态",关于这部分区别,在后文在详细阐述定义子 Screen
  接下来看一下两个 Screen 的具体实现  @Composable fun MvvmSearchBarScreen(     mvvmViewModel: MvvmViewModel, ) {      SearchBarScreen {          mvvmViewModel.searchKeyword(it)     }  }  @Composable fun MvvmSearchResultScreen(     mvvmViewModel: MvvmViewModel ) {      val result by mvvmViewModel.result.collectAsState()     val isLoading by mvvmViewModel.isLoading.collectAsState()      SearchResultScreen(result, isLoading, mvvmViewModel.key.value)  }
  大量逻辑都抽象到 ViewModel 中,所以 Screen 非常简洁  SearchBarScreen  接受用户输入,将搜索关键词发送给 ViewModel MvvmSearchResultScreen  作为结果页显示 ViewModel 发送的数据,包括 Loading 状态和搜索结果等。 collectAsState  用来将 Flow 转化为 Compose 的 state,每当 Flow 接收到新数据时会触发 Composable 重组。Compose 同时支持 LiveData、RxJava 等其他响应式库的collectAsState
  UI层的更多内容可以查阅  SearchBarScreen  和 SearchResultScreen  的源码。经过逻辑抽离后,这两个 Composable 只剩余布局相关的代码,可以在任何一种 MVX 中实现复用。 ViewModel 实现
  最后看一下 ViewModel 的实现  class MvvmViewModel(     private val searchService: DataRepository, ) {      private val coroutineScope = MainScope()     private val _isLoading: MutableStateFlow = MutableStateFlow(false)     val isLoading = _isLoading.asStateFlow()     private val _result: MutableStateFlow = MutableStateFlow(emptyList())     val result = _result.asStateFlow()     private val _key = MutableStateFlow("")     val key = _key.asStateFlow()          //使用Channel定义事件     private val _navigateToResults = Channel(Channel.BUFFERED)     val navigateToResults = _navigateToResults.receiveAsFlow()      fun searchKeyword(input: String) {         coroutineScope.launch {             _isLoading.value = true             _navigateToResults.send(true)             _key.value = input             val result = withContext(Dispatchers.IO) { searchService.getArticlesList(input) }             _result.emit(result.data.datas)             _isLoading.value = false         }     } }接收到用户输入后,通过  DataRepository  发起搜索请求 搜索过程中依次更新  loading (loading显示状态)、navigateToResult (页面跳转事件)、 key (搜索关键词)、result (搜索结果)等内容,不断驱动UI刷新
  所有状态集中在 ViewModel 管理,甚至页面跳转、Toast弹出等事件也由 ViewModel 负责通知,这对单元测试非常友好,在单测中无需再 mock 各种UI相关的上下文。  Jetpack MVVM
  Jeptack 的意义在于降低 MVVM 在 Android平台的落地成本。
  引入 Jetpack 后的代码变化不大,主要变动在于 ViewModel 的创建。
  Jetpack 提供了多个组件,降低了 ViewModel 的使用成本:  通过 hilt 的 DI 降低 ViewModel 构造成本,无需手动传入 DataRepository 等依赖  任意 Composable 都可以从最近的 Scope 中获取 ViewModel,无需层层传参。  @HiltViewModel class JetpackMvvmViewModel @Inject constructor(     private val searchService: DataRepository // DataRepository 依靠DI注入 ) : ViewModel() {     ... }@Composable fun JetpackMvvmApp() {     val navController = rememberNavController()      NavHost(navController, startDestination = "searchBar", route = "root") {         composable("searchBar") {             JetpackMvvmSearchBarScreen(                 viewModel(navController, "root") //viewModel 可以在需要时再获取, 无需实现创建好并通过参数传进来             )         }         composable("result") {              JetpackMvvmSearchResultScreen(                 viewModel(navController, "root") //可以获取跟同一个ViewModel实例             )         }     }  }@Composable inline fun  viewModel(     navController: NavController,     graphId: String = "" ): VM =     //在 NavGraph 全局范围使用 Hilt 创建 ViewModel     hiltNavGraphViewModel(          backStackEntry = navController.getBackStackEntry(graphId)     )
  Jetpack 甚至提供了  hilt-navigation-compose  库,可以在 Composable 中获取 NavGraph Scope 或 Destination Scope 的 ViewModel,并自动依赖 Hilt 构建。Destination Scope 的 ViewModel 会跟随 BackStack 的弹出自动 Clear ,避免泄露。 // build.gradle implementation androidx.hilt:hilt-navigation-compose:$latest_versioin
  "未来 Jetpack 各组件之间协同效应会变得越来越强。" 参考 https://developer.android.com/jetpack/compose/libraries#hiltMVI
  MVI 与 MVVM 很相似,其借鉴了前端框架的思想,更加强调 数据的单向流动 和 唯一数据源 ,可以看做是  MVVM + Redux  的结合。
  MVI 的 I 指 Intent,这里不是启动 Activity 那个 Intent,而是一种对用户操作的封装形式,为避免混淆,也可唤做 Action 等其他称呼。用户操作以 Action 的形式送给 Model层 进行处理。代码中,我们可以用 Jetpack 的 ViewModel 负责 Intent 的接受和处理,因为 ViewModel 可以在 Composable 中方便获取。
  在  SearchBarScreen  用户输入关键词后通过 Action  通知 ViewModel 进行搜索 @Composable fun MviSearchBarScreen(     mviViewModel: MviViewModel,     onConfirm: () -> Unit ) {     SearchBarScreen {         mviViewModel.onAction(MviViewModel.UiAction.SearchInput(it))     } }
  通过  Action  通信,有利于 View 与 ViewModel 之间的进一步解耦,同时所有调用以 Action 的形式汇总到一处,也有利于对行为的集中分析和监控 @Composable fun MviSearchResultScreen(     mviViewModel: MviViewModel ) {     val viewState by mviViewModel.viewState.collectAsState()      SearchResultScreen(         viewState.result, viewState.isLoading, viewState.key     )  }
  MVVM 的 ViewModle 中分散定义了多个 State ,MVI 使用  ViewState  对 State 集中管理,只需要订阅一个 ViewState 便可获取页面的所有状态,相对 MVVM 减少了不少模板代码。
  相对于 MVVM,ViewModel 也有一些变化  class MviViewModel(     private val searchService: DataRepository, ) {      private val coroutineScope = MainScope()      private val _viewState: MutableStateFlow = MutableStateFlow(ViewState())     val viewState = _viewState.asStateFlow()      private val _navigateToResults = Channel(Channel.BUFFERED)     val navigateToResults = _navigateToResults.receiveAsFlow()      fun onAction(uiAction: UiAction) {         when (uiAction) {             is UiAction.SearchInput -> {                 coroutineScope.launch {                     _viewState.value = _viewState.value.copy(isLoading = true)                     val result =                         withContext(Dispatchers.IO) { searchService.getArticlesList(uiAction.input) }                     _viewState.value =                         _viewState.value.copy(result = result.data.datas, key = uiAction.input)                     _navigateToResults.send(OneShotEvent.NavigateToResults)                     _viewState.value = _viewState.value.copy(isLoading = false)                 }             }         }     }      data class ViewState(         val isLoading: Boolean = false,         val result: List = emptyList(),         val key: String = ""     )      sealed class OneShotEvent {         object NavigateToResults : OneShotEvent()     }      sealed class UiAction {         class SearchInput(val input: String) : UiAction()     } }页面所有的状态都定义在  ViewState  这个 data class 中,状态的修改只能在 onAction  中进行, 其余场所都是 immutable 的, 保证了数据流只能单向修改。反观 MVVM ,MutableStateFlow  对外暴露时转成 immutable 才能保证这种安全性,需要增加不少模板代码且仍然容易遗漏。 事件则统一定义在  OneShotEvent 中。Event 不同于 State,同一类型的事件允许响应多次,因此定义事件使用 Channel  而不是 StateFlow 。
  Compose 鼓励多使用 State 少使用 Event, Event 只适合用在弹 Toast 等少数场景中
  通过浏览 ViewModel 的 ViewState 和 Aciton 定义就可以理清 ViewModel 的职责,可以直接拿来作为接口文档使用。  页面路由
  Sample 中之所以使用事件而非状态来处理路由跳转,一个主要原因是由于使用了  Navigation 。Navigation 有自己的 backstack  管理,当点击 back 键时会自动帮助我们返回前一页面。倘若我们使用状态来描述当前页面,当点击 back时,没有机会更新状态,这将造成 ViewState 与 UI 的不一致。
  关于路由方案的建议:简单项目使用事件控制页面跳转没有问题,但是对于复杂项目,推荐使用状态进行页面管理,有利于逻辑层时刻感知到当前的UI状态。
  我们可以将 NavController 的 backstack 状态 与 ViewModel 的状态建立同步:   class MvvmViewModel(     private val searchService: DataRepository, ) {      ...     //使用 StateFlow 描述页面     private val _destination = MutableStateFlow(DestSearchBar)     val destination = _destination.asStateFlow()      fun searchKeyword(input: String) {         coroutineScope.launch {             ...             _destination.value = DestSearchResult             ...         }     }      fun bindNavStack(navController: NavController) {         //navigation 的状态时刻同步到 viewModel         navController.addOnDestinationChangedListener { _, _, arguments ->             run {                 _destination.value = requireNotNull(arguments?.getString(KEY_ROUTE))             }         }     } }
  如上,当 navigation 状态变化时,会及时同步到 ViewModel ,这样就可以使用 StateFlow 而非 Channel 来描述页面状态了。  @Composable fun MvvmApp(     mvvmViewModel: MvvmViewModel ) {     val navController = rememberNavController()      LaunchedEffect(Unit) {         with(mvvmViewModel) {             bindNavStack(navController) //建立同步             destination                 .collect {                     navController.navigate(it)                 }         }     } }
  在入口处,为 NavController 和 ViewModel 建立同步绑定即可。  Clean Architecture
  更大型的项目中,会引入  Clean Architecture  ,通过 Use Case 将 ViewModel 内的逻辑进一步分解。Compose 只是个 UI 框架,对于 ViewModle 以下的逻辑层的治理方式与传统的 Andorid 开发没有区别。所以 Clean Architecture 这样的复杂架构仍然可以在 Compose 项目中使用 总结
  比较了这么多种架构,那种与 Compose 最契合呢?
  Compose 的声明式UI思想来自 React,所以同样来自 Redux 思想的 MVI 应该是 Compose 的最佳伴侣。当然 MVI 只是在 MVVM 的基础上做了一些改良,如果你已经有了一个 MVVM 的项目,只是想将 UI 部分改造成 Compose ,那么没必要为了改造成 MVI 而进行重构,MVVM 也可以很好地配合 Compose 使用的。但是如果你想将一个 MVP 项目改造成 Compose 可能成本就有点大了。
  关于 Jetpack,如果你的项目只用于 Android,那么 Jetpack 无疑是一个好工具。但是 Compose 未来的应用场景将会很广泛,如果你有预期未来会配合 KMP 开发跨平台应用,那么就需要学会不依赖 Jetpack 的开发方式,这也是本文为什么要介绍非 Jetpack 下的 MVVM 的一个初衷。  最后
  在这里我分享一份由多位大佬亲自收录整理的Android学习PDF+架构视频+面试文档+源码笔记,高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料
  这些都是我现在闲暇时还会反复翻阅的精品资料。里面对近几年的大厂面试高频知识点都有详细的讲解。相信可以有效地帮助大家掌握知识、理解原理,帮助大家在未来取得一份不错的答卷。
  当然,你也可以拿去查漏补缺,提升自身的竞争力。
  真心希望可以帮助到大家,Android路漫漫,共勉!
  如果你有需要的话,只需私信我【进阶】即可获取

B2B2C分销商城系统,带进货模块,二级分销商城Java系统源码B2B2C分销商城系统,带进货模块,二级分销商城Java系统源码功能简介一商家入驻体系B2B2C商城系统可以招商入驻,用户在商城中注册后可以根据要求向平台运营商提供相应的申请资料,国产操作系统双子星近年来,我国基于对于信息安全的考虑,同时推动了CPU和操作系统的国产化。国产CPU我们已经聊过,而国产操作系统国内主要是麒麟和UOS,都是基于开源的Linux平台。银河麒麟(Kyl如何提高测试用例的覆盖率问题?这是个面试经常会被问到的问题,下面说出个人编写测试用例的思路和顺序。1功能的连通性,即冒烟测试,正常的流程是否走的通。2页面元素的检验,即检查页面字段内容格式边界值数据类型特殊字符Lambda表达式Lambda表达式是JDK8的一个新特性,可以取代大部分的匿名内部类,写出更优雅的Java代码,尤其在集合的遍历和其他集合操作中,可以极大地优化代码结构。JDK也提供了大量的内置函如何实现页面广告随时上下线过期自动下线及到时自动上线?如何实现这个问题,我认为可以从三个方面回答直接用程序来实现。每访问一次就用程序按条件更新一次数据,最好写到基类里面,方便程序在使用时,每时每刻都在调用,这样就可以达到看起来是的效果为什么头条上不讲广义相对论,只讲狭义相对论?很简单,实力不允许。我自己也有时会回答狭义相对论的问答,但从来不敢碰广义相对论。广义相对论的几何拓扑模型不算太难理解,涉及到的高维微分流形概念上也还凑活,所以要是打几个比喻来解释广真正的创业者和伪创业者的区别在哪里?超现实问题所有创业者应该反思一下是真创业还是伪创业,80都是伪创业而已。马上静下心来,参照反思一下自己是不是在伪创业。大众创业,万众创业,点燃了很多人内心的梦想,更多的是欲望。探讨基于django开发的纯个人博客www。joephy。com这是我的个人博客,基于django,在前人的基础上二次开发,使用djangoadmin后台发布文章,用ckeditor实现图片上传,能分页显示文章,最新goLang执行Linux命令作为PHP转GO的新手司机虽然不用在语法上徘徊,但毕竟是不一样的语言有很多东西都是需要从头开始学习的,已经离开而立有一段路程的我走在这新的道路上已经稍显吃力。人,亦是如此,越是困难买了特斯拉的人就不爱国了吗?最近经常在头条上面看到大家讨论关于买了特斯拉的人就不爱国了,恰巧我也在最近提了一辆车,我来说说我的看法。先说下我的情况吧,今年36岁,和父母一起在不知道几线的小县城的农村做小本生意全球首个激光雷达量产车小鹏P5开启智能家轿新体验2021年9月,小鹏P5正式上市并开启智能家轿的新细分市场。在电动化和智能化变革的时代,P5凭借全方面实力成为智能家轿不二之选。近日,小鹏P5开启上市后首次大版本OTA,迎来行业首
iPhone12充电20分钟48VOKAMO20W快充我想有关iPhone12系列为环保作出的努力,大家都是看在眼里,爱在心里,终于有让消费者可以自主多花点钱换更好的充电器的机会啦,真的好开心。这次iPhone12终于突破性的将快充速春天到了,跑步减肥的季节到了咕咚5k走跑鞋2。0经过一个冬天的猫冬,春节又没少吃,总想着跑步减减肥,也下不了决心,有人说决心是不停靠更新装备提升的,我想这说的有道道理,因为我已经准备好了新跑鞋,咕咚5K跑鞋2。0咕咚应该是非常老Redmi在TWS耳机领域也想做焊门员redmiairdots3真无线蓝牙耳机Tws耳机目前已经进入杀疯了的阶段,又要动铁双单元,又要长续航,还要轻巧,重要的不能太贵看看redmi发展到第三代的redmiairdots3能不能做到。目录一,开箱二,老说动圈,专为通话设计的TWS真无线耳机酷狗M52自从主流旗舰手机相继取消耳机插孔,TWS耳机成了消费者青睐的耳机品类,TWS的全称是TrueWirelessStereo,意思是真正无线立体声,拥有真无线双耳立体声多重感知体积小巧愿你摔机半生,归来仍是决色Defense决色防摔小彩壳现在手机再也不是诺基亚那个时代了,越来越窄的金属中框,越来越曲线的玻璃屏幕与后盖,越来越贵的维修价格与碎屏险,让手机的保护必须被重视起来。这是本月我第二次测试高端手机壳了,上次是i随时喝热水的完美解决方案美的华凌即热饮水机WYR108首先讲下买这个东西的原因,家里有个生娃的亲戚问我,想买即热饮水机,买啥好?我就问她便宜的电热水壶不行吗?宝妈回答我,她家宝贝经常需要喝热水。这时候你知道,作为一名值得买用户,我是非可能是目前车载无线充最好的选择摩米士车载无线充15W自从我换了可以无线充电的小米10手机,我算是中毒了,先后购置了桌面无线充,与,无线充电宝,这两个产品也都写了开箱我终于买到了小米30W无线充电器小米无线充电宝30W开箱还剩下车载无人间纵有百媚千红,红不过大连美早樱桃人间纵有百媚千红,红不过大连美早樱桃我大学毕业已经接近十年了那些年我们逃过课,我们追女孩,我们考试前夜做小抄,我们熄灯之后讲鬼故事吃辣条,这一切充实着我们那些年的大学时光十年之间,PVE虚拟机下OPENWRT如何安装USB打印机P1106家里有个老式的打印机是惠普的P1106,只有usb接口,功能特少,但是特稳定,打印从来没问题以前就放在我电脑旁边,但是这边东西越来越多越来越挤,就像把打印机这种使用次数比较少的设备2023款宝马M2雷霆版谍照曝光,搭3。0TV6引擎,有望超车CLAAMG?在BBA中,宝马无疑代表着运动。可能对每个男人来说,每一辆宝马M车型,可能都是一种信仰,代表着最顶级的驾驶乐趣。就算只是一辆入门级的宝马M2,它仍旧有许多拥护。近日,Report汽三十万级7座SUV怎么选?谁才是这个级别的标杆?随着国内三孩政策的相继落实,7座SUV就成了一家人出游的优质首选。作为日系品牌中最受欢迎的品牌之一,丰田旗下的汽车产品一向以实用耐用的高性价比活跃在中国市场,其中广汽丰田汉兰达和一