Shared Preferences, Preference Fragment & transparent Navbar

PreferenceActivity is deprecated now but it’s a really nice new solution to manage your preferences. I’m gonna show you a simple implementation of PreferenceFragment, how to use it in Activity which you’d like to make a Preference Page and how to get values in your app. Example is about saving booleans and making preference fields clickable to call a method. Obviously you can put a wide variety of preference fields and it will work out of the box. PreferenceFragment makes everything easier, automated and it’s great.

When I started to learn about creating Android Apps, PreferenceActivity was already deprecated so I had an opportunity to learn something up to date. I really recommend that way of making a preference page. It’s more adjustable and complex solution. First thing to do is to create a preference layout. You can notice it’s located in xml directory while all normal layouts are in layout directory. Well, it’s somehow special and deserves its own location. You don’t have one? Just create the directory in your resources.

<PreferenceScreen
    xmlns:android="http://schemas.android.com/apk/res/android">

    <PreferenceCategory
        android:title="@string/preference_category1">

        <CheckBoxPreference
            android:key="example_key"
            android:title="@string/example_key"
            android:defaultValue="true"/>

        <CheckBoxPreference
            android:key="transparent_nav"
            android:title="@string/transparent_nav"
            android:summary="@string/transparent_nav_description"
            android:defaultValue="false"/>

    </PreferenceCategory>

    <PreferenceCategory
        android:title="@string/preference_category2">

        <Preference
            android:key="call_method"
            android:title="@string/call_method"
            android:summary="@string/call_method_description"/>

    </PreferenceCategory>

</PreferenceScreen>

I assume you are creating the app using Android Studio. Edit your build.gradle configuration file. While creating this example I used Minimum SDK 14 (Ice Cream Sandwich). Make sure you are using the same value as well to avoid possible errors. PreferenceFragment may not be present in older versions of Android SDK. I’m not sure when it was actually introduced but it’s not so important.

apply plugin: 'com.android.application'

android {
    compileSdkVersion 21
    buildToolsVersion "21.1.2"
    defaultConfig {
        applicationId "org.indywidualni.preferences"
        minSdkVersion 14
        targetSdkVersion 21
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    productFlavors {
    }
}

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    compile 'com.android.support:appcompat-v7:21.0.3'
}

Now a bit of real coding. Create a new class and put the code in there. It will create SettingsFragment which you can use later to display a preference page in an Activity of your choice.

package org.indywidualni.preferences;

import android.content.Context;
import android.os.Bundle;
import android.preference.Preference;
import android.preference.PreferenceFragment;
import android.widget.Toast;

public class SettingsFragment extends PreferenceFragment {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // load the preferences from an XML resource
        addPreferencesFromResource(R.xml.preferences);

        // listener for clearing cache preference
        findPreference("call_method").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
            @Override
            public boolean onPreferenceClick(Preference preference) {
                /** For example you can call a method here just like that! **/
                //catPictures();

                // show toast
                Context c = getActivity();
                Toast toast = Toast.makeText(c, R.string.call_method_clicked, Toast.LENGTH_LONG);
                toast.show();

                return true;
            }
        });
    }

}

So how to make it useful? How to use it with your Activity? Let’s check! Create a new Activity called SettingsActivity and follow my lead.

package org.indywidualni.preferences;

import android.os.Bundle;
import android.support.v7.app.ActionBarActivity;

public class SettingsActivity extends ActionBarActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // fix for loading fragment into ActionBarActivity
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        //setTheme(R.style.Theme_AppCompat_Preferences);

        getFragmentManager().beginTransaction().replace(android.R.id.content,
                new SettingsFragment()).commit();
    }

}

Easy, isn’t it? Well, it isn’t working now. We forgot (on puropse) to add strings to strings.xml. You should always save any strings into your strings.xml. It helps creating localizations and editing it in the future.

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <string name="app_name">Preferences</string>
    <string name="checked">example_key is checked!</string>
    <string name="not_checked">example_key is NOT checked!</string>
    <string name="action_settings">Settings</string>
    <string name="preference_category1">First Category</string>
    <string name="preference_category2">Second Category</string>
    <string name="example_key">Example key</string>
    <string name="transparent_nav">Transparent NavBar</string>
    <string name="transparent_nav_description">KitKat &amp; Lollipop. Only for devices with virtual nav buttons</string>
    <string name="call_method">Call a method</string>
    <string name="call_method_description">It actually does nothing. Click it and check!</string>
    <string name="call_method_clicked">Clicked! Good job :)</string>

</resources>

What’s more important we cannot use SettingsActivity without launching it. We need to create a simple intent to launch it from your app menu. Now a few modifications to your MainActivity which will show you how to get values from shared prefs. When nothing is written (preferences were never changed and are just empty) we define a default behaviour for each one of them. I will also make navigation bar transparent when the specific preference is checked. It’s just an example to show you how to get the values. You can do whatever you want when you know how to do it. Don’t be scared analyzing the code. It seems to be a little complicated but it’s actually not. I added a few things to make it look nice (for example height fix for transparent Navbar) only to show you preference changes the right way. The main goal is to show how to get the values and you can ignore some parts of the code :)

First your main layout.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/content_main"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin"
        android:paddingBottom="@dimen/activity_vertical_margin">
        <TextView
            android:id="@+id/text1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
    </LinearLayout>
</LinearLayout>

Now the complete code for MainActivity

package org.indywidualni.preferences;

import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Build;
import android.preference.PreferenceManager;
import android.support.v7.app.ActionBarActivity;
import android.os.Bundle;
import android.util.TypedValue;
import android.view.Menu;
import android.view.MenuItem;
import android.view.WindowManager;
import android.widget.LinearLayout;
import android.widget.TextView;

public class MainActivity extends ActionBarActivity {

    private TextView textView1;

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

        // get shared preferences
        final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            if (preferences.getBoolean("transparent_nav", false)) {
                getWindow().setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
                // apply top padding to avoid layout being hidden by the status bar
                LinearLayout contentMain = (LinearLayout) findViewById(R.id.content_main);
                contentMain.setPadding(0, fixHeight(), 0, 0);
            }
        }

        textView1 = (TextView)findViewById(R.id.text1);
        if(preferences.getBoolean("example_key", true)) {
            textView1.setText(getString(R.string.checked));
        } else {
            textView1.setText(getString(R.string.not_checked));
        }
    }

    // needed for transparent NavBar
    private int fixHeight() {
        return getStatusBarHeight() + getActionBarHeight();
    }

    private int getActionBarHeight() {
        int actionBarHeight = 0;
        TypedValue tv = new TypedValue();
        if (getTheme().resolveAttribute(android.R.attr.actionBarSize, tv, true))
        {
            actionBarHeight = TypedValue.complexToDimensionPixelSize(tv.data,getResources().getDisplayMetrics());
        }
        return actionBarHeight;
    }

    private int getStatusBarHeight() {
        int result = 0;
        int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
        if (resourceId > 0) {
            result = getResources().getDimensionPixelSize(resourceId);
        }
        return result;
    }

    // app is already running and gets a new intent (for example you changed preferences and want to return to MainActivity)
    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);

        // get shared preferences
        final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            // transparent navBar (above KitKat) when it's enabled
            if (preferences.getBoolean("transparent_nav", false)) {
                getWindow().setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
                // apply top padding to avoid layout being hidden by the status bar
                LinearLayout contentMain = (LinearLayout) findViewById(R.id.content_main);
                contentMain.setPadding(0, fixHeight(), 0, 0);
            } else {
                getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
                // reset padding when transparent NavBar is disabled
                LinearLayout contentMain = (LinearLayout) findViewById(R.id.content_main);
                contentMain.setPadding(0, 0, 0, 0);
            }
        }

        if(preferences.getBoolean("example_key", true)) {
            textView1.setText(getString(R.string.checked));
        } else {
            textView1.setText(getString(R.string.not_checked));
        }
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            Intent settings = new Intent(this, SettingsActivity.class);
            startActivity(settings);
            return true;
        }

        return super.onOptionsItemSelected(item);
    }
}

Now edit your Manifest and it’s almost done.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="org.indywidualni.preferences" >

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name=".MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity
            android:name=".SettingsActivity"
            android:label="@string/action_settings"
            android:theme="@style/AppTheme"
            android:parentActivityName=".MainActivity">
            <meta-data
                android:name="android.support.PARENT_ACTIVITY"
                android:value="org.indywidualni.preferences.MainActivity" />
        </activity>
    </application>

</manifest>

Last thing to do – a menu entry to have a possibility to open your Settings.

<menu 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" tools:context=".MainActivity">
    <item android:id="@+id/action_settings" android:title="@string/action_settings"
        android:orderInCategory="100" app:showAsAction="never" />
</menu>

You are ready to run the app and check it out. What’s important in this example is creating PreferenceFragment and using it in your app to build a Settings Activity. The rest of the code is only to illustrate the example. A really nice extra thing I’ve implemented is a transparent navigation bar with height fix because normally app with transparent Navbar (or transparent Status Bar) is treated like a fullscreen app and the Action Bar overlays the content. Not anymore!

Feel free to reproduce the code in your apps. Here is the complete source code with a live example (apk file) – Preferences.tar.gz

Do you like the post? Leave a comment now!