修复了界面中的样式一些问题,还有内存泄漏等问题

This commit is contained in:
ShiQi 2025-12-22 18:11:21 +08:00
parent a6b56bceb1
commit d3854e20c8
21 changed files with 814 additions and 26 deletions

View File

@ -72,4 +72,7 @@ dependencies {
implementation("androidx.media3:media3-exoplayer:$media3Version")
implementation("androidx.media3:media3-exoplayer-hls:$media3Version")
implementation("androidx.media3:media3-ui:$media3Version")
// 内存泄漏检测 (仅在debug版本中使用)
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.13")
}

View File

@ -5,6 +5,7 @@
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<application
android:name=".LiveStreamingApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="Live Streaming"

View File

@ -0,0 +1,181 @@
package com.example.livestreaming;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
/**
* 统一的空状态视图组件
* 支持自定义图标标题描述和操作按钮
*/
public class EmptyStateView extends ConstraintLayout {
private ImageView iconView;
private TextView titleView;
private TextView messageView;
private Button actionButton;
public EmptyStateView(@NonNull Context context) {
super(context);
init(context, null);
}
public EmptyStateView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public EmptyStateView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
LayoutInflater.from(context).inflate(R.layout.view_empty_state, this, true);
iconView = findViewById(R.id.emptyIcon);
titleView = findViewById(R.id.emptyTitle);
messageView = findViewById(R.id.emptyMessage);
actionButton = findViewById(R.id.emptyActionButton);
if (attrs != null) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.EmptyStateView);
try {
// 从XML属性设置内容
String title = a.getString(R.styleable.EmptyStateView_emptyTitle);
String message = a.getString(R.styleable.EmptyStateView_emptyMessage);
String actionText = a.getString(R.styleable.EmptyStateView_emptyActionText);
Drawable icon = a.getDrawable(R.styleable.EmptyStateView_emptyIcon);
if (title != null) setTitle(title);
if (message != null) setMessage(message);
if (actionText != null) setActionText(actionText);
if (icon != null) setIcon(icon);
} finally {
a.recycle();
}
}
}
/**
* 设置空状态图标
*/
public void setIcon(@DrawableRes int iconRes) {
iconView.setImageResource(iconRes);
}
/**
* 设置空状态图标
*/
public void setIcon(Drawable icon) {
iconView.setImageDrawable(icon);
}
/**
* 设置标题
*/
public void setTitle(String title) {
titleView.setText(title);
}
/**
* 设置描述信息
*/
public void setMessage(String message) {
messageView.setText(message);
}
/**
* 设置操作按钮文本
*/
public void setActionText(String text) {
actionButton.setText(text);
actionButton.setVisibility(View.VISIBLE);
}
/**
* 隐藏操作按钮
*/
public void hideActionButton() {
actionButton.setVisibility(View.GONE);
}
/**
* 设置操作按钮点击监听器
*/
public void setOnActionClickListener(OnClickListener listener) {
actionButton.setOnClickListener(listener);
}
/**
* 设置为网络错误状态
*/
public void setNetworkErrorState() {
setIcon(R.drawable.ic_globe_24);
setTitle("网络连接失败");
setMessage("请检查网络连接后重试");
setActionText("重试");
}
/**
* 设置为无直播间状态
*/
public void setNoRoomsState() {
setIcon(R.drawable.ic_home_24);
setTitle("暂无直播间");
setMessage("当前没有正在直播的房间,快去创建一个吧");
setActionText("刷新");
}
/**
* 设置为无消息状态
*/
public void setNoMessagesState() {
setIcon(R.drawable.ic_chat_24);
setTitle("暂无消息");
setMessage("还没有人给你发消息,快去和朋友聊天吧");
hideActionButton();
}
/**
* 设置为无搜索结果状态
*/
public void setNoSearchResultsState() {
setIcon(R.drawable.ic_search_24);
setTitle("没有找到相关内容");
setMessage("换个关键词试试吧");
hideActionButton();
}
/**
* 设置为无好友状态
*/
public void setNoFriendsState() {
setIcon(R.drawable.ic_people_24);
setTitle("暂无好友");
setMessage("去添加一些好友一起玩耍吧");
setActionText("去添加");
}
/**
* 设置为无作品状态
*/
public void setNoWorksState() {
setIcon(R.drawable.ic_person_24);
setTitle("暂无作品");
setMessage("快来发布你的精彩作品吧");
setActionText("去发布");
}
}

View File

@ -0,0 +1,85 @@
package com.example.livestreaming;
import android.content.Context;
import android.view.View;
import androidx.annotation.NonNull;
import com.google.android.material.snackbar.Snackbar;
/**
* 统一的错误处理工具类
* 提供统一的错误提示和重试机制
*/
public class ErrorHandler {
/**
* 显示网络错误Snackbar带重试按钮
*/
public static void showNetworkErrorWithRetry(@NonNull View view, @NonNull Runnable retryAction) {
showErrorWithRetry(view, "网络连接失败,请检查网络后重试", "重试", retryAction);
}
/**
* 显示服务器错误Snackbar带重试按钮
*/
public static void showServerErrorWithRetry(@NonNull View view, @NonNull Runnable retryAction) {
showErrorWithRetry(view, "服务器响应异常,请稍后重试", "重试", retryAction);
}
/**
* 显示加载失败Snackbar带重试按钮
*/
public static void showLoadErrorWithRetry(@NonNull View view, @NonNull Runnable retryAction) {
showErrorWithRetry(view, "加载失败,请重试", "重试", retryAction);
}
/**
* 显示通用错误Snackbar带重试按钮
*/
public static void showErrorWithRetry(@NonNull View view, @NonNull String message,
@NonNull String actionText, @NonNull Runnable retryAction) {
Snackbar snackbar = Snackbar.make(view, message, Snackbar.LENGTH_LONG);
snackbar.setAction(actionText, v -> retryAction.run());
snackbar.setActionTextColor(view.getContext().getResources().getColor(R.color.purple_500));
snackbar.show();
}
/**
* 显示简单错误提示不带重试按钮
*/
public static void showSimpleError(@NonNull View view, @NonNull String message) {
Snackbar.make(view, message, Snackbar.LENGTH_SHORT).show();
}
/**
* 显示成功提示
*/
public static void showSuccess(@NonNull View view, @NonNull String message) {
Snackbar snackbar = Snackbar.make(view, message, Snackbar.LENGTH_SHORT);
snackbar.setBackgroundTint(view.getContext().getResources().getColor(R.color.purple_500));
snackbar.show();
}
/**
* 显示信息提示
*/
public static void showInfo(@NonNull View view, @NonNull String message) {
Snackbar.make(view, message, Snackbar.LENGTH_SHORT).show();
}
/**
* 处理API响应错误
*/
public static void handleApiError(@NonNull View view, Throwable error, @NonNull Runnable retryAction) {
String message;
if (error.getMessage() != null && error.getMessage().contains("timeout")) {
message = "请求超时,请检查网络连接";
} else if (error.getMessage() != null && error.getMessage().contains("Unable to resolve host")) {
message = "网络连接失败,请检查网络设置";
} else {
message = "请求失败,请稍后重试";
}
showErrorWithRetry(view, message, "重试", retryAction);
}
}

View File

@ -447,4 +447,20 @@ public class FishPondActivity extends AppCompatActivity {
stopOrbitAnimation();
stopPulseLoop();
}
@Override
protected void onDestroy() {
super.onDestroy();
// 清理Handler和Runnable防止内存泄漏
if (uiHandler != null) {
uiHandler.removeCallbacks(pulseRunnable);
}
// 确保动画完全停止和清理
if (orbitAnimator != null) {
orbitAnimator.cancel();
orbitAnimator = null;
}
}
}

View File

@ -0,0 +1,20 @@
package com.example.livestreaming;
import android.app.Application;
import android.app.Application;
/**
* 自定义Application类用于初始化各种组件
* 包括内存泄漏检测等
*/
public class LiveStreamingApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
// 初始化LeakCanary内存泄漏检测仅在debug版本中生效
// LeakCanary会自动在debug版本中初始化无需手动调用
}
}

View File

@ -17,6 +17,7 @@ import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import android.widget.Toast;
@ -37,6 +38,8 @@ import com.example.livestreaming.databinding.ActivityMainBinding;
import com.example.livestreaming.databinding.DialogCreateRoomBinding;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.textfield.MaterialAutoCompleteTextView;
import com.google.android.material.textfield.TextInputLayout;
import com.example.livestreaming.net.ApiClient;
import com.example.livestreaming.net.ApiResponse;
import com.example.livestreaming.net.CreateRoomRequest;
@ -213,12 +216,13 @@ public class MainActivity extends AppCompatActivity {
// 立即显示演示数据提升用户体验
allRooms.clear();
allRooms.addAll(buildDemoRooms(12));
allRooms.addAll(buildDemoRooms(20));
applyCategoryFilter(currentCategory);
}
private void setupUI() {
binding.swipeRefresh.setOnRefreshListener(this::fetchRooms);
// 注释掉下拉刷新使用静态数据
// binding.swipeRefresh.setOnRefreshListener(this::fetchRooms);
setupDrawerCards();
@ -307,7 +311,8 @@ public class MainActivity extends AppCompatActivity {
if (lastVisible >= total - 4) {
long now = System.currentTimeMillis();
if (!isFetching && now - lastFetchMs > 1500) {
fetchRooms();
// 注释掉加载更多使用静态数据
// fetchRooms();
}
}
}
@ -587,6 +592,13 @@ public class MainActivity extends AppCompatActivity {
@Override
protected void onDestroy() {
super.onDestroy();
// 清理Handler和Runnable防止内存泄漏
if (handler != null && pollRunnable != null) {
handler.removeCallbacks(pollRunnable);
}
// 清理语音识别器
if (speechRecognizer != null) {
speechRecognizer.destroy();
speechRecognizer = null;
@ -660,7 +672,8 @@ public class MainActivity extends AppCompatActivity {
@Override
protected void onStart() {
super.onStart();
startPolling();
// 注释掉网络轮询使用静态数据
// startPolling();
}
@Override
@ -701,11 +714,24 @@ public class MainActivity extends AppCompatActivity {
}
private void showCreateRoomDialog() {
DialogCreateRoomBinding dialogBinding = DialogCreateRoomBinding.inflate(getLayoutInflater());
View dialogView = getLayoutInflater().inflate(R.layout.dialog_create_room, null);
DialogCreateRoomBinding dialogBinding = DialogCreateRoomBinding.bind(dialogView);
// 设置直播类型选择器
String[] liveTypes = {"游戏", "才艺", "户外", "音乐", "美食", "聊天"};
ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_dropdown_item_1line, liveTypes);
// 使用正确的资源ID获取typeSpinner
int typeSpinnerId = getResources().getIdentifier("typeSpinner", "id", getPackageName());
MaterialAutoCompleteTextView typeSpinner = dialogView.findViewById(typeSpinnerId);
if (typeSpinner != null) {
typeSpinner.setAdapter(adapter);
}
AlertDialog dialog = new AlertDialog.Builder(this)
.setTitle("创建直播间")
.setView(dialogBinding.getRoot())
.setView(dialogView)
.setNegativeButton("取消", null)
.setPositiveButton("创建", null)
.create();
@ -713,7 +739,7 @@ public class MainActivity extends AppCompatActivity {
dialog.setOnShowListener(d -> {
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> {
String title = dialogBinding.titleEdit.getText() != null ? dialogBinding.titleEdit.getText().toString().trim() : "";
String streamer = dialogBinding.streamerEdit.getText() != null ? dialogBinding.streamerEdit.getText().toString().trim() : "";
String type = (typeSpinner != null && typeSpinner.getText() != null) ? typeSpinner.getText().toString().trim() : "";
if (TextUtils.isEmpty(title)) {
dialogBinding.titleLayout.setError("标题不能为空");
@ -722,16 +748,28 @@ public class MainActivity extends AppCompatActivity {
dialogBinding.titleLayout.setError(null);
}
if (TextUtils.isEmpty(streamer)) {
dialogBinding.streamerLayout.setError("主播名称不能为空");
if (TextUtils.isEmpty(type)) {
int typeLayoutId = getResources().getIdentifier("typeLayout", "id", getPackageName());
TextInputLayout typeLayout = dialogView.findViewById(typeLayoutId);
if (typeLayout != null) {
typeLayout.setError("请先选择直播类型");
}
return;
} else {
dialogBinding.streamerLayout.setError(null);
int typeLayoutId = getResources().getIdentifier("typeLayout", "id", getPackageName());
TextInputLayout typeLayout = dialogView.findViewById(typeLayoutId);
if (typeLayout != null) {
typeLayout.setError(null);
}
}
// 获取用户昵称
String streamerName = getSharedPreferences("profile_prefs", MODE_PRIVATE)
.getString("profile_name", "未知用户");
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
ApiClient.getService().createRoom(new CreateRoomRequest(title, streamer))
ApiClient.getService().createRoom(new CreateRoomRequest(title, streamerName, type))
.enqueue(new Callback<ApiResponse<Room>>() {
@Override
public void onResponse(Call<ApiResponse<Room>> call, Response<ApiResponse<Room>> response) {
@ -852,26 +890,39 @@ public class MainActivity extends AppCompatActivity {
private void fetchRooms() {
// 避免重复请求
if (isFetching) return;
isFetching = true;
lastFetchMs = System.currentTimeMillis();
// 隐藏空状态和错误状态
hideEmptyState();
hideErrorState();
// 只在没有数据时显示loading
if (adapter.getItemCount() == 0) {
binding.loading.setVisibility(View.VISIBLE);
}
ApiClient.getService().getRooms().enqueue(new Callback<ApiResponse<List<Room>>>() {
Call<ApiResponse<List<Room>>> call = ApiClient.getService().getRooms();
NetworkUtils.enqueueWithLifecycle(call, this, new Callback<ApiResponse<List<Room>>>() {
@Override
public void onResponse(Call<ApiResponse<List<Room>>> call, Response<ApiResponse<List<Room>>> response) {
binding.loading.setVisibility(View.GONE);
binding.swipeRefresh.setRefreshing(false);
isFetching = false;
ApiResponse<List<Room>> body = response.body();
List<Room> rooms = body != null && body.getData() != null ? body.getData() : Collections.emptyList();
if (rooms == null || rooms.isEmpty()) {
rooms = buildDemoRooms(12);
// 使用演示数据但显示空状态
rooms = buildDemoRooms(0); // 生成0个演示房间
showNoRoomsState();
} else {
// 有真实数据隐藏空状态
hideEmptyState();
}
allRooms.clear();
allRooms.addAll(rooms);
applyCategoryFilter(currentCategory);
@ -883,18 +934,63 @@ public class MainActivity extends AppCompatActivity {
binding.loading.setVisibility(View.GONE);
binding.swipeRefresh.setRefreshing(false);
isFetching = false;
// 显示网络错误Snackbar和空状态
ErrorHandler.handleApiError(binding.getRoot(), t, () -> fetchRooms());
showNetworkErrorState();
// 仍然提供演示数据作为后备
allRooms.clear();
allRooms.addAll(buildDemoRooms(12));
allRooms.addAll(buildDemoRooms(0));
applyCategoryFilter(currentCategory);
adapter.bumpCoverOffset();
}
});
}
/**
* 显示无直播间空状态
*/
private void showNoRoomsState() {
if (binding.emptyStateView != null) {
binding.emptyStateView.setNoRoomsState();
binding.emptyStateView.setOnActionClickListener(v -> fetchRooms());
binding.emptyStateView.setVisibility(View.VISIBLE);
}
}
/**
* 显示网络错误状态
*/
private void showNetworkErrorState() {
if (binding.emptyStateView != null) {
binding.emptyStateView.setNetworkErrorState();
binding.emptyStateView.setOnActionClickListener(v -> fetchRooms());
binding.emptyStateView.setVisibility(View.VISIBLE);
}
}
/**
* 隐藏空状态视图
*/
private void hideEmptyState() {
if (binding.emptyStateView != null) {
binding.emptyStateView.setVisibility(View.GONE);
}
}
/**
* 隐藏错误状态实际上和hideEmptyState一样
*/
private void hideErrorState() {
hideEmptyState();
}
private void applyCategoryFilter(String category) {
String c = category != null ? category : "推荐";
if ("推荐".equals(c)) {
adapter.submitList(new ArrayList<>(allRooms));
updateEmptyStateForList(allRooms);
return;
}
@ -906,6 +1002,25 @@ public class MainActivity extends AppCompatActivity {
}
}
adapter.submitList(filtered);
updateEmptyStateForList(filtered);
}
/**
* 根据列表数据更新空状态显示
*/
private void updateEmptyStateForList(List<Room> rooms) {
if (rooms == null || rooms.isEmpty()) {
// 显示分类筛选无结果的状态
if (binding.emptyStateView != null) {
binding.emptyStateView.setIcon(R.drawable.ic_search_24);
binding.emptyStateView.setTitle("该分类下暂无直播间");
binding.emptyStateView.setMessage("换个分类看看吧");
binding.emptyStateView.hideActionButton();
binding.emptyStateView.setVisibility(View.VISIBLE);
}
} else {
hideEmptyState();
}
}
private String getDemoCategoryForRoom(Room room) {
@ -921,13 +1036,55 @@ public class MainActivity extends AppCompatActivity {
private List<Room> buildDemoRooms(int count) {
List<Room> list = new ArrayList<>();
for (int i = 0; i < count; i++) {
// 预定义的演示数据包含不同类型的直播内容
String[][] demoData = {
{"王者荣耀排位赛", "小明选手", "游戏", "true"},
{"吃鸡大逃杀", "游戏高手", "游戏", "true"},
{"唱歌连麦", "音乐达人", "音乐", "true"},
{"户外直播", "旅行者", "户外", "false"},
{"美食制作", "厨神小李", "美食", "true"},
{"才艺表演", "舞蹈小妹", "才艺", "true"},
{"聊天交友", "暖心姐姐", "聊天", "false"},
{"LOL竞技场", "电竞选手", "游戏", "true"},
{"古风演奏", "琴师小王", "音乐", "true"},
{"健身教学", "教练张", "户外", "false"},
{"摄影分享", "摄影师", "户外", "true"},
{"宠物秀", "萌宠主播", "才艺", "true"},
{"编程教学", "码农老王", "聊天", "false"},
{"读书分享", "书虫小妹", "聊天", "true"},
{"手工制作", "手艺人", "才艺", "true"},
{"英语口语", "外教老师", "聊天", "false"},
{"魔术表演", "魔术师", "才艺", "true"},
{"街头访谈", "记者小张", "户外", "true"},
{"乐器教学", "音乐老师", "音乐", "false"},
{"电影解说", "影评人", "聊天", "true"}
};
for (int i = 0; i < count && i < demoData.length; i++) {
String id = "demo-" + i;
String title = "王者荣耀陪练" + (i + 1);
String streamer = "虚拟主播" + (i + 1);
boolean live = i % 3 != 0;
list.add(new Room(id, title, streamer, live));
String title = demoData[i][0];
String streamer = demoData[i][1];
String type = demoData[i][2];
boolean live = Boolean.parseBoolean(demoData[i][3]);
Room room = new Room(id, title, streamer, live);
room.setType(type);
list.add(room);
}
// 如果需要更多数据继续生成
String[] categories = new String[]{"游戏", "才艺", "户外", "音乐", "美食", "聊天"};
for (int i = demoData.length; i < count; i++) {
String id = "demo-" + i;
String title = "直播房间" + (i + 1);
String streamer = "主播" + (i + 1);
String type = categories[i % categories.length];
boolean live = i % 3 != 0;
Room room = new Room(id, title, streamer, live);
room.setType(type);
list.add(room);
}
return list;
}

View File

@ -129,6 +129,9 @@ public class MessagesActivity extends AppCompatActivity {
conversations.addAll(buildDemoConversations());
conversationsAdapter.submitList(new ArrayList<>(conversations));
// 检查是否需要显示空状态
updateEmptyState();
attachSwipeToDelete(binding.conversationsRecyclerView);
}
@ -459,4 +462,20 @@ public class MessagesActivity extends AppCompatActivity {
UnreadMessageManager.updateBadge(binding.bottomNavInclude.bottomNavigation);
}
}
/**
* 更新空状态显示
*/
private void updateEmptyState() {
if (conversations.isEmpty()) {
if (binding.emptyStateView != null) {
binding.emptyStateView.setNoMessagesState();
binding.emptyStateView.setVisibility(View.VISIBLE);
}
} else {
if (binding.emptyStateView != null) {
binding.emptyStateView.setVisibility(View.GONE);
}
}
}
}

View File

@ -3,6 +3,7 @@ package com.example.livestreaming;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.View;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
@ -59,6 +60,7 @@ public class MyFriendsActivity extends AppCompatActivity {
private void applyFilter(String query) {
if (query == null || query.trim().isEmpty()) {
adapter.submitList(new ArrayList<>(all));
updateEmptyState(all);
return;
}
String q = query.toLowerCase();
@ -72,6 +74,23 @@ public class MyFriendsActivity extends AppCompatActivity {
}
}
adapter.submitList(filtered);
updateEmptyState(filtered);
}
/**
* 更新空状态显示
*/
private void updateEmptyState(List<FriendItem> friends) {
if (friends == null || friends.isEmpty()) {
if (binding.emptyStateView != null) {
binding.emptyStateView.setNoFriendsState();
binding.emptyStateView.setVisibility(View.VISIBLE);
}
} else {
if (binding.emptyStateView != null) {
binding.emptyStateView.setVisibility(View.GONE);
}
}
}
private List<FriendItem> buildDemoFriends() {

View File

@ -0,0 +1,78 @@
package com.example.livestreaming;
import androidx.annotation.NonNull;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleEventObserver;
import androidx.lifecycle.LifecycleOwner;
import java.util.HashSet;
import java.util.Set;
import retrofit2.Call;
/**
* 网络请求管理器
* 用于管理Activity/Fragment的网络请求在生命周期结束时自动取消
*/
public class NetworkRequestManager implements LifecycleEventObserver {
private final Set<Call<?>> activeCalls = new HashSet<>();
/**
* 添加网络请求到管理器
*/
public synchronized void addCall(Call<?> call) {
if (call != null && !call.isCanceled()) {
activeCalls.add(call);
}
}
/**
* 从管理器中移除网络请求
*/
public synchronized void removeCall(Call<?> call) {
if (call != null) {
activeCalls.remove(call);
}
}
/**
* 取消所有活跃的网络请求
*/
public synchronized void cancelAll() {
for (Call<?> call : activeCalls) {
if (call != null && !call.isCanceled()) {
call.cancel();
}
}
activeCalls.clear();
}
/**
* 获取活跃请求的数量
*/
public synchronized int getActiveCallCount() {
return activeCalls.size();
}
/**
* 绑定到LifecycleOwner在DESTROY时自动取消所有请求
*/
public void bindToLifecycle(LifecycleOwner lifecycleOwner) {
lifecycleOwner.getLifecycle().addObserver(this);
}
/**
* 解绑Lifecycle
*/
public void unbindFromLifecycle(LifecycleOwner lifecycleOwner) {
lifecycleOwner.getLifecycle().removeObserver(this);
}
@Override
public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) {
if (event == Lifecycle.Event.ON_DESTROY) {
cancelAll();
}
}
}

View File

@ -0,0 +1,53 @@
package com.example.livestreaming;
import androidx.annotation.NonNull;
import androidx.lifecycle.LifecycleOwner;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
/**
* 网络工具类
* 提供生命周期感知的网络请求管理
*/
public class NetworkUtils {
/**
* 执行网络请求并自动管理生命周期
* 当LifecycleOwner销毁时自动取消请求
*/
public static <T> void enqueueWithLifecycle(
@NonNull Call<T> call,
@NonNull LifecycleOwner lifecycleOwner,
@NonNull Callback<T> callback) {
// 创建请求管理器并绑定到生命周期
NetworkRequestManager manager = new NetworkRequestManager();
manager.bindToLifecycle(lifecycleOwner);
manager.addCall(call);
// 执行请求
call.enqueue(new Callback<T>() {
@Override
public void onResponse(Call<T> call, Response<T> response) {
manager.removeCall(call);
callback.onResponse(call, response);
}
@Override
public void onFailure(Call<T> call, Throwable t) {
manager.removeCall(call);
callback.onFailure(call, t);
}
});
}
/**
* 简单的网络请求执行不带生命周期管理
* 注意需要手动管理请求取消
*/
public static <T> void enqueue(@NonNull Call<T> call, @NonNull Callback<T> callback) {
call.enqueue(callback);
}
}

View File

@ -298,11 +298,12 @@ public class RoomDetailActivity extends AppCompatActivity {
binding.loading.setVisibility(View.VISIBLE);
}
ApiClient.getService().getRoom(roomId).enqueue(new Callback<ApiResponse<Room>>() {
Call<ApiResponse<Room>> call = ApiClient.getService().getRoom(roomId);
NetworkUtils.enqueueWithLifecycle(call, this, new Callback<ApiResponse<Room>>() {
@Override
public void onResponse(Call<ApiResponse<Room>> call, Response<ApiResponse<Room>> response) {
if (isFinishing() || isDestroyed()) return;
binding.loading.setVisibility(View.GONE);
isFirstLoad = false;
@ -452,5 +453,22 @@ public class RoomDetailActivity extends AppCompatActivity {
return url.substring(0, url.length() - ".m3u8".length()) + "/index.m3u8";
}
@Override
protected void onDestroy() {
super.onDestroy();
// 确保Handler回调被清理防止内存泄漏
if (handler != null) {
if (pollRunnable != null) {
handler.removeCallbacks(pollRunnable);
}
if (chatSimulationRunnable != null) {
handler.removeCallbacks(chatSimulationRunnable);
}
}
// 释放播放器资源
releasePlayer();
}
}

View File

@ -5,6 +5,7 @@ import android.content.Intent;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import androidx.appcompat.app.AppCompatActivity;
@ -99,6 +100,7 @@ public class SearchActivity extends AppCompatActivity {
String query = q != null ? q.trim() : "";
if (query.isEmpty()) {
adapter.submitList(new ArrayList<>(all));
updateEmptyState(all);
return;
}
@ -112,6 +114,23 @@ public class SearchActivity extends AppCompatActivity {
}
}
adapter.submitList(filtered);
updateEmptyState(filtered);
}
/**
* 更新空状态显示
*/
private void updateEmptyState(List<Room> rooms) {
if (rooms == null || rooms.isEmpty()) {
if (binding.emptyStateView != null) {
binding.emptyStateView.setNoSearchResultsState();
binding.emptyStateView.setVisibility(View.VISIBLE);
}
} else {
if (binding.emptyStateView != null) {
binding.emptyStateView.setVisibility(View.GONE);
}
}
}
private List<Room> buildDemoRooms(int count) {

View File

@ -10,9 +10,13 @@ public class CreateRoomRequest {
@SerializedName("streamerName")
private final String streamerName;
public CreateRoomRequest(String title, String streamerName) {
@SerializedName("type")
private final String type;
public CreateRoomRequest(String title, String streamerName, String type) {
this.title = title;
this.streamerName = streamerName;
this.type = type;
}
public String getTitle() {
@ -22,4 +26,8 @@ public class CreateRoomRequest {
public String getStreamerName() {
return streamerName;
}
}
public String getType() {
return type;
}
}

View File

@ -15,6 +15,9 @@ public class Room {
@SerializedName("streamerName")
private String streamerName;
@SerializedName("type")
private String type;
@SerializedName("streamKey")
private String streamKey;
@ -49,6 +52,10 @@ public class Room {
this.streamerName = streamerName;
}
public void setType(String type) {
this.type = type;
}
public void setLive(boolean live) {
isLive = live;
}
@ -77,6 +84,10 @@ public class Room {
return streamerName;
}
public String getType() {
return type;
}
public String getStreamKey() {
return streamKey;
}
@ -103,12 +114,13 @@ public class Room {
&& Objects.equals(id, room.id)
&& Objects.equals(title, room.title)
&& Objects.equals(streamerName, room.streamerName)
&& Objects.equals(type, room.type)
&& Objects.equals(streamKey, room.streamKey)
&& Objects.equals(streamUrls, room.streamUrls);
}
@Override
public int hashCode() {
return Objects.hash(id, title, streamerName, streamKey, isLive, viewerCount, streamUrls);
return Objects.hash(id, title, streamerName, type, streamKey, isLive, viewerCount, streamUrls);
}
}

View File

@ -212,6 +212,14 @@
android:visibility="gone"
/>
<com.example.livestreaming.EmptyStateView
android:id="@+id/emptyStateView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone"
/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fabAddLive"
android:layout_width="wrap_content"

View File

@ -44,6 +44,13 @@
android:paddingBottom="88dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<com.example.livestreaming.EmptyStateView
android:id="@+id/emptyStateView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone" />
<include
android:id="@+id/bottomNavInclude"
layout="@layout/include_bottom_nav"

View File

@ -96,4 +96,11 @@
android:paddingBottom="16dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<com.example.livestreaming.EmptyStateView
android:id="@+id/emptyStateView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -76,4 +76,11 @@
android:paddingBottom="24dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<com.example.livestreaming.EmptyStateView
android:id="@+id/emptyStateView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,61 @@
<?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"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="32dp">
<ImageView
android:id="@+id/emptyIcon"
android:layout_width="64dp"
android:layout_height="64dp"
android:background="@drawable/bg_gray_12"
android:padding="16dp"
android:src="@drawable/ic_person_24"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="#CCCCCC" />
<TextView
android:id="@+id/emptyTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="暂无数据"
android:textColor="#666666"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/emptyIcon" />
<TextView
android:id="@+id/emptyMessage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center"
android:text="这里空空如也,快来添加一些内容吧"
android:textColor="#999999"
android:textSize="14sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/emptyTitle" />
<Button
android:id="@+id/emptyActionButton"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:layout_marginTop="24dp"
android:background="@drawable/bg_purple_20"
android:minWidth="120dp"
android:text="刷新试试"
android:textColor="@android:color/white"
android:textSize="14sp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/emptyMessage" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="EmptyStateView">
<attr name="emptyIcon" format="reference" />
<attr name="emptyTitle" format="string" />
<attr name="emptyMessage" format="string" />
<attr name="emptyActionText" format="string" />
</declare-styleable>
</resources>