创建项目并初始化模版,创建下导航栏及点击切换页面功能
-
实现后效果展示

-
分析
使用 底部导航栏(BottomNavigationView) + Fragment 实现四个页面切换
- 实现步骤
- activity_main.xml(主界面布局)添加代码
xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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="match_parent"
android:orientation="vertical">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_navigation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:menu="@menu/bottom_nav_menu" />
</LinearLayout>
- 创建菜单文件
res/menu/bottom_nav_menu.xml
xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/nav_home"
android:icon="@android:drawable/ic_menu_home"
android:title="首页" />
<item
android:id="@+id/nav_discover"
android:icon="@android:drawable/ic_menu_search"
android:title="发现" />
<item
android:id="@+id/nav_notifications"
android:icon="@android:drawable/ic_menu_recent_history"
android:title="通知" />
<item
android:id="@+id/nav_profile"
android:icon="@android:drawable/ic_menu_manage"
android:title="我的" />
</menu>
- 创建四个页面
例:
java
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.fragment.app.Fragment;
public class HomeFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
TextView textView = new TextView(getContext());
textView.setText("首页页面");
textView.setTextSize(30);
textView.setGravity(android.view.Gravity.CENTER);
return textView;
}
}
- MainActivity.java(核心逻辑)
java
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import com.google.android.material.bottomnavigation.BottomNavigationView;
public class MainActivity extends AppCompatActivity {
private Fragment currentFragment = null;
private final Fragment homeFragment = new HomeFragment();
private final Fragment discoverFragment = new DiscoverFragment();
private final Fragment notificationsFragment = new NotificationsFragment();
private final Fragment profileFragment = new ProfileFragment();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
BottomNavigationView bottomNavigationView = findViewById(R.id.bottom_navigation);
// 默认加载首页
if (savedInstanceState == null) {
loadFragment(homeFragment);
}
// 设置导航栏点击监听
bottomNavigationView.setOnItemSelectedListener(item -> {
int itemId = item.getItemId();
if (itemId == R.id.nav_home) {
loadFragment(homeFragment);
return true;
} else if (itemId == R.id.nav_discover) {
loadFragment(discoverFragment);
return true;
} else if (itemId == R.id.nav_notifications) {
loadFragment(notificationsFragment);
return true;
} else if (itemId == R.id.nav_profile) {
loadFragment(profileFragment);
return true;
}
return false;
});
}
private void loadFragment(Fragment fragment) {
if (currentFragment == fragment) return; // 避免重复加载
currentFragment = fragment;
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.replace(R.id.nav_host_fragment, fragment);
transaction.commit();
}
}
- 添加依赖(build.gradle)
确保 app/build.gradle 中有以下依赖:
json
dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.10.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
}
- 修改系统私有图标为自定义
报错ERROR: F:\AndroidStudioProjects\FitnessGuide\fitness-guide\android\app\src\main\res\menu\bottom_nav_menu.xml:6: AAPT: error: resource android:drawable/ic_menu_home is private.
修改 res/menu/bottom_nav_menu.xml:
android:icon="@drawable/自定义图标"
xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/nav_home"
android:title="首页"
android:icon="@drawable/ic_home"
app:showAsAction="ifRoom" />
<item
android:id="@+id/nav_discover"
android:title="发现"
android:icon="@drawable/ic_search"
app:showAsAction="ifRoom" />
<item
android:id="@+id/nav_notifications"
android:title="通知"
android:icon="@drawable/ic_notifications"
app:showAsAction="ifRoom" />
<item
android:id="@+id/nav_profile"
android:title="我的"
android:icon="@drawable/ic_person"
app:showAsAction="ifRoom" />
</menu>
- (项目中使用图标) 引用规则总结
| 资源类型 | 引用方式 | 示例 |
|---|---|---|
| 项目自定义图标 | @drawable/文件名 | @drawable/ic_home |
| 系统内置图标 | @android:drawable/文件名 | @android:drawable/ic_menu_home |
| Material 图标库 | @drawable/图标名 | @drawable/baseline_home_24 |
- 修改项目版本
项目版本低于依赖支持版本
翻译一下Dependency 'androidx.activity:activity:1.8.0' requires libraries and applications that depend on it to compile against version 34 or later of the Android APIs. :app is currently compiled against android-33.
● 翻译
依赖项 'androidx.activity:activity:1.8.0' 要求依赖它的库和应用必须针对 Android API 版本 34 或更高版本进行编译。:app 当前编译目标是 android-33。
解决方案
升级 compileSdk 到 34(推荐)
在 app/build.gradle 文件中修改:
json
android {
compileSdk 34 // 原来是 33,改成 34
defaultConfig {
targetSdk 34 // 也建议同步升级
// ...
}
}
然后点击 Sync Now。
编写锻炼页面
- 添加自定义xml文件
去掉状态栏
方法一:沉浸式全屏(推荐,最符合"去掉"的视觉效果)
- 在 AndroidManifest.xml中设置主题:
xml
<activity
android:name=".MainActivity"
android:theme="@style/Theme.AppCompat.NoActionBar"> <!-- 或者使用 FullScreen 主题 -->
...
</activity>
- 在代码中实现(Java/Kotlin):
在您的 Activity 的 onCreate方法中,添加以下代码:
java
// Java
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 必须在 setContentView 之前调用
requestWindowFeature(Window.FEATURE_NO_TITLE); // 去掉标题栏
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN); // 设置全屏
setContentView(R.layout.activity_main);
}
Room和原生数据库区别
Room
java
@Query("SELECT * FROM checkin_records WHERE date = :date")
CheckInRecord getRecordByDate(String date);
原生数据库
java
SQLiteDatabase db = dbHelper.getReadableDatabase();
String sql = "SELECT * FROM checkin_records WHERE date = ?";
Cursor cursor = db.rawQuery(sql, new String[]{
String.valueOf(date) // 这里传参数
});
while (cursor.moveToNext()) {
long id = cursor.getLong(cursor.getColumnIndexOrThrow("id"));
long date = cursor.getLong(cursor.getColumnIndexOrThrow("date"));
}
cursor.close();
db.close();
内存泄漏
短生命周期对象拥有长生命周期对象引用。(对象销毁了,GC回收不了)
- 例:
数据库操作对象在创建时使用activity,该对象的存活时间是整个app,当activity销毁时就会出现内存泄漏。
- 错误写法
java
public class AppDatabase {
private static AppDatabase instance;
public static AppDatabase getInstance(Activity activity) {
if (instance == null) {
instance = Room.databaseBuilder(
activity, // ❌ Activity Context
AppDatabase.class,
"db"
).build();
}
return instance;
}
}
- 正确写法
java
public static AppDatabase getInstance(Context context) {
if (instance == null) {
synchronized (AppDatabase.class) {
if (instance == null) {
instance = Room.databaseBuilder(
context.getApplicationContext(),
AppDatabase.class,
"checkin_database"
).build();
}
}
}
return instance;
}
三层模型
UI层、业务层、数据层
数据库升级
操作数据库报错
notifyUnstableAppInfo: Bundle[{unstableTime=1781691427207, reason=crash, userId=0, exceptionMsg=Cannot access database on the main thread since it may potentially lock the UI for a long period of time., exceptionClass=java.lang.IllegalStateException, app_channel_type=unstable, packageName=com.fitnessguide, unstable_restrict_switch=true}] 不能在主线程中操作数据库(耗时操作)
解决办法,使用RxJava
java
Disposable disposable = Observable.fromCallable(() -> {
// ✅ 这里一定在子线程
Log.d("TAG", "开始查数据库");
String todayStr = today.toString();
// 例: 操作数据库
CheckInRecord todayCheckInRecord = checkInDao.getRecordByDate(todayStr);
return todayCheckInRecord != null ? todayCheckInRecord : new CheckInRecord();
})
.flatMap(existing -> Observable.fromCallable(() ->
{
if (existing != null && existing.isChecked) {
// .. ...
return false;
}
// 例: 操作数据库
checkInDao.insert(record);
return true;
}
))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
(status) -> {
Log.d("TAG", "查询结果:");
if(status) {
// .. ...
}
return;
},
throwable -> {
Log.e("TAG", "数据库异常", throwable);
}
);
// 一定要保存
compositeDisposable.add(disposable);
- fromCallable返回值不能为空,否则会报错
java.lang.NullPointerException: Callable returned a null value. Null values are generally not allowed in 3.x operators and sources. - 报错
java.lang.NullPointerException: Can't toast on a thread that has not called Looper.prepare()
不能在子线程中使用Toast
解决办法:在UI线程中使用Toast。
闹钟提醒可能出现的问题
- 设置闹钟
java
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
Intent intent = new Intent(OPEN_APP_INTENT_FLAG);
intent.setClass(context, ReminderReceiver.class);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context,
0,// requestCode intent,
PendingIntent.FLAG_IMMUTABLE);
// 每天上午 8 点提醒
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.HOUR_OF_DAY, 8);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
alarmManager.setRepeating(AlarmManager.RTC_WAKEUP,
calendar.getTimeInMillis(),
AlarmManager.INTERVAL_DAY,
pendingIntent);
- 取消闹钟
java
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
Intent intent = new Intent(OPEN_APP_INTENT_FLAG);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context,
0, // requestCode
intent,
PendingIntent.FLAG_IMMUTABLE);
alarmManager.cancel(pendingIntent);
AlarmManager 是怎么区分闹钟的?
不是靠时间,而是靠 PendingIntent的匹配规则
✅ AlarmManager 判断是不是"同一个闹钟"的依据:
requestCode + Intent 的 action / component / extras
只要 PendingIntent 相等(equals),
就认为是 同一个闹钟。
其中
FLAG_IMMUTABLE表示:这个 PendingIntent 一旦创建,里面的 Intent 就不能再被修改。
frament跳转闪退
java
private void loadFragment(Fragment fragment) {
if (currentFragment == fragment) return; // 避免重复加载
currentFragment = fragment;
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.replace(R.id.nav_host_fragment, fragment);
transaction.commit();
}
尽量使用replace用add可能会闪退。
👨 男性
BMR = (10 × 体重kg) + (6.25 × 身高cm) - (5 × 年龄) + 5
👩 女性
BMR = (10 × 体重kg) + (6.25 × 身高cm) - (5 × 年龄) - 161
键值对存储
① DataStore(官方推荐 ✅)
Preferences DataStore
异步, 线程安全, 支持 Java
适合存:
-
用户登录态
-
主题模式(深色/浅色)
-
是否首次启动
-
简单配置项
java
DataStore<Preferences> dataStore =
new PreferenceDataStoreFactory().create(
() -> getApplicationContext().getFilesDir().resolve("settings.preferences_pb")
);
Preferences.Key<String> KEY_TOKEN =
PreferencesKeys.stringKey("token");
// 写
dataStore.edit(prefs -> prefs.set(KEY_TOKEN, "abc123"));
// 读
String token = dataStore.data()
.map(prefs -> prefs.get(KEY_TOKEN))
.first();
RecycleView不渲染
- 设置布局
java
GridLayoutManager gridLayoutManager = new GridLayoutManager(getContext(), 7);
rvCalendar.setLayoutManager(gridLayoutManager);
更新RecycleView的List
java
list reset
adapt.notifyDataSetChanged();
修改app图标


实现app计算每天的步数
registerStepReceiver();为啥在onresume里写而不是oncreate里
一、核心答案
广播接收器必须在 onResume 中注册,在 onPause 中取消注册!
如果写在 onCreate 中,会带来严重的内存泄漏和重复注册问题。
二、为什么不能在 onCreate 注册?
问题1:内存泄漏(最严重)
java
// ❌ 错误:在 onCreate 中注册
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LocalBroadcastManager.getInstance(getContext())
.registerReceiver(stepReceiver, new IntentFilter("STEP_UPDATE"));
// ❌ 没有取消注册的地方!
}
// 场景:Fragment 被销毁重建
// 1. 第一次 onCreate → 注册 receiver A
// 2. Fragment 被销毁(但 receiver 还在)
// 3. 第二次 onCreate → 注册 receiver B
// 4. receiver A 还在内存中(泄漏!)
// 5. 多次操作后 → 内存溢出(OOM)```
问题2:重复注册
```java
// ❌ 错误:onCreate 多次调用
// 当 Fragment 被销毁后重新创建时,onCreate 会再次执行
// 导致同一个 receiver 被注册多次
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 第一次:注册成功
// 第二次:IllegalArgumentException: Receiver already registered
registerReceiver();
}```
问题3:在不可见时接收广播
```java
// ❌ 错误:onCreate 注册
// 即使 Fragment 不可见(在后台),也能收到广播
// 造成不必要的 UI 更新和性能浪费
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
registerReceiver(); // Fragment 在后台时也会收到广播
}```
三、为什么 onResume 是最佳选择?
生命周期对应关系
```text
Fragment 可见且可交互
↓
onResume() ← ★ 在这里注册广播(开始接收)
↓
Fragment 运行中(用户可见)
↓
onPause() ← ★ 在这里取消注册(停止接收)
↓
Fragment 不可见```
标准写法
```java
@Override
public void onResume() {
super.onResume();
// ★ 在 onResume 注册 ★
// 原因:Fragment 可见且可交互,需要接收更新
registerStepReceiver();
loadTodaySteps();
}
@Override
public void onPause() {
super.onPause();
// ★ 在 onPause 取消注册 ★
// 原因:Fragment 不可见,不需要接收更新
unregisterStepReceiver();
saveData();
}
报错
AAPT: error: 'fab_add' is incompatible with attribute layout_constraintEnd_toStartOf (attr) reference|enum [parent=0].
你把 字符串 'fab_add' 当成 id 引用 用了,但它不是合法的 @+id/...引用。
- 错误写法
java
app:layout_constraintEnd_toStartOf="fab_add"
- 正确写法
java
app:layout_constraintEnd_toStartOf="@id/fab_add"
调整UI
TextView和ImageView 左边自适应右边固定宽度,垂直对齐


- 分析
- 左边自适应右边固定宽度 使用链式实现,即 左边end连接右边start,右边start连接左边end。左边宽度设置0dp,右边根据需要设置就行。
- 上下对齐,给TextVIew添加PaddingTop
实现目标
recycleVIew里面的元素按从左往右从上往下排列,实现图中效果
.
- 添加依赖
json
dependencies {
implementation 'com.google.android.flexbox:flexbox:3.0.0'
}
- 代码设置 LayoutManager
java
RecyclerView recyclerView = findViewById(R.id.rv_food_list);
// 关键:使用 FlexboxLayoutManager
FlexboxLayoutManager layoutManager = new FlexboxLayoutManager(this);
// 主轴方向为水平(默认就是水平,可不写)
layoutManager.setOrientation(RecyclerView.HORIZONTAL);
// 交叉轴方向为垂直(默认就是垂直,可不写)
layoutManager.setFlexDirection(FlexDirection.ROW);
recyclerView.setLayoutManager(layoutManager);
recyclerView.setAdapter(yourAdapter);
- 间距控制:
如果你想控制元素之间的间距,可以在 item的根布局加 layout_margin。
如果你想控制行与行之间的间距,可以在 Java/Kotlin 代码中设置:
java
layoutManager.setLineSpacing(10f, 1f); // 行间距 10px
不同RecycleView不能用同一个layoutManager
- 错误示例
java
FlexboxLayoutManager layoutManager = new FlexboxLayoutManager(requireContext());
rvFoodList.setLayoutManager(layoutManager);
rvAddedFoodList.setLayoutManager(layoutManager); // ❌ 错误
安卓开发java,解决横屏页面只占屏幕一半(左边)
- menifest.xml 中添加
java
<activity
android:name=".MainActivity"
android:resizeableActivity="true"
android:configChanges="orientation|screenSize|smallestScreenSize"
android:screenOrientation="fullSensor"
android:exported="true">
