From d3854e20c81640b03608d3ab1b57f228734992e8 Mon Sep 17 00:00:00 2001 From: ShiQi <3572915148@qq.com> Date: Mon, 22 Dec 2025 18:11:21 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BA=86=E7=95=8C=E9=9D=A2?= =?UTF-8?q?=E4=B8=AD=E7=9A=84=E6=A0=B7=E5=BC=8F=E4=B8=80=E4=BA=9B=E9=97=AE?= =?UTF-8?q?=E9=A2=98=EF=BC=8C=E8=BF=98=E6=9C=89=E5=86=85=E5=AD=98=E6=B3=84?= =?UTF-8?q?=E6=BC=8F=E7=AD=89=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android-app/app/build.gradle.kts | 3 + android-app/app/src/main/AndroidManifest.xml | 1 + .../example/livestreaming/EmptyStateView.java | 181 ++++++++++++++++ .../example/livestreaming/ErrorHandler.java | 85 ++++++++ .../livestreaming/FishPondActivity.java | 16 ++ .../LiveStreamingApplication.java | 20 ++ .../example/livestreaming/MainActivity.java | 199 ++++++++++++++++-- .../livestreaming/MessagesActivity.java | 19 ++ .../livestreaming/MyFriendsActivity.java | 19 ++ .../livestreaming/NetworkRequestManager.java | 78 +++++++ .../example/livestreaming/NetworkUtils.java | 53 +++++ .../livestreaming/RoomDetailActivity.java | 22 +- .../example/livestreaming/SearchActivity.java | 19 ++ .../livestreaming/net/CreateRoomRequest.java | 12 +- .../com/example/livestreaming/net/Room.java | 14 +- .../app/src/main/res/layout/activity_main.xml | 8 + .../src/main/res/layout/activity_messages.xml | 7 + .../main/res/layout/activity_my_friends.xml | 7 + .../src/main/res/layout/activity_search.xml | 7 + .../src/main/res/layout/view_empty_state.xml | 61 ++++++ android-app/app/src/main/res/values/attrs.xml | 9 + 21 files changed, 814 insertions(+), 26 deletions(-) create mode 100644 android-app/app/src/main/java/com/example/livestreaming/EmptyStateView.java create mode 100644 android-app/app/src/main/java/com/example/livestreaming/ErrorHandler.java create mode 100644 android-app/app/src/main/java/com/example/livestreaming/LiveStreamingApplication.java create mode 100644 android-app/app/src/main/java/com/example/livestreaming/NetworkRequestManager.java create mode 100644 android-app/app/src/main/java/com/example/livestreaming/NetworkUtils.java create mode 100644 android-app/app/src/main/res/layout/view_empty_state.xml create mode 100644 android-app/app/src/main/res/values/attrs.xml diff --git a/android-app/app/build.gradle.kts b/android-app/app/build.gradle.kts index b3654bb8..b6567a36 100644 --- a/android-app/app/build.gradle.kts +++ b/android-app/app/build.gradle.kts @@ -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") } diff --git a/android-app/app/src/main/AndroidManifest.xml b/android-app/app/src/main/AndroidManifest.xml index 01deb8dd..94f898be 100644 --- a/android-app/app/src/main/AndroidManifest.xml +++ b/android-app/app/src/main/AndroidManifest.xml @@ -5,6 +5,7 @@ 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); + } +} diff --git a/android-app/app/src/main/java/com/example/livestreaming/FishPondActivity.java b/android-app/app/src/main/java/com/example/livestreaming/FishPondActivity.java index ec8bd96f..a3ff27a8 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/FishPondActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/FishPondActivity.java @@ -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; + } + } } diff --git a/android-app/app/src/main/java/com/example/livestreaming/LiveStreamingApplication.java b/android-app/app/src/main/java/com/example/livestreaming/LiveStreamingApplication.java new file mode 100644 index 00000000..db4879a8 --- /dev/null +++ b/android-app/app/src/main/java/com/example/livestreaming/LiveStreamingApplication.java @@ -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版本中初始化,无需手动调用 + } +} diff --git a/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java b/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java index 22f08039..4fd83fea 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java @@ -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 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>() { @Override public void onResponse(Call> call, Response> 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>>() { + Call>> call = ApiClient.getService().getRooms(); + NetworkUtils.enqueueWithLifecycle(call, this, new Callback>>() { @Override public void onResponse(Call>> call, Response>> response) { binding.loading.setVisibility(View.GONE); binding.swipeRefresh.setRefreshing(false); isFetching = false; + ApiResponse> body = response.body(); List 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 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 buildDemoRooms(int count) { List 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; } diff --git a/android-app/app/src/main/java/com/example/livestreaming/MessagesActivity.java b/android-app/app/src/main/java/com/example/livestreaming/MessagesActivity.java index 7124c021..d3b13271 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/MessagesActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/MessagesActivity.java @@ -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); + } + } + } } diff --git a/android-app/app/src/main/java/com/example/livestreaming/MyFriendsActivity.java b/android-app/app/src/main/java/com/example/livestreaming/MyFriendsActivity.java index 03aeddc5..84c4e5fb 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/MyFriendsActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/MyFriendsActivity.java @@ -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 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 buildDemoFriends() { diff --git a/android-app/app/src/main/java/com/example/livestreaming/NetworkRequestManager.java b/android-app/app/src/main/java/com/example/livestreaming/NetworkRequestManager.java new file mode 100644 index 00000000..a65a2580 --- /dev/null +++ b/android-app/app/src/main/java/com/example/livestreaming/NetworkRequestManager.java @@ -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> 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(); + } + } +} diff --git a/android-app/app/src/main/java/com/example/livestreaming/NetworkUtils.java b/android-app/app/src/main/java/com/example/livestreaming/NetworkUtils.java new file mode 100644 index 00000000..269318cd --- /dev/null +++ b/android-app/app/src/main/java/com/example/livestreaming/NetworkUtils.java @@ -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 void enqueueWithLifecycle( + @NonNull Call call, + @NonNull LifecycleOwner lifecycleOwner, + @NonNull Callback callback) { + + // 创建请求管理器并绑定到生命周期 + NetworkRequestManager manager = new NetworkRequestManager(); + manager.bindToLifecycle(lifecycleOwner); + manager.addCall(call); + + // 执行请求 + call.enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + manager.removeCall(call); + callback.onResponse(call, response); + } + + @Override + public void onFailure(Call call, Throwable t) { + manager.removeCall(call); + callback.onFailure(call, t); + } + }); + } + + /** + * 简单的网络请求执行,不带生命周期管理 + * 注意:需要手动管理请求取消 + */ + public static void enqueue(@NonNull Call call, @NonNull Callback callback) { + call.enqueue(callback); + } +} diff --git a/android-app/app/src/main/java/com/example/livestreaming/RoomDetailActivity.java b/android-app/app/src/main/java/com/example/livestreaming/RoomDetailActivity.java index ee2cdeb3..5185ed93 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/RoomDetailActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/RoomDetailActivity.java @@ -298,11 +298,12 @@ public class RoomDetailActivity extends AppCompatActivity { binding.loading.setVisibility(View.VISIBLE); } - ApiClient.getService().getRoom(roomId).enqueue(new Callback>() { + Call> call = ApiClient.getService().getRoom(roomId); + NetworkUtils.enqueueWithLifecycle(call, this, new Callback>() { @Override public void onResponse(Call> call, Response> 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(); + } } diff --git a/android-app/app/src/main/java/com/example/livestreaming/SearchActivity.java b/android-app/app/src/main/java/com/example/livestreaming/SearchActivity.java index 0f750b92..f47ffd8e 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/SearchActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/SearchActivity.java @@ -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 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 buildDemoRooms(int count) { diff --git a/android-app/app/src/main/java/com/example/livestreaming/net/CreateRoomRequest.java b/android-app/app/src/main/java/com/example/livestreaming/net/CreateRoomRequest.java index 5c1703ef..6348b714 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/net/CreateRoomRequest.java +++ b/android-app/app/src/main/java/com/example/livestreaming/net/CreateRoomRequest.java @@ -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; + } +} \ No newline at end of file diff --git a/android-app/app/src/main/java/com/example/livestreaming/net/Room.java b/android-app/app/src/main/java/com/example/livestreaming/net/Room.java index e7f48058..56b7aee4 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/net/Room.java +++ b/android-app/app/src/main/java/com/example/livestreaming/net/Room.java @@ -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); } } diff --git a/android-app/app/src/main/res/layout/activity_main.xml b/android-app/app/src/main/res/layout/activity_main.xml index fa8abccb..2df115f7 100644 --- a/android-app/app/src/main/res/layout/activity_main.xml +++ b/android-app/app/src/main/res/layout/activity_main.xml @@ -212,6 +212,14 @@ android:visibility="gone" /> + + + + + + diff --git a/android-app/app/src/main/res/layout/activity_search.xml b/android-app/app/src/main/res/layout/activity_search.xml index 9ce5fd31..d13bcf3c 100644 --- a/android-app/app/src/main/res/layout/activity_search.xml +++ b/android-app/app/src/main/res/layout/activity_search.xml @@ -76,4 +76,11 @@ android:paddingBottom="24dp" app:layout_behavior="@string/appbar_scrolling_view_behavior" /> + + diff --git a/android-app/app/src/main/res/layout/view_empty_state.xml b/android-app/app/src/main/res/layout/view_empty_state.xml new file mode 100644 index 00000000..6160e71a --- /dev/null +++ b/android-app/app/src/main/res/layout/view_empty_state.xml @@ -0,0 +1,61 @@ + + + + + + + + + +