文章目录
-
- [1. 为什么要学 Fragment?](#1. 为什么要学 Fragment?)
- [2. Fragment 是什么](#2. Fragment 是什么)
- [3. 写一个 Fragment 的步骤](#3. 写一个 Fragment 的步骤)
- [4. Fragment 的生命周期](#4. Fragment 的生命周期)
- [5. Fragment 里用 RecyclerView](#5. Fragment 里用 RecyclerView)
- [6. Fragment 之间怎么通信?](#6. Fragment 之间怎么通信?)
-
- [同一屏上的两个 Fragment:直接找](#同一屏上的两个 Fragment:直接找)
- [不在同一屏:通过 Activity 中转](#不在同一屏:通过 Activity 中转)
- [7. 把 Adapter 写成内部类](#7. 把 Adapter 写成内部类)
- [8. 总结](#8. 总结)
1. 为什么要学 Fragment?
刚学 Android 的时候,我们都会写 Activity。一个页面就是一个 Activity,跳来跳去,挺好理解。
但问题来了------你去看看平板上的新闻 APP。屏幕左边是标题列表,右边是内容详情,两个区域并排显示、各玩各的。如果还按"一个页面 = 一个 Activity"的思路写,你得分两个 Activity 还是合一个?怎么复用手机上的代码?
Fragment 就是来解决这类问题的。把界面上可以独立运作的一块区域抽成一个 Fragment,想塞进哪个 Activity 就塞进哪个。
拿新闻应用来说:
手机(小屏): 平板(大屏):
┌───────────────┐ ┌─────────┬───────────┐
│ │ │ 标题 │ 内容 │
│ 标题列表 │ 点击一条 → │ Fragment│ Fragment │
│ Fragment │ 跳新页面 │ 在同一个│ 在同一个 │
│ (占满全屏) │ │Activity │ Activity │
└───────────────┘ └─────────┴───────────┘
同一套 Fragment 代码,手机上分两屏展示,平板上并排展示。 这就是 Fragment 最核心的价值。
下面结合实际代码,一步一步说清楚 Fragment 怎么写、怎么用。
2. Fragment 是什么
Fragment 说白了就是一个"可以塞进 Activity 的 UI 组件"。它有自己的布局文件、自己的生命周期、自己的事件处理,但不能独立存在,必须挂在某个 Activity 里面。
对比一下 Activity 和 Fragment:
| Activity | Fragment | |
|---|---|---|
| 独立运行? | 能 | 不能,必须依附 Activity |
| 需要在 AndroidManifest 注册? | 需要 | 不需要 |
| 怎么拿到 Context? | this 就是 |
用 getActivity() |
| 生命周期 | 自己的 | 跟着宿主 Activity 走 |
| 启动方式 | startActivity() |
FragmentTransaction |
从 AndroidManifest.xml 里就能看出来------只注册了 Activity,Fragment 根本不用管:
xml
<application ...>
<activity android:name=".MainActivity" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".NewsContentActivity" android:exported="false" />
</application>
3. 写一个 Fragment 的步骤
最少三步:布局文件 → Fragment 类 → 放进 Activity。来看最简单的例子。
第一步,写布局文件。 和 Activity 的布局完全一样,没有任何特殊标签:
xml
<!-- news_content_frag.xml -->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/visibility_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:visibility="invisible">
<TextView
android:id="@+id/news_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="20sp" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#000" />
<TextView
android:id="@+id/news_content"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:padding="15dp"
android:textSize="18sp" />
</LinearLayout>
</RelativeLayout>
内容区域初始设为 invisible,等用户点击标题后再显示,避免一开始就显示一片空白。
第二步,写 Fragment 类。 继承 Fragment,重写 onCreateView():
java
public class NewsContentFragment extends Fragment {
private View view;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
view = inflater.inflate(R.layout.news_content_frag, container, false);
return view;
}
public void refresh(String newsTitle, String newsContent) {
View visibilityLayout = view.findViewById(R.id.visibility_layout);
visibilityLayout.setVisibility(View.VISIBLE);
TextView newsTitleText = (TextView) view.findViewById(R.id.news_title);
TextView newsContentText = (TextView) view.findViewById(R.id.news_content);
newsTitleText.setText(newsTitle);
newsContentText.setText(newsContent);
}
}
注意几个容易踩坑的地方:
inflate()的第三个参数必须传false,否则布局会被重复添加导致异常findViewById()得写在view.后面,不能直接调用,因为 Fragment 不是 Contextview要存成成员变量,后续refresh()里还要用
第三步,把 Fragment 放进 Activity。 有两种方式。
方式一,在 Activity 的 XML 里直接写 <fragment> 标签:
xml
<!-- activity_main.xml -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/news_title_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="@+id/news_title_fragment"
android:name="com.example.fragmentbestpractice.NewsTitleFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
android:name 填 Fragment 的完整类名。Activity 里啥都不用干,setContentView() 之后 Fragment 自动加载好了。
方式二,在代码里动态添加:
java
getSupportFragmentManager()
.beginTransaction()
.add(R.id.fragment_container, new NewsTitleFragment())
.commit();
两种方式怎么选?布局不会变就用静态的,简单省事;需要运行时动态切换(比如底部导航栏切 Tab)就用动态的。
4. Fragment 的生命周期
Fragment 有自己的一套生命周期,但它和宿主 Activity 的生命周期是绑在一起的。
Activity Fragment
onCreate() → onAttach()
onCreate()
onCreateView() ← 加载布局的地方
→ onActivityCreated() ← Activity 的 View 就绪了
onStart() → onStart()
onResume() → onResume() ← 界面可见,可交互
onPause() → onPause()
onStop() → onStop()
onDestroy() → onDestroyView()
onDestroy()
onDetach()
实际开发中,最常用的就两个:onCreateView() 和 onActivityCreated()。
onCreateView() 用来加载布局,相当于 Activity 的 setContentView()。
onActivityCreated() 是一个很特殊的节点------此时宿主 Activity 的 onCreate() 已经执行完了,布局里的所有控件都已经可以 findViewById 了。 所以如果要访问 Activity 布局里的控件,必须放在这里。例如判断当前是手机还是平板:
java
public class NewsTitleFragment extends Fragment {
private boolean isTwoPane;
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (getActivity().findViewById(R.id.news_content_layout) != null) {
isTwoPane = true; // 平板双页模式
} else {
isTwoPane = false; // 手机单页模式
}
}
}
这里的设计思路比硬编码判断 isTablet 高明:不去猜设备类型,而是去查当前布局里有没有某个控件。 有就是平板布局,没有就是手机布局。这种方式不管设备怎么变,代码都不用改。
5. Fragment 里用 RecyclerView
Fragment 里用 RecyclerView 和 Activity 里完全一个套路,唯一的区别是 findViewById() 要写到 view. 后面。
先看列表项的布局,就一个 TextView:
xml
<!-- news_item.xml -->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/news_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="1"
android:ellipsize="end"
android:textSize="18sp"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:paddingTop="15dp"
android:paddingBottom="15dp" />
Fragment 的布局里放一个 RecyclerView:
xml
<!-- news_title_frag.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/news_title_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
然后 onCreateView() 里初始化:
java
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.news_title_frag, container, false);
RecyclerView newsTitleRecyclerView = (RecyclerView) view.findViewById(
R.id.news_title_recycler_view);
LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity());
newsTitleRecyclerView.setLayoutManager(layoutManager);
NewsAdapter adapter = new NewsAdapter(getNews());
newsTitleRecyclerView.setAdapter(adapter);
return view;
}
三步走:设 LayoutManager → 造 Adapter → 绑上去。
再来造些测试数据:
java
private List<News> getNews() {
List<News> newsList = new ArrayList<>();
for (int i = 1; i <= 50; i++) {
News news = new News();
news.setTitle("This is news title " + i);
news.setContent(getRandomLengthContent("This is news content " + i + ". "));
newsList.add(news);
}
return newsList;
}
private String getRandomLengthContent(String content) {
Random random = new Random();
int length = random.nextInt(20) + 1;
StringBuilder builder = new StringBuilder();
for (int i = 0; i < length; i++) {
builder.append(content);
}
return builder.toString();
}
数据模型就是一个普通类:
java
public class News {
private String title;
private String content;
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
}
6. Fragment 之间怎么通信?
同一屏上的两个 Fragment:直接找
平板模式下,标题 Fragment 和内容 Fragment 在同一个 Activity 里。标题 Fragment 想刷新右边的内容,直接用 FragmentManager 捞出目标 Fragment,调它的公开方法:
java
NewsContentFragment newsContentFragment = (NewsContentFragment) getFragmentManager()
.findFragmentById(R.id.news_content_fragment);
newsContentFragment.refresh(news.getTitle(), news.getContent());
目标 Fragment 暴露一个 refresh() 方法即可。
不在同一屏:通过 Activity 中转
手机模式下,只有一个 Fragment 在屏幕上,要看详情得跳一个新 Activity。这时候数据通过 Intent 传递:
java
// NewsTitleFragment 里点击标题时
NewsContentActivity.actionStart(getActivity(), news.getTitle(), news.getContent());
把 Intent 的构造封装成静态方法是个好习惯,调用方不用关心传参细节:
java
public class NewsContentActivity extends AppCompatActivity {
public static void actionStart(Context context, String newsTitle,
String newsContent) {
Intent intent = new Intent(context, NewsContentActivity.class);
intent.putExtra("news_title", newsTitle);
intent.putExtra("news_content", newsContent);
context.startActivity(intent);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.news_content);
String newsTitle = getIntent().getStringExtra("news_title");
String newsContent = getIntent().getStringExtra("news_content");
NewsContentFragment newsContentFragment = (NewsContentFragment)
getSupportFragmentManager().findFragmentById(R.id.news_content_fragment);
newsContentFragment.refresh(newsTitle, newsContent);
}
}
整个数据流在这两条路径:
手机:NewsTitleFragment → Intent → NewsContentActivity → findFragmentById → NewsContentFragment.refresh()
平板:NewsTitleFragment → findFragmentById → NewsContentFragment.refresh()
↑
跳过 Activity 中转
7. 把 Adapter 写成内部类
Adapter 写成 Fragment 的内部类挺好用,能直接访问外部类的字段和方法:
java
public class NewsTitleFragment extends Fragment {
private boolean isTwoPane; // 外部类字段
class NewsAdapter extends RecyclerView.Adapter<NewsAdapter.ViewHolder> {
private List<News> mNewsList;
class ViewHolder extends RecyclerView.ViewHolder {
TextView newsTitleText;
public ViewHolder(View view) {
super(view);
newsTitleText = (TextView) view.findViewById(R.id.news_title);
}
}
public NewsAdapter(List<News> newsList) {
mNewsList = newsList;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.news_item, parent, false);
final ViewHolder holder = new ViewHolder(view);
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
News news = mNewsList.get(holder.getAdapterPosition());
if (isTwoPane) {
// 平板:直接刷新同屏的内容 Fragment
NewsContentFragment newsContentFragment =
(NewsContentFragment) getFragmentManager()
.findFragmentById(R.id.news_content_fragment);
newsContentFragment.refresh(news.getTitle(), news.getContent());
} else {
// 手机:跳新 Activity
NewsContentActivity.actionStart(getActivity(),
news.getTitle(), news.getContent());
}
}
});
return holder;
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
News news = mNewsList.get(position);
holder.newsTitleText.setText(news.getTitle());
}
@Override
public int getItemCount() {
return mNewsList.size();
}
}
}
isTwoPane 和 getFragmentManager() 都是外部类的,内部类直接用,不用传参。点击事件里的判断逻辑也一目了然------是平板就刷右边,是手机就跳页面。
8. 总结
回顾一下,写一个 Fragment 应用大概就这几步:
- 写布局 XML(和 Activity 没区别)
- 写 Fragment 类,继承
Fragment,重写onCreateView(),用inflate()加载布局 - 控件的
findViewById()前面要加view. - 放进 Activity:静态用
<fragment>标签,动态用FragmentTransaction - 同屏的 Fragment 之间用
findFragmentById()直接拿实例调方法 - 跨屏的通过 Intent → Activity → Fragment 传数据
<fragment>标签的android:name填类全名,不需要在AndroidManifest里注册
Fragment 说到底,就是把 UI 切成可复用的独立模块。手机上分页用,平板上拼着用,一套代码服务所有屏幕。理解了这个,基本就能上手了。