视图绑定

视图绑定是一项能让您更轻松地在代码中访问视图的功能。它会为每个 XML 布局文件生成绑定类。绑定类的实例包含对在相应布局中具有 ID 的所有视图的直接引用。例如,Unscramble 应用目前使用了视图绑定,因此可在代码中使用生成的绑定类引用视图。
notion image

启用视图绑定

在应用级build.gradle中android部分下加入
buildFeatures { viewBinding = true }

使用

class MainActivity : AppCompatActivity() { lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) // Old way with findViewById() val myButton: Button = findViewById(R.id.my_button) myButton.text = "A button" // Better way with view binding val myButton: Button = binding.myButton myButton.text = "A button" // Best way with view binding and no extra variable binding.myButton.text = "A button" } }
override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { // Inflate the layout XML file and return a binding object instance binding = GameFragmentBinding.inflate(inflater, container, false) return binding.root }
系统会通过以下方式生成绑定类的名称:将 XML 文件的名称转换为驼峰式大小写,并在末尾添加“Binding”一词。

滚动列表

概念

  • 列表项:要显示的列表的数据项。
  • Adapter:获取数据并准备数据以供RecyclerView显示。
  • ViewHolder:供RecyclerView用于和重用于显示的视图池。
  • RecyclerView:屏幕上的视图。
notion image

使用

视图

<FrameLayout 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=".MainActivity"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/recycler_view" android:layout_width="match_parent" // 使用match_parent使充满屏幕 android:layout_height="match_parent" android:scrollbars="vertical" // 滚动条 app:layoutManager="LinearLayoutManager" /> // 以垂直列表的形式显示列表项,使用LinearlayoutManager </FrameLayout>

创建Adapter

Adapter是一种设计模式,可将数据调整为可供RecyclerView使用的内容。
在运行应用时,RecyclerView会使用Adapter确定如何在屏幕上显示数据。
RecyclerView要求Adapter为列表中的第一个数据项创建新的列表项视图,有了此视图后,会要求Adapter提供用于绘制该列表项的数据。该过程不断重复,直到RecyclerView不需要更多视图来填满屏幕。
  1. 为列表项创建布局
RecyclerView都有自己的布局,可以在单独的布局文件中定义这些布局:在res → layout中新建名为 list_item.xml 的文件,用于定义单个列表项的布局
<?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/item_title" android:layout_width="wrap_content" android:layout_height="wrap_content" />
  1. 创建ItemAdapter类
在app > java > com.example.affirmations下新建包adapter,再新建类ItemAdapter
ItemAdapter需要有关如何解析字符串资源的信息。这些信息与有关应用的其他信息一起存储在 Context对象实例中,您可将此实例传入 ItemAdapter实例。
// 第一个参数 class ItemAdapter(private val context: Context, private val dataset: List<Affirmation>) { }
  1. 创建ViewHolder
RecyclerView不会直接与列表项视图交互,而是处理ViewHolder。
在ItemAdapter类中,创建一个ItemViewHolder类,ItemAdapter继承Adapter,并将ViewHolder传进去
class ItemAdapter( private val context: Context, private val dataset: List<Affirmation> ) : RecyclerView.Adapter<ItemAdapter.ItemViewHolder>() { class ItemViewHolder(private val view: View) : RecyclerView.ViewHolder(view) { val textView: TextView = view.findViewById(R.id.item_title) } }
  1. 实现抽象方法
// onCreateViewHolder() 方法由布局管理器调用,用于为 RecyclerView 创建新的 ViewHolder(如果不存在可重复使用的 ViewHolder)。 // 请注意,一个 ViewHolder 代表一个列表项视图。 /* onCreateViewHolder接受两个参数并返回一个新的ViewHolder : parant: 新列表视图将作为子视图附加到的试图组,即为RecyclerView viewType: 当同一个 RecyclerView 中存在多种列表项视图类型时,此参数可发挥重要作用。如果在 RecyclerView 内显示不同的列表项布局,就会有不同的列表项视图类型。 */ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder { // 从提供的上下文(parent 的 context)中获取 LayoutInflater 的实例。布局膨胀器知道如何将 XML 布局膨胀为视图对象的层次结构。 val adaoterLayout = LayoutInflater.from(parent.context) .inflate(R.layout.list_item, parent, false) // 获取 LayoutInflater 对象实例后,添加一个句点,后跟另一个方法调用以膨胀实际的列表项视图。传入 XML 布局资源 ID R.layout.list_item 和 parent 视图组。第三个布尔值参数是 attachToRoot。此参数需为 false,因为 RecyclerView 会在需要的时候替您将此列表项添加到视图层次结构中。 // 现在,adapterLayout 存储着对列表项视图的引用 return ItemViewHolder(adaoterLayout) } // getItemCount()方法需要返回数据集的大小 override fun getItemCount(): Int { return dataset.size } // 由布局管理器调用,以便替换列表项视图的内容 // 有两个参数,一个是先前由onCreateViewHolder()创建的ItemViewHolder;另一个是int,代表当前列表项的position // 在此方法中,可以根据位置从数据集中找到正确的数据对象 override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { val item = dataset[position] holder.textView.text = context.resources.getString(item.stringResourceId) }

修改MainActivity以使用RecyclerView

最后,需要使用Datasource和ItemAdapter类在RecyclerView中创建和显示列表项。
MainActivity中的 onCreate() 方法中,在调用 setContentView 后,加入以下代码。
// 加载数据集 val myDataset = Datasource().loadAffirmations() // 获取rv的引用 val recyclerView = findViewById<RecyclerView>(R.id.recycler_view) // 创建adapter recyclerView.adapter = ItemAdapter(this, myDataset) // 由于 activity 布局中的 RecyclerView 布局大小是固定的,因此您可以将 RecyclerView 的 setHasFixedSize 参数设为 true。只有在提高性能时才需要使用此设置。如果您知道内容的更改不会更改 RecyclerView 的布局大小,请使用此设置。 recyclerView.setHasFixedSize(true)

ListAdapter

ListAdapter 是 RecyclerView.Adapter 类的子类,用于在 RecyclerView 中显示列表数据,包括后台线程上列表之间的计算差异。
notion image

总结

  • RecyclerView 微件可帮助您显示数据列表。
  • RecyclerView 使用 Adapter 模式来调整和显示数据。
  • ViewHolder 为 RecyclerView 创建并存储视图。
  • RecyclerView 附带内置 LayoutManagersRecyclerView 将列表项的布局工作委托给 LayoutManagers
为实现 Adapter,请执行以下操作:
  • 为 Adapter 创建一个新类,例如 ItemAdapter
  • 创建用于代表单个列表项视图的自定义 ViewHolder 类。从 RecyclerView.ViewHolder 类进行扩展。
  • 修改 ItemAdapter 类,以便从 RecyclerView.Adapter 类通过自定义 ViewHolder 类进行扩展。
  • 在 Adapter 内实现以下方法:getItemsCount()onCreateViewHolder() 和 onBindViewHolder()

Navigations

Between Screens

intent

intent最常见的用途是启动activity,分为显式和隐式:您可以使用显式 intent 来执行操作或显示自己应用中的屏幕,并对整个流程负责。隐式 intent 一般用来执行涉及其他应用的操作,并依赖系统来确定最终结果。
  • 显式intent非常具体,您知道要启动的具体activity,通常是您自己应用中的屏幕
// LetterAdapter val context = holder.view.context val intent = Intent(context, DatailActivity::class.java) // intent只是一组指令,目前还没有目标activity的实例 intent.putExtra("letter", holder.button.text.toString()) // 传入一段数据 context.startActivity(intent) // 对context对象调用startActivity()方法,并传入intent // DetailActivity val letterId = intent?.extra?.getString("letter").toString() // 接传过来的数据, intent和extra都可能为null
  • 隐式intent更抽象,通过这类intent告知系统要执行的操作类型,系统则负责确定如何执行相应请求。
val queryUrl: Uri = Uri.parse("${DetailActivity.SEARCH_PREFIX}${item}") // Uri 统一资源标识符 /* 将Intent.ACTIONVIEW与URI一同传入(显式是传入context和ctivity) ACTION_VIEW 是一个通用 intent,可以接受 URI(在本例中为网址)。然后,系统就会知道应通过在用户的网络浏览器中打开该 URI 来处理此 intent。一些其他 intent 类型包括: [通用Intent](https://developer.android.com/guide/components/intents-common) */ val intent = Intent(Intent.ACTION_VIEW, queryUrl) context.startActivity(intent)
// 在使用该 intent 启动 activity 之前,请检查是否有应用能够处理该 intent。 // 如果没有可用的应用来处理 intent,执行该项检查可防止应用崩溃,从而提升代码的安全性。 if (activity?.packageManager?.resolveActivity(intent, 0) != null) { startActivity(intent) }

在应用栏添加自定义按钮

  1. 添加图标
  1. 告知系统应用栏应显示哪些选项和使用哪些图标。右击res文件夹,新建Android Resource File,Name = ‘layout_menu’,Resource Type = ’menu‘
  1. 打开res/menu/layout_menu,将其内容替换为
<menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <item android:id="@+id/action_switch_layout" android:title="@string/action_switch_layout" android:icon="@drawable/ic_linear_layout" app:showAsAction="always" /> </menu> <!--> id:与视图一样,菜单选项也有一个 ID,以便在代码中加以引用。 title:在本例中,此文本实际上并不可见,但它可能有助于屏幕阅读器识别菜单。 icon:默认值为 ic_linear_layout。但是,当用户选择按钮后,系统便会切换显示列表和网格图标。 showAsAction:此属性可告知系统如何显示该按钮。由于该属性被设置为“always”,对应按钮将始终显示在应用栏中,不会成为溢出菜单的一部分。 </!-->
  1. 添加逻辑
    1. 添加布局属性`private var isLinearLayoutManager = true`
    2. 用户切换时,需要转换布局、更新图标
    3. private fun chooseLayout() { if (isLinearLayoutManager) { recyclerView.layoutManager = LinearLayoutManager(this) } else { recyclerView.layoutManager = GridLayoutManager(this, 4) // GridLayoutManager允许在单行显示多项 } recyclerVire.adapter = LetterAdapter() } private fun setIcon(menuItem: MenuItem?) { if (menuItem == null) { return } menuItem.icon = if (isLinearLayoutManager) ContextCompat.getDrawable( this, R.drawable.ic_grid_layout ) else ContextCompat.getDrawable(this, R.drawable.ic_linear_layout) }
      c. 重写关于菜单的方法
      // onCreateOptionsMenu:用于膨胀选项菜单并执行任何其他设置。 override fun onCreateOptionsMenu(menu: Menu?): Boolean { menuInflater.inflate(R.menu.layout_menu, menu) val layoutButton = menu?.findItem(R.id.action_switch_layout) setIcon(layoutButton) return true } // onOptionsItemSelected:用于在选中按钮后实际调用 chooseLayout()。 override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.action_switch_layout -> { // Sets isLinearLayoutManager (a Boolean) to the opposite value isLinearLayoutManager = !isLinearLayoutManager // Sets layout and icon chooseLayout() setIcon(item) return true } else -> super.onOptionsItemSelected(item) } }

Activity生命周期

  • 生命周期和声明周期方法
    • notion image
      onCreate()
      系统会在初始化 activity 之后(在内存中创建新的 Activity 对象后)立即调用一次 onCreate() 生命周期方法。执行 onCreate() 后,相应 activity 会被视为已创建。
      onStart()
      系统会在调用 onCreate() 生命周期方法之后立即调用 onStart()onStart() 运行后,您的 activity 会显示在屏幕上。与为初始化 activity 而仅调用一次的 onCreate() 不同,onStart() 可在 activity 的生命周期内多次调用。需要与相应的onStop()声明周期方法配对使用。
      onResume()
      用于使相应的activity成为焦点,并让用户能与其互动
      onDestroy()
      执行 onDestroy() 方法意味着相应 activity 已完全关闭,可以进行垃圾回收。垃圾回收是指自动清理您不再使用的对象。调用 onDestroy() 后,系统会知道这些资源是可丢弃的,然后开始清理这部分内存。 如果您的代码手动调用该 activity 的 finish() 方法,或者用户强行退出该应用(例如,用户强行退出,或在“最近使用的应用”屏幕关闭该应用),您的 activity 也可能会完全关闭。如果您的应用长时间没有在屏幕上显示,Android 系统也可能会自行关闭您的 activity。Android 这样做是为了节省电量,同时允许其他应用使用您应用的资源。
      onPause()
      该应用不会再获得焦点。
      onStop()
      在 onStop() 之后,该应用将不再显示在屏幕上。虽然该 activity 已停止,但 Activity 对象仍位于内存中(在后台)。该 activity 尚未销毁。用户可能会返回该应用,因此 Android 会保留您的 activity 资源。
      onSaveInstanceState()
      • 用于保存 Activity 被销毁后可能需要的任何数据。在生命周期回调图中,系统会在相应 activity 停止后调用 onSaveInstanceState()。每当您的应用进入后台时,系统都会调用它。
      • 请将 onSaveInstanceState() 调用视为一项安全措施;这让您有机会在相应 activity 退出前台时将少量信息保存到 bundle 中。系统现在会保存这些数据,这是因为如果等到关闭应用时再保存,系统可能会面临资源压力。
  • 部分过程
    • 使用“最近使用的应用”屏幕返回该应用。请注意,在 Logcat 中,该 activity 使用 onRestart() 和 onStart() 重启,然后使用 onResume() 恢复。当该 activity 返回前台时,系统不会再次调用 onCreate() 方法。相应 activity 对象未被销毁,因此不需要重新创建。系统会调用 onRestart() 方法,而不是 onCreate()。请注意,这一次该 activity 返回前台时,系统会保留 Desserts Sold 数值。
    • onSaveInstanceState() 回调是紧接着 onPause() 和 onStop() 发生的
    • activity 状态可以在 onCreate(Bundle) 或 onRestoreInstanceState(Bundle) 中恢复(通过 onSaveInstanceState() 方法填充的 Bundle 将传递给这两种生命周期回调方法)。
  • 配置变更会影响activity生命周期

Log(android.util.log)

Log.d("MainActivity", "onCreate Called")
Log 类会将消息写入 LogcatLogcat 是用于记录消息的控制台。来自 Android 的关于您应用的消息会出现在这里,包括您使用 Log.d() 方法或其他 Log 类方法显式发送到日志的消息。
此命令包含三个部分:
  • 日志消息的优先级,即消息的重要性。在本示例中,Log.d() 方法会写入调试消息。Log 类中的其他方法包括 Log.i()(表示信息性消息)、Log.e()(表示错误)、Log.w()(表示警告)或 Log.v()(表示详细消息)。
  • 日志标签(第一个参数),在本示例中为 "MainActivity"。该标签是一个字符串,可让您更轻松地在 Logcat 中找到自己的日志消息。该标签通常是类的名称。
  • 实际的日志消息(第二个参数)是一个简短的字符串,在本示例中为 "onCreate called"
注意:一种比较好的做法是,在类中声明 TAG 常量:

Fragement and Navigation

Intro: 许多 Android 应用不需要每个屏幕都有单独的 activity。实际上,许多常见的界面模式(例如标签页)都存在于名为“fragment”的单个 activity 中。

Fragement

fragment 是可重复使用的界面片段;fragment 可以嵌入一个或多个 activity 中并重复使用。您甚至可以在一个屏幕上同时显示多个 fragment。
  1. Fragement生命周期
    1. notion image
    2. 生命周期状态
        • INITIALIZED:fragment 的一个新实例已实例化。
        • CREATED:系统已调用第一批 fragment 生命周期方法。在 fragment 处于此状态期间,系统也会创建与其关联的视图。
        • STARTED:fragment 在屏幕上可见,但没有焦点,这意味着其无法响应用户输入。
        • RESUMED:fragment 可见并已获得焦点。
        • DESTROYED:fragment 对象已解除实例化。
    3. 生命周期方法
        • onCreate():fragment 已实例化并处于 CREATED 状态。不过,其对应的视图尚未创建。
        • onCreateView():此方法可用于膨胀布局。fragment 已进入 CREATED 状态。
        • onViewCreated():此方法在创建视图后调用。在此方法中,您通常会通过调用 findViewById() 将特定视图绑定到属性。
        • onStart():fragment 已进入 STARTED 状态。
        • onResume():fragment 已进入 RESUMED 状态,现已具有焦点(可响应用户输入)。
        • onPause():fragment 已重新进入 STARTED 状态。相应界面对用户可见。
        • onStop():fragment 已重新进入 CREATED 状态。该对象已实例化,但它在屏幕上不再显示。
        • onDestroyView():该方法在 fragment 进入 DESTROYED 状态之前调用。视图已从内存中移除,但 fragment 对象仍然存在。
        • onDestroy():fragment 进入 DESTROYED 状态。
    4. 请注意 onCreate() 方法的差异。通过 activity,您可以使用此方法膨胀布局和绑定视图。不过,在 fragment 生命周期中,系统会在创建视图之前调用 onCreate(),所以您无法在此处膨胀布局。您可以改为在 onCreateView() 中执行此操作。然后,在创建视图后,系统会调用 onViewCreated() 方法,您可以在该方法中将属性绑定到特定视图。
  1. 实现

Navigation组件

  1. 三个关键部分
      • NavGraph导航图是一种 XML 文件,可以直观地呈现应用内的导航。该文件由若干目的地组成,对应于各个 activity 和 fragment,以及它们之间可通过代码用来从一个目的地导航到另一个目的地的操作。与布局文件一样,Android Studio 提供了一个可视化编辑器,用于向导航图添加目的地和操作。
      • NavHostNavHost 用于在 activity 内显示导航图中的目的地。当您在 fragment 之间导航时,NavHost 中显示的目的地会相应更新。您将在 MainActivity 中使用名为 NavHostFragment 的内置实现。
      • NavController您可以使用 NavController 对象控制 NavHost 中显示的目的地之间的导航。在使用 intent 时,您必须通过调用 startActivity 导航到新屏幕。借助 Navigation 组件,您可以通过调用 NavController 的 navigate() 方法来交换显示的 fragment。NavController 还可帮您处理常见任务,例如响应系统“向上”按钮可回到之前显示的 fragment。
  1. 准备
    1. 在项目级build.gradle的buildscript > ext 中,在 material_version 下,将 nav_version 设置为 2.3.1
    2. 在应用级 build.gradle 文件中,将以下内容添加到依赖项组中。
      1. implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
    3. Safe Args插件
      1. 在 fragment 之间传递数据时,该插件可帮助您确保类型安全。
        1) 在顶级 build.gradle 文件的 buildscript > dependencies 中,添加以下类路径。
        classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
        2) 在应用级 build.gradle 文件中,在顶部的pluginsandroidx.navigation.safeargs.kotlin
        3) Sync Now
  1. NavGraph
导航图(简称 NavGraph)是应用导航的虚拟映射。每个屏幕(对您而言,就是每个 fragment)都是可能的导航“目的地”。NavGraph 可以用显示各目的地之间关系的 XML 文件来表示。在后台,它实际上会创建 NavGraph 类的新实例。不过,导航图中的目的地由 FragmentContainerView 向用户显示。
使用 Android Basics in Kotlin - Navigation - Intro to the Navigation component | Android Developers
  1. 创建导航图
  1. 发送、接受数据
// 传 val action = LetterListFragmentDirections.actionLetterListFragmentToWordListFragment(letter = holder.button.text.toString()) holder.view.findNavController().navigate(action) // 接 private lateinit var letterId: String override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) arguments?.let { letterId = it.getString(LETTER).toString() } }
c. MainActivity
// ... private lateinit var navController: NavController override fun onCreate(savedInstanceState: Bundle?) { // ... val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment navController = navHostFragment.navController setupActionBarWithNavController(navController) // 确保操作栏/应用栏可见 } // 支持向上操作 override fun onSupportNavigateUp(): Boolean { return navController.navigateUp() || super.onSupportNavigateUp() }

ViewModel and Live Data

  1. ViewModel
    1. 使用
      1. 1) 应用级的`build.gradle` - dependencies块内有ViewModel依赖项
        // ViewModel implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
        2) 新建xxxViewModel类,集成ViewModel()
        3) 在使用该ViewModel类的界面控制器文件中,引用该对象,创建属性委托
        private val viewModel: GameViewModel by viewModels()
        4) 使用后备属性暴露数据
    2. 生命周期
      1. 只要 activity 或 fragment 的范围处于有效状态,框架就会让 ViewModel 保持有效。即使所有者因配置更改(如屏幕旋转)而被销毁,ViewModel 也不会被销毁。所有者的新实例会重新连接到现有 ViewModel 实例,如下图所示:
        notion image
  1. Live Data
    1. LiveData 是一种具有生命周期感知能力、可观察的数据存储器类。
      LiveData 的部分特性如下:
      • LiveData 可存储数据;LiveData 是一种可存储任何类型的数据的封装容器。
      • LiveData 是可观察的,这意味着当 LiveData 对象存储的数据发生更改时,观察器会收到通知。
      • LiveData 具有生命周期感知能力。当您将观察器附加到 LiveData 后,观察器就会与 LifecycleOwner(通常是 activity 或 fragment)相关联。LiveData 仅更新处于活跃生命周期状态(例如 STARTED 或 RESUMED)的观察器。您可以在此处详细了解 LiveData 和观察。
      /* ViewModel */ private var _currentWordCount = MutableLiveData<Int>(0) private val _currentScrambledWord = MutableLiveData<String>() val currentScrambledWord: LiveData<String> get() = _currentScrambledWord val currentWordCount: LiveData<Int> get() = _currentWordCount // 引用 pritln(_currentWordCount.value) /* Observer */ viewModel.currentScrambledWord.observe(viewLifecycleOwner, { newWord -> binding.textViewUnscrambledWord.text = newWord })
  1. 数据绑定
    1. 简而言之,数据绑定就是将数据(从代码)绑定到视图 + 视图绑定(将视图绑定到代码)
      在布局文件中使用数据绑定的示例,该示例显示了如何使用数据绑定库直接再布局文件中将应用数据赋值给视图/微件。`@{}`
      android:text="@{gameViewModel.currentScrambledWord}"
      使用数据绑定的主要优势在于,您可以移除 activity 中的许多界面框架调用,使其维护起来更简单、方便。还可以提高应用性能,并且有助于防止内存泄漏以及避免发生 null 指针异常。
      • 准备工作:
    2. 在 build.gradle(Module) 文件中的 buildFeatures 部分下,启用 dataBinding 属性。
    3. // 将 buildFeatures { viewBinding = true } // 替换为 buildFeatures { dataBinding = true }
      b. 若要在任何 Kotlin 项目中使用数据绑定,应当应用 kotlin-kapt 插件。
      plugins { //... id 'kotlin-kapt' }
      c. 将局部文件转换为数据绑定布局
      d. 将视图绑定改为数据绑定
      binding = GameFragmentBinding.inflate(inflater, container, false) // 替换为 binding = DataBindingUtil.inflate(inflater, R.layout.game_fragment, container, false)
      • 使用
    4. 在布局文件中的 <data> 标记内添加名为 <variable> 的子标记内,声明一个名为 gameViewModel、类型为 GameViewModel 的属性。您将使用此属性将 ViewModel 中的数据绑定到布局。
    5. <data> <variable name="gameViewModel" type="com.example.android.unscramble.ui.game.GameViewModel" /> </data>
      b. 在 gameViewModel 声明下的 <data> 标记内,添加类型为 Integer 的另一个变量,并将其命名为 maxNoOfWords。您将使用此变量绑定到 ViewModel 中的变量,以存储每局游戏的单词数。
      <data> ... <variable name="maxNoOfWords" type="int" /> </data>
      c. 在 GameFragment 中的 onViewCreated() 方法开头,初始化布局变量 gameViewModel 和 maxNoOfWords
      override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding.gameViewModel = viewModel binding.maxNoOfWords = MAX_NO_OF_WORDS ... }
      d. LiveData 是生命周期感知型可观察对象,因此您必须将生命周期所有者传递给布局。在 GameFragment 中的 onViewCreated() 方法内,在绑定变量的初始化下方添加以下代码。
      // Specify the fragment view as the lifecycle owner of the binding. // This is used so that the binding can observe LiveData updates binding.lifecycleOwner = viewLifecycleOwner
      e. 使用绑定表达式
      • 语法: 绑定表达式以 @ 符号开头并用花括号 {} 括起来。在以下示例中,TextView 文本被设为 user 变量的 firstName 属性
        • 示例
          <TextView android:id="@+id/textView_unscrambled_word" ... android:text="@{gameViewModel.currentScrambledWord}" .../>
      f. 移除观察器代码
      • 其他用法
        • 引用资源
          • // 在上面的示例中,padding 属性被赋予 dimen.xml资源文件中的值 largePadding。 android:padding="@{@dimen/largePadding}"
        • 参数传递
          • android:text="@{@string/example_resource(user.lastName)}" // string.xml <string name="example_resource">Last Name: %s</string>
  1. Fragment之间共享ViewModel - 将使用 activity 实例而不是 fragment 实例
    1. 为了在 StartFragment 中使用共享视图模型,您将使用 activityViewModels() 而不是 viewModels() 委托类来初始化 OrderViewModel
      • 在每个Fragment类中属性托管
      • 在每个Fragment xml中声明ViewModel变量
private val sharedViewModel: OrderViewModel by activityViewModels()
// onViewCreated binding?.apply { viewModel = sharedViewModel lifecycleOwner = viewLifecycleOwner startFragment = this@startFragment // 在apply内,this 关键字是指绑定实例,而不是 fragment 实例。使用 @ 并明确指定 fragment 类名称。 } // or binding?.startFragment = this // fragment.xml <Button //... android:onClick="@{() -> startFragment.orderCupcake(6)}" />
  1. 任务和返回堆栈
    1. Android 中的 activity 存在于任务中。当您从启动器图标首次打开应用时,Android 会使用主 activity 创建一个新任务。任务是用户在执行某项作业(例如,查看电子邮件、创建纸杯蛋糕订单、拍照)时与之互动的一系列 activity。
      activity 排列在一个堆栈中,称为“返回堆栈”,其中,用户访问的每个新的 activity 都会推送到任务的返回堆栈中。您可以将它看作是一摞煎饼,每一张新的煎饼都会加到这摞煎饼的最上方。堆栈顶部的 activity 是用户当前正在与之互动的 activity。堆栈中位于下方的 activity 已置于后台,并且已停止。
      // 返回到首页时,应在action的attributes中设置以下属性 app:popUpTo="@id/startFragment" // 首页id app:popUpToInclusive="true"

应用架构

分离关注点、通过模型驱动界面
  1. 分离关注点
    1. 分离关注点设计原则指出,应将应用分为类,每个类有各自的责任。
  1. 通过模型驱动界面
    1. Android 架构中主要的类或组件是界面控制器 (activity/fragment)、ViewModelLiveData 和 Room。这些组件负责处理生命周期的某些复杂情况,并帮助您避免与生命周期相关的问题。
      • 界面控制器 (activity/fragment)
        • activity 和 fragment 是界面控制器。界面控制器通过在屏幕上绘制视图、捕获用户事件以及与用户与之互动的界面相关的所有其他操作来控制界面。应用中的数据或有关该数据的任何决策逻辑都不应放到界面控制器类中。
          Android 系统可能会根据某些用户互动情况或因内存不足等系统条件而随时销毁界面控制器。由于这些事件不受您的控制,因此您不应将任何应用数据或状态存储到界面控制器中,而应将有关数据的决策逻辑添加到 ViewModel 中。
      • ViewModel
        • ViewModel 是视图中显示的应用数据的模型。模型是负责处理应用数据的组件,能够让应用遵循架构原则,通过模型驱动界面。
          ViewModel 存储应用相关的数据,这些数据不会在 Android 框架销毁并重新创建 activity 或 fragment 时销毁。在配置更改期间会自动保留 ViewModel 对象(不会像销毁 activity 或 fragment 实例一样将其销毁),以便它们存储的数据立即可供下一个 activity 或 fragment 实例使用。
      fragment/activity(界面控制器)的责任
      ViewModel 的责任
      ViewModel 负责存储和处理界面需要的所有数据。它绝不应访问视图层次结构(例如视图绑定对象)或存储对 activity 或 fragment 的引用。

Connect to the Internet

  1. 准备
    1. 应用级build.gradle - dependencies中,为Retrofit库添加以下代码行
      1. // Retrofit implementation "com.squareup.retrofit2:retrofit:2.9.0" // Retrofit with Moshi Converter 允许Retrofit将JSON结果作为String返回 implementation "com.squareup.retrofit2:converter-scalars:2.9.0"
    2. 应用级build.gradle添加对Java 8的支持
      1. android { ... compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = '1.8' } }
  1. network.xxApiService.kt
private const val BASE_URL = "https://android-kotlin-fun-mars-server.appspot.com" private val retrofit = Retrofit.Builder() .addConvertFactoru(ScalarsConverterFactory.create())
  1. 使用协程
    1. ViewModelScope 是为应用中的每个 ViewModel 定义的内置协程作用域。在此作用域内启动的协程会在 ViewModel 被清除时自动取消。
  1. 添加Android网络权限
// mainfests/AndroidManifest.xml的<application>前 <uses-permission android:name="android.permission.INTERNET" />
  1. 异常处理
  1. Moshi解析JSON响应
// 将1.准备中的导入换为下面的导入 // Moshi implementation 'com.squareup.moshi:moshi-kotlin:1.9.3' // Retrofit with Moshi Converter implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'
private val moshi = Moshi.Builder() .add(KotlinJsonAdapterFactory()) .build() private val retrofit = Retrofit.Builder() .addConverterFactory(MoshiConverterFactory.create(moshi)) .baseUrl(BASE_URL) .build()
  1. 使用数据类
数据类中的每个变量都对应于 JSON 对象中的一个键名。为了匹配特定 JSON 响应中的类型,请为所有值使用 String 对象。
  1. 使用Coil显示图片
    1. 添加依赖
    2. // Coil app implementation "io.coil-kt:coil:1.1.1" // project repositories { // ... mavenCentral() }
      b. 使用绑定适配器
      绑定适配器是用于为视图的自定义属性创建自定义 setter 的注解方法。
      @BindingAdapter("listData") fun bindRecyclerView(recyclerView: RecyclerView, data: List<MarsPhoto>?) { val adapter = recyclerView.adapter as PhotoGridAdapter adapter.submitList(data) }
      // xml <androidx.recyclerview.widget.RecyclerView // ... app:listData="@{viewModel.photos}" />

Room — SQL

介绍

Room 包含三个主要组件:
  • 数据实体表示应用的数据库中的表。数据实体用于更新表中的行所存储的数据以及创建新行供插入。
  • 数据库类持有数据库,并且是应用数据库底层连接的主要访问点。数据库类为应用提供与该数据库关联的 DAO 的实例。

使用

  1. 依赖
    1. // - project build.gradle ext { // ... room_version = '2.3.0' } // - app build.gradle implementation "androidx.room:room-runtime:$room_version" kapt "androidx.room:room-compiler:$room_version" // optional - Kotlin Extensions and Coroutines support for Room implementation "androidx.room:room-ktx:$room_version"
  1. 创建实体
    1. 使用 Room 时,每个表都由一个类表示。在 Room 等 ORM(对象关系映射)库中,这些类通常称为模型类或实体。
      package com.example.busschedule.database.schedule import androidx.annotation.NonNull import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey @Entity // 为了让 Room 认为该类可用于定义数据库表,需要添加@Entity,@Entity 注解用于将某个类标记为数据库实体类。 // 默认情况下,Room将类名称用作数据库表名;或者可以指定@Entity(tableName="schedule") data class Schedule( @PrimaryKey val id: Int, // @PrimaryKey主键 @NonNull @ColumnInfo(name = "stop_name") val stopName: String, // @ColumnInfo指定名称 @NonNull @ColumnInfo(name = "arrival_time") val arrivalTime: Int ) { }
  1. 定义DAO
    1. DAO代表数据访问对象,是提供数据访问的Kotlin类。在DAO中包含用于读取和操作数据的函数。
      package com.example.busschedule.database.schedule import androidx.room.Dao import androidx.room.Query @Dao interface ScheduleDao { // 使用 @Query 注解并提供 SQLite 查询。 @Query("SELECT * FROM schedule ORDER BY arrival_time ASC") fun getAll(): Flow<List<Schedule>> @Query("SELECT * FROM schedule WHERE stop_name = :stopName ORDER BY arrival_time ASC") fun getByStopName(stopName: String): Flow<List<Schedule>> // 数据库操作的执行可能用时较长,因此,应该在单独的线程上运行这些操作。 // @Insert(onConflict = OnConflictStrategy.IGNORE) 参数 OnConflict 用于告知 Room 在发生冲突时应该执行的操作。 // 冲突策略 https://developer.android.com/reference/androidx/room/OnConflictStrategy.html @Insert suspend fun insert(item: Item) // Room 将生成在数据库中插入 item 所需的全部代码。 // @Update suspend fun update(item: Item) // 更新的实体与传入的实体具有相同的键, 可以更新该实体的部分或全部属性 @Delete suspend fun delete(item: Item) // 需要传入需要删除的实体,拖没有该实体,需要调用delect()之前获取该实体 }
  1. 定义ViewModel
    1. // viewmodel.kt package com.example.busschedule.viewmodels import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.example.busschedule.database.schedule.Schedule import com.example.busschedule.database.schedule.ScheduleDao class BusScheduleViewModel(private val scheduleDao: ScheduleDao) : ViewModel() { fun fullSchedule(): List<Schedule> = scheduleDao.getAll() fun scheduleForStopName(name: String): List<Schedule> = scheduleDao.getByStopName(name) private fun insertItem(item: Item) { viewModelScope.launch { itemDao.insert(item) } } } // 可以使用工厂类来实例化ViewModel对象,以使fragment对象不用处理所有内存管理任务。 class BusScheduleViewModelFactory( private val scheduleDao: ScheduleDao ) : ViewModelProvider.Factory { // 可以使用 BusScheduleViewModelFactory.create() 实例化 BusScheduleViewModelFactory 对象,让ViewModel可以感知生命周期而不必由 fragment 直接进行处理。 override fun <T : ViewModel> create(modelClass: Class<T>): T { if (modelClass.isAssignableFrom(BusScheduleViewModel::class.java)) { @Suppress("UNCHECKED_CAST") return BusScheduleViewModel(scheduleDao) as T } throw IllegalArgumentException("Unknown ViewModel class") } }
      // fragment.kt private val viewModel: BusScheduleViewModel by activityViewModels { BusScheduleViewModelFactory( (activity?.application as BusScheduleApplication).database.scheduleDao() ) }
  1. 创建数据库类和预先填充数据库
    1. package com.example.busschedule.database import android.content.Context import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase import com.example.busschedule.database.schedule.Schedule import com.example.busschedule.database.schedule.ScheduleDao // 与模型类和 DAO 一样,数据库类也需要用注解来提供一些特定信息。所有实体类型(访问类型本身需要使用 ClassName::class)都列于一个数组中。数据库还会获得一个版本号,每次您对架构做出更改时,版本号都会递增。应用会对照数据库中的版本检查此版本,以确定是否应执行迁移以及应如何执行迁移。 @Database(entities = [Schedule::class], version = 1) // exportSchema = false 将 exportSchema 设为 false,这样就不会保留架构版本记录的备份。 abstract class AppDatabase : RoomDatabase() { abstract fun scheduleDao(): ScheduleDao // 单例,以防出现竞态条件或其他潜在问题。 companion object { @Volatile // volatile 变量的值绝不会缓存,所有读写操作都将在主内存中完成。这有助于确保 INSTANCE 的值始终是最新的值,并且对所有执行线程都相同。也就是说,一个线程对 INSTANCE 所做的更改会立即对所有其他线程可见。 private var INSTANCE: AppDatabase? = null fun getDatabase(context: Context): AppDatabase { // 使用 Elvis 运算符返回数据库(如果已存在)的现有实例或根据需要首次创建数据库。 // 多个线程有可能会遇到竞态条件并同时请求数据库实例,导致产生两个数据库而不是一个。封装代码以在 synchronized 块内获取数据库意味着一次只有一个执行线程可以进入此代码块,从而确保数据库仅初始化一次。 return INSTANCE ?: synchronized(this) { INSTANCE = Room.databaseBuilder( context, AppDatabase::class.java, "app_database" ) .createFromAsset("database/bus_schedule.db") // createFromAsset() 以加载现有数据,该db文件在assets.database软件包中 .build() return INSTANCE as AppDatabase } } } }
  1. 创建自定义Application以实例化database
    1. // BusScheduleApplication.kt package com.example.busschedule import android.app.Application import com.example.busschedule.database.AppDatabase class BusScheduleApplication: Application() { val database: AppDatabase by lazy { AppDatabase.getDatabase(this) } }
      // AndroidManifest.xml <application android:name="com.example.busschedule.BusScheduleApplication" ...
  1. 使用Flow相应数据更改
    1. 数据库发生变化时如果不手动触发更新,App内容是不会动态更新的。问题在于,系统只会从每个DAO函数返回一次List<Schedule> ,即便底层数据已更新,系统也不会调用 submitList()  来更新界面。
      未解决该问题,可以使用Kotlin的异步flow功能,DAO可以借助该功能从数据库连续发出数据,如果数据库更新,结果会被发送回fragment。通过名为 collect() 函数,可以使用从flow发出的新值调用视图更新函数。
      // ScheduleDao.kt fun getAll(): Flow<List<Schedule>> fun getByStopName(stopName: String): Flow<List<Schedule>>
      // viewModel class BusScheduleViewModel(private val scheduleDao: ScheduleDao): ViewModel() { fun fullSchedule(): Flow<List<Schedule>> = scheduleDao.getAll() fun scheduleForStopName(name: String): Flow<List<Schedule>> = scheduleDao.getByStopName(name) }
      // fragment // busStopAdapter.submitList(viewModel.fullSchedule()) 改为 lifecycle.coroutineScope.launch { viewModel.fullSchedule().collect() { busStopAdapter.submitList(it) } }
      Video preview

缓存与存储库模式

存储库模式是一种将数据层与应用的其余部分隔离开来的设计模式。
存储库可以解决数据源(例如持久性模型、网络服务和缓存)之间的冲突,和集中管理对这些数据做出的更改。右2图就展示了应用组件(如 activity)如何通过存储库与数据源进行交互。
notion image
notion image
使用存储库类可确保该代码与ViewModel类是分开的,是推荐代码分离架构采用的最佳做法。
优势
  • 存储库模块可以处理数据操作,并且允许使用多个后台数据来源。在实际应用中,存储库可实现以下任务逻辑:是从网络中提取数据,还是使用缓存在本地数据库中的结果。
  • 可以使用存储库来实现数据交互细节(例如数据来源迁移),而不会影响到发起调用的代码。
  • 有助于模块化,易于测试。

缓存

缓存是指用于存储应用所用数据的存储空间。可以采用多种形式。
实现网络缓存的几种方式
缓存方法
用途
Retrofit 是一个网络库,用于为 Android 实现类型安全的 REST 客户端。您可以配置 Retrofit,以为每个网络结果在本地存储一个副本。
您可以访问应用的内部存储空间目录,然后在其中保存数据文件。应用的软件包名称会指定该应用的内部存储空间目录(即 Android 文件系统中的一个特定位置)。此目录供您的应用专用,并且会在应用卸载后被清除。
您可以使用 Room 缓存数据。Room 是一个 SQLite 对象映射库,它会在 SQLite 的基础上提供一个抽象层。

使用Room存储结构化数据

  1. 创建存储库 - 创建一个存储库来管理离线缓存
    1. 添加存储库
      1. class VideosRepository(private val database: VideosDatabase) { suspend fun refreshVideos() { withContext(Dispatchers.IO) { val playList = DevByteNetwork.devbytes.getPlaylist() database.videoDao.insertAll(playList.asDatabaseModel()) } } }
        注意:Android 上的数据库存储在文件系统(即磁盘)中。若要进行保存,这类数据库必须执行磁盘 I/O 操作。磁盘 I/O(即磁盘读写)速度缓慢,并且在该操作完成之前总是会阻塞当前线程。因此,您必须在 I/O 调度程序 中运行磁盘 I/O。此调度程序旨在使用 withContext(Dispatchers.IO) { ... } 将阻塞的 I/O 任务分流到共享线程池。
    2. 从数据库中检索数据
      1. // VideosRepository // 为简单起见,使用了Livedata,建议使用Flow,因为其与生命周期无关 val videos: LiveData<List<DevByteVideo>> = Transformations.map(database.videoDao.getVideos()) { // Transformations.map() 方法使用转换函数将一个 LiveData 对象转换为另一个 LiveData 对象。仅当处于活动状态的 activity 或 fragment 在观察返回的 LiveData 属性时,才会计算转换。 it.asDomainModel() }
  1. 使用刷新策略继承存储库
    1. // viewmodel val playlist = videosRepository.videos // 存储存储库中的LiveData viewModelScope.launch { try { videosRepository.refreshVideos() // 刷新数据 // ... } catch (networkError: IOException) { // ... } }
现在,数据是从网络中提取并保存在 Room 数据库中。屏幕上显示的数据是取自 Room 数据库,而不是直接来自网络。
  1. 总结
  • 缓存是将从网络中提取的数据存储到设备存储空间的过程。通过缓存,您的应用可在设备处于离线状态时,或者需要再次访问相同的数据时,访问相应数据。
  • 让应用将结构化数据存储在设备文件系统上的最佳方法是使用本地 SQLite 数据库。Room 是一个 SQLite 对象映射库,这意味着它会在 SQLite 的基础上提供一个抽象层。在实现离线缓存时,使用 Room 是推荐采用的最佳做法。
  • 存储库类会将数据源(例如 Room 数据库和网络服务)与应用的其余部分隔离开来,并提供一个干净的 API 来访问应用其余部分中的数据。
  • 对于代码分离和架构,使用存储库是推荐采用的最佳做法。
  • 在设计离线缓存时,最佳做法是将应用的网络、网域和数据库对象分离开。此策略就是一个关注点分离的例子

Preferences DataStore

代替SharedPreferences
使用 Jetpack DataStore 库,可以创建一个简单、安全的异步 API 来存储数据。它提供两种不同的实现:Preferences DataStore 和 Proto DataStore,它们保存数据的方式不同:
  • Preferences DataStore 根据键访问和存储数据,而无需事先定义架构(数据库模型)。
  • Proto DataStore 使用协议缓冲区来定义架构。使用协议缓冲区(即 Protobuf),您可以持久保留强类型数据。与 XML 和其他类似的数据格式相比,协议缓冲区速度更快、规格更小、使用更简单,并且更清楚明了。
Room 与 Datastore:适用情形
如果您的应用需要以 SQL 等结构化格式存储大型/复杂数据,请考虑使用 Room。不过,如果您只需要存储能以键值对形式保存的简单或少量数据,那么 DataStore 就是理想的选择。
Proto 与 Preferences DataStore:适用情形
Proto DataStore 具有类型安全和高效的优点,但需要进行配置和设置。如果您的应用数据足够简单,能够以键值对的形式保存,那么 Preferences DataStore 就是更合适的选择,因为它的设置要容易得多。

使用

  1. 依赖
implementation "androidx.datastore:datastore-preferences:1.0.0" implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1"
  1. 创建Preferences DataStore
package com.example.wordsapp.data import android.content.Context import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.emptyPreferences import androidx.datastore.preferences.preferencesDataStore import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.map import java.io.IOException // 要实例化的Preferencees Datastore名称 private const val LAYOUT_PREFERENCES_NAME = "layout_preferences" // 使用 preferencesDataStore 委托创建一个 DataStore 实例。 // 因为您要使用 Preferences Datastore,所以需要传递 Preferences 作为数据存储区类型。 // 此外,还要将数据存储区 name 设为 LAYOUT_PREFERENCES_NAME。 private val Context.dataStore: DataStore<Preferences> by preferencesDataStore( name = LAYOUT_PREFERENCES_NAME ) class SettingDataStore(context: Context) { // 使用相应的键类型函数为存储的DataStore<Preferences> 实例中的每个值定义一个键 private val IS_LINEAR_LAYOUT_MANAGER = booleanPreferencesKey("is_linear_layout_manager") // 不用公开整个Preferences对象,只公开需要的内容。 val preferenceFlow: Flow<Boolean> = context.dataStore.data.catch { // 异常处理 if (it is IOException) { it.printStackTrace() emit(emptyPreferences()) } else { throw it } }.map { preferences -> preferences[IS_LINEAR_LAYOUT_MANAGER] ?: true } suspend fun saveLayoutToPreferencesStore(isLinearLayoutManager: Boolean, context: Context) { // edit()挂起函数,用于以事务的方式更新数据 context.dataStore.edit { preferences -> preferences[IS_LINEAR_LAYOUT_MANAGER] = isLinearLayoutManager } } }
  1. 使用DataStore类
// fragment.kt private lateinit var SettingsDataStore: SettingsDataStore override fun onViewCreated(view: View, savedInstanceState: Bundle?) { // ... // Initialize SettingsDataStore SettingsDataStore = SettingsDataStore(requireContext()) // 使用 asLiveData() 将 preferenceFlow 转换为 Livedata。附加一个观察器。 SettingsDataStore.preferenceFlow.asLiveData().observe(viewLifecycleOwner, { value -> isLinearLayoutManager = value chooseLayout() }) } override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.action_switch_layout -> { // ... // 写入 lifecycleScope.launch { SettingDataStore.saveLayoutToPreferencesStore( isLinearLayoutManager, requireContext() ) } return true } else -> super.onOptionsItemSelected(item) } }

WorkManager

  1. WorkManager类
      • Worker:此位置用于放置您希望在后台执行的实际工作的代码。您需要扩展此类并替换 doWork() 方法。
      • WorkRequest:此类表示请求执行某些工作。您将在创建 WorkRequest 的过程中传入 Worker。在创建 WorkRequest 时,您还可以指定 Constraints 等内容,例如运行 Worker 的时间。
      • WorkManager:此类实质上可以调度 WorkRequest 并使其运行。它以一种在系统资源上分散负载的方式调度 WorkRequest,同时遵循您指定的约束条件。

Else

  1. 您无法针对 ConstraintLayout中的界面元素设置 match_parent 。相反,您需要约束该视图的起始和结束边缘,并将宽度设置为 0dp。如果将宽度设置为 0dp,就表示告知系统不计算宽度,只尝试匹配针对视图设置的约束条件即可。
您无法将 match_parent 用于 ConstraintLayout 中的任何视图。请改用 0dp,这意味着需要匹配约束条件。
  1. string复数资源
// string.xml <plurals name="cupcakes"> <item quantity="one">%d cupcake</item> <item quantity="other">%d cupcakes</item> </plurals> // 使用 getQuantityString(R.plurals.cupcakes, 1, 1) // 将返回字符串 1 cupcake getQuantityString(R.plurals.cupcakes, 6, 6) // 将返回字符串 6 cupcakes getQuantityString(R.plurals.cupcakes, 0, 0) // 将返回字符串 0 cupcakes
  1. <include/>引用资源布局
<include layout="@layout/title" />
  1. getString()
// 将在%s处插入值 <string name="tip_amount">Tip Amount: %s</string>
fun calculateTip() { // .... val formattedTip = NumberFormat.getCurrencyInstance().format(tip) binding.tipResult.text = getString(R.string.tip_amount, formattedTip) }
  1. 资源注解
// 在本例中,向字符串资源 ID 属性添加 @StringRes 注解,并向可绘制资源 ID 属性添加 @DrawableRes 注解。 // 这样一来,如果您提供错误类型的资源 ID,就会收到警告。 data class Affirmation( @StringRes val stringResourceId: Int, @DrawableRes val imageResourceId: Int )
  1. SimpleDateFormat
这个类用于以语言区域敏感的方式对日期进行格式设置和解析。使用该类可以对日期进行格式设置(日期 → 文本)和解析(文本 → 日期)。
SimpleDateFormat("E MMM d", Locale.getDefault())
Locale对象表示特定的地理、政治或文化区域。它表示语言/国家/地区/变体的组合。语言区域用于改变数字或日期等信息的表示方式,使其符合相应区域的惯例。
private fun getPickupOptions(): List<String> { val options = mutableListOf<String>() val formatter = SimpleDateFormat("E MMM d", Locale.getDefault()) val calendar = Calendar.getInstance() // Create a list of dates starting with the current date and the following 3 dates repeat(4) { options.add(formatter.format(calendar.time)) calendar.add(Calendar.DATE, 1) } return options }
 
badge