在功能模块中使用导航 | MAD Skills

在功能模块中使用导航 | MAD Skills
www.zeeklog.com  - 在功能模块中使用导航 | MAD Skills

这是关于导航 (Navigation) 的第二个 MAD Skills 系列,本文是导航组件系列的第四篇文章,如果您想回顾过去发布的内容,请通过下面链接查看:

△ 功能模块的导航视频

概述

在  中,您已经学会了如何在多模块工程中使用导航 (Navigation)。在本文中,我们将更进一步,将咖啡模块转换成功能模块 (Feature Module)。如果对功能模块不太熟悉,您可以先查看以下视频内容:

△ App Bundles

功能模块在安装时并未下载到本地,而是当应用使用到某个功能时才会下载相应的功能模块。这不仅节省了应用下载和安装时的时间和带宽,也节省了设备存储空间。

那么让我们为用户节省一些空间!现在直接开始编程吧!

功能模块

由于我在  中已经将 DonutTracker 应用进行了模块化,我会从将现有的咖啡模块转换成功能模块开始。

首先,我在咖啡模块的 build.gradle 中将库插件 (library plugin) 替换为动态功能插件 (dynamic-feature plugin):

id 'com.android.dynamic-feature'

接着,我在 AndroidManifest.xml 中将咖啡模块声明为按需 (on-demand) 模块:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:dist="http://schemas.android.com/apk/distribution"
   package="com.android.samples.donuttracker.coffee">
   <dist:module
       dist:instant="false"
       dist:title="@string/title_coffee">
       <dist:delivery>
           <dist:on-demand />
       </dist:delivery>
       <dist:fusing dist:include="true" />
   </dist:module>
</manifest>

现在咖啡模块已经转换完成,我将该模块添加为动态功能 (dynamicFeature):

android {
   //...

   packagingOptions {
       exclude 'META-INF/atomicfu.kotlin_module'
   }

   dynamicFeatures = [':coffee']

}

同时在 app 模块的 build.gradle 中,我从依赖列表中移除了咖啡模块并添加了 navigation-dynamic-features 依赖:

implementation "androidx.navigation:navigation-dynamic-features-fragment:$navigationVersion"

当 Gradle 同步完成时,即可更新导航图了。我将 include 标签改为 include-dynamic,并添加 idgraphResName 以及指向功能模块的 moduleName:

<include-dynamic
   android:id="@+id/coffeeGraph"
   app:moduleName="coffee"
   app:graphResName="coffee_graph"/>

此时,我可以安全地移除 coffee_graph.xmlnavigation 标签的 id 属性,原因在于,如果导航图是使用 include 标签引入的,那么 Dynamic Navigator 库会忽略根元素的 id 属性。

<navigation 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"
   app:startDestination="@id/coffeeList">
   <fragment
       android:id="@+id/coffeeList"
       android:name="com.android.samples.donuttracker.coffee.CoffeeList"
       android:label="@string/coffee_list">
       <action
           android:id="@+id/action_coffeeList_to_coffeeEntryDialogFragment"
           app:destination="@id/coffeeEntryDialogFragment" />
   </fragment>
   <dialog
       android:id="@+id/coffeeEntryDialogFragment"
       android:name="com.android.samples.donuttracker.coffee.CoffeeEntryDialogFragment"
       android:label="CoffeeEntryDialogFragment">
       <argument
           android:name="itemId"
           android:defaultValue="-1L"
           app:argType="long" />
   </dialog>
</navigation>

activity_main 布局中,我将 FragmentContainerViewname 属性值由 NavHostFragment 改为 DynamicNavHostFragment:

<androidx.fragment.app.FragmentContainerView
       android:id="@+id/nav_host_fragment"
       android:name="androidx.navigation.dynamicfeatures.fragment.DynamicNavHostFragment"
       android:layout_width="match_parent"
       android:layout_height="0dp"
       android:layout_weight="1"
       app:defaultNavHost="true"
       app:navGraph="@navigation/nav_graph" />

与通过 include 引入导航图类似,要使动态引入 (include-dynamic) 生效,咖啡菜单项的 id 值需要与导航图名称相匹配,而不是目的地页面 id:

<menu xmlns:android="http://schemas.android.com/apk/res/android">
   <item
       android:id="@id/donutList"
       android:icon="@drawable/donut_with_sprinkles"
       android:title="@string/donut_name" />
   <item
       android:id="@id/coffeeGraph"
       android:icon="@drawable/coffee_cup"
       android:title="@string/coffee_name" />
</menu>

这就是添加动态导航所需的全部工作。现在我将使用 bundletool 来测试功能模块,您也可以使用 Play 控制台来测试功能模块。如果您想了解更多关于如何使用 bundletool 和 Play 控制台来测试功能模块安装的内容,请查看以下视频:

△ bundletool 视频

我也想测试当模块无法安装时会发生什么。为此,在 Run/Debug Configurations 弹窗中,我从待部署列表中取消勾选了 donuttracker.coffee。这时当我再次运行应用并导航到 coffeeList 页面时,将会显示一条通用错误信息。

www.zeeklog.com  - 在功能模块中使用导航 | MAD Skills

△ 通用错误信息

至此,功能模块的设置已经完成,是时候打磨用户体验了。当功能模块处于下载过程时,向用户显示自定义反馈信息或者显示一条更有意义的报错信息而不是通用的信息会不会更好?

为此,我可以添加一个监听器,当用户停留在同一个页面时,它可以处理安装状态、进度变化或错误信息。或者,当功能模块正在下载时,我可以添加一个自定义进度 Fragment 来展示进度。

导航库已经内置了对  的支持。我所需要做的就是创建一个继承了 AbstractProgressFragment 的 Fragment。

class ProgressFragment : AbstractProgressFragment(R.layout.fragment_progress) {
}

我添加了一个 ImageView、一个 TextView 和一个 ProgressBar 来展示下载状态。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:orientation="vertical"
   android:paddingLeft="@dimen/default_margin"
   android:paddingTop="@dimen/default_margin"
   android:paddingRight="@dimen/default_margin"
   android:paddingBottom="@dimen/default_margin">
   <ImageView
       android:id="@+id/progressImage"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:src="@drawable/coffee_cup"
       android:layout_marginBottom="@dimen/default_margin"
       android:layout_gravity="center"/>
   <TextView
       android:id="@+id/message"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       tools:text="@string/installing_coffee_module"/>
   <ProgressBar
       android:id="@+id/progressBar"
       style="@style/Widget.AppCompat.ProgressBar.Horizontal"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       tools:progress="10" />
</LinearLayout>


接着,我覆写了 onProgress() 函数来更新 progressBar,我还覆写了 onFailed()onCanceled() 函数来更新 TextView 以向用户展示相关反馈。

override fun onProgress(status: Int, bytesDownloaded: Long, bytesTotal: Long) {
   progressBar?.progress = (bytesDownloaded.toDouble() * 100 / bytesTotal).toInt()
}
 
override fun onFailed(errorCode: Int) {
   message?.text = getString(R.string.install_failed)
}
 
override fun onCancelled() {
   message?.text = getString(R.string.install_cancelled)
}

我需要将 progressFragment 目的地添加到导航图中。最后,将 progressFragment 声明为导航图的 progressDestination

<navigation 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"
   app:startDestination="@id/donutList"
   app:progressDestination="@+id/progressFragment">
<fragment
       android:id="@+id/donutList"
       android:name="com.android.samples.donuttracker.donut.DonutList"
       android:label="@string/donut_list" >
       <action
           android:id="@+id/action_donutList_to_donutEntryDialogFragment"
           app:destination="@id/donutEntryDialogFragment" />
       <action
           android:id="@+id/action_donutList_to_selectionFragment"
           app:destination="@id/selectionFragment" />
   </fragment>
   <dialog
       android:id="@+id/donutEntryDialogFragment"
       android:name="com.android.samples.donuttracker.donut.DonutEntryDialogFragment"
       android:label="DonutEntryDialogFragment">
       <deepLink app:uri="myapp://navdonutcreator.com/donutcreator" />
       <argument
           android:name="itemId"
           app:argType="long"
           android:defaultValue="-1L" />
   </dialog>
   <fragment
       android:id="@+id/selectionFragment"
       android:name="com.android.samples.donuttracker.setup.SelectionFragment"
       android:label="@string/settings"
       tools:layout="@layout/fragment_selection" >
       <action
           android:id="@+id/action_selectionFragment_to_donutList"
           app:destination="@id/donutList" />
   </fragment>
   <fragment
       android:id="@+id/progressFragment"
       android:name="com.android.samples.donuttracker.ProgressFragment"
       android:label="ProgressFragment" />
   <include-dynamic
       android:id="@+id/coffeeGraph"
       app:moduleName="coffee"
       app:graphResName="coffee_graph"/>
</navigation>

此时,我再次取消勾选咖啡模块,运行应用并导航至 coffeeList 页面时,应用展示了自定义进度页面 progressFragment

www.zeeklog.com  - 在功能模块中使用导航 | MAD Skills

△ 自定义 progressFragment

类似地,我可以使用 bundletool 测试应用以查看当咖啡模块正在下载时,进度条会如何工作。

小结

感谢大家!在本系列中,我们再次使用了  并添加了咖啡记录功能。因为…我喜欢咖啡。

新功能带来了新责任。为了提供更好的用户体验,首先我使用导航添加了 NavigationUI 以集成 UI 组件。然后,我实现了一次性流程和条件导航。之后,我使用了嵌套图和 include 标签来组织导航图并将应用模块化以节省用户的网络和存储空间。至此,我们已经完成了该应用,是时候去享用一杯美味的咖啡和甜甜圈了!

欢迎您  向我们提交反馈,或分享您喜欢的内容、发现的问题。您的反馈对我们非常重要,感谢您的支持!

Read more

系统架构师成长之路(一)

系统架构师成长之路(一)

背景:系统师是近几年来在国内外迅速成长并发展良好的一个职业,它对系统开发和信息化建设的重要性及给IT业所带来的影响是不言而喻的。在我国,虽然系统架构师的职业在工作内容、工作职责以及工作边界等方面还存在一定的模糊性和不确定性,但它确实是时代发展的需要,并正在实践中不断完善和成熟。 通常从组织上划分,架构师分为以下几大类:业务架构师(Business Architect)、主题领域架构师(Domain Architect)、技术架构师(Technology Architect)、项目架构师(J2EE架构师、.NET架构师等)以及系统架构师(System Architecture)。 系统架构师是系统或产品线的设计责任人,是一个负责理解和管理并最终确认和评估非功能性系统需求(如软件的可维护性、性能、复用性、可靠性、有效性和可测试性等),给出开发规范,搭建系统实现的核心架构,对整个软件架构、关键架构、接口进行总体设计并澄清关键技术细节的高级技术人员。 系统架构师主要着眼于系统的“技术实现”,同时还要考虑系统的“组织协调”。因此,系统架构师是特定的开发平台、语言、工具的大师,对常见应用场景

By Ne0inhk
系统架构师成长之路(二)

系统架构师成长之路(二)

本篇主要介绍操作系统基础知识。 操作系统(Operating System,OS)是计算机系统的核心系统软件,其他软件是建立在操作系统基础上,并在操作系统的统一管理和支持下运行。 计算机系统的硬件资源包括中央处理机(CPU)、存储器(主存与外存)和输入/输出设备等物理设备。 操作系统按功能不同可分为:单用户操作系统和批处理操作系统、分时操作系统和实时操作系统、网络操作系统和分布式操作系统以及嵌入式操作系统。 操作系统具有并发性(Concurrency)、共享性(Sharing)、虚拟性(Virtual)和不确定性(non-determinacy)。 操作系统的功能:进程管理、文件管理、存储管理、设备管理和作业管理。 进程调度与死锁 进程调度:进程调度即处理器调度(上线文转换),它的主要功能是确定把处理器在什么时候分配给哪一个进程。在有些操作系统中,一个作业从提交到完成需要经历高、中、低三级调度。 调度方式:调度方式是指当有更高优先级的进程到来时如何分配CPU。调度方式分为可剥夺和不可剥夺两种。 调度:常用的有先来先服务、时间片轮转(roundrobin)、优先级调度和

By Ne0inhk
系统架构师成长之路(三)

系统架构师成长之路(三)

在当前的互联网领域,的应用已经十分广泛,尤其以企业为主,企业成为大数据应用的主体。大数据真能改变企业的运作方式吗?答案毋庸置疑是肯定的。随着企业开始利用大数据,我们每天都会看到大数据新的奇妙的应用,帮助人们真正从中获益。大数据的应用已广泛深入我们生活的方方面面,涵盖医疗、交通、金融、教育、体育、零售等各行各业。   古代,人们用牛来拉重物。当一头牛拉不动一根圆木时,他们不曾想过培育更大更壮的牛。同样,我们也不需要尝试开发超级计算机,而应试着结合使用更多计算机系统。                                                                                                                          --格蕾斯·霍珀 大数据概念   何为大数据?大数据是指无法再一定时间内用常规软件工具对其内容进行抓取、管理和处理的数据集合。大数据技术,是指从各种各样类型中的数据中,快速获得有价值信息的能力。适用于大数据的技术,包括大规模并行处理(MPP),数据挖掘电网、分布式文件系统、分布式

By Ne0inhk
动态规划-背包问题(状态转换)

动态规划-背包问题(状态转换)

动态规划最经典的例子,背包问题浅述: 背包问题状态转换方程 f[i,j] = Max{ f[i-1,j-Wi]+Pi( j >= Wi ),  f[i-1,j] }    f[i,j]表示在前i件物品中选择若干件放在承重为 j 的背包中,可以取得的最大价值。     Pi表示第i件物品的价值。     决策:为了背包中物品总价值最大化,第 i件物品应该放入背包中吗 ?   题目描述: 有编号分别为a,b,c,d,e的五件物品,它们的重量分别是2,2,6,5,4,它们的价值分别是6,3,5,4,6,现在给你个承重为10的背包,如何让背包里装入的物品具有最大的价值总和? nameweightvalue12345678910a26066991212151515b23033669991011c65000666661011d54000666661010e460006666666 思路:当向背包中加入第i个物品时,

By Ne0inhk