安卓开发 从零到一 开发一个APP

创建项目并初始化模版,创建下导航栏及点击切换页面功能

  1. 实现后效果展示

  2. 分析

使用 底部导航栏(BottomNavigationView) + Fragment 实现四个页面切换

  1. 实现步骤
  • 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。

编写锻炼页面

  1. 添加自定义xml文件

去掉状态栏

方法一:沉浸式全屏(推荐,最符合"去掉"的视觉效果)

  1. 在 AndroidManifest.xml中设置主题:
xml 复制代码
<activity
    android:name=".MainActivity"
    android:theme="@style/Theme.AppCompat.NoActionBar"> <!-- 或者使用 FullScreen 主题 -->
    ...
</activity>
  1. 在代码中实现(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 左边自适应右边固定宽度,垂直对齐

  • 分析
  1. 左边自适应右边固定宽度 使用链式实现,即 左边end连接右边start,右边start连接左边end。左边宽度设置0dp,右边根据需要设置就行。
  2. 上下对齐,给TextVIew添加PaddingTop

实现目标

recycleVIew里面的元素按从左往右从上往下排列,实现图中效果

.

  1. 添加依赖
json 复制代码
dependencies {
    implementation 'com.google.android.flexbox:flexbox:3.0.0'
}
  1. 代码设置 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);
  1. 间距控制:

如果你想控制元素之间的间距,可以在 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">