ViewModel

ViewModel preserves data beyond the lifecycle of individual Activities i.e. it is lifecycle-aware – it will hold data for the UI even if e.g. the screen is rotated, causing the Activity to be remade. It persists from onCreate() until onCleared() (after finish() is applied to the Activity), thus helping to avoid re-querying a data source repeatedly upon Activity changes. This goes beyond the savedInstanceState which is limited to small amounts of data which can be easily serialised and de-serialised.

It also helps to avoid zombie threads (memory leaks), which were begun but not terminated as the Activity which created them was destroyed before they completed. When the Activity or Fragment is finished any Observers are closed, so no orphaned processes are left running.

Works in collaboration with LiveData. For more information see here. If your ViewModel needs access to the application context then extend AndroidViewModel rather than ViewModel (as below).

Caution: A ViewModel must never reference a view, Lifecycle, or any class that may hold a reference to the activity context.

To add the dependencies for ViewModel, see here.

To implement a ViewModel, first create a ViewModel class(this example corresponds to the code in the @Entity, @DAO, Build a Database and LiveData posts):

public class MainViewModel extends AndroidViewModel {

    LiveData<List<ContactEntry>> contacts;

    public MainViewModel(@NonNull Application application) {
        super(application);
        contacts = AppDatabase.getInstance(this.getApplication()).contactDao().loadAllContacts(); // Gets the List of contact entries from the instance of the database (see @DAO post)
    }

    public LiveData<List<ContactEntry>> getContacts() { // Create a getter for the LivaData List above
        return contacts;
    }
}

In your Activity, set an Observer on the getContacts() method in the ViewModel, and tell it what to do if the data changes:

MainViewModel viewModel = ViewModelProviders.of(this).get(MainViewModel.class);
viewModel.getContacts().observe(this, new Observer<List<ContactEntry>>() {
            @Override
            public void onChanged(@Nullable List<ContactEntry> contactEntries) {
                mAdapter.setContacts(contactEntries); // Code to run if data changes
            }
        });

For applications which require variables to be passed to the ViewModel refer to the ViewModelFactory post. The Factory holds a template for creating ViewModels that you can customise to allow passing arguments into the ViewModel constructor.

Settings or Preferences – Making Changes In App

How to make user changes to options within the Settings Fragment affect changes in the app.

In the Activity call a function to setup shared preferences, then create that function. We will also link the shared preference object with the onSharedPreferenceChangeListener using .registerOnSharedPreferenceChangeListener:

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_xxxxx);
        mTargetView =  findViewById(R.id.activity_xxxxx); // View to be affected by the preference change
        setupSharedPreferences(); // Call custom function to set up preferences
    }

    private void setupSharedPreferences() {
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); // Get a reference to the default shared preferences from the PreferenceManager class
        mTargetView.setShowText(sharedPreferences.getBoolean(getString(R.string.pref_xxxxx), getResources().getBoolean(R.bool.pref_xxxxx_default))); // Get the value of a preference and use it to call a function (in this case, the custom function mTargetView.setShowText) which will change behaviour in the app
        sharedPreferences.registerOnSharedPreferenceChangeListener(this); // We can use 'this' as context as the Activity will implement OnSharedPreferenceChangeListener (see next)
    }

To make sure the Activity implements the change without having to force an onCreate (e.g.rotate device) implement an OnSharedPreferenceChangeListener within the relevant Activity:

public class xxxxxActivity extends AppCompatActivity implements SharedPreferences.OnSharedPreferenceChangeListener {

    @Override
    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { // This function already passes us a reference to the sharedPreferences and key, so we don't need to use the PreferenceManager
        if (key.equals(getString(R.string.pref_show_bass_key))) {
            mTargetView.setShowBass(sharedPreferences.getBoolean(key, getResources().getBoolean(R.bool.pref_show_text_default)));
        }
    }
}

Unregister the OnSharedPreferenceChangeListener when the Activity is shut down using .unregisterOnSharedPreferenceChangeListener:

    @Override
    protected void onDestroy() {
        super.onDestroy();
        PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(this);
    }

ud851-Exercises-student\Lesson06-Visualizer-Preferences\T06.05

Settings or Preferences Fragment

How to set up a settings Activity with a PreferenceFragment.

Create an Activity named SettingsActivity (including the activity_settings.xml layout file) with the following code:

public class SettingsActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_settings);
        ActionBar actionBar = this.getSupportActionBar();

        if (actionBar != null) {
            actionBar.setDisplayHomeAsUpEnabled(true); // Enables the up button in the Action Bar
        }
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();
        if (id == android.R.id.home) {
            NavUtils.navigateUpFromSameTask(this); // Navigates up to parent activity (add supporting code to AndroidManifest.xml) OR...
            onBackPressed(); // ...replicates the function of the Back button (i.e. returns to last Activity) DO NOT ADD BOTH
        }
        return super.onOptionsItemSelected(item);
    }
}

To construct a menu with the option to enter the Settings fragment, create a new resource folder called 'menu' (if it doesn't already exist) and create 'xxxxx_menu.xml' (customise) as follows:

<?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/action_settings" // Rename the id as necessary
     android:title="Settings" // Text that will appear in the menu listing - move to strings.xml
     android:orderInCategory="100" // Order of item in menu
     app:showAsAction="never" /> // Determine whether it will appear in the action bar (if room) as opposed to the dropdown menu

</menu>

Add the following code to your Activity to add the menu:

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.xxxxx_menu, menu); // Customise according to xml name
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        if(item.getItemId() == R.id.action_settings) {
            Intent startSettingsActivity = new Intent(this, SettingsActivity.class);
            startActivity(startSettingsActivity);
            return true; // Deliver explicit Intent to start Settings Activity on click
        }
        return super.onOptionsItemSelected(item);
    }

Within the AndroidManifest.xml file, make the SettingsActivity a child of the main activity (DO NOT do this if you intend to access your Settings screen from multiple Activities as this will lock it to one parent Activity. Instead, leave out lines 12-15 and use the onBackPressed() option is your Settings Activity) and also make the main activity launch mode 'singleTop' to prevent the Activity being remade on returning:

        <activity
         android:name=".xxxxxActivity" // Customise
         android:launchMode="singleTop"> // Stops parent activity being remade on exiting Settings
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity // Register
         android:name=".SettingsActivity"
         android:label="Settings" // ADD THESE LINES. Move text value to strings.xml
         android:parentActivityName=".xxxxxActivity"> // ADD THESE LINES ONLY IF NAVIGATING TO PARENT. Customise
            <meta-data // ADD THESE LINES ONLY IF NAVIGATING TO PARENT
             android:name="android.support.PARENT_ACTIVITY" // ADD THESE LINES ONLY IF NAVIGATING TO PARENT
             android:value=".xxxxxActivity" /> // ADD THESE LINES ONLY IF NAVIGATING TO PARENT. Customise
        </activity>

In the res -> xml folder create a file called pref_xxxxx.xml (customise) to define the options within the Settings Fragment:

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

    <CheckBoxPreference // In this example we have a tickbox option android:defaultValue="true" // Default value of true/ticked
     android:key="show_text" // The reference of the value which can be changed
     android:summaryOff="@string/hidden" // Text to be shown when tickbox is not checked
     android:summaryOn="@string/shown" // Text to be shown when tickbox is checked
     android:title="@string/show_text" // Title of option
     />

</PreferenceScreen>

Create a class called SettingsFragment as follows (may require the addition of: implementation 'com.android.support:preference-v7:28.0.0' to your gradle file):

public class SettingsFragment extends PreferenceFragmentCompat {
    @Override
    public void onCreatePreferences(Bundle bundle, String s) {
        addPreferencesFromResource(R.xml.pref_xxxxx); // Add the preferences xml to display the options (customise)
    }
}

Adjust the activity_settings.xml file to reflect a Fragment layout:

<?xml version="1.0" encoding="utf-8"?>
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_settings"
android:name="xxx.xxxxx.xxx.xxxxx.SettingsFragment" // Customise xxxxx with app name e.g. android.com.example.app
android:layout_width="match_parent"
android:layout_height="match_parent">
</fragment>

Add a theme in the styles.xml file:

        <item name="preferenceTheme">@style/PreferenceThemeOverlay</item> // App will crash without this addition

To make user changes to the Settings Fragment take effect, follow this post: Settings or Preferences - Making Changes In App

ud851-Exercises-student\Lesson06-Visualizer-Preferences\T06.01 & 02

Adding An Activity, Up Navigation, Accessing With Explicit Intents, Passing Data

How to add an Activity to your app and navigate to it using an explicit Intent.

Create an empty Activity through the IDE. IDE will automatically add the essential code to the AndroidManifests.xml file. Make any desirable additions.

        <activity android:name="com.example.android.explicitintent.MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <activity android:name="com.example.android.explicitintent.ChildActivity"
            android:label="@string/action_settings" // Name at top of screen for Activity
            android:parentActivityName=".MainActivity"> // Make sure the back arrow functions properly by declaring parent activity
            <meta-data // To support Android 4.0 and lower
                android:name="android.support.PARENT_ACTIVITY"
                android:value=".MainActivity" />
        </activity>

In new Activity, enable Up navigation.

getSupportActionBar().setDisplayHomeAsUpEnabled(true); // Or getActionBar() on older versions before support library

In Parent Activity, set up an explicit Intent.

        private Button doSomethingCoolButton = (Button) findViewById(R.id.b_do_something_cool);
        private String text = "Send me!"; // Text to be passed to new Activity
        mDoSomethingCoolButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                Context context = MainActivity.this;

                /* This is the class that we want to start (and open) when the button is clicked. */
                Class destinationActivity = ChildActivity.class;

                /*
                 * Here, we create the Intent that will start the Activity we specified above in
                 * the destinationActivity variable. The constructor for an Intent also requires a
                 * context, which we stored in the variable named "context".
                 */
                Intent startChildActivityIntent = new Intent(context, destinationActivity);

                startChildActivityIntent.putExtra(Intent.EXTRA_TEXT, text); // Add text as extra data

                /*
                 * Once the Intent has been created, we can use Activity's method, "startActivity"
                 * to start the ChildActivity.
                 */
                startActivity(startChildActivityIntent);
            }
        });

In new Activity, access and use the extra data.

        mDisplayText = (TextView) findViewById(R.id.tv_display); // Find a TextView

        // Use the getIntent method to store the Intent that started this Activity in a variable
        Intent intent = getIntent();

        // Check if this Intent has the extra text we passed from MainActivity
        if (intent.hasExtra(Intent.EXTRA_TEXT)) {
            String enteredText = intent.getStringExtra(Intent.EXTRA_TEXT);
            mDisplayText.setText(enteredText); // Assign text to TextView
        }