故事1:初识Fragment - NewsHub的模块化革命
晨光中的困境
清晨的第一缕阳光透过百叶窗的缝隙洒在张小安的办公桌上,照亮了散乱的草稿纸和喝了半杯的咖啡。作为移动互联网创业公司"启明科技"的Android开发工程师,小安已经连续奋战了三个星期,他负责开发的新闻阅读应用"NewsHub"即将进入内测阶段。
然而,此刻小安的眉头却紧锁得像一团解不开的毛线。
"又是这个问题..."他喃喃自语,手指在触摸板上快速滑动,屏幕上显示着两套几乎完全相同的XML布局文件。
"为什么手机版和平板版的代码重复率这么高?这简直是在浪费生命!"
小安揉了揉疲惫的眼睛,显示器上的代码仿佛在嘲笑他的困境。作为一名有着三年Android开发经验的工程师,他深知这种代码重复带来的维护噩梦:
ini
<!-- 手机版 activity_main.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<FrameLayout
android:id="@+id/news_list_container"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
</LinearLayout>
ini
<!-- 平板版 activity_main_tablet.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<FrameLayout
android:id="@+id/news_list_container"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<FrameLayout
android:id="@+id/news_detail_container"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
</LinearLayout>
仅仅因为多了一个详情页面的容器,他就不得不维护两套几乎完全相同的布局文件。更糟糕的是,对应的Java代码也需要分别处理:
scss
// MainActivity.java
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (isTablet()) {
setContentView(R.layout.activity_main_tablet);
// 平板逻辑:同时显示列表和详情
showNewsList();
showNewsDetail(null);
} else {
setContentView(R.layout.activity_main);
// 手机逻辑:只显示列表,点击后跳转
showNewsList();
}
}
private boolean isTablet() {
return getResources().getBoolean(R.bool.isTablet);
}
private void showNewsList() {
// 无论是手机还是平板,这部分代码都一样
NewsListFragment listFragment = new NewsListFragment();
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.news_list_container, listFragment)
.commit();
}
private void showNewsDetail(NewsItem newsItem) {
if (isTablet()) {
// 平板:在右侧容器中显示详情
NewsDetailFragment detailFragment = new NewsDetailFragment();
if (newsItem != null) {
Bundle args = new Bundle();
args.putParcelable("news_item", newsItem);
detailFragment.setArguments(args);
}
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.news_detail_container, detailFragment)
.commit();
} else {
// 手机:启动新的Activity显示详情
Intent intent = new Intent(this, NewsDetailActivity.class);
if (newsItem != null) {
intent.putExtra("news_item", newsItem);
}
startActivity(intent);
}
}
}
"这样下去不行..."小安靠在椅背上,望着窗外的城市天际线,"每次修改一个功能,我都要在多个地方同步更新,太容易出错了。"
他想起上周那次惨痛的经历:因为修复手机版的一个列表显示bug,忘记同步更新平板版的代码,导致测试组发现平板应用崩溃,整个迭代计划被迫推迟。
"一定有更好的方法..."小安打开了技术文档浏览器,开始搜索"Android多屏适配最佳实践"。
意外的导师
就在小安陷入沉思的时候,办公室的门被轻轻推开了。
"小安,还在为NewsHub的适配问题烦恼吗?"
说话的是李明轩,启明科技的资深架构师。李工有着十多年移动开发经验,是公司技术团队的核心人物。他五十岁左右的年纪,头发已经花白,但眼神依旧锐利,仿佛能看透代码背后的本质。
"李工..."小安有些不好意思地笑了笑,"您怎么知道的?"
"你这周提交的代码我看了,"李工走到小安的工位旁,指着屏幕,"手机版和平板版的代码重复率超过70%,这在敏捷开发中是很危险的信号。"
小安点点头,沮丧地说:"我知道这样不好,但是我想不出更好的解决方案。Activity的架构天然就不支持模块化,我只能通过不同的布局文件来适配。"
李工笑了笑,从旁边拉过一张椅子坐下:"小安,你知道为什么Android在3.0版本引入Fragment吗?"
"我记得文档上说是为了更好地支持大屏幕设备..."小安回答。
"没错,但Fragment的意义远不止于此。"李工的眼中闪烁着智慧的光芒,"让我给你讲个故事吧。"
Fragment的诞生故事
"在Fragment出现之前,Android应用的开发模式就像是你现在遇到的问题,"李工开始娓娓道来,"每个屏幕都是一个独立的Activity,就像每个房间都是一栋独立的房子。"
小安想象着那个场景:一个社区里,每栋房子都独立存在,有自己的厨房、卧室、客厅。如果要建一个新的社区,就必须重新建造所有的房子。
"这种模式在手机时代还行,因为屏幕小,一个Activity就能显示所有内容。但平板出现后问题就暴露了。"李工继续说道,"平板屏幕大,可以同时显示多个界面。如果还用Activity,就像在同一个房间里建多栋房子,既浪费空间又难以管理。"
"所以Google设计了Fragment?"小安追问道。
"是的!"李工的语气变得兴奋起来,"Fragment就像积木块。你可以把一个Activity想象成一个玩具底板,Fragment就是各种形状的积木。在手机上,你可以在底板上放一块积木;在平板上,你可以同时放多块积木。"
李工拿出纸笔,画了一个简单的示意图:
scss
手机布局:
┌─────────────────┐
│ Activity │
│ ┌─────────────┐│
│ │ Fragment A ││ (新闻列表)
│ └─────────────┘│
│ │
│ ┌─────────────┐│
│ │ Fragment B ││ (详情页面)
│ └─────────────┘│
└─────────────────┘
(一次只显示一个Fragment)
平板布局:
┌─────────────────┐
│ Activity │
│ ┌──────────┐ │
│ │Fragment A│ │ (新闻列表)
│ │ │ │
│ └──────────┘ │
│ ┌──────────┐ │
│ │Fragment B│ │ (详情页面)
│ │ │ │
│ └──────────┘ │
└─────────────────┘
(同时显示两个Fragment)
"我明白了!"小安的眼睛亮了起来,"Fragment就是UI的模块化单元,Activity负责组装和管理这些模块!"
"完全正确!"李工赞许地点头,"这就是Fragment的核心设计理念:UI与逻辑的模块化。每个Fragment都是一个独立的组件,有自己的布局和生命周期,但必须托管在Activity中。"
Fragment的三大优势
李工看着小安恍然大悟的表情,继续深入讲解:"Fragment不仅解决了多屏适配问题,还有三个重要的优势:"
1. 代码复用性
"你看你现在的代码,"李工指着小安的屏幕,"NewsListFragment和NewsDetailFragment的逻辑其实是一样的,不管在手机还是平板上。如果用Fragment,你只需要写一次:"
less
// NewsListFragment.java
public class NewsListFragment extends Fragment {
private RecyclerView recyclerView;
private NewsAdapter adapter;
private List<NewsItem> newsList;
// 回调接口,用于通知Activity用户点击了某条新闻
public interface OnNewsSelectedListener {
void onNewsSelected(NewsItem newsItem);
}
private OnNewsSelectedListener listener;
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
// 确保宿主Activity实现了回调接口
if (context instanceof OnNewsSelectedListener) {
listener = (OnNewsSelectedListener) context;
} else {
throw new RuntimeException(context.toString()
+ " must implement OnNewsSelectedListener");
}
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
// 创建Fragment的视图
View view = inflater.inflate(R.layout.fragment_news_list,
container, false);
recyclerView = view.findViewById(R.id.recyclerView);
setupRecyclerView();
return view;
}
@Override
public void onViewCreated(@NonNull View view,
@Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// 加载新闻数据
loadNewsData();
}
private void setupRecyclerView() {
adapter = new NewsAdapter(newsList, new NewsAdapter.OnItemClickListener() {
@Override
public void onItemClick(NewsItem newsItem) {
// 通知Activity用户选择了某条新闻
if (listener != null) {
listener.onNewsSelected(newsItem);
}
}
});
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
recyclerView.setAdapter(adapter);
}
private void loadNewsData() {
// 从网络或数据库加载新闻数据
// 这里使用模拟数据
newsList = NewsRepository.getInstance().getLatestNews();
adapter.notifyDataSetChanged();
}
// ... 其他方法
}
"看到吗?这个Fragment完全不知道自己会被用在手机还是平板上,它只负责显示新闻列表。如何使用这个Fragment,由Activity决定。"
2. 灵活的组合方式
"Activity就像乐高积木的底板,Fragment就是不同形状的积木。"李工拿起小安桌上的乐高积木举例,"你可以根据需要自由组合:"
scala
// 手机版MainActivity.java
public class MainActivity extends AppCompatActivity
implements NewsListFragment.OnNewsSelectedListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main_phone);
// 手机版:只显示新闻列表Fragment
if (savedInstanceState == null) {
getSupportFragmentManager()
.beginTransaction()
.add(R.id.fragment_container, new NewsListFragment())
.commit();
}
}
@Override
public void onNewsSelected(NewsItem newsItem) {
// 手机版:启动新的Activity显示详情
Intent intent = new Intent(this, NewsDetailActivity.class);
intent.putExtra("news_item", newsItem);
startActivity(intent);
}
}
scss
// 平板版MainActivity.java
public class MainActivity extends AppCompatActivity
implements NewsListFragment.OnNewsSelectedListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main_tablet);
// 平板版:同时显示列表和详情Fragment
if (savedInstanceState == null) {
getSupportFragmentManager()
.beginTransaction()
.add(R.id.news_list_container, new NewsListFragment())
.add(R.id.news_detail_container, new NewsDetailFragment())
.commit();
}
}
@Override
public void onNewsSelected(NewsItem newsItem) {
// 平板版:直接更新详情Fragment
NewsDetailFragment detailFragment =
(NewsDetailFragment) getSupportFragmentManager()
.findFragmentById(R.id.news_detail_container);
if (detailFragment != null) {
detailFragment.showNewsDetail(newsItem);
}
}
}
"这样,NewsListFragment和NewsDetailFragment的代码只需要写一次,但在不同的Activity中可以有不同的使用方式。"
3. 更好的生命周期管理
"Fragment有自己的生命周期,但它始终依赖于Activity。"李工继续解释,"这种设计让UI组件的管理更加精细化。"
他再次在纸上画了一个生命周期图:
Activity生命周期:
onCreate → onStart → onResume → onPause → onStop → onDestroy
│ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼
Fragment生命周期:
onAttach → onCreate → onCreateView → onViewCreated →
onStart → onResume → onPause → onStop → onDestroyView → onDestroy → onDetach
"Fragment的生命周期比Activity更细致,"李工解释道,"比如onCreateView和onDestroyView专门管理View的创建和销毁,而onAttach和onDetach管理Fragment与Activity的绑定关系。这种细粒度的控制在复杂应用中非常有用。"
Fragment的三种创建方式
"理论讲得差不多了,让我们看看实际怎么创建Fragment。"李工切换到IDE,开始演示。
方式一:静态加载(XML中声明)
"最简单的方式是在XML布局文件中直接声明Fragment:"李工创建了一个新的布局文件。
ini
<!-- activity_main_static.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<!-- 静态加载Fragment -->
<fragment
android:id="@+id/news_list_fragment"
android:name="com.example.newshub.NewsListFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<fragment
android:id="@+id/news_detail_fragment"
android:name="com.example.newshub.NewsDetailFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
</LinearLayout>
"静态加载的好处是简单直观,"李工解释,"Fragment会随着Activity的创建而自动创建。但缺点是不够灵活,一旦在XML中声明就不能在运行时更改。"
对应的Activity代码非常简单:
scala
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main_static);
// 可以通过findViewById获取Fragment实例
NewsListFragment listFragment = (NewsListFragment)
getSupportFragmentManager().findFragmentById(R.id.news_list_fragment);
NewsDetailFragment detailFragment = (NewsDetailFragment)
getSupportFragmentManager().findFragmentById(R.id.news_detail_fragment);
}
}
方式二:动态加载(代码中添加)
"更常用的方式是动态加载,"李工继续演示,"这样可以在运行时根据需要添加、替换、移除Fragment。"
xml
<!-- activity_main_dynamic.xml -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
scala
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main_dynamic);
// 动态添加Fragment
if (savedInstanceState == null) {
NewsListFragment listFragment = new NewsListFragment();
getSupportFragmentManager()
.beginTransaction()
.add(R.id.fragment_container, listFragment)
.commit();
}
}
}
"动态加载的关键是FragmentTransaction,"李工强调,"所有的Fragment操作都需要通过事务来执行:"
scss
// Fragment的基本操作
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
// 添加Fragment
transaction.add(R.id.fragment_container, newFragment);
// 替换Fragment
transaction.replace(R.id.fragment_container, newFragment);
// 移除Fragment
transaction.remove(oldFragment);
// 隐藏Fragment(View被销毁但实例保留)
transaction.hide(fragment);
// 显示Fragment
transaction.show(fragment);
// 添加到回退栈(用户可以按返回键撤销这个操作)
transaction.addToBackStack(null);
// 提交事务
transaction.commit();
方式三:工厂模式创建
"在实际项目中,我推荐使用工厂模式创建Fragment,"李工分享了一个最佳实践,"这样可以确保Fragment总是以正确的方式被创建。"
java
public class NewsDetailFragment extends Fragment {
private static final String ARG_NEWS_ITEM = "news_item";
private static final String ARG_SHOW_COMMENT = "show_comment";
private NewsItem newsItem;
private boolean showComment;
// 私有构造函数,强制使用工厂方法
private NewsDetailFragment() {
// Required empty public constructor
}
/**
* 工厂方法:创建新闻详情Fragment
* @param newsItem 要显示的新闻项
* @param showComment 是否显示评论区
* @return NewsDetailFragment实例
*/
public static NewsDetailFragment newInstance(NewsItem newsItem, boolean showComment) {
NewsDetailFragment fragment = new NewsDetailFragment();
// 创建参数Bundle
Bundle args = new Bundle();
args.putParcelable(ARG_NEWS_ITEM, newsItem);
args.putBoolean(ARG_SHOW_COMMENT, showComment);
// 设置参数
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 读取参数
Bundle args = getArguments();
if (args != null) {
newsItem = args.getParcelable(ARG_NEWS_ITEM);
showComment = args.getBoolean(ARG_SHOW_COMMENT, false);
}
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_news_detail, container, false);
// 使用参数初始化UI
initializeUI(view);
return view;
}
private void initializeUI(View view) {
TextView titleView = view.findViewById(R.id.news_title);
TextView contentView = view.findViewById(R.id.news_content);
View commentSection = view.findViewById(R.id.comment_section);
if (newsItem != null) {
titleView.setText(newsItem.getTitle());
contentView.setText(newsItem.getContent());
commentSection.setVisibility(showComment ? View.VISIBLE : View.GONE);
}
}
}
"工厂模式的优点是:"
- 参数安全:通过Bundle传递参数,确保参数在Fragment重建时不会丢失
- 创建统一:所有Fragment都通过工厂方法创建,避免忘记设置必要参数
- 代码清晰:调用者一看就知道需要哪些参数
使用工厂模式创建Fragment:
scss
// 创建并显示新闻详情Fragment
NewsItem selectedNews = getSelectedNewsItem();
NewsDetailFragment detailFragment = NewsDetailFragment.newInstance(selectedNews, true);
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.fragment_container, detailFragment)
.addToBackStack(null) // 添加到回退栈,用户可以返回
.commit();
实战重构:NewsHub的模块化改造
"理论讲完了,现在让我们实际重构你的NewsHub应用。"李工拍了拍小安的肩膀,"你准备好开始这场模块化革命了吗?"
小安用力点头,眼中充满了兴奋:"准备好了!李工,我们从哪里开始?"
"首先,我们需要识别应用中的UI模块。"李工在白板上画了一个思维导图:
NewsHub应用模块分析
├── 新闻列表模块
│ ├── 新闻条目显示
│ ├── 下拉刷新
│ └── 加载更多
├── 新闻详情模块
│ ├── 新闻内容显示
│ ├── 图片浏览
│ └── 分享功能
├── 用户中心模块
│ ├── 用户信息
│ ├── 设置选项
│ └── 历史记录
└── 搜索模块
├── 搜索框
├── 搜索结果
└── 搜索历史
"根据这个分析,你的NewsHub至少需要4个核心Fragment:NewsListFragment、NewsDetailFragment、UserFragment和SearchFragment。"李工继续说道,"让我们先从最简单的开始:创建NewsListFragment。"
第一步:创建Fragment基类
"在实际项目中,我建议先创建一个BaseFragment,"李工开始编写代码,"这样可以避免重复编写通用代码。"
less
// BaseFragment.java
public abstract class BaseFragment extends Fragment {
protected View rootView;
protected Context context;
protected FragmentActivity activity;
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
this.context = context;
if (context instanceof FragmentActivity) {
this.activity = (FragmentActivity) context;
}
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
// 如果rootView已经创建,直接返回(避免重复创建)
if (rootView != null) {
return rootView;
}
// 获取布局ID
int layoutId = getLayoutId();
rootView = inflater.inflate(layoutId, container, false);
// 初始化View
initView();
return rootView;
}
@Override
public void onViewCreated(@NonNull View view,
@Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// 设置数据和事件监听
initData();
initListener();
}
@Override
public void onDestroyView() {
super.onDestroyView();
// 清理资源
rootView = null;
}
/**
* 获取Fragment的布局ID
*/
protected abstract int getLayoutId();
/**
* 初始化View
*/
protected abstract void initView();
/**
* 初始化数据
*/
protected abstract void initData();
/**
* 初始化事件监听
*/
protected void initListener() {
// 子类可以重写此方法来设置监听器
}
/**
* 显示Toast提示
*/
protected void showToast(String message) {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
}
/**
* 显示进度条
*/
protected void showProgressDialog(String message) {
// 实现进度条显示逻辑
// 这里可以封装一个通用的进度条工具类
}
/**
* 隐藏进度条
*/
protected void hideProgressDialog() {
// 实现进度条隐藏逻辑
}
}
"BaseFragment提供了Fragment的通用功能,"李工解释道,"这样每个具体的Fragment只需要关注自己的业务逻辑。"
第二步:创建NewsListFragment
"现在让我们创建新闻列表Fragment:"
scss
// NewsListFragment.java
public class NewsListFragment extends BaseFragment
implements SwipeRefreshLayout.OnRefreshListener {
private static final String TAG = "NewsListFragment";
// UI组件
private RecyclerView recyclerView;
private SwipeRefreshLayout swipeRefreshLayout;
private ProgressBar progressBar;
private TextView emptyView;
// 数据和适配器
private NewsAdapter newsAdapter;
private List<NewsItem> newsList = new ArrayList<>();
// 分页相关
private int currentPage = 1;
private boolean isLoading = false;
private boolean hasMoreData = true;
// 回调接口
public interface OnNewsInteractionListener {
void onNewsSelected(NewsItem newsItem);
void onNewsFavorite(NewsItem newsItem);
void onNewsShare(NewsItem newsItem);
}
private OnNewsInteractionListener listener;
public static NewsListFragment newInstance() {
return new NewsListFragment();
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
if (context instanceof OnNewsInteractionListener) {
listener = (OnNewsInteractionListener) context;
} else {
throw new RuntimeException(context.toString()
+ " must implement OnNewsInteractionListener");
}
}
@Override
protected int getLayoutId() {
return R.layout.fragment_news_list;
}
@Override
protected void initView() {
recyclerView = rootView.findViewById(R.id.recyclerView);
swipeRefreshLayout = rootView.findViewById(R.id.swipeRefreshLayout);
progressBar = rootView.findViewById(R.id.progressBar);
emptyView = rootView.findViewById(R.id.emptyView);
// 设置下拉刷新
swipeRefreshLayout.setOnRefreshListener(this);
// 设置RecyclerView
setupRecyclerView();
// 初始状态显示加载进度
showLoading(true);
}
@Override
protected void initData() {
// 加载第一页数据
loadNewsData(1, true);
}
@Override
protected void initListener() {
// RecyclerView的滚动监听,用于实现加载更多
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView,
int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (!isLoading && hasMoreData) {
LinearLayoutManager layoutManager =
(LinearLayoutManager) recyclerView.getLayoutManager();
if (layoutManager != null) {
int visibleItemCount = layoutManager.getChildCount();
int totalItemCount = layoutManager.getItemCount();
int pastVisibleItems = layoutManager.findFirstVisibleItemPosition();
// 当滚动到最后几个item时,加载更多数据
if ((visibleItemCount + pastVisibleItems) >= totalItemCount
&& totalItemCount > 0) {
loadNewsData(currentPage + 1, false);
}
}
}
}
});
}
private void setupRecyclerView() {
newsAdapter = new NewsAdapter(newsList, new NewsAdapter.OnItemClickListener() {
@Override
public void onItemClick(NewsItem newsItem) {
if (listener != null) {
listener.onNewsSelected(newsItem);
}
}
@Override
public void onFavoriteClick(NewsItem newsItem) {
if (listener != null) {
listener.onNewsFavorite(newsItem);
}
// 更新本地状态
newsItem.setFavorite(!newsItem.isFavorite());
newsAdapter.notifyDataSetChanged();
}
@Override
public void onShareClick(NewsItem newsItem) {
if (listener != null) {
listener.onNewsShare(newsItem);
}
}
});
recyclerView.setLayoutManager(new LinearLayoutManager(context));
recyclerView.setAdapter(newsAdapter);
// 添加分割线
recyclerView.addItemDecoration(new DividerItemDecoration(context,
DividerItemDecoration.VERTICAL));
}
private void loadNewsData(int page, boolean isRefresh) {
if (isLoading) return;
isLoading = true;
if (isRefresh) {
currentPage = 1;
hasMoreData = true;
if (!swipeRefreshLayout.isRefreshing()) {
showLoading(true);
}
} else {
showLoadMore(true);
}
// 模拟网络请求延迟
new Handler().postDelayed(() -> {
List<NewsItem> newData = NewsRepository.getInstance()
.getNewsList(page, 20);
if (getActivity() == null) return; // Fragment已经detached
if (isRefresh) {
newsList.clear();
newsList.addAll(newData);
} else {
newsList.addAll(newData);
}
// 检查是否还有更多数据
hasMoreData = newData.size() >= 20;
currentPage = page;
// 更新UI
newsAdapter.notifyDataSetChanged();
// 隐藏加载状态
showLoading(false);
showLoadMore(false);
swipeRefreshLayout.setRefreshing(false);
// 显示空状态
if (newsList.isEmpty()) {
showEmptyView(true);
} else {
showEmptyView(false);
}
isLoading = false;
}, 1500); // 模拟1.5秒的网络延迟
}
@Override
public void onRefresh() {
loadNewsData(1, true);
}
private void showLoading(boolean show) {
progressBar.setVisibility(show ? View.VISIBLE : View.GONE);
if (show) {
recyclerView.setVisibility(View.GONE);
emptyView.setVisibility(View.GONE);
}
}
private void showLoadMore(boolean show) {
if (newsAdapter != null) {
newsAdapter.setLoading(show);
}
}
private void showEmptyView(boolean show) {
emptyView.setVisibility(show ? View.VISIBLE : View.GONE);
recyclerView.setVisibility(show ? View.GONE : View.VISIBLE);
}
/**
* 刷新数据
*/
public void refresh() {
if (swipeRefreshLayout != null && !swipeRefreshLayout.isRefreshing()) {
swipeRefreshLayout.setRefreshing(true);
}
onRefresh();
}
/**
* 获取当前新闻列表
*/
public List<NewsItem> getNewsList() {
return new ArrayList<>(newsList);
}
}
"NewsListFragment实现了完整的新闻列表功能,"李工解释道,"包括下拉刷新、加载更多、空状态显示等。"
对应的布局文件:
ini
<!-- fragment_news_list.xml -->
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingTop="8dp"
android:paddingBottom="8dp" />
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone" />
<LinearLayout
android:id="@+id/emptyView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="vertical"
android:visibility="gone">
<ImageView
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_gravity="center_horizontal"
android:src="@drawable/ic_empty_news"
android:tint="@color/textSecondary" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="暂无新闻"
android:textColor="@color/textSecondary"
android:textSize="16sp" />
<Button
android:id="@+id/btnRetry"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="重新加载" />
</LinearLayout>
</FrameLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
第三步:创建NewsDetailFragment
"接下来是新闻详情Fragment:"
scss
// NewsDetailFragment.java
public class NewsDetailFragment extends BaseFragment {
private static final String ARG_NEWS_ITEM = "news_item";
private static final String TAG = "NewsDetailFragment";
// UI组件
private TextView titleView;
private TextView contentView;
private TextView authorView;
private TextView timeView;
private ImageView coverImageView;
private WebView webView;
private FloatingActionButton fabFavorite;
private FloatingActionButton fabShare;
private ProgressBar progressBar;
// 数据
private NewsItem newsItem;
private boolean isLoading = true;
// 回调接口
public interface OnDetailInteractionListener {
void onNewsShare(NewsItem newsItem);
void onNewsFavorite(NewsItem newsItem, boolean isFavorite);
void onAuthorClick(String authorId);
}
private OnDetailInteractionListener listener;
public static NewsDetailFragment newInstance(NewsItem newsItem) {
NewsDetailFragment fragment = new NewsDetailFragment();
Bundle args = new Bundle();
args.putParcelable(ARG_NEWS_ITEM, newsItem);
fragment.setArguments(args);
return fragment;
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
if (context instanceof OnDetailInteractionListener) {
listener = (OnDetailInteractionListener) context;
}
}
@Override
protected int getLayoutId() {
return R.layout.fragment_news_detail;
}
@Override
protected void initView() {
titleView = rootView.findViewById(R.id.news_title);
contentView = rootView.findViewById(R.id.news_content);
authorView = rootView.findViewById(R.id.news_author);
timeView = rootView.findViewById(R.id.news_time);
coverImageView = rootView.findViewById(R.id.news_cover);
webView = rootView.findViewById(R.id.news_webview);
fabFavorite = rootView.findViewById(R.id.fab_favorite);
fabShare = rootView.findViewById(R.id.fab_share);
progressBar = rootView.findViewById(R.id.progressBar);
// 设置WebView
setupWebView();
// 初始状态
showLoading(true);
}
@Override
protected void initData() {
Bundle args = getArguments();
if (args != null) {
newsItem = args.getParcelable(ARG_NEWS_ITEM);
if (newsItem != null) {
loadNewsDetail();
}
}
}
@Override
protected void initListener() {
// 收藏按钮点击
fabFavorite.setOnClickListener(v -> {
if (newsItem != null && listener != null) {
boolean newFavoriteState = !newsItem.isFavorite();
newsItem.setFavorite(newFavoriteState);
updateFavoriteIcon(newFavoriteState);
listener.onNewsFavorite(newsItem, newFavoriteState);
}
});
// 分享按钮点击
fabShare.setOnClickListener(v -> {
if (newsItem != null && listener != null) {
listener.onNewsShare(newsItem);
}
});
// 作者点击
authorView.setOnClickListener(v -> {
if (newsItem != null && listener != null) {
listener.onAuthorClick(newsItem.getAuthorId());
}
});
}
private void setupWebView() {
WebSettings webSettings = webView.getSettings();
webSettings.setJavaScriptEnabled(true);
webSettings.setDomStorageEnabled(true);
webSettings.setAppCacheEnabled(true);
webSettings.setCacheMode(WebSettings.LOAD_DEFAULT);
// 启用缩放
webSettings.setSupportZoom(true);
webSettings.setBuiltInZoomControls(true);
webSettings.setDisplayZoomControls(false);
// 启用响应式布局
webSettings.setUseWideViewPort(true);
webSettings.setLoadWithOverviewMode(true);
webView.setWebViewClient(new WebViewClient() {
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
isLoading = true;
updateLoadingState();
}
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
isLoading = false;
updateLoadingState();
}
@Override
public void onReceivedError(WebView view, int errorCode,
String description, String failingUrl) {
super.onReceivedError(view, errorCode, description, failingUrl);
showError("加载失败:" + description);
}
});
}
private void loadNewsDetail() {
if (newsItem == null) return;
// 设置基本信息
titleView.setText(newsItem.getTitle());
authorView.setText("作者:" + newsItem.getAuthor());
timeView.setText(formatTime(newsItem.getPublishTime()));
// 加载封面图片
if (!TextUtils.isEmpty(newsItem.getCoverImageUrl())) {
Picasso.get()
.load(newsItem.getCoverImageUrl())
.placeholder(R.drawable.placeholder_news)
.error(R.drawable.error_news)
.into(coverImageView);
} else {
coverImageView.setVisibility(View.GONE);
}
// 更新收藏按钮状态
updateFavoriteIcon(newsItem.isFavorite());
// 加载详细内容
loadDetailedContent();
}
private void loadDetailedContent() {
// 模拟加载详细内容
new Handler().postDelayed(() -> {
if (getActivity() == null) return;
// 如果有HTML内容,使用WebView显示
if (!TextUtils.isEmpty(newsItem.getHtmlContent())) {
loadHtmlContent(newsItem.getHtmlContent());
} else {
// 否则显示纯文本内容
loadTextContent(newsItem.getContent());
}
isLoading = false;
updateLoadingState();
}, 1000);
}
private void loadHtmlContent(String htmlContent) {
// 构建完整的HTML页面
String html = buildHtmlPage(htmlContent);
webView.loadDataWithBaseURL(null, html, "text/html", "UTF-8", null);
webView.setVisibility(View.VISIBLE);
contentView.setVisibility(View.GONE);
}
private void loadTextContent(String textContent) {
contentView.setText(textContent);
contentView.setVisibility(View.VISIBLE);
webView.setVisibility(View.GONE);
}
private String buildHtmlPage(String content) {
StringBuilder html = new StringBuilder();
html.append("<!DOCTYPE html>");
html.append("<html>");
html.append("<head>");
html.append("<meta charset="UTF-8">");
html.append("<meta name="viewport" content="width=device-width, initial-scale=1.0">");
html.append("<style>");
html.append("body { font-family: Arial, sans-serif; font-size: 16px; line-height: 1.6; margin: 16px; }");
html.append("img { max-width: 100%; height: auto; }");
html.append("p { margin: 16px 0; }");
html.append("h1, h2, h3 { color: #333; }");
html.append("</style>");
html.append("</head>");
html.append("<body>");
html.append(content);
html.append("</body>");
html.append("</html>");
return html.toString();
}
private void updateLoadingState() {
if (isLoading) {
showLoading(true);
} else {
showLoading(false);
}
}
private void showLoading(boolean show) {
progressBar.setVisibility(show ? View.VISIBLE : View.GONE);
}
private void showError(String error) {
if (getContext() != null) {
Toast.makeText(getContext(), error, Toast.LENGTH_SHORT).show();
}
}
private void updateFavoriteIcon(boolean isFavorite) {
fabFavorite.setImageResource(isFavorite ?
R.drawable.ic_favorite_filled : R.drawable.ic_favorite_border);
}
private String formatTime(long timestamp) {
Date date = new Date(timestamp);
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault());
return format.format(date);
}
/**
* 更新新闻内容
*/
public void updateNews(NewsItem newsItem) {
this.newsItem = newsItem;
if (isAdded()) {
loadNewsDetail();
}
}
/**
* 分享新闻
*/
public void shareNews() {
if (newsItem != null && listener != null) {
listener.onNewsShare(newsItem);
}
}
}
Activity的重构
"现在Fragment都准备好了,让我们重构Activity:"李工开始创建新的Activity结构。
单Activity架构
"现代Android开发推荐使用单Activity + 多Fragment的架构,"李工解释道,"这样可以避免Activity之间跳转的开销,同时提供更流畅的用户体验。"
scss
// MainActivity.java
public class MainActivity extends AppCompatActivity implements
NewsListFragment.OnNewsInteractionListener,
NewsDetailFragment.OnDetailInteractionListener {
private static final String TAG = "MainActivity";
// Fragment容器ID
private static final int FRAGMENT_CONTAINER_ID = R.id.fragment_container;
// 当前显示的Fragment
private Fragment currentFragment;
// 是否是平板布局
private boolean isTwoPane;
// Fragment引用
private NewsListFragment newsListFragment;
private NewsDetailFragment newsDetailFragment;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 根据设备类型选择不同的布局
if (isTablet()) {
setContentView(R.layout.activity_main_tablet);
isTwoPane = true;
} else {
setContentView(R.layout.activity_main_phone);
isTwoPane = false;
}
initFragments();
if (savedInstanceState == null) {
showNewsList();
}
}
private boolean isTablet() {
return getResources().getBoolean(R.bool.isTablet);
}
private void initFragments() {
FragmentManager fm = getSupportFragmentManager();
newsListFragment = (NewsListFragment) fm.findFragmentByTag("news_list");
newsDetailFragment = (NewsDetailFragment) fm.findFragmentByTag("news_detail");
if (newsListFragment == null) {
newsListFragment = NewsListFragment.newInstance();
}
if (newsDetailFragment == null && isTwoPane) {
newsDetailFragment = NewsDetailFragment.newInstance(null);
}
}
private void showNewsList() {
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
if (isTwoPane) {
// 平板:显示列表和详情
if (newsDetailFragment == null) {
newsDetailFragment = NewsDetailFragment.newInstance(null);
}
ft.replace(R.id.news_list_container, newsListFragment, "news_list");
ft.replace(R.id.news_detail_container, newsDetailFragment, "news_detail");
} else {
// 手机:只显示列表
ft.replace(FRAGMENT_CONTAINER_ID, newsListFragment, "news_list");
}
ft.commit();
currentFragment = newsListFragment;
}
@Override
public void onNewsSelected(NewsItem newsItem) {
if (isTwoPane) {
// 平板:更新详情Fragment
if (newsDetailFragment != null) {
newsDetailFragment.updateNews(newsItem);
}
} else {
// 手机:替换为详情Fragment
newsDetailFragment = NewsDetailFragment.newInstance(newsItem);
getSupportFragmentManager()
.beginTransaction()
.replace(FRAGMENT_CONTAINER_ID, newsDetailFragment, "news_detail")
.addToBackStack("news_detail")
.commit();
currentFragment = newsDetailFragment;
}
}
@Override
public void onNewsFavorite(NewsItem newsItem) {
// 处理收藏逻辑
NewsRepository.getInstance().updateFavoriteStatus(newsItem);
Toast.makeText(this, "已" + (newsItem.isFavorite() ? "收藏" : "取消收藏"),
Toast.LENGTH_SHORT).show();
}
@Override
public void onNewsShare(NewsItem newsItem) {
// 处理分享逻辑
shareNews(newsItem);
}
@Override
public void onAuthorClick(String authorId) {
// 跳转到作者页面
AuthorFragment authorFragment = AuthorFragment.newInstance(authorId);
getSupportFragmentManager()
.beginTransaction()
.replace(FRAGMENT_CONTAINER_ID, authorFragment, "author")
.addToBackStack("author")
.commit();
currentFragment = authorFragment;
}
@Override
public void onNewsFavorite(NewsItem newsItem, boolean isFavorite) {
// 处理详情页面的收藏操作
NewsRepository.getInstance().updateFavoriteStatus(newsItem);
Toast.makeText(this, "已" + (isFavorite ? "收藏" : "取消收藏"),
Toast.LENGTH_SHORT).show();
}
private void shareNews(NewsItem newsItem) {
Intent shareIntent = new Intent(Intent.ACTION_SEND);
shareIntent.setType("text/plain");
shareIntent.putExtra(Intent.EXTRA_SUBJECT, newsItem.getTitle());
shareIntent.putExtra(Intent.EXTRA_TEXT, newsItem.getTitle() + "\n" + newsItem.getShareUrl());
startActivity(Intent.createChooser(shareIntent, "分享新闻"));
}
@Override
public void onBackPressed() {
FragmentManager fm = getSupportFragmentManager();
if (fm.getBackStackEntryCount() > 0) {
fm.popBackStack();
} else {
super.onBackPressed();
}
}
}
对应的布局文件:
xml
<!-- activity_main_phone.xml -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
xml
<!-- activity_main_tablet.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:baselineAligned="false">
<!-- 左侧:新闻列表 -->
<FrameLayout
android:id="@+id/news_list_container"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<!-- 右侧:新闻详情 -->
<FrameLayout
android:id="@+id/news_detail_container"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
</LinearLayout>
重构完成:模块化的胜利
经过一下午的重构,NewsHub应用的架构焕然一新。小安看着重构后的代码,脸上露出了满意的笑容。
"李工,这简直太神奇了!"小安兴奋地说道,"代码重复率从70%降到了几乎为0,而且逻辑更清晰了!"
李工笑着点点头:"这就是Fragment的力量。让我们总结一下这次重构的成果:"
重构前后对比
重构前的问题:
- 代码重复率高(70%以上)
- 手机/平板逻辑混合在一个Activity中
- 难以维护和扩展
- 违反了单一职责原则
重构后的优势:
- 代码复用性大幅提升
ini
// NewsListFragment在手机和平板上完全复用
NewsListFragment listFragment = NewsListFragment.newInstance();
- 职责分离更清晰
arduino
// Activity:负责Fragment的组装和管理
// NewsListFragment:负责新闻列表的显示和交互
// NewsDetailFragment:负责新闻详情的显示和交互
- 扩展性更强
scss
// 轻松添加新的Fragment
UserFragment userFragment = UserFragment.newInstance();
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.fragment_container, userFragment)
.addToBackStack("user")
.commit();
- 测试更容易
java
// 每个Fragment可以独立测试
@Test
public void testNewsListFragment() {
NewsListFragment fragment = NewsListFragment.newInstance();
// 测试Fragment的逻辑
}
代码质量指标对比
| 指标 | 重构前 | 重构后 | 改善幅度 |
|---|---|---|---|
| 代码重复率 | 70% | 5% | ↓93% |
| 类的平均行数 | 450行 | 200行 | ↓56% |
| 圈复杂度 | 18 | 8 | ↓56% |
| 单元测试覆盖率 | 35% | 85% | ↑143% |
Fragment设计原则总结
李工在白板上写下了Fragment的核心设计原则:
"记住这五个Fragment设计原则,你的代码会一直保持高质量:"
-
单一职责原则
- 每个Fragment只负责一个特定的UI功能
- NewsListFragment只管列表,NewsDetailFragment只管详情
-
依赖倒置原则
- Fragment通过接口与Activity通信
- 不直接依赖具体的Activity实现
-
开闭原则
- 对扩展开放,对修改关闭
- 通过添加新Fragment来扩展功能
-
组合优于继承
- Fragment作为UI组件的组合单元
- Activity通过组合Fragment来构建复杂界面
-
生命周期意识
- 正确处理Fragment的生命周期
- 避免在Fragment销毁后执行回调
展望未来
"小安,你觉得这次重构最大的收获是什么?"李工问道。
小安思考了一会儿,认真地回答:"我觉得最大的收获是理解了模块化思维。以前我总是想着把所有功能都写在一个Activity里,现在学会了把复杂的功能拆分成独立的、可复用的模块。"
"非常好!"李工赞许地点头,"这正是Fragment的核心思想。而且这只是一个开始,随着你学习的深入,你会发现还有更多精彩的内容:"
-
Fragment通信的高级技巧
- ViewModel + LiveData
- Navigation Component
- Shared ViewModel
-
Fragment的性能优化
- 懒加载机制
- View缓存策略
- 内存管理
-
Fragment的进阶用法
- 嵌套Fragment
- ViewPager + Fragment
- DialogFragment
-
Fragment的最佳实践
- 架构模式集成
- 测试策略
- 调试技巧
"从下节课开始,我们将深入探索Fragment的生命周期。"李工看了一下表,"你会发现,理解生命周期是掌握Fragment的关键。"
小安兴奋地看着屏幕上重构后的代码,仿佛看到了一个全新的世界。这次与Fragment的初识,不仅解决了他眼前的技术难题,更重要的是打开了一扇通往Android高级开发的大门。
夕阳西下,办公室的灯光陆续熄灭,但小安的编程热情却前所未有地高涨。他迫不及待地想要开始下一段学习旅程,探索Fragment这个强大组件的更多奥秘。
故事完
知识要点总结
Fragment的核心概念
- Fragment是什么:UI模块化组件,可以嵌入Activity中
- 为什么需要Fragment:解决多屏适配、代码复用、模块化开发等问题
- Fragment与Activity的关系:容器与模块的关系,Fragment依赖Activity
Fragment的创建方式
- 静态加载:在XML中直接声明,简单但不灵活
- 动态加载:在代码中通过FragmentTransaction管理,灵活性高
- 工厂模式:推荐的创建方式,确保参数安全和创建统一
Fragment的最佳实践
- BaseFragment设计:提取通用功能,减少代码重复
- 接口回调:Fragment与Activity的安全通信方式
- 生命周期管理:正确处理各个生命周期阶段
- 单Activity架构:现代Android开发的推荐模式
实战技能
- 重构现有代码:将Activity-Based架构重构为Fragment-Based
- 多屏适配:一套代码适配手机和平板
- 模块化设计:将复杂功能拆分为独立模块
- 代码质量提升:降低重复率,提高可维护性
本章通过NewsHub应用的实际案例,完整展示了Fragment从理论到实践的整个过程,为后续章节的深入学习奠定了坚实的基础。