Jetpack——DataStore

DataStore

Jetpack DataStore 是一种数据存储解决方案,让您可以使用 协议缓冲区存储键值对或类型化对象

Preferences DataStore 可用于替代 SharedPreferences,其是线程安全的

需要注意:

  • 请勿在同一进程中为给定文件创建多个 DataStore 实例
  • DataStore 的通用类型必须不可变
  • 请勿对同一文件混用 SingleProcessDataStore 和 MultiProcessDataStore

依赖

复制代码
dependencies {
	// Preferences DataStore (SharedPreferences like APIs)
    implementation "androidx.datastore:datastore-preferences:1.2.1"

    // Alternatively - without an Android dependency.
    implementation "androidx.datastore:datastore-preferences-core:1.2.1"

    // optional - RxJava3 support
    implementation "androidx.datastore:datastore-preferences-rxjava3:1.2.1"

    // Typed DataStore for custom data objects (for example, using Proto or JSON).
    implementation "androidx.datastore:datastore:1.2.1"

    // Alternatively - without an Android dependency.
    implementation "androidx.datastore:datastore-core:1.2.1"

    // optional - RxJava3 support
    implementation "androidx.datastore:datastore-rxjava3:1.2.1"

    // RxAndroid: 提供 AndroidSchedulers.mainThread()
    implementation "io.reactivex.rxjava3:rxandroid:3.0.2"
    implementation "io.reactivex.rxjava3:rxjava:3.1.8"
    
    // Gson:用于 JSON DataStore 的序列化/反序列化
    implementation "com.google.code.gson:gson:2.10.1"

    // 协程 RxJava3 桥接:让 suspend 函数和 Flow 能转成 Single/Flowable
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx3:1.7.3"

	// Lifecycle: ViewModel + LiveData + 把 RxJava Publisher 适配成 LiveData
    def lifecycle_version = "2.7.0"
    implementation "androidx.lifecycle:lifecycle-viewmodel:$lifecycle_version"
    implementation "androidx.lifecycle:lifecycle-livedata:$lifecycle_version"
    implementation "androidx.lifecycle:lifecycle-reactivestreams:$lifecycle_version"
}

Preferences DataStore

MyApplication

java 复制代码
public class MyApplication extends Application {
    private static MyApplication instance;

    /**
     * 进程内单例的 DataStore.放在 Application 中持有,整个 App 生命周期复用,不要 dispose。
     * /data/data/com.example.demo1/files/datastore/settings.preferences_pb
     */
    private static RxDataStore<Preferences> settingsDataStore;

    @Override
    public void onCreate() {
        super.onCreate();
        instance = this;

        settingsDataStore = new RxPreferenceDataStoreBuilder(this, "settings").build();
    }

    public static Context getContext() {
        return instance;
    }

    public static RxDataStore<Preferences> getSettingsDataStore() {
        return settingsDataStore;
    }
}

SettingsRepository

复制代码
public class SettingsRepository {

    private static volatile SettingsRepository INSTANCE;

    public static SettingsRepository getInstance() {
        if (INSTANCE == null) {
            synchronized (SettingsRepository.class) {
                if (INSTANCE == null) {
                    INSTANCE = new SettingsRepository(MyApplication.getSettingsDataStore());
                }
            }
        }
        return INSTANCE;
    }

    // key 集中维护,public 让 ViewModel 引用;新增字段只在这加一行
    public static final Preferences.Key<Integer> KEY_COUNTER_A = PreferencesKeys.intKey("counter_a");
    public static final Preferences.Key<Integer> KEY_COUNTER_B = PreferencesKeys.intKey("counter_b");
    private final RxDataStore<Preferences> dataStore;

    private SettingsRepository(RxDataStore<Preferences> dataStore) {
        this.dataStore = dataStore;
    }

    /**
     * 通用读:调用方用 mapper 把整份 Preferences 映射成自己关心的 T。
     * 内部已包成去重(distinctUntilChanged),避免 UI 层收到重复数据。
     */
    public <T> Flowable<T> observeChanged(Function<Preferences, T> mapper) {
        return dataStore.data()
                .map(mapper)
                .distinctUntilChanged()
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread());
    }

    /**
     * 通用读:调用方用 mapper 把整份 Preferences 映射成自己关心的 T。
     */
    public <T> Flowable<T> observe(Function<Preferences, T> mapper) {
        return dataStore.data()
                .map(mapper)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread());
    }

    /**
     * 整体快照流,调试日志用。
     */
    public Flowable<Preferences> observeAll() {
        return observeChanged(prefs -> prefs);
    }

    /**
     * 通用写:调用方用 action 对 MutablePreferences 做任意修改。
     * 内部已包成原子事务(DataStore 的 updateDataAsync 语义)。
     */
    public Single<Preferences> update(Consumer<MutablePreferences> action) {
        return dataStore.updateDataAsync(prefsIn -> {
            MutablePreferences m = prefsIn.toMutablePreferences();
            try {
                action.accept(m);
            } catch (Throwable t) {
                return Single.error(t);
            }
            return Single.just(m);
        });
    }
}

SettingsViewModel

java 复制代码
public class SettingsViewModel extends ViewModel {

    private static final String TAG = "DataStoreDemo";

    private final SettingsRepository repo = SettingsRepository.getInstance();
    private final CompositeDisposable disposables = new CompositeDisposable();

    // 读:用 mapper 选取自己关心的字段
    private final LiveData<Integer> counterA = LiveDataReactiveStreams.fromPublisher(
            repo.observeChanged(prefs -> getOrZero(prefs, SettingsRepository.KEY_COUNTER_A)));
    private final LiveData<Integer> counterB = LiveDataReactiveStreams.fromPublisher(
            repo.observeChanged(prefs -> getOrZero(prefs, SettingsRepository.KEY_COUNTER_B)));
    private final LiveData<Preferences> allEntries =
            LiveDataReactiveStreams.fromPublisher(repo.observeAll());

    public LiveData<Integer> getCounterA() {
        return counterA;
    }

    public LiveData<Integer> getCounterB() {
        return counterB;
    }

    public LiveData<Preferences> getAllEntries() {
        return allEntries;
    }

    // 写:用 Consumer 描述要做的修改
    public void incA() {
        disposables.add(
                repo.update(m -> {
                    Integer cur = m.get(SettingsRepository.KEY_COUNTER_A);
                    m.set(SettingsRepository.KEY_COUNTER_A, (cur == null ? 0 : cur) + 1);
                }).subscribe(
                        p -> Log.d(TAG, "inc A ok"),
                        e -> Log.e(TAG, "inc A err", e)
                ));
    }

    public void incB() {
        disposables.add(
                repo.update(m -> {
                    Integer cur = m.get(SettingsRepository.KEY_COUNTER_B);
                    m.set(SettingsRepository.KEY_COUNTER_B, (cur == null ? 0 : cur) + 1);
                }).subscribe(
                        p -> Log.d(TAG, "inc B ok"),
                        e -> Log.e(TAG, "inc B err", e)
                ));
    }

    public void reset() {
        disposables.add(
                repo.update(m -> {
                    m.set(SettingsRepository.KEY_COUNTER_A, 0);
                    m.set(SettingsRepository.KEY_COUNTER_B, 0);
                }).subscribe(
                        p -> Log.d(TAG, "reset ok"),
                        e -> Log.e(TAG, "reset err", e)
                ));
    }

    private static int getOrZero(Preferences prefs, Preferences.Key<Integer> key) {
        Integer v = prefs.get(key);
        return v == null ? 0 : v;
    }

    @Override
    protected void onCleared() {
        super.onCleared();
        disposables.clear();
    }
}

MainActivity

java 复制代码
public class MainActivity extends AppCompatActivity {

    private static final String TAG = "DataStoreDemo";

    private TextView tvCounterA;
    private TextView tvCounterB;
    private Button mBtnInc;
    private Button mBtnIncB;
    private Button mBtnReset;

    private SettingsViewModel viewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
        initViewModel();
        initListener();
    }

    private void initView() {
        tvCounterA = findViewById(R.id.tv_counterA);
        tvCounterB = findViewById(R.id.tv_counter_b);
        mBtnInc = findViewById(R.id.btn_inc);
        mBtnIncB = findViewById(R.id.btn_inc_b);
        mBtnReset = findViewById(R.id.btn_reset);
    }

    private void initViewModel() {
        viewModel = new ViewModelProvider(this).get(SettingsViewModel.class);

        viewModel.getCounterA().observe(this, value ->
                tvCounterA.setText("A=" + value));
        viewModel.getCounterB().observe(this, value ->
                tvCounterB.setText("B=" + value));
        viewModel.getAllEntries().observe(this, this::logAllEntries);
    }

    private void initListener() {
        mBtnInc.setOnClickListener(v -> viewModel.incA());
        mBtnIncB.setOnClickListener(v -> viewModel.incB());
        mBtnReset.setOnClickListener(v -> viewModel.reset());
    }

    private void logAllEntries(Preferences prefs) {
        Log.d(TAG, "all entries = " + prefs.asMap());
    }
}

activity_main.xml

xml 复制代码
<?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:id="@+id/page_list_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/tv_counterA"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="0"
        android:textSize="24sp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toTopOf="@id/tv_counter_b"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

    <TextView
        android:id="@+id/tv_counter_b"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="0"
        android:textSize="24sp"
        android:layout_marginTop="16dp"
        app:layout_constraintTop_toBottomOf="@id/tv_counterA"
        app:layout_constraintBottom_toTopOf="@id/btn_inc"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

    <Button
        android:id="@+id/btn_inc"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="A +1"
        android:layout_marginEnd="8dp"
        app:layout_constraintTop_toBottomOf="@id/tv_counter_b"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toStartOf="@id/btn_inc_b" />

    <Button
        android:id="@+id/btn_inc_b"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="B +1"
        android:layout_marginEnd="8dp"
        app:layout_constraintTop_toTopOf="@id/btn_inc"
        app:layout_constraintBottom_toBottomOf="@id/btn_inc"
        app:layout_constraintStart_toEndOf="@id/btn_inc"
        app:layout_constraintEnd_toStartOf="@id/btn_reset" />

    <Button
        android:id="@+id/btn_reset"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="重置"
        app:layout_constraintTop_toTopOf="@id/btn_inc"
        app:layout_constraintBottom_toBottomOf="@id/btn_inc"
        app:layout_constraintStart_toEndOf="@id/btn_inc_b"
        app:layout_constraintEnd_toEndOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

查看数据

从以下路径取出文件

复制代码
/data/data/com.example.demo1/files/datastore/settings.preferences_pb

安装protobuf

复制代码
brew install protobuf

进入如下网站搜索datastore-preferences-proto

复制代码
https://github.com/androidx/androidx/tree/androidx-main

找到preferences.proto

根据package通过如下命令解析

复制代码
protoc --decode=androidx.datastore.testing.PreferenceMap \
       preferences.proto \
       < settings.preferences_pb

解析结果如下

复制代码
preferences {
  key: "counter_a"
  value {
    integer: 1
  }
}
preferences {
  key: "counter_b"
  value {
    integer: 1
  }
}

JSON DataStore

Settings

java 复制代码
public class Settings {

    /**
     * 使用final 字段 + withXxx 方法,保证不可变性。
     */

    public final int counterA;
    public final int counterB;

    /**
     * 无参构造:Gson 反序列化时需要;默认值全为 0。
     */
    public Settings() {
        this(0, 0);
    }

    public Settings(int counterA, int counterB) {
        this.counterA = counterA;
        this.counterB = counterB;
    }

    public Settings withCounterA(int value) {
        return new Settings(value, counterB);
    }

    public Settings withCounterB(int value) {
        return new Settings(counterA, value);
    }

    @NonNull
    @Override
    public String toString() {
        return "Settings{counterA=" + counterA + ", counterB=" + counterB + "}";
    }
}

SettingsSerializer.kt

kotlin 复制代码
object SettingsSerializer : Serializer<Settings> {

    private val gson = Gson()

    override val defaultValue: Settings = Settings()

    override suspend fun readFrom(input: InputStream): Settings {
        return try {
            InputStreamReader(input, StandardCharsets.UTF_8).use { reader ->
                gson.fromJson(reader, Settings::class.java) ?: defaultValue
            }
        } catch (e: Exception) {
            throw CorruptionException("settings.json corrupted", e)
        }
    }

    override suspend fun writeTo(t: Settings, output: OutputStream) {
        OutputStreamWriter(output, StandardCharsets.UTF_8).use { writer ->
            gson.toJson(t, writer)
        }
    }
}

SettingsRepository

java 复制代码
public class SettingsRepository {

    private static volatile SettingsRepository INSTANCE;

    public static SettingsRepository getInstance() {
        if (INSTANCE == null) {
            synchronized (SettingsRepository.class) {
                if (INSTANCE == null) {
                    INSTANCE = new SettingsRepository();
                }
            }
        }
        return INSTANCE;
    }

    private final RxDataStore<Settings> dataStore;

    private SettingsRepository() {
        // SettingsSerializer 是 Kotlin object,Java 里通过 .INSTANCE 引用
        dataStore = new RxDataStoreBuilder<>(
                MyApplication.getContext(), "settings.json", SettingsSerializer.INSTANCE
        ).build();
    }

    /**
     * 通用读:调用方用 mapper 从整份 Settings 里提取关心的字段 T。
     * 已包含 distinctUntilChanged 去重。
     */
    public <T> Flowable<T> observeChanged(Function<Settings, T> mapper) {
        return dataStore.data()
                .map(mapper)
                .distinctUntilChanged()
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread());
    }

    public <T> Flowable<T> observe(Function<Settings, T> mapper) {
        return dataStore.data()
                .map(mapper)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread());
    }

    /**
     * 整体快照流,调试日志用。
     */
    public Flowable<Settings> observeAll() {
        return observeChanged(settings -> settings);
    }

    /**
     * 通用写:调用方传入 updater,接收当前 Settings,返回新的 Settings。
     * 注意与 Preferences DataStore 的区别:
     * - Preferences:{@code Consumer<MutablePreferences>}(修改 mutable 对象,无返回值)
     * - JSON DataStore:{@code Function<Settings, Settings>}(不可变,返回新对象)
     */
    public Single<Settings> update(Function<Settings, Settings> updater) {
        return dataStore.updateDataAsync(current -> {
            try {
                return Single.just(updater.apply(current));
            } catch (Throwable t) {
                return Single.error(t);
            }
        });
    }
}

SettingsViewModel

java 复制代码
public class SettingsViewModel extends ViewModel {

    private static final String TAG = "JsonDataStoreDemo";

    private final SettingsRepository repo = SettingsRepository.getInstance();
    private final CompositeDisposable disposables = new CompositeDisposable();

    private final LiveData<Integer> counterA =
            LiveDataReactiveStreams.fromPublisher(repo.observeChanged(settings -> settings.counterA));
    private final LiveData<Integer> counterB =
            LiveDataReactiveStreams.fromPublisher(repo.observeChanged(settings -> settings.counterB));
    private final LiveData<Settings> allEntries =
            LiveDataReactiveStreams.fromPublisher(repo.observeAll());

    public LiveData<Integer> getCounterA() {
        return counterA;
    }

    public LiveData<Integer> getCounterB() {
        return counterB;
    }

    public LiveData<Settings> getAllEntries() {
        return allEntries;
    }

    // 写:updater 接收当前 Settings,返回新的 Settings(不可变拷贝)
    public void incA() {
        disposables.add(
                repo.update(s -> s.withCounterA(s.counterA + 1))
                        .subscribe(
                                s -> Log.d(TAG, "inc A ok: " + s),
                                e -> Log.e(TAG, "inc A err", e)
                        )
        );
    }

    public void incB() {
        disposables.add(
                repo.update(s -> s.withCounterB(s.counterB + 1))
                        .subscribe(
                                s -> Log.d(TAG, "inc B ok: " + s),
                                e -> Log.e(TAG, "inc B err", e)
                        )
        );
    }

    public void reset() {
        disposables.add(
                repo.update(s -> new Settings())
                        .subscribe(
                                s -> Log.d(TAG, "reset ok: " + s),
                                e -> Log.e(TAG, "reset err", e)
                        )
        );
    }

    @Override
    protected void onCleared() {
        super.onCleared();
        disposables.clear();
    }
}

MainActivity

java 复制代码
public class MainActivity extends AppCompatActivity {

    private static final String TAG = "DataStoreDemo";

    private TextView tvCounterA;
    private TextView tvCounterB;
    private Button mBtnInc;
    private Button mBtnIncB;
    private Button mBtnReset;

    private SettingsViewModel viewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
        initViewModel();
        initListener();
    }

    private void initView() {
        tvCounterA = findViewById(R.id.tv_counterA);
        tvCounterB = findViewById(R.id.tv_counter_b);
        mBtnInc = findViewById(R.id.btn_inc);
        mBtnIncB = findViewById(R.id.btn_inc_b);
        mBtnReset = findViewById(R.id.btn_reset);
    }

    private void initViewModel() {
        viewModel = new ViewModelProvider(this).get(SettingsViewModel.class);

        viewModel.getCounterA().observe(this, value ->
                tvCounterA.setText("A=" + value));
        viewModel.getCounterB().observe(this, value ->
                tvCounterB.setText("B=" + value));
        viewModel.getAllEntries().observe(this, this::logAllEntries);
    }

    private void initListener() {
        mBtnInc.setOnClickListener(v -> viewModel.incA());
        mBtnIncB.setOnClickListener(v -> viewModel.incB());
        mBtnReset.setOnClickListener(v -> viewModel.reset());
    }

    private void logAllEntries(Settings settings) {
        Log.d(TAG, "all entries = " + settings);
    }
}

activity_main.xml

xml 复制代码
<?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:id="@+id/page_list_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/tv_counterA"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="0"
        android:textSize="24sp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toTopOf="@id/tv_counter_b"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

    <TextView
        android:id="@+id/tv_counter_b"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="0"
        android:textSize="24sp"
        android:layout_marginTop="16dp"
        app:layout_constraintTop_toBottomOf="@id/tv_counterA"
        app:layout_constraintBottom_toTopOf="@id/btn_inc"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

    <Button
        android:id="@+id/btn_inc"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="A +1"
        android:layout_marginEnd="8dp"
        app:layout_constraintTop_toBottomOf="@id/tv_counter_b"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toStartOf="@id/btn_inc_b" />

    <Button
        android:id="@+id/btn_inc_b"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="B +1"
        android:layout_marginEnd="8dp"
        app:layout_constraintTop_toTopOf="@id/btn_inc"
        app:layout_constraintBottom_toBottomOf="@id/btn_inc"
        app:layout_constraintStart_toEndOf="@id/btn_inc"
        app:layout_constraintEnd_toStartOf="@id/btn_reset" />

    <Button
        android:id="@+id/btn_reset"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="重置"
        app:layout_constraintTop_toTopOf="@id/btn_inc"
        app:layout_constraintBottom_toBottomOf="@id/btn_inc"
        app:layout_constraintStart_toEndOf="@id/btn_inc_b"
        app:layout_constraintEnd_toEndOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

查看数据

从以下路径取出文件

复制代码
/data/data/com.example.demo1/files/datastore/settings.json

json格式可直接打开,结果如下

复制代码
{"counterA":0,"counterB":0}

多进程代码中使用 DataStore

DataStore提供多进程同步

  • 读取仅返回已持久存储到磁盘的数据。
  • 写后读一致性。
  • 写入会序列化。
  • 写入绝不会阻塞读取。

MyApplication

java 复制代码
public class MyApplication extends Application {

    private static MyApplication instance;

    @Override
    public void onCreate() {
        super.onCreate();
        instance = this;

        // JSON DataStore(多进程):每个进程的 Application.onCreate() 各自调一次 init,
        // 两个进程指向同一个 settings.json 文件,由文件锁保证跨进程一致性。
        MultiProcessSettingsDataStore.INSTANCE.init(this);
    }

    public static Context getContext() {
        return instance;
    }

}

Settings

java 复制代码
public class Settings {

    /**
     * 使用final 字段 + withXxx 方法,保证不可变性。
     */

    public final int counterA;
    public final int counterB;

    /**
     * 无参构造:Gson 反序列化时需要;默认值全为 0。
     */
    public Settings() {
        this(0, 0);
    }

    public Settings(int counterA, int counterB) {
        this.counterA = counterA;
        this.counterB = counterB;
    }

    public Settings withCounterA(int value) {
        return new Settings(value, counterB);
    }

    public Settings withCounterB(int value) {
        return new Settings(counterA, value);
    }

    @NonNull
    @Override
    public String toString() {
        return "Settings{counterA=" + counterA + ", counterB=" + counterB + "}";
    }
}

SettingsSerializer.kt

java 复制代码
object SettingsSerializer : Serializer<Settings> {

    private val gson = Gson()

    override val defaultValue: Settings = Settings()

    override suspend fun readFrom(input: InputStream): Settings {
        return try {
            InputStreamReader(input, StandardCharsets.UTF_8).use { reader ->
                gson.fromJson(reader, Settings::class.java) ?: defaultValue
            }
        } catch (e: Exception) {
            throw CorruptionException("settings.json corrupted", e)
        }
    }

    override suspend fun writeTo(t: Settings, output: OutputStream) {
        OutputStreamWriter(output, StandardCharsets.UTF_8).use { writer ->
            gson.toJson(t, writer)
        }
    }
}

SettingsRepository

java 复制代码
public class SettingsRepository {

    private static volatile SettingsRepository INSTANCE;

    public static SettingsRepository getInstance() {
        if (INSTANCE == null) {
            synchronized (SettingsRepository.class) {
                if (INSTANCE == null) {
                    INSTANCE = new SettingsRepository();
                }
            }
        }
        return INSTANCE;
    }

    private SettingsRepository() {
        // MultiProcessSettingsDataStore 已在 MyApplication.onCreate() 中 init,这里直接使用
    }

    // ---------- 读 ----------

    /**
     * 通用读:mapper 从 Settings 里提取关心的字段 T,已包含 distinctUntilChanged。
     * 任意进程写入后,此 Flowable 都会收到通知。
     */
    public <T> Flowable<T> observeChanged(Function<Settings, T> mapper) {
        return MultiProcessSettingsDataStore.INSTANCE.data()
                .map(mapper)
                .distinctUntilChanged()
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread());
    }

    /** 整体快照流,调试日志用。 */
    public Flowable<Settings> observeAll() {
        return observeChanged(settings -> settings);
    }

    // ---------- 写 ----------

    /**
     * 通用写:updater 接收当前 Settings,返回新 Settings。
     * 底层使用文件锁,跨进程写操作串行,不会丢数据。
     */
    public Single<Settings> update(Function<Settings, Settings> updater) {
        return MultiProcessSettingsDataStore.INSTANCE.update(updater);
    }
}

SettingsViewModel

java 复制代码
public class SettingsViewModel extends ViewModel {

    private static final String TAG = "JsonDataStoreDemo";

    private final SettingsRepository repo = SettingsRepository.getInstance();
    private final CompositeDisposable disposables = new CompositeDisposable();

    private final LiveData<Integer> counterA =
            LiveDataReactiveStreams.fromPublisher(repo.observeChanged(settings -> settings.counterA));
    private final LiveData<Integer> counterB =
            LiveDataReactiveStreams.fromPublisher(repo.observeChanged(settings -> settings.counterB));
    private final LiveData<Settings> allEntries =
            LiveDataReactiveStreams.fromPublisher(repo.observeAll());

    public LiveData<Integer> getCounterA() {
        return counterA;
    }

    public LiveData<Integer> getCounterB() {
        return counterB;
    }

    public LiveData<Settings> getAllEntries() {
        return allEntries;
    }

    // 写:updater 接收当前 Settings,返回新的 Settings(不可变拷贝)
    public void incA() {
        disposables.add(
                repo.update(s -> s.withCounterA(s.counterA + 1))
                        .subscribe(
                                s -> Log.d(TAG, "inc A ok: " + s),
                                e -> Log.e(TAG, "inc A err", e)
                        )
        );
    }

    public void incB() {
        disposables.add(
                repo.update(s -> s.withCounterB(s.counterB + 1))
                        .subscribe(
                                s -> Log.d(TAG, "inc B ok: " + s),
                                e -> Log.e(TAG, "inc B err", e)
                        )
        );
    }

    public void reset() {
        disposables.add(
                repo.update(s -> new Settings())
                        .subscribe(
                                s -> Log.d(TAG, "reset ok: " + s),
                                e -> Log.e(TAG, "reset err", e)
                        )
        );
    }

    @Override
    protected void onCleared() {
        super.onCleared();
        disposables.clear();
    }
}

MultiProcessSettingsDataStore.kt

kotlin 复制代码
object MultiProcessSettingsDataStore {

    // "lateinit var" 表示延迟初始化的变量(先声明,后赋值)
    // 相当于 Java 的 private DataStore<Settings> dataStore;
    // "private" 只在本文件内可见
    private lateinit var dataStore: DataStore<Settings>
    
    /**
     * 初始化,在 MyApplication.onCreate() 中调用。
     * 每个进程各自调一次,但两个进程指向同一个 settings.json 文件。
     */
    // "fun" 是 Kotlin 的方法声明,相当于 Java 的 void/返回值类型 方法名()
    fun init(context: Context) {
        // "::dataStore.isInitialized" 判断 lateinit 变量是否已赋值,避免重复初始化
        // 相当于 Java 的 if (dataStore != null) return;
        if (::dataStore.isInitialized) return

        // MultiProcessDataStoreFactory.create() 创建支持跨进程的 DataStore
        // 相当于 Java 的 new RxDataStoreBuilder<>(...).build(),但支持文件锁
        dataStore = MultiProcessDataStoreFactory.create(
            serializer = SettingsSerializer,   // 序列化器,告诉 DataStore 如何读写 Settings 对象
            produceFile = {
                // 这是一个 lambda(匿名函数),返回文件路径
                // 相当于 Java 的 () -> new File(...)
                // 两个进程必须返回同一个路径,才能共享数据
                File("${context.applicationContext.filesDir}/datastore/settings.json")
                // "${...}" 是 Kotlin 的字符串模板,相当于 Java 的 + 拼接
            },
            corruptionHandler = ReplaceFileCorruptionHandler { Settings(1,1) }
        )
    }

    /**
     * 读:把 Kotlin 的 Flow<Settings> 转成 Java 能用的 Flowable<Settings>。
     * 任意进程写入后,其他进程的订阅者都会收到通知。
     */
    fun data(): Flowable<Settings> =
        // rxFlowable { } 把协程 block 转成 Flowable,来自 kotlinx-coroutines-rx3 库
        rxFlowable(Dispatchers.IO) {
            // dataStore.data 是 Flow<Settings>(Kotlin 的数据流,类似 RxJava 的 Flowable)
            // .collect { } 是订阅操作,相当于 RxJava 的 .subscribe(value -> ...)
            // send(it) 把每次收到的 Settings 发射出去给 Flowable 的订阅者
            // "it" 是 Kotlin lambda 只有一个参数时的默认名字,相当于 Java 的 s -> send(s)
            dataStore.data.collect { send(it) }
        }

    /**
     * 写:接收 Java 的 RxFunction,底层调用 suspend updateData,包装成 Single。
     */
    // "Function<Settings, Settings>" 是 RxJava3 的函数接口
    // 用 RxJava 的 Function 而不是 Kotlin 的 (Settings) -> Settings,
    // 是为了让 Java 调用方可以直接传 lambda,不需要感知 Kotlin 类型
    fun update(transform: Function<Settings, Settings>): Single<Settings> =
        // rxSingle { } 把一个 suspend 操作转成 Single,来自 kotlinx-coroutines-rx3 库
        rxSingle(Dispatchers.IO) {
            // dataStore.updateData 是 suspend 函数(只能在协程里调用)
            // 在文件锁内原子执行:先读 → 执行 transform → 写盘
            // transform.apply(it) 调用 Java 的 Function 接口方法,相当于 Java 的 transform.apply(current)
            dataStore.updateData { transform.apply(it) }
        }
}

AndroidManifest.xml

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <application
        android:name=".MyApplication"
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Demo1"
        tools:targetApi="31">
        <activity
            android:name=".datastore.MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <!--
            android:process=":b_process" 让 ActivityB 运行在独立进程。
            前缀 ":" 表示这是 App 私有进程,完整名为 com.example.demo1:b_process。
            该进程有自己的 MyApplication.onCreate(),会重新初始化 MultiProcessSettingsDataStore,
            但指向同一个 settings.json 文件,由文件锁保证跨进程数据一致性。
        -->
        <activity
            android:name=".datastore.json.ActivityB"
            android:exported="false"
            android:process=":b_process" />
    </application>

</manifest>

MainActivity

java 复制代码
public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";

    private TextView tvCounterA;
    private TextView tvCounterB;
    private Button mBtnInc;
    private Button mBtnIncB;
    private Button mBtnReset;
    private Button mBtnGoB;
    private Button mBtnCorrupt;

    private SettingsViewModel viewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main2);
        initView();
        initViewModel();
        initListener();
    }

    private void initView() {
        tvCounterA = findViewById(R.id.tv_counterA);
        tvCounterB = findViewById(R.id.tv_counter_b);
        mBtnInc = findViewById(R.id.btn_inc);
        mBtnIncB = findViewById(R.id.btn_inc_b);
        mBtnReset = findViewById(R.id.btn_reset);
        mBtnGoB = findViewById(R.id.btn_go_b);
        mBtnCorrupt = findViewById(R.id.btn_corrupt);
    }

    private void initViewModel() {
        viewModel = new ViewModelProvider(this).get(SettingsViewModel.class);

        viewModel.getCounterA().observe(this, value ->
                tvCounterA.setText("A=" + value));
        viewModel.getCounterB().observe(this, value ->
                tvCounterB.setText("B=" + value));
        viewModel.getAllEntries().observe(this, this::logAllEntries);
    }

    private void initListener() {
        mBtnInc.setOnClickListener(v -> viewModel.incA());
        mBtnIncB.setOnClickListener(v -> viewModel.incB());
        mBtnReset.setOnClickListener(v -> viewModel.reset());
        mBtnGoB.setOnClickListener(v ->
                startActivity(new Intent(this, ActivityB.class)));
        mBtnCorrupt.setOnClickListener(v -> corruptAndRestart());
    }

    /**
     * 写入非法 JSON 并杀进程,下次启动会抛异常CorruptionException,然后通过 ReplaceFileCorruptionHandler 修复文件
     */
    private void corruptAndRestart() {
        File f = new File(getFilesDir(), "datastore/settings.json");
        try (FileOutputStream fos = new FileOutputStream(f)) {
            fos.write("not a json".getBytes());
            Log.d(TAG, "corrupted: " + f.getAbsolutePath());
            Toast.makeText(this, "已破坏文件,进程将重启", Toast.LENGTH_SHORT).show();
        } catch (IOException e) {
            Log.e(TAG, "corrupt failed", e);
            return;
        }
        // 延迟一下,让 Toast 有机会显示
        mBtnCorrupt.postDelayed(
                () -> android.os.Process.killProcess(android.os.Process.myPid()),
                500);
    }

    private void logAllEntries(Settings settings) {
        Log.d(TAG, "all entries = " + settings);
    }
}

activity_main.xml

xml 复制代码
<?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:id="@+id/page_list_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/tv_counterA"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="0"
        android:textSize="24sp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toTopOf="@id/tv_counter_b"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

    <TextView
        android:id="@+id/tv_counter_b"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="0"
        android:textSize="24sp"
        android:layout_marginTop="16dp"
        app:layout_constraintTop_toBottomOf="@id/tv_counterA"
        app:layout_constraintBottom_toTopOf="@id/btn_inc"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

    <Button
        android:id="@+id/btn_inc"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="A +1"
        android:layout_marginEnd="8dp"
        app:layout_constraintTop_toBottomOf="@id/tv_counter_b"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toStartOf="@id/btn_inc_b" />

    <Button
        android:id="@+id/btn_inc_b"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="B +1"
        android:layout_marginEnd="8dp"
        app:layout_constraintTop_toTopOf="@id/btn_inc"
        app:layout_constraintBottom_toBottomOf="@id/btn_inc"
        app:layout_constraintStart_toEndOf="@id/btn_inc"
        app:layout_constraintEnd_toStartOf="@id/btn_reset" />

    <Button
        android:id="@+id/btn_reset"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="重置"
        app:layout_constraintTop_toTopOf="@id/btn_inc"
        app:layout_constraintBottom_toBottomOf="@id/btn_inc"
        app:layout_constraintStart_toEndOf="@id/btn_inc_b"
        app:layout_constraintEnd_toEndOf="parent" />

    <Button
        android:id="@+id/btn_go_b"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="跳到进程B"
        android:layout_marginTop="16dp"
        app:layout_constraintTop_toBottomOf="@id/btn_inc"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

    <Button
        android:id="@+id/btn_corrupt"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="破坏文件并重启"
        android:layout_marginTop="16dp"
        app:layout_constraintTop_toBottomOf="@id/btn_go_b"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

ActivityB

java 复制代码
public class ActivityB extends AppCompatActivity {

    private static final String TAG = "ActivityB";

    private TextView tvCounterA;
    private TextView tvCounterB;
    private SettingsViewModel viewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_b);

        tvCounterA = findViewById(R.id.tv_counterA);
        tvCounterB = findViewById(R.id.tv_counter_b);
        Button btnInc = findViewById(R.id.btn_inc);
        Button btnIncB = findViewById(R.id.btn_inc_b);
        Button btnReset = findViewById(R.id.btn_reset);

        // SettingsViewModel 和 SettingsRepository 与主进程用同一套代码,
        // 但这是 :b_process 里全新的实例,通过文件锁与主进程协调
        viewModel = new ViewModelProvider(this).get(SettingsViewModel.class);

        viewModel.getCounterA().observe(this, value -> {
            tvCounterA.setText("A = " + value);
            Log.d(TAG, "counterA updated: " + value);
        });
        viewModel.getCounterB().observe(this, value -> {
            tvCounterB.setText("B = " + value);
            Log.d(TAG, "counterB updated: " + value);
        });

        viewModel.getAllEntries().observe(this, s ->
                Log.d(TAG, "all entries = " + s));

        btnInc.setOnClickListener(v -> viewModel.incA());
        btnIncB.setOnClickListener(v -> viewModel.incB());
        btnReset.setOnClickListener(v -> viewModel.reset());
    }
}

activity_b.xml

xml 复制代码
<?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="match_parent">

    <TextView
        android:id="@+id/tv_process_tag"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Process B"
        android:textSize="14sp"
        android:textColor="#FF5722"
        android:layout_marginTop="32dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

    <TextView
        android:id="@+id/tv_counterA"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="0"
        android:textSize="24sp"
        android:layout_marginTop="16dp"
        app:layout_constraintTop_toBottomOf="@id/tv_process_tag"
        app:layout_constraintBottom_toTopOf="@id/tv_counter_b"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

    <TextView
        android:id="@+id/tv_counter_b"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="0"
        android:textSize="24sp"
        android:layout_marginTop="16dp"
        app:layout_constraintTop_toBottomOf="@id/tv_counterA"
        app:layout_constraintBottom_toTopOf="@id/btn_inc"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

    <Button
        android:id="@+id/btn_inc"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="A +1"
        android:layout_marginEnd="8dp"
        app:layout_constraintTop_toBottomOf="@id/tv_counter_b"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toStartOf="@id/btn_inc_b" />

    <Button
        android:id="@+id/btn_inc_b"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="B +1"
        android:layout_marginEnd="8dp"
        app:layout_constraintTop_toTopOf="@id/btn_inc"
        app:layout_constraintBottom_toBottomOf="@id/btn_inc"
        app:layout_constraintStart_toEndOf="@id/btn_inc"
        app:layout_constraintEnd_toStartOf="@id/btn_reset" />

    <Button
        android:id="@+id/btn_reset"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="重置"
        app:layout_constraintTop_toTopOf="@id/btn_inc"
        app:layout_constraintBottom_toBottomOf="@id/btn_inc"
        app:layout_constraintStart_toEndOf="@id/btn_inc_b"
        app:layout_constraintEnd_toEndOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
相关推荐
咸鱼翻身小阿橙1 小时前
文件读写 + Qt Model/View + 自定义分页+搜索过滤
java·数据库·qt
在繁华处1 小时前
Java从零到熟练(十):JVM基础与性能优化
java·jvm·性能优化
Demon1_Coder1 小时前
Day1-SpringAI-1.0.0版本
java·开发语言·前端
老码观察1 小时前
设计模式实战解读(九):责任链模式——流水线上层层把关的艺术
java·设计模式·责任链模式
眸生1 小时前
基于NeteaseCloudMusicApi的音乐app 支持 DeepSeek 自然语言找歌、批量导入歌单、下载音乐转换成MP3,下载歌词
android·python·kotlin·android studio·音频·fastapi·android jetpack
basketball6161 小时前
C++进阶:2. std::move 和 std::forward 函数
java·开发语言·c++
霸道流氓气质1 小时前
Maven 批处理脚本与 Qoder 配置使用指南
java·maven
架构源启1 小时前
Spring AI进阶系列(14)- 2026 可观测性最佳实践:从链路追踪到企业级 AI 治理落地
java·人工智能·spring
码上有光1 小时前
c++: 继承(下)
android·java·c++·多继承·菱形继承·虚继承