Content
Adapter是一种设计模式,可将数据调整为可供RecyclerView使用的内容。
list_item.xml
的文件,用于定义单个列表项的布局ItemAdapter
需要有关如何解析字符串资源的信息。这些信息与有关应用的其他信息一起存储在 Context
对象实例中,您可将此实例传入 ItemAdapter
实例。RecyclerView不会直接与列表项视图交互,而是处理ViewHolder。
ListAdapter
是RecyclerView.Adapter
类的子类,用于在RecyclerView
中显示列表数据,包括后台线程上列表之间的计算差异。
RecyclerView
微件可帮助您显示数据列表。RecyclerView
使用 Adapter 模式来调整和显示数据。ViewHolder
为 RecyclerView
创建并存储视图。RecyclerView
附带内置 LayoutManagers
。RecyclerView
将列表项的布局工作委托给 LayoutManagers
。ItemAdapter
。ViewHolder
类。从 RecyclerView.ViewHolder
类进行扩展。ItemAdapter
类,以便从 RecyclerView
.Adapter
类通过自定义 ViewHolder
类进行扩展。getItemsCount()
、onCreateViewHolder()
和 onBindViewHolder()
。Activity
对象后)立即调用一次 onCreate()
生命周期方法。执行 onCreate()
后,相应 activity 会被视为已创建。onCreate()
生命周期方法之后立即调用 onStart()
。onStart()
运行后,您的 activity 会显示在屏幕上。与为初始化 activity 而仅调用一次的 onCreate()
不同,onStart()
可在 activity 的生命周期内多次调用。需要与相应的onStop()声明周期方法配对使用。onStop()
之后,该应用将不再显示在屏幕上。虽然该 activity 已停止,但 Activity
对象仍位于内存中(在后台)。该 activity 尚未销毁。用户可能会返回该应用,因此 Android 会保留您的 activity 资源。Activity
被销毁后可能需要的任何数据。在生命周期回调图中,系统会在相应 activity 停止后调用 onSaveInstanceState()
。每当您的应用进入后台时,系统都会调用它。onSaveInstanceState()
调用视为一项安全措施;这让您有机会在相应 activity 退出前台时将少量信息保存到 bundle 中。系统现在会保存这些数据,这是因为如果等到关闭应用时再保存,系统可能会面临资源压力。onRestart()
和 onStart()
重启,然后使用 onResume()
恢复。当该 activity 返回前台时,系统不会再次调用 onCreate()
方法。相应 activity 对象未被销毁,因此不需要重新创建。系统会调用 onRestart()
方法,而不是 onCreate()
。请注意,这一次该 activity 返回前台时,系统会保留 Desserts Sold 数值。onSaveInstanceState()
回调是紧接着 onPause()
和 onStop()
发生的onCreate(Bundle)
或 onRestoreInstanceState(Bundle)
中恢复(通过 onSaveInstanceState()
方法填充的 Bundle
将传递给这两种生命周期回调方法)。Log
类会将消息写入 Logcat。Logcat 是用于记录消息的控制台。来自 Android 的关于您应用的消息会出现在这里,包括您使用 Log.d()
方法或其他 Log
类方法显式发送到日志的消息。Log.d()
方法会写入调试消息。Log
类中的其他方法包括 Log.i()
(表示信息性消息)、Log.e()
(表示错误)、Log.w()
(表示警告)或 Log.v()
(表示详细消息)。"MainActivity"
。该标签是一个字符串,可让您更轻松地在 Logcat 中找到自己的日志消息。该标签通常是类的名称。"onCreate called"
。注意:一种比较好的做法是,在类中声明 TAG 常量:
fragment 是可重复使用的界面片段;fragment 可以嵌入一个或多个 activity 中并重复使用。您甚至可以在一个屏幕上同时显示多个 fragment。
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
状态。onCreate()
方法的差异。通过 activity,您可以使用此方法膨胀布局和绑定视图。不过,在 fragment 生命周期中,系统会在创建视图之前调用 onCreate()
,所以您无法在此处膨胀布局。您可以改为在 onCreateView()
中执行此操作。然后,在创建视图后,系统会调用 onViewCreated()
方法,您可以在该方法中将属性绑定到特定视图。NavGraph
:导航图是一种 XML 文件,可以直观地呈现应用内的导航。该文件由若干目的地组成,对应于各个 activity 和 fragment,以及它们之间可通过代码用来从一个目的地导航到另一个目的地的操作。与布局文件一样,Android Studio 提供了一个可视化编辑器,用于向导航图添加目的地和操作。NavHost
:NavHost
用于在 activity 内显示导航图中的目的地。当您在 fragment 之间导航时,NavHost
中显示的目的地会相应更新。您将在 MainActivity
中使用名为 NavHostFragment
的内置实现。NavController
:您可以使用 NavController
对象控制 NavHost
中显示的目的地之间的导航。在使用 intent 时,您必须通过调用 startActivity 导航到新屏幕。借助 Navigation 组件,您可以通过调用 NavController
的 navigate()
方法来交换显示的 fragment。NavController
还可帮您处理常见任务,例如响应系统“向上”按钮可回到之前显示的 fragment。build.gradle
的buildscript > ext 中,在 material_version
下,将 nav_version
设置为 2.3.1
。build.gradle
文件中,将以下内容添加到依赖项组中。在 fragment 之间传递数据时,该插件可帮助您确保类型安全。
导航图(简称 NavGraph)是应用导航的虚拟映射。每个屏幕(对您而言,就是每个 fragment)都是可能的导航“目的地”。NavGraph
可以用显示各目的地之间关系的 XML 文件来表示。在后台,它实际上会创建NavGraph
类的新实例。不过,导航图中的目的地由FragmentContainerView
向用户显示。
ViewModel
保持有效。即使所有者因配置更改(如屏幕旋转)而被销毁,ViewModel
也不会被销毁。所有者的新实例会重新连接到现有 ViewModel
实例,如下图所示:LiveData
是一种具有生命周期感知能力、可观察的数据存储器类。LiveData
的部分特性如下:LiveData
可存储数据;LiveData
是一种可存储任何类型的数据的封装容器。LiveData
是可观察的,这意味着当 LiveData
对象存储的数据发生更改时,观察器会收到通知。LiveData
具有生命周期感知能力。当您将观察器附加到 LiveData
后,观察器就会与 LifecycleOwner
(通常是 activity 或 fragment)相关联。LiveData
仅更新处于活跃生命周期状态(例如 STARTED
或 RESUMED
)的观察器。您可以在此处详细了解 LiveData
和观察。简而言之,数据绑定就是将数据(从代码)绑定到视图 + 视图绑定(将视图绑定到代码)
build.gradle(Module)
文件中的 buildFeatures
部分下,启用 dataBinding
属性。kotlin-kapt
插件。<data>
标记内添加名为 <variable>
的子标记内,声明一个名为 gameViewModel
、类型为 GameViewModel
的属性。您将使用此属性将 ViewModel
中的数据绑定到布局。gameViewModel
声明下的 <data>
标记内,添加类型为 Integer
的另一个变量,并将其命名为 maxNoOfWords
。您将使用此变量绑定到 ViewModel 中的变量,以存储每局游戏的单词数。GameFragment
中的 onViewCreated()
方法开头,初始化布局变量 gameViewModel
和 maxNoOfWords
。LiveData
是生命周期感知型可观察对象,因此您必须将生命周期所有者传递给布局。在 GameFragment
中的 onViewCreated()
方法内,在绑定变量的初始化下方添加以下代码。@
符号开头并用花括号 {}
括起来。在以下示例中,TextView
文本被设为 user
变量的 firstName
属性为了在StartFragment
中使用共享视图模型,您将使用activityViewModels()
而不是viewModels()
委托类来初始化OrderViewModel
。
分离关注点、通过模型驱动界面
ViewModel
、LiveData
和 Room
。这些组件负责处理生命周期的某些复杂情况,并帮助您避免与生命周期相关的问题。ViewModel
中。ViewModelScope
是为应用中的每个ViewModel
定义的内置协程作用域。在此作用域内启动的协程会在ViewModel
被清除时自动取消。
数据类中的每个变量都对应于 JSON 对象中的一个键名。为了匹配特定 JSON 响应中的类型,请为所有值使用 String
对象。
绑定适配器是用于为视图的自定义属性创建自定义 setter 的注解方法。
List<Schedule>
,即便底层数据已更新,系统也不会调用 submitList()
来更新界面。collect()
函数,可以使用从flow发出的新值调用视图更新函数。存储库模式是一种将数据层与应用的其余部分隔离开来的设计模式。
存储库可以解决数据源(例如持久性模型、网络服务和缓存)之间的冲突,和集中管理对这些数据做出的更改。右2图就展示了应用组件(如 activity)如何通过存储库与数据源进行交互。
注意:Android 上的数据库存储在文件系统(即磁盘)中。若要进行保存,这类数据库必须执行磁盘 I/O 操作。磁盘 I/O(即磁盘读写)速度缓慢,并且在该操作完成之前总是会阻塞当前线程。因此,您必须在 I/O 调度程序 中运行磁盘 I/O。此调度程序旨在使用withContext
(Dispatchers.IO) { ... }
将阻塞的 I/O 任务分流到共享线程池。
WorkRequest
:此类表示请求执行某些工作。您将在创建 WorkRequest
的过程中传入 Worker
。在创建 WorkRequest
时,您还可以指定 Constraints
等内容,例如运行 Worker
的时间。WorkManager
:此类实质上可以调度 WorkRequest
并使其运行。它以一种在系统资源上分散负载的方式调度 WorkRequest
,同时遵循您指定的约束条件。ConstraintLayout
中的界面元素设置 match_parent
。相反,您需要约束该视图的起始和结束边缘,并将宽度设置为 0dp
。如果将宽度设置为 0dp
,就表示告知系统不计算宽度,只尝试匹配针对视图设置的约束条件即可。您无法将match_parent
用于ConstraintLayout
中的任何视图。请改用0dp
,这意味着需要匹配约束条件。
这个类用于以语言区域敏感的方式对日期进行格式设置和解析。使用该类可以对日期进行格式设置(日期 → 文本)和解析(文本 → 日期)。
Locale
对象表示特定的地理、政治或文化区域。它表示语言/国家/地区/变体的组合。语言区域用于改变数字或日期等信息的表示方式,使其符合相应区域的惯例。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 }
<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>
<?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" />
// 第一个参数 class ItemAdapter(private val context: Context, private val dataset: List<Affirmation>) { }
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) } }
// 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) }
// 加载数据集 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)
// 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
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) }
<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”,对应按钮将始终显示在应用栏中,不会成为溢出菜单的一部分。 </!-->
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) }
// 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) } }
Log.d("MainActivity", "onCreate Called")
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
// 传 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() } }
// ... 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 implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
private val viewModel: GameViewModel by viewModels()
/* 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 })
android:text="@{gameViewModel.currentScrambledWord}"
// 将 buildFeatures { viewBinding = true } // 替换为 buildFeatures { dataBinding = true }
plugins { //... id 'kotlin-kapt' }
binding = GameFragmentBinding.inflate(inflater, container, false) // 替换为 binding = DataBindingUtil.inflate(inflater, R.layout.game_fragment, container, false)
<data> <variable name="gameViewModel" type="com.example.android.unscramble.ui.game.GameViewModel" /> </data>
<data> ... <variable name="maxNoOfWords" type="int" /> </data>
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding.gameViewModel = viewModel binding.maxNoOfWords = MAX_NO_OF_WORDS ... }
// 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
<TextView android:id="@+id/textView_unscrambled_word" ... android:text="@{gameViewModel.currentScrambledWord}" .../>
// 在上面的示例中,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>
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)}" />
// 返回到首页时,应在action的attributes中设置以下属性 app:popUpTo="@id/startFragment" // 首页id app:popUpToInclusive="true"
// 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"
android { ... compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = '1.8' } }
private const val BASE_URL = "https://android-kotlin-fun-mars-server.appspot.com" private val retrofit = Retrofit.Builder() .addConvertFactoru(ScalarsConverterFactory.create())
// mainfests/AndroidManifest.xml的<application>前 <uses-permission android:name="android.permission.INTERNET" />
// 将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()
// Coil app implementation "io.coil-kt:coil:1.1.1" // project repositories { // ... mavenCentral() }
@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}" />
// - 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"
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 ) { }
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()之前获取该实体 }
// 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() ) }
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 } } } }
// 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" ...
// 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) } }
class VideosRepository(private val database: VideosDatabase) { suspend fun refreshVideos() { withContext(Dispatchers.IO) { val playList = DevByteNetwork.devbytes.getPlaylist() database.videoDao.insertAll(playList.asDatabaseModel()) } } }
// VideosRepository // 为简单起见,使用了Livedata,建议使用Flow,因为其与生命周期无关 val videos: LiveData<List<DevByteVideo>> = Transformations.map(database.videoDao.getVideos()) { // Transformations.map() 方法使用转换函数将一个 LiveData 对象转换为另一个 LiveData 对象。仅当处于活动状态的 activity 或 fragment 在观察返回的 LiveData 属性时,才会计算转换。 it.asDomainModel() }
// viewmodel val playlist = videosRepository.videos // 存储存储库中的LiveData viewModelScope.launch { try { videosRepository.refreshVideos() // 刷新数据 // ... } catch (networkError: IOException) { // ... } }
implementation "androidx.datastore:datastore-preferences:1.0.0" implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1"
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 } } }
// 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) } }
// 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
<include layout="@layout/title" />
// 将在%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) }
// 在本例中,向字符串资源 ID 属性添加 @StringRes 注解,并向可绘制资源 ID 属性添加 @DrawableRes 注解。 // 这样一来,如果您提供错误类型的资源 ID,就会收到警告。 data class Affirmation( @StringRes val stringResourceId: Int, @DrawableRes val imageResourceId: Int )
SimpleDateFormat("E MMM d", Locale.getDefault())
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 }