Sans Pareil Technologies, Inc.

Key To Your Business

Lab 6: Preferences

Extend the application we worked on in Lab 5 to display a preference activity that allows user to select the preferred database size when launching the application.  As part of this exercise, we will develop a custom NumberPicker preference dialog.

  • Copy the Lab5 project to Lab6
  • Add a new resource file of type Layout named database_size_preference.
    • Add a NumberPicker widget to the layout
    • Set the id property for the widget to numberPicker.
  • Add a new Java source file named DatabaseSizeDialogPreference that inherits from DialogPreference.
    • Add fields to maintain state
      private static final int DEFAULT_VALUE = 100; // Needed to provide Java API a default value as we cannot get it from the XML file
      private NumberPicker picker;
      private int value;
    • Implement the second constructor the IDE shows you from the parent class (the one with two parameters).
    • In the constructor set the dialog layout to the layout created above.
      setDialogLayoutResource( R.layout.database_size_preference );
    • Create a static inner class that implements NumberPicker.Formatter which will return the values we want our picker to display.  We want to display the same values we display in our app bar menu.
      return String.format( "%d", value * 100 );
    • Over-ride the onBindDialogView method
      • Store a reference to the NumberPicker in a class field.
        picker = (NumberPicker) view.findViewById( R.id.numberPicker );
      • Set the minimum and maximum values displayed in the NumberPicker to 1 and 3.
      • Set the formatter used by the NumberPicker to an instance of our custom formatter.
      • Set the initial value of the NumberPicker
        if ( value > 0 ) picker.setValue( value / 100 );
    • Over-ride the onSetInitialValue method.  This is used to use a stored value, or save the configured (via XML) default value for the preference.
      if ( restorePersistedValue ) value = getPersistedInt( DEFAULT_VALUE );
      else
      {
        value = (int) defaultValue;
        persistInt( value );
      }
    • Over-ride the onGetDefaultValue method
      return a.getInteger( index, DEFAULT_VALUE );
    • Over-ride the onDialogClosed method
      if ( positiveResult )
      {
        value = picker.getValue() * 100;
        if ( ! persistInt( value ) ) Log.w( getClass().getName(), format( "Unable to save value: %d for key: %s", value, getKey() ) );
      }
  • Create a new resource file of type XML named preferences.  Add a PreferenceCategory element to the file for our DatabaseSizeDialogPreference instance.
    <PreferenceCategory android:title="@string/num_items">
        <mis142.gridsample.DatabaseSizeDialogPreference
            android:key="databaseSizePreference" android:title="@string/databaseSize"
            android:defaultValue="100" />
    </PreferenceCategory>
  • Create a new class SettingsFragment that inherits from PreferenceFragment.  Over-ride the onCreate method and load the preferences resource file after the super-class method invocation.
    addPreferencesFromResource( R.xml.preferences );
  • Create a new empty activity class named SettingsActivity that inherits from PreferenceActivity.
    • Add a public static final String field named DATABASE_SIZE_PREFERENCE with value equal to the android:key specified in the preferences.xml file (databaseSizePreference for eg).
    • Add an inner class named ChangeListener that implements SharedPreferences.OnSharedPreferenceChangeListener.  Make the interface implementation method display a Toast message with the key and new value selected for the key.
    • Add fields to maintain references to the SettingsFragment and ChangeListener.
      private SettingsFragment databaseSize;
      private ChangeListener listener;
    • Unregister the change listener in the onPause method.
      databaseSize.getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener( listener );
    • Register the change listener in the onResume method.
      databaseSize.getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener( listener );
    • Initialize the fields and load the fragment in the onCreate method.
      databaseSize = new SettingsFragment();
      listener = new ChangeListener();

      getFragmentManager()
          .beginTransaction()
          .replace( android.R.id.content, databaseSize )
          .commit();
  • Modify the Database class to require a Context parameter in the getInstance factory method.  Also modify the constructor to take a Context parameter.  Find all usages of Database.getInstance and update the invocation to pass in the context (all Activities inherit from Context).
    public static synchronized Database getInstance( final Context context )
    {
      if ( ourInstance == null ) ourInstance = new Database( context.getApplicationContext() );
      return ourInstance;
    }

    private Database( final Context context )
    {
      this.context = context;
      count = getDefaultSharedPreferences( context ).getInt( SettingsActivity.DATABASE_SIZE_PREFERENCE, 1 );
    }
  • Edit the resource file for the app bar (item.xml) and add an item for Settings.
    <item android:id="@+id/settings" android:title="@string/settings" android:showAsAction="ifRoom" />
  • Edit MainActivity class and handle the new Settings menu item in onOptionItemSelected method.

case R.id.settings:

  startActivity( new Intent( this, SettingsActivity.class ) );     return true;

  • Run the app and test modifying the database size settings.  Re-launch the app after modifying the setting to verify that the grid is displayed with the saved number of items on start.
  • Modify the MainActivityTest to test the Settings menu.
    • Add a custom method that returns a ViewAction that sets the value displayed in the NumberPicker widget.
      public static ViewAction setSize( final int size )
      {
        return new ViewAction() {
          @Override
          public Matcher<View> getConstraints()
          {
            return isAssignableFrom( NumberPicker.class );
          }

          @Override
          public String getDescription()
          {
            return "Set the database size";
          }

          @Override
          public void perform( UiController uiController, View view )
          {
            ( (NumberPicker) view ).setValue( size );
          }
        };
      }
    • Add  a new test method for the settings menu.
      @Test
      public void settings()
      {
        onView( withText( R.string.settings ) ).perform( click() );
        onView( withText( R.string.num_items ) ).perform( click() );
        onView( withText( R.string.databaseSize ) ).perform( click() );
        onView( withClassName( equalTo( NumberPicker.class.getName() ) ) ).perform( setSize( 200 ) );
        onView( withClassName( equalTo( NumberPicker.class.getName() ) ) )
            .inRoot( withDecorView( not( activityTestRule.getActivity().getWindow().getDecorView() ) ) )
            .check( matches( isDisplayed() ) );

        sleep( 500 );
        onView( withId( R.id.numberPicker ) ).perform( setSize( 100 ) );
      }