Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Using AndroidViewModel to Access Application Context and Persist Data with SharedPreferences

Tech May 9 11

This example demonstrates a basic counter application that retains its state across process death by leveraging AndroidViewModel, SavedStateHandle, and SharedPreferences.

AndroidViewModel extends the base ViewModel class and provides access to the application context via getApplication(). This allows safe access to global resources such as strings, shared preferences, or system services without leaking activity contxets.

The implementation uses:

  • SavedStateHandle to retain UI-related data during configuration changes.
  • LiveData to observe changes and automatically update the UI.
  • SharedPreferences for persistent storage—ensuring data survives app restarts or device reboots.

MyViewModel.java

package com.example.viewmodelshp;

import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;

import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.SavedStateHandle;

public class CounterViewModel extends AndroidViewModel {
    private final SavedStateHandle savedState;
    private final String dataKey;
    private final String prefsName;

    public CounterViewModel(@NonNull Application app, SavedStateHandle handle) {
        super(app);
        this.savedState = handle;
        this.dataKey = app.getResources().getString(R.string.data_key);
        this.prefsName = app.getResources().getString(R.string.shp_name);

        if (!savedState.contains(dataKey)) {
            restoreFromDisk();
        }
    }

    public LiveData<Integer> getCurrentValue() {
        return savedState.getLiveData(dataKey);
    }

    private void restoreFromDisk() {
        SharedPreferences prefs = getApplication().getSharedPreferences(prefsName, Context.MODE_PRIVATE);
        int savedValue = prefs.getInt(dataKey, 0);
        savedState.set(dataKey, savedValue);
    }

    public void persistToDisk() {
        Integer current = getCurrentValue().getValue();
        if (current == null) current = 0;

        SharedPreferences prefs = getApplication().getSharedPreferences(prefsName, Context.MODE_PRIVATE);
        prefs.edit().putInt(dataKey, current).apply();
    }

    public void increment(int delta) {
        Integer current = getCurrentValue().getValue();
        savedState.set(dataKey, (current == null ? 0 : current) + delta);
    }
}

MainActivity.java

package com.example.viewmodelshp;

import androidx.appcompat.app.AppCompatActivity;
import androidx.databinding.DataBindingUtil;
import androidx.lifecycle.SavedStateViewModelFactory;
import androidx.lifecycle.ViewModelProvider;

import android.os.Bundle;

import com.example.viewmodelshp.databinding.ActivityMainBinding;

public class MainActivity extends AppCompatActivity {

    private CounterViewModel viewModel;
    private ActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main);

        viewModel = new ViewModelProvider(
            this,
            new SavedStateViewModelFactory(getApplication(), this)
        ).get(CounterViewModel.class);

        binding.setViewModel(viewModel);
        binding.setLifecycleOwner(this);
    }

    @Override
    protected void onPause() {
        super.onPause();
        viewModel.persistToDisk();
    }
}

String Resources (res/values/strings.xml)

<resources>
    <string name="app_name">ViewModelSHP</string>
    <string name="button_plus">+</string>
    <string name="button_minus">-</string>
    <string name="data_key">counter_value</string>
    <string name="shp_name">app_preferences</string>
</resources>

Data Binding Layout (activity_main.xml)

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>
        <variable
            name="viewModel"
            type="com.example.viewmodelshp.CounterViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <TextView
            android:id="@+id/textView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{String.valueOf(viewModel.currentValue)}"
            android:textSize="30sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintVertical_bias="0.3"
            tools:text="0" />

        <Button
            android:id="@+id/button_minus"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="@{() -> viewModel.increment(-1)}"
            android:text="@string/button_minus"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.76"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintVertical_bias="0.48" />

        <Button
            android:id="@+id/button_plus"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="@{() -> viewModel.increment(1)}"
            android:text="@string/button_plus"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.24"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintVertical_bias="0.48" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

Related Articles

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

SBUS Signal Analysis and Communication Implementation Using STM32 with Fus Remote Controller

Overview In a recent project, I utilized the SBUS protocol with the Fus remote controller to control a vehicle's basic operations, including movement, lights, and mode switching. This article is aimed...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.