Android 学习笔记

技术学习 / 2022-01-22

系统控件

TextView

TextView 的 setText 方法接受的必须是一个 String,否则会报错

private TextView infoText;
infoText = (TextView) findViewById(R.id.infoText);
infoText.setText(String.valueOf(mainViewModel.counter));

数据持久化

SharedPreferences

SharedPreferences 可以提供简单数据持久化存储

存储

通过 sharedPreferences 中的 edit() 生成 editor 的实例,通过一系列的 set 方法进行数据的存储

private SharedPreferences sharedPreferences;
private SharedPreferences.Editor editor;
editor = sharedPreferences.edit();
editor.putInt("count_reserved", mainViewModel.counter);
editor.apply();

读取

通过 PreferenceManager.getDefaultSharedPreferences(this) 获取到对应的 SharedPreferences 对象,然后通过一系列的 get 方法进行读取,第一个参数为自定义的名字,第二个为默认值

private SharedPreferences sharedPreferences;
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
int countReserved = sharedPreferences.getInt("count_reserved", 0);

Room

ORM 框架,简单化 SQLite 数据库的使用

包含三部分:

  • Entity,实体类,每一个实体类代表一张对应的表
  • Dao,数据访问对象,封装数据库的各项操作
  • Database,定义数据库的关键信息

添加依赖:

def room_version = "2.4.1"
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"

配置

User 实体类:

@Entity // 表示这是一个 Entity,get 和 set 方法需要写明
public class User {

    @PrimaryKey(autoGenerate = true) // 设置主键,并设置为自动增长
    private long id;
    private String firstName;
    private String lastName;
    private int age;

    public User(String firstName, String lastName, int age) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", firstName='" + firstName + '\'' +
                ", lastName='" + lastName + '\'' +
                ", age=" + age +
                '}';
    }
}

UserDao:

利用 @Insert,@Update,@Delete 可以进行单个对象的操作,但是对于需要有 where 条件的操作,则需要使用到 @Query,@Query 采用 SQL 语句的形式执行命令,如果需要指定参数,则在 SQL 命令中所要添加的参数前加上 : 然后通过参数传入方法,具体使用如下:

@Dao
public interface UserDao {

    @Insert
    public long insertUser(User user);

    @Update
    public void updateUser(User newUser);

    @Query("select * from User")
    public List<User> loadAllUsers();

    @Query("select * from User where age > :age")
    public List<User> loadUsersOlderThan(int age);

    @Delete
    public void deleteUser(User user);

    @Query("delete from User where lastName = :lastName")
    public int deleteUserByLastName(String lastName);
}

Database:

抽象方法继承 RoomDatabase,一般不改变

@Database(version = 1, entities = {User.class}) // 标记这是一个 Database,同时设置版本号,实体类
public abstract class AppDatabase extends RoomDatabase {

    private static final String DATABASE_NAME = "app_database";

    private static AppDatabase databaseInstance;

    // 单例模式创建数据库实例,保证全局只有一个数据库,synchronized 保证线程安全
    public static synchronized AppDatabase getDatabaseInstance(Context context) {
        if (databaseInstance == null) {
            databaseInstance = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME).build();
        }
        return databaseInstance;
    }

    public abstract UserDao userDao(); // 抽象方法,具体在 Room 底层实现
}

增删改查具体使用

耗时逻辑写在子线程中,不能放在主线程内

UserDao userDao = AppDatabase.getDatabaseInstance(this).userDao();
User user1 = new User("Tom", "Brady", 40);
User user2 = new User("Tom", "Hanks", 63);
Button addData = (Button) findViewById(R.id.addData);
Button updateData = (Button) findViewById(R.id.updateData);
Button deleteData = (Button) findViewById(R.id.deleteData);
Button queryData = (Button) findViewById(R.id.queryData);
addData.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                user1.setId(userDao.insertUser(user1));
                user2.setId(userDao.insertUser(user2));
            }
        }).start();
    }
});
updateData.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                user1.setAge(42);
                userDao.updateUser(user1);
            }
        }).start();
    }
});
deleteData.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                userDao.deleteUserByLastName("Hanks");
            }
        }).start();
    }
});
queryData.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (User user : userDao.loadAllUsers()) {
                    Log.d(TAG, user.toString());
                }
            }
        }).start();
    }
});
}

升级数据库

开发测试阶段可以直接使用 fallbackToDestructiveMigration(),表示在每一次版本号更新后都会重建数据库,之前所有保存的数据都丢失

databaseInstance = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME)
.fallbackToDestructiveMigration()
.build();

升级到版本2

添加数据库表:Book

@Entity
public class Book {

    @PrimaryKey(autoGenerate = true)
    private long id = 0;
    private String name;
    private int pages;
    private String author;

    public Book(String name, int pages) {
        this.name = name;
        this.pages = pages;
    }

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getPages() {
        return pages;
    }

    public void setPages(int pages) {
        this.pages = pages;
    }

    @Override
    public String toString() {
        return "Book{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", pages=" + pages +
                '}';
    }
}

添加 BookDao:

@Dao
public interface BookDao {

    @Insert
    public Long insertBook(Book book);

    @Query("select * from Book")
    public List<Book> loadAllBooks();
}

修改 Database:

@Database(version = 2, entities = {User.class, Book.class}) // 修改版本号
private static final Migration MIGRATION_1_2 = new Migration(1, 2) { // 通过 Migration 进行数据库的迁移更新,表示从版本1到版本2
    @Override
    public void migrate(@NonNull SupportSQLiteDatabase database) { // 写明相应建表语句
        database.execSQL("create table Book " +
                "(id integer primary key autoincrement not null, " +
                "name text not null, " +
                "pages integer not null)");
    }
};
databaseInstance = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME)
        .addMigrations(MIGRATION_1_2) //添加更新操作
        .build();

升级到版本3

添加 author 列到 Book 表中

Book 实体类:

@Entity
public class Book {

    @PrimaryKey(autoGenerate = true)
    private long id = 0;
    private String name;
    private int pages;
    private String author;

    public Book(String name, int pages) {
        this.name = name;
        this.pages = pages;
    }

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getPages() {
        return pages;
    }

    public void setPages(int pages) {
        this.pages = pages;
    }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }

    @Override
    public String toString() {
        return "Book{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", pages=" + pages +
                ", author='" + author + '\'' +
                '}';
    }
}

Database:

@Database(version = 3, entities = {User.class, Book.class}) // 修改版本号
public static final Migration MIGRATION_2_3 = new Migration(2, 3) { // 添加相应的 Migration
    @Override
    public void migrate(@NonNull SupportSQLiteDatabase database) {
        database.execSQL("alter table Book" +
                "add column author text not null default 'unknown'");
    }
};
databaseInstance = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME)
        .addMigrations(MIGRATION_1_2, MIGRATION_2_3) // 可以连续升级到版本3,如果用户数据库处于版本1,则升级两次操作
        .build();

报错

Schema export directory is not provided to the annotation processor so we cannot export the schema. You can either provide room.schemaLocation annotation processor argument OR set exportSchema to false.

安卓room构建错误

  • 解决方案一

    Database 设置 exportSchema 注解为 false(不建议)

    @Database(version = 2, entities = {User.class, Book.class}, exportSchema = false)
    
  • 解决方案二

    在项目中gradle中通过 annotationProcessorOptions 注解,为 room.schemaLocation 指定schemas的子文件夹

    android {
        compileSdk 32
    
        defaultConfig {
            applicationId "com.lsilencej.counter"
            minSdk 24
            targetSdk 32
            versionCode 1
            versionName "1.0"
    
            testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    
    		// 指定 room.schemaLocation 生成的文件路径
            javaCompileOptions {
                annotationProcessorOptions {
                    arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
                }
            }
        }
    }
    

Jetpack

ViewModel

通过 ViewModel 来存储数据,为了不让 Activity 处理太多东西,最主要的是维持数据的持久性存储,而不是每次重建 Activity 数据丢失,MVVM 框架中的第一个 V

在此之前需要导入相关依赖:(具体版本参考谷歌开发者文档

def lifecycle_version = "2.4.0"
implementation "androidx.lifecycle:lifecycle-viewmodel:$lifecycle_version"

新建 MainViewModel 继承 ViewModel:

public class MainViewModel extends ViewModel {

    public int counter = 0;

    public MainViewModel(int countReserved) {
        this.counter = countReserved;
    }
}

初始化:( ViewModelProvider 传入 Activity 实例,get 传入 ViewModel )

private MainViewModel mainViewModel;
mainViewModel = new ViewModelProvider(this).get(MainViewModel.class);

如果需要和参数绑定,则需要新建一个 MainViewModelFactory 类实现 ViewModelProvider.Factory 接口,同时重写 create 方法:

public class MainViewModelFactory implements ViewModelProvider.Factory {

    private int countReserved;

    public MainViewModelFactory(int countReserved) {
        this.countReserved = countReserved;
    }

    @NonNull
    @Override
    public <T extends ViewModel> T create(@NonNull Class<T> aClass) {
        return (T) new MainViewModel(countReserved);
    }
}

同时初始化也需要增加一个参数:

private MainViewModel mainViewModel;
mainViewModel = new ViewModelProvider(this, new MainViewModelFactory(countReserved)).get(MainViewModel.class);

LifecycleEventObserver

生命周期监听器

实现接口,重写 onStateChanged 方法,能够在类中感知到 Activity 的生命周期,并进行相关操作:

public class MyObserver implements LifecycleEventObserver {

    private static final String TAG = "MyObserver";

    @Override
    public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) {
        switch (event) {
            case ON_CREATE:
                Log.d(TAG, "ON CREATE");
                break;
            case ON_STOP:
                Log.d(TAG, "ON STOP");
                break;
            default:
                break;
        }
    }
}

同时在相关 Activity 中 add 进去:

getLifecycle().addObserver(new MyObserver());

LiveData

响应式组件,观察数据变化,通常与 ViewModel 结合使用,当 ViewModel 处理耗时线程时使用

初始化,通过 getValue() 和 setValue() 方法来取放值,postValue() 用于在子线程中给 LiveData 设置数据:

private MutableLiveData<Integer> counter = new MutableLiveData<>();
counter.setValue(countReserved);
int count = 0;
if (counter.getValue() != null) {
	count = counter.getValue();
}
counter.setValue(count + 1);

WorkManager

处理定时任务,适合执行一些定期和服务器交互的任务,比如周期性同步数据等等

添加依赖:

def work_version = "2.7.1"
// (Java only)
implementation "androidx.work:work-runtime:$work_version"
  • 定义一个后台任务,并实现具体逻辑

    public class SimpleWorker extends Worker { // 继承 Worker 类
    
        private static final String TAG = "SimpleWorker";
    
        private Context context;
        private WorkerParameters workerParameters;
    
        public SimpleWorker(Context context, WorkerParameters workerParameters) {
            super(context, workerParameters); // 调用构造函数
            this.context = context;
            this.workerParameters = workerParameters;
        }
    
        @NonNull
        @Override
        public Result doWork() { // 重写 doWork() 方法,可以执行耗时逻辑
            Log.d(TAG, "do work is SimpleWorker");
            return Result.success(); // 返回结果
        }
    }
    
  • 配置后台任务

    WorkRequest workRequest = new OneTimeWorkRequest.Builder(SimpleWorker.class).build(); // 执行一次后台任务
    WorkRequest workRequest = new PeriodicWorkRequest.Builder(SimpleWorker.class, 15, TimeUnit.MINUTES).build(); // 周期性后台任务,传入的运行时间间隔不能少于 15min
    
  • 通过 enqueue() 方法执行

    WorkManager.getInstance(MainActivity.this).enqueue(workRequest);
    

其他操作

WorkRequest workRequest = new OneTimeWorkRequest.Builder(SimpleWorker.class)
        .setInitialDelay(5, TimeUnit.MINUTES) // 指定延迟 5min 后执行
        .addTag("simple") // 添加标签
        .setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.SECONDS) // 如果返回的是 Result.retry(),则 10s 继续执行,第一个参数如果是 LINEAR,则表示下次重试时间以线性方式延迟,如果是 EXPONENTIAL 则表示下次重试时间以指数方式延迟
        .build();
WorkManager.getInstance(MainActivity.this).cancelAllWorkByTag("simple"); // 取消指定标签的后台任务,标签可以包括多个后台任务
WorkManager.getInstance(MainActivity.this).cancelWorkById(workRequest.getId()); // 取消指定 id 的后台任务,只能取消一个
WorkManager.getInstance(MainActivity.this).cancelAllWork(); // 取消所有后台任务
WorkManager.getInstance(MainActivity.this)
        .getWorkInfoByIdLiveData(workRequest.getId()) // 返回的是 LiveData
        .observe(MainActivity.this, new Observer<WorkInfo>() { // 监听后台的运行结果
            @Override
            public void onChanged(WorkInfo workInfo) {
                if (workInfo.getState() == WorkInfo.State.SUCCEEDED) {
                    Log.d(TAG, "do work succeeded");
                } else if (workInfo.getState() == WorkInfo.State.FAILED) {
                    Log.d(TAG, "do work failed");
                }
            }
        });
WorkManager.getInstance(MainActivity.this) // 链式任务
        .beginWith(sync)
        .then(compress)
        .then(upload)
        .enqueue()