Skip to content

Commit

Permalink
fix(FieldIDGeneration): Add generated ID for field to avoid crash
Browse files Browse the repository at this point in the history
Having static IDs made the application crash on rotation, because of the
reconstruction of the FormActivity.

This commit protect the form against this crash, generating unique ID
for each view and saving the FormData (as a retained fragment) between
Activity reconstruction.
  • Loading branch information
aveuiller committed Nov 27, 2015
1 parent 77d327a commit 3805358
Show file tree
Hide file tree
Showing 8 changed files with 77 additions and 24 deletions.
@@ -1,6 +1,7 @@
package com.github.dkharrat.nexusdialog;

import android.app.Activity;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;
import android.os.Bundle;
import android.view.ViewGroup;
import android.view.WindowManager.LayoutParams;
Expand All @@ -10,8 +11,8 @@
* create and manage form fields. If you'd like the Activity to be based on <code>AppCompatActivity</code>, you can use
* {@link FormWithAppCompatActivity}
*/
public abstract class FormActivity extends Activity {

public abstract class FormActivity extends FragmentActivity {
private static final String MODEL_BUNDLE_KEY = "nd_model";
private FormController formController;

@Override
Expand All @@ -24,14 +25,22 @@ public void onCreate(Bundle savedInstanceState) {
formController = new FormController(this);
initForm();

FragmentManager fm = getSupportFragmentManager();
FormModel retainedModel = (FormModel) fm.findFragmentByTag(MODEL_BUNDLE_KEY);

if (retainedModel == null) {
retainedModel = formController.getModel();
fm.beginTransaction().add(retainedModel, MODEL_BUNDLE_KEY).commit();
}
formController.setModel(retainedModel);
recreateViews();
}

/**
* Reconstructs the form element views. This must be called after form elements are dynamically added or removed.
*/
protected void recreateViews() {
ViewGroup containerView = (ViewGroup)findViewById(R.id.form_elements_container);
ViewGroup containerView = (ViewGroup) findViewById(R.id.form_elements_container);
formController.recreateViews(containerView);
}

Expand Down
@@ -1,6 +1,7 @@
package com.github.dkharrat.nexusdialog;

import android.content.Context;
import android.view.View;
import android.view.ViewGroup;

import com.github.dkharrat.nexusdialog.controllers.FormSectionController;
Expand All @@ -15,6 +16,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

/**
* <code>FormController</code> is the main class that manages the form elements of NexusDialog. It provides simple APIs
Expand All @@ -30,6 +32,7 @@ public class FormController {

private final Context context;
private ValidationErrorDisplay validationErrorDisplay;
private static final AtomicInteger nextGeneratedViewId = new AtomicInteger(1);

public FormController(Context context) {
this.context = context;
Expand Down Expand Up @@ -61,6 +64,24 @@ private void registerFormModelListener() {
getModel().addPropertyChangeListener(modelListener);
}

/**
* Generate an available ID for the view.
* Uses the same implementation as {@link View#generateViewId}
*
* @return the next available view identifier.
*/
public static int generateViewId(){
for (;;) {
final int result = nextGeneratedViewId.get();
// aapt-generated IDs have the high byte nonzero; clamp to the range under that.
int newValue = result + 1;
if (newValue > 0x00FFFFFF) newValue = 1; // Roll over to 1, not 0.
if (nextGeneratedViewId.compareAndSet(result, newValue)) {
return result;
}
}
}

/**
* Returns a list of the sections of this form.
*
Expand Down
@@ -1,14 +1,16 @@
package com.github.dkharrat.nexusdialog;

import android.os.Bundle;
import android.support.v4.app.Fragment;

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;

/**
* <code>FormModel</code> is an abstract class that represents the backing data for a form. It provides a mechanism
* for form elements to retrieve their values to display to the user and persist changes to the model upon changes.
*/
public abstract class FormModel {

public abstract class FormModel extends Fragment {
private final PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(this);

/**
Expand Down Expand Up @@ -39,7 +41,14 @@ public final Object getValue(String name) {
return getBackingValue(name);
}

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

/**
*
* Sets a value for the specified field name. A property change notification is fired to registered listeners if
* the field's value changed.
*
Expand Down
@@ -1,6 +1,7 @@
package com.github.dkharrat.nexusdialog;

import android.os.Bundle;
import android.support.v4.app.FragmentManager;
import android.support.v7.app.AppCompatActivity;
import android.view.ViewGroup;
import android.view.WindowManager.LayoutParams;
Expand All @@ -10,7 +11,7 @@
* like the Activity to be based on the standard Android <code>Activity</code>, you can use {@link FormActivity}
*/
public abstract class FormWithAppCompatActivity extends AppCompatActivity {

private static final String MODEL_BUNDLE_KEY = "nd_model";
private FormController formController;

@Override
Expand All @@ -23,14 +24,22 @@ public void onCreate(Bundle savedInstanceState) {
formController = new FormController(this);
initForm();

FragmentManager fm = getSupportFragmentManager();
FormModel retainedModel = (FormModel) fm.findFragmentByTag(MODEL_BUNDLE_KEY);

if (retainedModel == null) {
retainedModel = formController.getModel();
fm.beginTransaction().add(retainedModel, MODEL_BUNDLE_KEY).commit();
}
formController.setModel(retainedModel);
recreateViews();
}

/**
* Reconstructs the form element views. This must be called after form elements are dynamically added or removed.
*/
protected void recreateViews() {
ViewGroup containerView = (ViewGroup)findViewById(R.id.form_elements_container);
ViewGroup containerView = (ViewGroup) findViewById(R.id.form_elements_container);
formController.recreateViews(containerView);
}

Expand Down
Expand Up @@ -18,14 +18,16 @@
import android.widget.DatePicker;
import android.widget.EditText;

import com.github.dkharrat.nexusdialog.FormController;

/**
* Represents a field that allows selecting a specific date via a date picker.
* <p/>
* For the field value, the associated FormModel must return a {@link Date} instance. No selected date can be
* represented by returning {@code null} for the value of the field.
*/
public class DatePickerController extends LabeledFieldController {
private final static int EDIT_TEXT_ID = 1001;
private final int editTextId = FormController.generateViewId();

private DatePickerDialog datePickerDialog = null;
private final SimpleDateFormat displayFormat;
Expand Down Expand Up @@ -59,7 +61,7 @@ public DatePickerController(Context context, String name, String labelText) {
@Override
protected View createFieldView() {
final EditText editText = new EditText(getContext());
editText.setId(EDIT_TEXT_ID);
editText.setId(editTextId);

editText.setSingleLine(true);
editText.setInputType(InputType.TYPE_CLASS_DATETIME);
Expand Down Expand Up @@ -119,7 +121,7 @@ public void onDismiss(DialogInterface dialog) {
}

private EditText getEditText() {
return (EditText)getView().findViewById(EDIT_TEXT_ID);
return (EditText)getView().findViewById(editTextId);
}

private void refresh(EditText editText) {
Expand All @@ -130,4 +132,4 @@ private void refresh(EditText editText) {
public void refresh() {
refresh(getEditText());
}
}
}
Expand Up @@ -7,12 +7,13 @@
import android.view.View;
import android.widget.EditText;

import com.github.dkharrat.nexusdialog.FormController;

/**
* Represents a field that allows free-form text.
*/
public class EditTextController extends LabeledFieldController {

private final static int EDIT_TEXT_ID = 1001;
private final int editTextId = FormController.generateViewId();

private int inputType;
private final String placeholder;
Expand Down Expand Up @@ -76,7 +77,7 @@ public EditTextController(Context ctx, String name, String labelText) {
* @return the EditText view associated with this element
*/
public EditText getEditText() {
return (EditText)getView().findViewById(EDIT_TEXT_ID);
return (EditText)getView().findViewById(editTextId);
}

/**
Expand Down Expand Up @@ -139,7 +140,7 @@ public void setSecureEntry(boolean isSecureEntry) {
@Override
protected View createFieldView() {
final EditText editText = new EditText(getContext());
editText.setId(EDIT_TEXT_ID);
editText.setId(editTextId);

editText.setSingleLine(!isMultiLine());
if (placeholder != null) {
Expand Down Expand Up @@ -176,4 +177,4 @@ private void refresh(EditText editText) {
public void refresh() {
refresh(getEditText());
}
}
}
Expand Up @@ -25,6 +25,7 @@
import android.widget.EditText;
import android.widget.ListView;

import com.github.dkharrat.nexusdialog.FormController;
import com.github.dkharrat.nexusdialog.R;
import com.github.dkharrat.nexusdialog.utils.MessageUtil;

Expand All @@ -40,7 +41,7 @@
* can be represented by returning {@code null} for the value of the field.
*/
public class SearchableSelectionController extends LabeledFieldController {
private final static int EDIT_TEXT_ID = 1001;
private final int editTextId = FormController.generateViewId();

private final String placeholder;
private boolean isFreeFormTextAllowed = true;
Expand Down Expand Up @@ -93,7 +94,7 @@ public boolean isFreeFormTextAllowed() {

protected View createFieldView() {
final EditText editText = new EditText(getContext());
editText.setId(EDIT_TEXT_ID);
editText.setId(editTextId);

editText.setSingleLine(true);
editText.setInputType(InputType.TYPE_CLASS_TEXT);
Expand Down Expand Up @@ -224,7 +225,7 @@ public void onDismiss(DialogInterface dialog) {
}

private EditText getEditText() {
return (EditText)getView().findViewById(EDIT_TEXT_ID);
return (EditText)getView().findViewById(editTextId);
}

private void refresh(EditText editText) {
Expand Down
Expand Up @@ -9,6 +9,7 @@
import android.widget.ArrayAdapter;
import android.widget.Spinner;

import com.github.dkharrat.nexusdialog.FormController;
import com.github.dkharrat.nexusdialog.R;

/**
Expand All @@ -20,7 +21,7 @@
*/
public class SelectionController extends LabeledFieldController {

private final static int SPINNER_ID = 1001;
private final int spinnerId = FormController.generateViewId();

private final String prompt;
private final List<String> items;
Expand Down Expand Up @@ -69,13 +70,13 @@ public SelectionController(Context ctx, String name, String labelText, boolean i
* @return the Spinner view associated with this element
*/
public Spinner getSpinner() {
return (Spinner)getView().findViewById(SPINNER_ID);
return (Spinner)getView().findViewById(spinnerId);
}

@Override
protected View createFieldView() {
Spinner spinnerView = new Spinner(getContext());
spinnerView.setId(SPINNER_ID);
spinnerView.setId(spinnerId);
spinnerView.setPrompt(prompt);
ArrayAdapter<String> spinnerAdapter = new ArrayAdapter<String>(getContext(), android.R.layout.simple_spinner_item, items);
spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
Expand Down Expand Up @@ -132,4 +133,4 @@ private void refresh(Spinner spinner) {
public void refresh() {
refresh(getSpinner());
}
}
}

0 comments on commit 3805358

Please sign in to comment.