Files
AndroidJava/devbricksx-android/docs/sample_notebook_tutorial_02.md
coco 7846a45f2c a
2026-07-03 15:47:27 +08:00

348 lines
14 KiB
Markdown

# Write a Notebook #2 - Display in lists
![](./assets/sample_notes.png)
In the last chapter, we explained how to define objects in a notebook application. In this chapter, you will see how to represent them on the user interfaces.
## MVVM Components
If you are familiar with the MVVM design pattern, to represent an object on user interfaces, you usually have to create several classes, such as Entity, Repository, ViewModel, etc.
![](./assets/mad.png)
Generally, you have to write these classes above by yourself. But, if you are using **DevBricksX**, it will save a lot of work on the repetitive work:
- **devbricks-java-annotaion** and **devbricks-java-compiler** generates **Entiy**, **Dao**, **Database**, and **Repository** for a specific data object.
- **devbricks-java-annotaion** and **devbricks-java-compiler** generates rest, including **ViewModal**, **Adapter**, and **Fragment**.
In the last chapter, you have already seen how **RoomCompanion** helps us to generate codes for low-level database operations.
Now, let us see how **DevBricksX** annotation processors help you to generate codes for high-level interactions.
## Your magic props
To make the magic happens, you need to prepare some magic props. They are annotations, **ViewModel**, **Adapter** and **ListFragment**. Now, we add these three annotations on booth **Notebook** and **Note**.
### 1.**Notebook**
Let us still starts with **NoteBook**, the new defition looks like this:
```kotlin
@RoomCompanion(primaryKeys = ["id"],
autoGenerate = true,
converters = [DateConverter::class],
extension = NotebookDaoExtension::class,
database = "notes",
)
@ViewModel
@Adapter(viewType = ViewType.Customized,
paged = false,
layout = R.layout.layout_notebook,
viewHolder = NotebookViewHolder::class)
@ListFragment
class Notebook(id: Int = 0) : Record(id) {
@JvmField var name: String? = null
@Ignore var notesCount: Int = 0
}
```
Compare to the declaration in the last chapter, there are three new annotations added. For each annotation, the compiler will generate a corresponding companion class.
- **ViewModel**
This annotation indicates the compiler to generate a ViewModel class. For **Notebook** class, its name is **NotebookViewModel**. It encapsulates low-level CRUD operations and exposes MAD friendly interfaces for the application, like LiveData, suspends functions, and coroutines, etc. By default, the generated **ViewModel** class will reply on the implementation provided by **NotebookRepository** which is auto-generated by **RoomCompany** annotations. If you don't use **RoomCompanion** or have your different implementation, you have to derive this auto-generated class and override parts of it.
- **Adapter**
This annotation indicates the compiler to generate an Adapter class. For **Notebook** class, its name is **NotebooksAdapter** and it is a subclass of **ListAdapter**. You can set **paged** parameter to *true* to make it derived from **PageListAdapter**.
**viewType** parameter of Notebook is **ViewType.Customized**, since we want to use our layout instead of using pre-defined ones.
For more configuration of **Adapter** annotation, please refer to [Devbricks X Kotlin Annotations](https://github.com/dailystudio/devbricksx-android/blob/master/devbricksx-kotlin-annotations/README.md#2-adapter)
- **ListFragment**
This annotation indicates the compiler to generate a fragment class. For **Notebook** class, its name is **NotebooksListFragment**. We have no extra customization of this annotation and it displays all of the notebooks in a list.
As mentioned above, we use customized layout to represent notebooks. Here is the content of **layout_notebook.xml**:
```xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:paddingStart="@dimen/lv_item_space_h"
android:paddingEnd="@dimen/lv_item_space_h"
android:paddingTop="@dimen/lv_item_space_v"
android:paddingBottom="@dimen/lv_item_space_v"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.cardview.widget.CardView
android:id="@+id/list_item_root"
style="@style/DefaultCardView"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<RelativeLayout
android:background="?attr/selectableItemBackground"
android:layout_width="match_parent"
android:layout_height="@dimen/lv_single_line_item_height">
<ImageView
android:id="@+id/list_item_icon"
style="@style/DefaultListIcon"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"/>
<TextView
android:id="@+id/notes_count"
style="@style/NotesCount"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/list_item_text_line_1st"
style="@style/DefaultListItemText1stLine"
android:layout_toEndOf="@id/list_item_icon"
android:layout_toStartOf="@id/notes_count"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_alignWithParentIfMissing="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</RelativeLayout>
</androidx.cardview.widget.CardView>
</FrameLayout>
```
For each notebook, we display its name, an icon, and the count of notes in it in a horizontal single-line layout. Correspondingly, the implementation of **NotebookViewHolder** is here:
```kotlin
class NotebookViewHolder(itemView: View)
: AbsSingleLineViewHolder<Notebook>(itemView) {
override fun bind(item: Notebook) {
super.bind(item)
val notesCountView: TextView? = itemView.findViewById(R.id.notes_count)
notesCountView?.text = if (item.notesCount == 0) {
null
} else {
item.notesCount.toString()
}
}
override fun getIcon(item: Notebook): Drawable? {
val resId = if (item.isItemSelected()) {
R.drawable.ic_selected
} else {
R.drawable.ic_notebook
}
return ResourcesCompatUtils.getDrawable(itemView.context,
R.drawable.ic_notebook)
}
override fun getText(item: Notebook): CharSequence? {
return item.name?.capitalize()
}
}
```
**NotebookViewHolder** derives from **AbsSingleLineViewHolder** which has already implemented a set of essential functions that represent an object to a single-line layout. Basically, overriding **getIcon()** and **getText** is enough. Because we also want to display the count of notes in a notebook, we need to override **bind()** to handle displaying the count on the user interface.
### 2.**Note**
Similiarly, we also add three annotations on the **Note** declaration:
```kotlin
@RoomCompanion(primaryKeys = ["id"],
autoGenerate = true,
extension = NoteDaoExtension::class,
database = "notes",
foreignKeys = [ ForeignKey(entity = Notebook::class,
parentColumns = ["id"],
childColumns = ["notebook_id"],
onDelete = ForeignKey.CASCADE
)]
)
@ViewModel
@Adapter(viewType = ViewType.Customized,
layout = R.layout.layout_note,
viewHolder = NoteViewHolder::class)
@ListFragment(layout = R.layout.fragment_recycler_view_with_new_button,
gridLayout = true)
class Note(id: Int = 0) : Record(id) {
@JvmField var notebook_id: Int = -1
@JvmField var title: String? = null
@JvmField var desc: String? = null
}
```
The configuration of these annotations is almost the same as **Notebook**. The difference is that the parameter **gridLayout** of **ListFragment** annotation is set to *true* as we want to display notes in a two-column grid.
For **Note** class, we also use a customized informative card layout. The content of **layout_note.xml** is:
```xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:paddingStart="@dimen/lv_item_space_h"
android:paddingEnd="@dimen/lv_item_space_h"
android:paddingTop="@dimen/lv_item_space_v"
android:paddingBottom="@dimen/lv_item_space_v"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.cardview.widget.CardView
android:id="@+id/list_item_root"
style="@style/DefaultCardView"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:background="?attr/selectableItemBackground"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/card_title"
style="@style/InformativeCardViewTitle"
android:maxLines="2"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/card_supporting_text"
style="@style/NoteContent"
android:layout_below="@id/card_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</RelativeLayout>
</androidx.cardview.widget.CardView>
</FrameLayout>
```
For each note, we display its title and 6 lines brief of its content. **NoteViewHolder** derives from a pre-defined class **AbsInformativeCardViewHolder**. **AbsInformativeCardViewHolder** is used to map an object to a card with a media image on the top, a title, and a small paragraph of supporting text. **NoteViewHolder** only overrides its abstract functions and return the correct information. Here are its codes:
```kotlin
class NoteViewHolder(itemView: View)
: AbsInformativeCardViewHolder<Note>(itemView) {
override fun getSupportingText(item: Note): CharSequence? {
val context = itemView.context
val text = item.desc
return if (text.isNullOrEmpty()) {
context.getString(R.string.label_empty)
} else {
text
}
}
override fun getMedia(item: Note): Drawable? {
return null
}
override fun getTitle(item: Note): CharSequence? {
return item.title?.capitalize()
}
override fun shouldDisplayDivider(): Boolean {
return true
}
}
```
### 3. **DaoExtension**
In most cases, you needn't write an extension for **Dao** class. Auto-generated **Dao** classes have already included plenty of standard functions, e.g. CRUD, querying, etc. But, for special purposes, as we used in our application, you can inject your customization by set parameter **extension** of **RoomCompanion** annotation.
Different from normal cases, we have two special requirements:
- Display notes and notebooks in reverse chronological order
- Display a count of notes for each notebook
According to this, we defined two **DaoExtention** annotated class:
```kotlin
@DaoExtension(entity = Notebook::class)
abstract class NotebookDaoExtension {
@Query("SELECT * FROM notebook ORDER BY last_modified DESC")
abstract fun getAllNotebooksOrderedByLastModifiedLivePaged(): LiveData<List<Notebook>>
}
@DaoExtension(entity = Note::class)
abstract class NoteDaoExtension {
@Query("SELECT * FROM note WHERE notebook_id = :notebookId ORDER BY last_modified DESC ")
@Page(pageSize = 50)
abstract fun getAllNotesOrderedByLastModifiedLivePaged(notebookId: Int): LiveData<PagedList<Note>>
@Query("SELECT COUNT(*) FROM note WHERE notebook_id = :notebookId")
abstract fun countNotes(notebookId: Int): Int
}
```
**DaoExtention** tells the compiler to generate helper **Dao** classes and use them in the final compilation. Eventually, generated **NoteDao** and **NotebookDao** will have these extended functions. Such kind of extension will be also passed encapsulated to higher levels, such as **Repository** and **ViewModal**.
## Last step
By default, auto-generated list fragments use the default interface in auto-generated **ViewModels** to retrieve data. For example, **NotebooksListFragment** uses **allNotebooksLive** in **NotebookViewModel** to get a list of notebooks which is sorted in ascending order of notebook identifiers.
As we have extended low-level database manipulation. Now, we need to tell list fragments to use these capabilities.
### 1. NotebooksListFragmentExt
We extend auto-generated class **NotebooksListFragment** and add override **getLiveData()** to do transformation:
```kotlin
override fun getLiveData(): LiveData<List<Notebook>> {
notebookViewModel = ViewModelProvider(this).get(NotebookViewModel::class.java)
val liveData = notebookViewModel.getAllNotebooksOrderedByLastModifiedLivePaged()
return Transformations.switchMap(liveData) { notebooks ->
val wrapper = mutableListOf<Notebook>()
val ret = MutableLiveData<List<Notebook>>(wrapper)
lifecycleScope.launch(Dispatchers.IO) {
for (notebook in notebooks) {
val noteViewModel =
ViewModelProvider(this@NotebooksFragmentExt)
.get(NoteViewModel::class.java)
notebook.notesCount = noteViewModel.countNotes(notebook.id)
wrapper.add(notebook)
}
ret.postValue(wrapper)
}
ret
}
}
```
Firstly, we retrieve a list of notebooks sorted by reverse chronological order through extended fucntion **getAllNotebooksOrderedByLastModifiedLivePaged()**. Then, for each notebook, we attach the count of notes by calling extended function **countNotes()**.
### 2. NotesFragmentExt
Extension work of **Note** is a bit simpler. We override **getLiveData()** by replace **allNotesPaged** with extended function **getAllNotesOrderedByLastModifiedLivePaged**:
```kotlin
override fun getLiveData(): LiveData<PagedList<Note>> {
notebookViewModel = ViewModelProvider(this).get(NoteViewModel::class.java)
return notebookViewModel.getAllNotesOrderedByLastModifiedLivePaged(notebookId)
}
```
## Magic happens
We are ready to watch the magic happens again. Include **NotebooksListFragmentExt** and **NotesFragmentExt** somewhere, then build the project and run it on your devices or emulators. You will see something like the image below.
![](./assets/notes_and_notebooks.png)
## Summary
Now, you are clear about how to use **DevBricksX** to display your objects. Next time, I will show how to add a notebook or note through the user interface rather than codes.