Android学习笔记六(JAVA):Fragment,生命周期感知型组件(Lifecycle-aware components)和RecyclerView
前面几篇笔记中,实现了一个简单的QuizDemo。目前所学习的知识点足够我们开发一个简单的(复杂的也OK)的计算器应用了(个人认为实现计算器的难点在于中缀表达式转换成后缀表达式,如果要实现科学计算器,能算sin,cos啥的,那就更复杂了。这是考验JAVA能力,而不是安卓了)。但是大概看一下手机上的APP,大部分没网络都没法使用,要不也是需要网络恢复后上传数据。另外,一个APP在使用过程中也经常会使用到另外一个APP,比如使用相机,具有分享功能等。因此从本篇笔记开始,我们开发一个更像那么回事儿的高级APP——Note。它运行起来大约是下面的样子。涉及到的内容包括:Fragment,RecyclerView,Navigation组件,数据库与Room持久性库,数据绑定,隐式Intent等。
1.Fragment简介与初步使用
https://developer.android.google.cn/guide/fragments?hl=zh-cn
Fragment(碎片、片段)表示应用界面中可重复使用的一部分。Fragment定义和管理自己的布局,具有自己的生命周期,并且可以处理自己的输入事件。但是Fragment不能独立存在,它必须由Activity或另一个Fragment托管。Fragment的视图层次结构会成为宿主的视图层次结构的一部分,或附加到宿主的视图层次结构。先来搞个实战,体会一下什么是fragment。
新建一个名为Note的项目,首先添加模型类,即Note.java文件。Note.java代码清单如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package com.larissa.android.note; import java.util.Date; import java.util.UUID; public class Note { private UUID mId; private String mTitle; private String mContent; private Date mDate; private String mStatus; public String getNoteLog(){ String noteLog=String.format("%s at %s",mStatus,mDate); return noteLog; } public void setNoteLog(String status){ mDate=new Date(); mStatus=status; } public UUID getId() { return mId; } public void setId(UUID id) { mId = id; } public String getTitle() { return mTitle; } public void setTitle(String title) { mTitle = title; } public String getContent() { return mContent; } public void setContent(String content) { mContent = content; } }
然后在res/layout中新建一个名为fragment_note的Android Resource File。这是后面要创建的fragment类文件的布局文件。该布局文件主要显示一条Note的详情。fragment_note.xml代码清单如下(以后不再把strings.xml中的字符串资源贴上了):
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:orientation="vertical" android:layout_margin="16dp" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:textStyle="bold" android:text="@string/title_label"/> <EditText android:id="@+id/note_title" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="Enter a title for the note"/> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/content_label" android:textStyle="bold"/> <EditText android:id="@+id/note_content" android:layout_width="match_parent" android:layout_height="wrap_content" android:inputType="textMultiLine"/> <TextView android:id="@+id/note_log" android:layout_width="match_parent" android:layout_height="wrap_content" tools:text="Created at Aug,26th,2022" android:textStyle="italic"/> </LinearLayout>
接着就是新建fragment类文件了。新建一个名为NoteFragment的java文件。NoteFragment类需要继承Fragment类。NoteFragment.java代码清单如下(注意要使用视图绑定,需要先在build.gradle(Module)里面添加viewBinding true):
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package com.larissa.android.note; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.fragment.app.Fragment; import com.larissa.android.note.databinding.FragmentNoteBinding; import java.util.UUID; public class NoteFragment extends Fragment { private static final String TAG="NoteFragment"; private Note mNote; private FragmentNoteBinding mFragmentNoteBinding; @Override public void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); // 这里先实例化一个Note类,并赋值。后面用到数据库的时候,会从数据库中查询数据。 mNote=new Note(); mNote.setTitle("NoteDemo"); mNote.setContent("This is a note demo. This is a note demo. This is note demo"); mNote.setId(UUID.randomUUID()); mNote.setNoteLog("Created"); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){ super.onCreateView(inflater,container,savedInstanceState); mFragmentNoteBinding=FragmentNoteBinding.inflate(inflater,container,false); mFragmentNoteBinding.noteTitle.setText(mNote.getTitle()); mFragmentNoteBinding.noteContent.setText(mNote.getContent()); mFragmentNoteBinding.noteLog.setText(mNote.getNoteLog()); View view=mFragmentNoteBinding.getRoot(); return view; } @Override public void onDestroyView(){ super.onDestroyView(); mFragmentNoteBinding=null; } }
好了,现在我们已经创建好了一个NoteFragment,想要使用它,需要把它托管给一个Activity。可以在Activity布局文件中使用FragmentContainerView来托管Fragment。如下方代码所示。
<?xml version="1.0" encoding="utf-8"?> <androidx.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:name="com.larissa.android.note.NoteFragment" android:id="@+id/fragment_container"> </androidx.fragment.app.FragmentContainerView>
托管的重点在于把fragment的全称赋值给android:name属性。OK,现在在模拟器上运行app,屏幕上显示了fragment_note的布局样式。如下图所示。显而易见的,这种托管方式的灵活性差。
还是在代码中动态托管Fragment比较灵活。这是用FragmentManager类和FragmentTransaction类实现的。先把activity_main.xml中的android:name属性那行代码删掉。然后在MainActivity.java中托管NoteFragement。
MainActivity.java代码清单如下:
public class MainActivity extends AppCompatActivity { private static final String TAG="MainActivity"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if(savedInstanceState==null){ getSupportFragmentManager().beginTransaction() .add(R.id.fragment_container,NoteFragment.class,null) .commit(); } } }
getSupportFragmentManager()是FragmentActivity类中的方法,用于返回一个FragmentManager对象。为什么能在MainActivity中调用这个方法呢,因为MainActivity是继承自AppCompatActivity类的,而AppCompatActivity类是继承自FragmentActivity类的。然后调用FragmentManager类的beginTransaction()方法返回FragmentTransaction抽象类(Transaction,即事务,事务就是一个完整的业务逻辑)。FragmentTransaction类提供一系列操作Fragment的API(如添加、移除、替代等)。接下来调用FragmentTransaction.add()方法,把NoteFragment托管给activity。该方法有6种调用方式,这里使用的是下图所示的这个。该方法所需的三个参数很好懂,就不多说了。最后别忘了使用commit()提交事务。
现在再次运行app,应该可以看到屏幕上显示了fragment_note.xml布局。注意到上方代码使用了savedInstanceState==null的条件判断,只有当savedInstanceState为空的时候,也就是首次创建activity的时候,才添加fragment。当配置发生变化,重新调用onCreate()方法时,fragment不需要再添加一次,因为它会自动从savedInstanceState中恢复。
2.Fragment的生命周期
在笔记四中,学习了Activity的生命周期。现在再来看下Fragment的生命周期。在Activity生命周期中,我们提到了六个回调方法,这些方法是由操作系统负责调用的,而fragment的生命周期的回调方法是由托管它的activity的FragmentManager对象负责调用的。Fragment的生命周期中,除了有之前我们见过的六个回调方法,还多了好几个,包括:onAttach(), onCreateView(), onViewCreated(), onViewStateRestored(), onDestroyView, onDetach()。调用顺序如下图所示。可见,在Fragment生命周期中,View有其独立的生命周期。
关于Activity和Fragment生命周期回调方法之间的调用顺序详情可见:https://www.cnblogs.com/larissa-0464/p/17900871.html
3.生命周期感知型组件(Lifecycle-aware components)
Fragment和Activity都有生命周期,并在其生命周期内发生状态改变。假设现在有个方法funtion_1()是在Activity A调用onStart()时发生的,按理说可以在Activity A的onStart()方法中这样写:
@Override public void onStart(){ super.onStart(); function_1(); }
那么假设现在有不同类中的好多方法都需要在Activity A的onStart()中调用,那么Activity A的onStart()方法将变得非常复杂和混乱。这种做法并不是一个好方法。如果Activity A的onStart()发生时,能够对外发出一个信号,function_1()等方法观察到这个信号就开启调用。这样就方便多了。
Android提供了生命周期感知型组件用于执行操作响应另一个组件的生命周期状态的变化。androidx.lifecycle包提供了可用于构建生命周期感知型组件的类和接口,这些组件可根据Activity或Fragment的当前生命周期状态自动调整其行为。还记得笔记四中使用过的ViewModel吗,它就在androidx.lifecycle包里。所以ViewModel能够知道与它关联的Activity是配置发生变化了还是finish()了,前者的话ViewModel要为Activity保留状态,后者的话ViewModel则跟着Activity一同消亡。
LifecycleOwner是androidx.lifecycle包中定义的一个单一方法接口,表示类具有Lifecycle。如下图所示,ComponentActivity和Fragment都实现了该接口。
Lifecycle是androidx.lifecycle包中定义的一个抽象类,用于存储有关Activity或Fragment组件的生命周期状态的信息,并允许其他对象观察此状态。Lifecycle使用事件Event和状态State枚举跟踪其关联组件的生命周期状态。
也就是说,如果现在有一个生命周期感知型组件与Activity A生命周期相关联,那么当A.onCreate()发生时,该生命周期感知组件将收到ON_CREATE事件。
生命周期感知型组件的最佳做法:
- 使界面控制器Activity和Fragment尽可能保持精简。它们不应该试图获取自己的数据,而应使用ViewModel执行此操作,并观察LiveData对象以将更改体现到视图中(LiveData将在后面的笔记中介绍)。
- 设法编写数据驱动型界面,对于此类界面,界面控制器的责任是随着数据更改而更新视图,或者将用户操作通知给ViewModel。
- 将数据逻辑放在ViewModel类中。ViewModel应充当界面控制器与应用其余部分之间的连接器。但是ViewModel不负责获取数据。它调用相应的组件来获取数据,然后将结果提供给界面控制器。
- 使用数据绑定在视图与界面控制器之间维持干净的接口。
- 如果界面很复杂,可以考虑创建presenter类来处理界面的修改。
- 避免在ViewModel中应用View或Activity上下文。
更多内容可见:https://www.cnblogs.com/larissa-0464/p/17823798.html
4.初识RecyclerView
https://developer.android.google.cn/guide/topics/ui/layout/recyclerview?hl=zh-cn
RecyclerView可以轻松高效地显示大量数据,只要提供数据并定义每个列表项的外观,RecyclerView就会根据需要动态地创建元素。比如用于显示Note清单的视图,如下方动图所示。
实现RecyclerView可以分三步走:
1.创建RecyclerView相关文件以及设计列表项的布局(也就是下图中红色框的部分)。因为RecyclerView就是这个布局按照某种方式的重复。这里某种方式称为布局管理器,RecyclerView提供了三种布局管理器,用LayoutManager类进行排列:
- LinearLayoutManager,将各个项排列在一维列表中(下图用的就是这种)。
- GridLayoutManager,将所有项排列在二维网格中。
- StaggeredGridLayoutManager,与GridLayoutManager类似,但不要求同一行中的列表项具有相同的高度或同一列中的列表项具有相同的宽度。
我们现在就先来进行这第一步。先新建一个RecyclerView的布局文件,名为fragment_note_list.xml。如下图所示,新建时,可以在Root element处选择根元素是RecyclerView,省着布局文件建好之后再修改。
fragment_note_list.xml代码清单如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<?xml version="1.0" encoding="utf-8"?> <androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/note_recycler_view" android:layout_width="match_parent" android:layout_height="match_parent"> </androidx.recyclerview.widget.RecyclerView>
然后设计列表项的布局。新建名为note_item.xml的布局文件。note_item.xml代码清单如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout 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="wrap_content"> <TextView android:id="@+id/note_title" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginTop="16dp" android:layout_marginEnd="8dp" app:layout_constraintEnd_toStartOf="@+id/note_delete" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:text="Note Title" /> <TextView android:id="@+id/note_log" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginTop="8dp" android:layout_marginEnd="8dp" app:layout_constraintEnd_toStartOf="@+id/note_delete" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/note_title" tools:text="Note Log" /> <ImageButton android:id="@+id/note_delete" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="20dp" android:layout_marginEnd="16dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" app:srcCompat="@drawable/ic_delete" /> </androidx.constraintlayout.widget.ConstraintLayout>
注意到这里用了一个垃圾桶的图标,表示可以删除选中的Note。这个图标是从哪里来的呢。只要依次点击菜单栏File->New->Vector Asset。再打开的页面中点击"Clip Art"处的图片,从Select Icon页面中搜索你想要的图标,添加到res/drawable文件夹中就可以了。
在新建NoteListFragment.java文件之前,我们先来添加一个ViewModel类,用于创建和保存Note数据。新建NoteListViewModel.java文件。为了方便观察,先新建一些Note数据。NoteListViewModel.java代码清单:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package com.larissa.android.note; import androidx.lifecycle.ViewModel; import java.util.ArrayList; import java.util.List; public class NoteListViewModel extends ViewModel { List<Note> mNotes=new ArrayList<>(); public NoteListViewModel(){ for(int i=0;i<50;i++){ Note note=new Note(); note.setTitle(String.format("Note No.%s",i)); if(i%2==0) note.setNoteLog("Created"); else note.setNoteLog("Modified"); note.setContent(String.format("This is the content of Note No.%s",i)); mNotes.add(note); } } }
接下来添加NoteListFragment.java文件。NoteListFragment.java代码清单:
public class NoteListFragment extends Fragment { private static final String TAG="NoteListFragment"; private NoteListViewModel mNoteListViewModel; private RecyclerView mNoteRecyclerView; @Override public void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); mNoteListViewModel=new ViewModelProvider(this).get(NoteListViewModel.class); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){ super.onCreateView(inflater,container,savedInstanceState); View view=inflater.inflate(R.layout.fragment_note_list,container,false); mNoteRecyclerView=(RecyclerView) view.findViewById(R.id.note_recycler_view);
// 给RecyclerView设置LinearLayoutManager布局管理器 mNoteRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); return view; } }
2.继承ViewHolder类。Adapter和ViewHolder类配合使用,共同定义数据的显示方式。ViewHolder是列表项布局View的封装容器。Adapter则会根据需要创建ViewHolder对象,以及给这些视图写入数据。将视图与其数据相关联的过程称为“绑定”。我觉得这里可以这样理解。看我们之前实现过的项目,每个布局文件都会有一个控制器,比如activity_main.xml与MainActivity.java,fragment_note.xml与NoteFragment.java,fragment_note_list.xml与NoteListFragment.java。这么看,note_item.xml缺了一个属于它的控制器。它的控制器与其他控制器不同,需要继承ViewHolder类,并且要为Adapter类提供一个绑定数据的方法。
在NoteListFragment.java中新建一个名为NoteItemHolder的私有类,继承RecyclerView.ViewHolder类。NoteItemHolder类代码清单:
private class NoteItemHolder extends RecyclerView.ViewHolder{ private TextView mTitleTextView; private TextView mLogTextView; private ImageButton mImageButton;public NoteItemHolder(View view){ super(view); mTitleTextView=(TextView) view.findViewById(R.id.note_title); mLogTextView=(TextView) view.findViewById(R.id.note_log); mImageButton=(ImageButton) view.findViewById(R.id.note_delete); } public void bind(Note note){ mTitleTextView.setText(note.getTitle()); mLogTextView.setText(note.getNoteLog()); } }
activity_main.xml与MainActivity.java的关联是通过setContentView()方法实现的,fragment_note.xml与NoteFragment.java的关联是在onCreateView()中实现的,而note_item.xml与NoteItemHolder的关联是通过构造函数的参数注入实现的。
3.现在该实现Adpater类了,用于创建ViewHolder对象,并绑定数据。 同样地新建一个名为NoteListAdapter的私有类,继承RecyclerView.Adapter类。NoteListAdapter.java代码清单如下:
private class NoteItemAdapter extends RecyclerView.Adapter<NoteItemHolder>{ private List<Note> mNotes; public NoteItemAdapter(List<Note> notes){mNotes=notes;} @NonNull @Override public NoteItemHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { // 创建ViewHolder对象 View view=getLayoutInflater().inflate(R.layout.note_item,parent,false); return new NoteItemHolder(view); } @Override public void onBindViewHolder(@NonNull NoteItemHolder holder, int position) { Note note=mNotes.get(position); holder.bind(note); } @Override public int getItemCount() { return mNotes.size(); } }
Adapter类需要实现三个方法。刚创建类的时候,会提示错误。这时候只要用alt+enter就可以自动添加这三个方法。onCreateViewHolder()方法用于创建指定类型的ViewHolder对象;onBindViewHolder()方法用于实现View对象数据的绑定;getItemCount()方法返回列表项的数量。至此,RecyclerView该完成的都已经完成了,下面回到NoteListFragment.java,调用RecyclerView并给它赋值。
NoteListFragment.java代码清单:
public class NoteListFragment extends Fragment { ... @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){ super.onCreateView(inflater,container,savedInstanceState); View view=inflater.inflate(R.layout.fragment_note_list,container,false); mNoteRecyclerView=(RecyclerView) view.findViewById(R.id.note_recycler_view); mNoteRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); updateUI(); return view; } private void updateUI(){ List<Note>notes=mNoteListViewModel.mNotes; NoteItemAdapter adapter=new NoteItemAdapter(notes);
// 给RecyclerView设置Adapter mNoteRecyclerView.setAdapter(adapter); } private class NoteItemHolder extends RecyclerView.ViewHolder{ ... } private class NoteItemAdapter extends RecyclerView.Adapter<NoteItemHolder>{ ... } }
Run app,看看是不是已经出现了本小节开头的那个动图。源代码见:https://gitee.com/larissaLiu/note_-v1
Refs:
https://developer.android.google.cn/guide/fragments/saving-state