├── .gitignore ├── app ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── example │ │ └── providerexample │ │ ├── PersonDetailActivity.java │ │ ├── PersonDetailFragment.java │ │ ├── PersonListActivity.java │ │ ├── PersonListFragment.java │ │ └── database │ │ ├── DatabaseHandler.java │ │ ├── Person.java │ │ └── PersonProvider.java │ └── res │ ├── drawable-hdpi │ └── ic_launcher.png │ ├── drawable-ldpi │ └── ic_launcher.png │ ├── drawable-mdpi │ └── ic_launcher.png │ ├── drawable-xhdpi │ └── ic_launcher.png │ ├── drawable │ └── rounded_corners_white.xml │ ├── layout │ ├── activity_person_detail.xml │ ├── activity_person_list.xml │ ├── activity_person_twopane.xml │ ├── fragment_person_detail.xml │ ├── fragment_person_list.xml │ └── person_listitem.xml │ ├── menu │ └── list_activity.xml │ ├── values-large │ └── refs.xml │ ├── values-sw600dp │ └── refs.xml │ ├── values-v11 │ └── styles.xml │ ├── values-v14 │ └── styles.xml │ └── values │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── readme.md ├── readme_img ├── cardItem.png ├── changetolongarg.png ├── gnex_framed.png ├── gnexdetail1.png ├── gnexdetail2.png ├── gnexlist.png ├── new.png ├── newandroid.png ├── newandroid2.png ├── newandroid3.png ├── newandroidobject.png ├── newcontentprovider.png ├── newcontentprovider2.png ├── newdbhandler.png ├── newdbhandler2.png ├── newdbhandler3.png ├── newlayoutfile.png ├── newmenu.png ├── newpkg.png ├── nexus7.png ├── nexus7_framed.png └── persondetailview.png └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | gen 3 | *.iml 4 | **/build 5 | .gradle 6 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 17 5 | buildToolsVersion "21.1.2" 6 | 7 | defaultConfig { 8 | applicationId "com.example.providerexample" 9 | minSdkVersion 14 10 | targetSdkVersion 17 11 | } 12 | 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' 17 | } 18 | } 19 | } 20 | 21 | dependencies { 22 | compile 'com.android.support:support-v4:18.0.0' 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 10 | 11 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 29 | 32 | 33 | 34 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/providerexample/PersonDetailActivity.java: -------------------------------------------------------------------------------- 1 | package com.example.providerexample; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.support.v4.app.FragmentActivity; 6 | import android.support.v4.app.NavUtils; 7 | import android.view.MenuItem; 8 | 9 | /** 10 | * An activity representing a single Person detail screen. This 11 | * activity is only used on handset devices. On tablet-size devices, 12 | * item details are presented side-by-side with a list of items 13 | * in a {@link PersonListActivity}. 14 | *

15 | * This activity is mostly just a 'shell' activity containing nothing 16 | * more than a {@link PersonDetailFragment}. 17 | */ 18 | public class PersonDetailActivity extends FragmentActivity { 19 | 20 | @Override 21 | protected void onCreate(Bundle savedInstanceState) { 22 | super.onCreate(savedInstanceState); 23 | setContentView(R.layout.activity_person_detail); 24 | 25 | // Show the Up button in the action bar. 26 | getActionBar().setDisplayHomeAsUpEnabled(true); 27 | 28 | // savedInstanceState is non-null when there is fragment state 29 | // saved from previous configurations of this activity 30 | // (e.g. when rotating the screen from portrait to landscape). 31 | // In this case, the fragment will automatically be re-added 32 | // to its container so we don't need to manually add it. 33 | // For more information, see the Fragments API guide at: 34 | // 35 | // http://developer.android.com/guide/components/fragments.html 36 | // 37 | if (savedInstanceState == null) { 38 | // Create the detail fragment and add it to the activity 39 | // using a fragment transaction. 40 | Bundle arguments = new Bundle(); 41 | arguments.putLong(PersonDetailFragment.ARG_ITEM_ID, 42 | getIntent().getLongExtra(PersonDetailFragment.ARG_ITEM_ID, -1)); 43 | PersonDetailFragment fragment = new PersonDetailFragment(); 44 | fragment.setArguments(arguments); 45 | getSupportFragmentManager().beginTransaction() 46 | .add(R.id.person_detail_container, fragment) 47 | .commit(); 48 | } 49 | } 50 | 51 | @Override 52 | public boolean onOptionsItemSelected(MenuItem item) { 53 | switch (item.getItemId()) { 54 | case android.R.id.home: 55 | // This ID represents the Home or Up button. In the case of this 56 | // activity, the Up button is shown. Use NavUtils to allow users 57 | // to navigate up one level in the application structure. For 58 | // more details, see the Navigation pattern on Android Design: 59 | // 60 | // http://developer.android.com/design/patterns/navigation.html#up-vs-back 61 | // 62 | NavUtils.navigateUpTo(this, new Intent(this, PersonListActivity.class)); 63 | return true; 64 | } 65 | return super.onOptionsItemSelected(item); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/providerexample/PersonDetailFragment.java: -------------------------------------------------------------------------------- 1 | package com.example.providerexample; 2 | 3 | import android.os.Bundle; 4 | import android.support.v4.app.Fragment; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.widget.TextView; 9 | 10 | import com.example.providerexample.database.DatabaseHandler; 11 | import com.example.providerexample.database.Person; 12 | 13 | /** 14 | * A fragment representing a single Person detail screen. 15 | * This fragment is either contained in a {@link PersonListActivity} 16 | * in two-pane mode (on tablets) or a {@link PersonDetailActivity} 17 | * on handsets. 18 | */ 19 | public class PersonDetailFragment extends Fragment { 20 | /** 21 | * The fragment argument representing the item ID that this fragment 22 | * represents. 23 | */ 24 | public static final String ARG_ITEM_ID = "item_id"; 25 | 26 | /** 27 | * The person this fragment is presenting. 28 | */ 29 | private Person mItem; 30 | 31 | /** 32 | * The UI elements showing the details of the Person 33 | */ 34 | private TextView textFirstName; 35 | private TextView textLastName; 36 | private TextView textBio; 37 | 38 | /** 39 | * Mandatory empty constructor for the fragment manager to instantiate the 40 | * fragment (e.g. upon screen orientation changes). 41 | */ 42 | public PersonDetailFragment() { 43 | } 44 | 45 | @Override 46 | public void onCreate(Bundle savedInstanceState) { 47 | super.onCreate(savedInstanceState); 48 | 49 | if (getArguments().containsKey(ARG_ITEM_ID)) { 50 | // Should use the contentprovider here ideally 51 | mItem = DatabaseHandler.getInstance(getActivity()).getPerson(getArguments().getLong(ARG_ITEM_ID)); 52 | } 53 | } 54 | 55 | @Override 56 | public View onCreateView(LayoutInflater inflater, ViewGroup container, 57 | Bundle savedInstanceState) { 58 | View rootView = inflater.inflate(R.layout.fragment_person_detail, container, false); 59 | 60 | if (mItem != null) { 61 | textFirstName = ((TextView) rootView.findViewById(R.id.textFirstName)); 62 | textFirstName.setText(mItem.firstname); 63 | 64 | textLastName = ((TextView) rootView.findViewById(R.id.textLastName)); 65 | textLastName.setText(mItem.lastname); 66 | 67 | textBio = ((TextView) rootView.findViewById(R.id.textBio)); 68 | textBio.setText(mItem.bio); 69 | } 70 | 71 | return rootView; 72 | } 73 | 74 | @Override 75 | public void onPause() { 76 | super.onPause(); 77 | updatePersonFromUI(); 78 | } 79 | 80 | private void updatePersonFromUI() { 81 | if (mItem != null) { 82 | mItem.firstname = textFirstName.getText().toString(); 83 | mItem.lastname = textLastName.getText().toString(); 84 | mItem.bio = textBio.getText().toString(); 85 | 86 | DatabaseHandler.getInstance(getActivity()).putPerson(mItem); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/providerexample/PersonListActivity.java: -------------------------------------------------------------------------------- 1 | package com.example.providerexample; 2 | 3 | import com.example.providerexample.database.DatabaseHandler; 4 | import com.example.providerexample.database.Person; 5 | 6 | import android.content.Intent; 7 | import android.os.Bundle; 8 | import android.support.v4.app.FragmentActivity; 9 | import android.view.Menu; 10 | import android.view.MenuInflater; 11 | import android.view.MenuItem; 12 | import android.view.View; 13 | 14 | /** 15 | * An activity representing a list of Persons. This activity has different 16 | * presentations for handset and tablet-size devices. On handsets, the activity 17 | * presents a list of items, which when touched, lead to a 18 | * {@link PersonDetailActivity} representing item details. On tablets, the 19 | * activity presents the list of items and item details side-by-side using two 20 | * vertical panes. 21 | *

22 | * The activity makes heavy use of fragments. The list of items is a 23 | * {@link PersonListFragment} and the item details (if present) is a 24 | * {@link PersonDetailFragment}. 25 | *

26 | * This activity also implements the required 27 | * {@link PersonListFragment.Callbacks} interface to listen for item selections. 28 | */ 29 | public class PersonListActivity extends FragmentActivity implements 30 | PersonListFragment.Callbacks { 31 | 32 | /** 33 | * Whether or not the activity is in two-pane mode, i.e. running on a tablet 34 | * device. 35 | */ 36 | private boolean mTwoPane; 37 | 38 | @Override 39 | protected void onCreate(Bundle savedInstanceState) { 40 | super.onCreate(savedInstanceState); 41 | setContentView(R.layout.activity_person_list); 42 | 43 | if (findViewById(R.id.person_detail_container) != null) { 44 | // The detail container view will be present only in the 45 | // large-screen layouts (res/values-large and 46 | // res/values-sw600dp). If this view is present, then the 47 | // activity should be in two-pane mode. 48 | mTwoPane = true; 49 | 50 | // In two-pane mode, list items should be given the 51 | // 'activated' state when touched. 52 | ((PersonListFragment) getSupportFragmentManager().findFragmentById( 53 | R.id.person_list)).setActivateOnItemClick(true); 54 | } 55 | 56 | // TODO: If exposing deep links into your app, handle intents here. 57 | } 58 | 59 | /** 60 | * Callback method from {@link PersonListFragment.Callbacks} indicating that 61 | * the item with the given ID was selected. 62 | */ 63 | @Override 64 | public void onItemSelected(long id) { 65 | if (mTwoPane) { 66 | // In two-pane mode, show the detail view in this activity by 67 | // adding or replacing the detail fragment using a 68 | // fragment transaction. 69 | Bundle arguments = new Bundle(); 70 | arguments.putLong(PersonDetailFragment.ARG_ITEM_ID, id); 71 | PersonDetailFragment fragment = new PersonDetailFragment(); 72 | fragment.setArguments(arguments); 73 | getSupportFragmentManager().beginTransaction() 74 | .replace(R.id.person_detail_container, fragment).commit(); 75 | 76 | } else { 77 | // In single-pane mode, simply start the detail activity 78 | // for the selected item ID. 79 | Intent detailIntent = new Intent(this, PersonDetailActivity.class); 80 | detailIntent.putExtra(PersonDetailFragment.ARG_ITEM_ID, id); 81 | startActivity(detailIntent); 82 | } 83 | } 84 | 85 | @Override 86 | public boolean onCreateOptionsMenu(Menu menu) { 87 | MenuInflater inflater = getMenuInflater(); 88 | inflater.inflate(R.menu.list_activity, menu); 89 | return true; 90 | } 91 | 92 | @Override 93 | public boolean onOptionsItemSelected(MenuItem item) { 94 | boolean result = false; 95 | if (R.id.newPerson == item.getItemId()) { 96 | result = true; 97 | // Create a new person. 98 | Person p = new Person(); 99 | DatabaseHandler.getInstance(this).putPerson(p); 100 | // Open a new fragment with the new id 101 | onItemSelected(p.id); 102 | } 103 | 104 | return result; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/providerexample/PersonListFragment.java: -------------------------------------------------------------------------------- 1 | package com.example.providerexample; 2 | 3 | import android.app.Activity; 4 | import android.support.v4.app.ListFragment; 5 | import android.support.v4.app.LoaderManager.LoaderCallbacks; 6 | import android.support.v4.content.CursorLoader; 7 | import android.support.v4.content.Loader; 8 | import android.support.v4.widget.SimpleCursorAdapter; 9 | import android.database.Cursor; 10 | import android.os.Bundle; 11 | import android.view.LayoutInflater; 12 | import android.view.View; 13 | import android.view.ViewGroup; 14 | import android.widget.ListView; 15 | 16 | import com.example.providerexample.database.Person; 17 | import com.example.providerexample.database.PersonProvider; 18 | 19 | /** 20 | * A list fragment representing a list of Persons. This fragment also supports 21 | * tablet devices by allowing list items to be given an 'activated' state upon 22 | * selection. This helps indicate which item is currently being viewed in a 23 | * {@link PersonDetailFragment}. 24 | *

25 | * Activities containing this fragment MUST implement the {@link Callbacks} 26 | * interface. 27 | */ 28 | public class PersonListFragment extends ListFragment { 29 | 30 | /** 31 | * The serialization (saved instance state) Bundle key representing the 32 | * activated item position. Only used on tablets. 33 | */ 34 | private static final String STATE_ACTIVATED_POSITION = "activated_position"; 35 | 36 | /** 37 | * The fragment's current callback object, which is notified of list item 38 | * clicks. 39 | */ 40 | private Callbacks mCallbacks = sDummyCallbacks; 41 | 42 | /** 43 | * The current activated item position. Only used on tablets. 44 | */ 45 | private int mActivatedPosition = ListView.INVALID_POSITION; 46 | 47 | /** 48 | * A callback interface that all activities containing this fragment must 49 | * implement. This mechanism allows activities to be notified of item 50 | * selections. 51 | */ 52 | public interface Callbacks { 53 | /** 54 | * Callback for when an item has been selected. 55 | */ 56 | public void onItemSelected(long l); 57 | } 58 | 59 | /** 60 | * A dummy implementation of the {@link Callbacks} interface that does 61 | * nothing. Used only when this fragment is not attached to an activity. 62 | */ 63 | private static Callbacks sDummyCallbacks = new Callbacks() { 64 | @Override 65 | public void onItemSelected(long id) { 66 | } 67 | }; 68 | 69 | /** 70 | * Mandatory empty constructor for the fragment manager to instantiate the 71 | * fragment (e.g. upon screen orientation changes). 72 | */ 73 | public PersonListFragment() { 74 | } 75 | 76 | @Override 77 | public void onCreate(Bundle savedInstanceState) { 78 | super.onCreate(savedInstanceState); 79 | 80 | setListAdapter(new SimpleCursorAdapter(getActivity(), 81 | R.layout.person_listitem, null, new String[] { 82 | Person.COL_FIRSTNAME, Person.COL_LASTNAME, 83 | Person.COL_BIO }, new int[] { R.id.cardFirstName, 84 | R.id.cardLastName, R.id.cardDescription }, 0)); 85 | 86 | // Load the content 87 | getLoaderManager().initLoader(0, null, new LoaderCallbacks() { 88 | @Override 89 | public Loader onCreateLoader(int id, Bundle args) { 90 | return new CursorLoader(getActivity(), 91 | PersonProvider.URI_PERSONS, Person.FIELDS, null, null, 92 | null); 93 | } 94 | 95 | @Override 96 | public void onLoadFinished(Loader loader, Cursor c) { 97 | ((SimpleCursorAdapter) getListAdapter()).swapCursor(c); 98 | } 99 | 100 | @Override 101 | public void onLoaderReset(Loader arg0) { 102 | ((SimpleCursorAdapter) getListAdapter()).swapCursor(null); 103 | } 104 | }); 105 | } 106 | 107 | @Override 108 | public View onCreateView(LayoutInflater inflater, ViewGroup container, 109 | Bundle savedInstanceState) { 110 | return inflater.inflate(R.layout.fragment_person_list, null); 111 | } 112 | 113 | @Override 114 | public void onViewCreated(View view, Bundle savedInstanceState) { 115 | super.onViewCreated(view, savedInstanceState); 116 | 117 | // Restore the previously serialized activated item position. 118 | if (savedInstanceState != null 119 | && savedInstanceState.containsKey(STATE_ACTIVATED_POSITION)) { 120 | setActivatedPosition(savedInstanceState 121 | .getInt(STATE_ACTIVATED_POSITION)); 122 | } 123 | } 124 | 125 | @Override 126 | public void onAttach(Activity activity) { 127 | super.onAttach(activity); 128 | 129 | // Activities containing this fragment must implement its callbacks. 130 | if (!(activity instanceof Callbacks)) { 131 | throw new IllegalStateException( 132 | "Activity must implement fragment's callbacks."); 133 | } 134 | 135 | mCallbacks = (Callbacks) activity; 136 | } 137 | 138 | @Override 139 | public void onDetach() { 140 | super.onDetach(); 141 | 142 | // Reset the active callbacks interface to the dummy implementation. 143 | mCallbacks = sDummyCallbacks; 144 | } 145 | 146 | @Override 147 | public void onListItemClick(ListView listView, View view, int position, 148 | long id) { 149 | super.onListItemClick(listView, view, position, id); 150 | 151 | // Notify the active callbacks interface (the activity, if the 152 | // fragment is attached to one) that an item has been selected. 153 | mCallbacks.onItemSelected(getListAdapter().getItemId(position)); 154 | } 155 | 156 | @Override 157 | public void onSaveInstanceState(Bundle outState) { 158 | super.onSaveInstanceState(outState); 159 | if (mActivatedPosition != ListView.INVALID_POSITION) { 160 | // Serialize and persist the activated item position. 161 | outState.putInt(STATE_ACTIVATED_POSITION, mActivatedPosition); 162 | } 163 | } 164 | 165 | /** 166 | * Turns on activate-on-click mode. When this mode is on, list items will be 167 | * given the 'activated' state when touched. 168 | */ 169 | public void setActivateOnItemClick(boolean activateOnItemClick) { 170 | // When setting CHOICE_MODE_SINGLE, ListView will automatically 171 | // give items the 'activated' state when touched. 172 | getListView().setChoiceMode( 173 | activateOnItemClick ? ListView.CHOICE_MODE_SINGLE 174 | : ListView.CHOICE_MODE_NONE); 175 | } 176 | 177 | private void setActivatedPosition(int position) { 178 | if (position == ListView.INVALID_POSITION) { 179 | getListView().setItemChecked(mActivatedPosition, false); 180 | } else { 181 | getListView().setItemChecked(position, true); 182 | } 183 | 184 | mActivatedPosition = position; 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/providerexample/database/DatabaseHandler.java: -------------------------------------------------------------------------------- 1 | package com.example.providerexample.database; 2 | 3 | import android.content.Context; 4 | import android.database.Cursor; 5 | import android.database.sqlite.SQLiteDatabase; 6 | import android.database.sqlite.SQLiteOpenHelper; 7 | 8 | public class DatabaseHandler extends SQLiteOpenHelper { 9 | 10 | private static DatabaseHandler singleton; 11 | 12 | public static DatabaseHandler getInstance(final Context context) { 13 | if (singleton == null) { 14 | singleton = new DatabaseHandler(context); 15 | } 16 | return singleton; 17 | } 18 | 19 | private static final int DATABASE_VERSION = 1; 20 | private static final String DATABASE_NAME = "providerExample"; 21 | 22 | private final Context context; 23 | 24 | public DatabaseHandler(Context context) { 25 | super(context, DATABASE_NAME, null, DATABASE_VERSION); 26 | // Good idea to have the context that doesn't die with the window 27 | this.context = context.getApplicationContext(); 28 | } 29 | 30 | @Override 31 | public void onCreate(SQLiteDatabase db) { 32 | db.execSQL(Person.CREATE_TABLE); 33 | 34 | Person person = new Person(); 35 | person.firstname = "Sylvester"; 36 | person.lastname = "Stallone"; 37 | person.bio = "..."; 38 | db.insert(Person.TABLE_NAME, null, person.getContent()); 39 | 40 | person.firstname = "Danny"; 41 | person.lastname = "DeVito"; 42 | person.bio = "..."; 43 | db.insert(Person.TABLE_NAME, null, person.getContent()); 44 | } 45 | 46 | @Override 47 | public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 48 | } 49 | 50 | public synchronized Person getPerson(final long id) { 51 | final SQLiteDatabase db = this.getReadableDatabase(); 52 | final Cursor cursor = db.query(Person.TABLE_NAME, Person.FIELDS, 53 | Person.COL_ID + " IS ?", new String[] { String.valueOf(id) }, 54 | null, null, null, null); 55 | if (cursor == null || cursor.isAfterLast()) { 56 | return null; 57 | } 58 | 59 | Person item = null; 60 | if (cursor.moveToFirst()) { 61 | item = new Person(cursor); 62 | } 63 | cursor.close(); 64 | return item; 65 | } 66 | 67 | public synchronized boolean putPerson(final Person person) { 68 | boolean success = false; 69 | int result = 0; 70 | final SQLiteDatabase db = this.getWritableDatabase(); 71 | 72 | if (person.id > -1) { 73 | result += db.update(Person.TABLE_NAME, person.getContent(), 74 | Person.COL_ID + " IS ?", 75 | new String[] { String.valueOf(person.id) }); 76 | } 77 | 78 | if (result > 0) { 79 | success = true; 80 | } else { 81 | // Update failed or wasn't possible, insert instead 82 | final long id = db.insert(Person.TABLE_NAME, null, 83 | person.getContent()); 84 | 85 | if (id > -1) { 86 | person.id = id; 87 | success = true; 88 | } 89 | } 90 | 91 | if (success) { 92 | notifyProviderOnPersonChange(); 93 | } 94 | 95 | return success; 96 | } 97 | 98 | public synchronized int removePerson(final Person person) { 99 | final SQLiteDatabase db = this.getWritableDatabase(); 100 | final int result = db.delete(Person.TABLE_NAME, 101 | Person.COL_ID + " IS ?", 102 | new String[] { Long.toString(person.id) }); 103 | 104 | if (result > 0) { 105 | notifyProviderOnPersonChange(); 106 | } 107 | return result; 108 | } 109 | 110 | private void notifyProviderOnPersonChange() { 111 | context.getContentResolver().notifyChange( 112 | PersonProvider.URI_PERSONS, null, false); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/providerexample/database/Person.java: -------------------------------------------------------------------------------- 1 | package com.example.providerexample.database; 2 | 3 | import android.content.ContentValues; 4 | import android.database.Cursor; 5 | 6 | /** 7 | * A class representation of a row in table "Person". 8 | */ 9 | public class Person { 10 | 11 | // SQL convention says Table name should be "singular", so not Persons 12 | public static final String TABLE_NAME = "Person"; 13 | // Naming the id column with an underscore is good to be consistent 14 | // with other Android things. This is ALWAYS needed 15 | public static final String COL_ID = "_id"; 16 | // These fields can be anything you want. 17 | public static final String COL_FIRSTNAME = "firstname"; 18 | public static final String COL_LASTNAME = "lastname"; 19 | public static final String COL_BIO = "bio"; 20 | 21 | // For database projection so order is consistent 22 | public static final String[] FIELDS = { COL_ID, COL_FIRSTNAME, COL_LASTNAME, 23 | COL_BIO }; 24 | 25 | /* 26 | * The SQL code that creates a Table for storing Persons in. 27 | * Note that the last row does NOT end in a comma like the others. 28 | * This is a common source of error. 29 | */ 30 | public static final String CREATE_TABLE = 31 | "CREATE TABLE " + TABLE_NAME + "(" 32 | + COL_ID + " INTEGER PRIMARY KEY," 33 | + COL_FIRSTNAME + " TEXT NOT NULL DEFAULT ''," 34 | + COL_LASTNAME + " TEXT NOT NULL DEFAULT ''," 35 | + COL_BIO + " TEXT NOT NULL DEFAULT ''" 36 | + ")"; 37 | 38 | // Fields corresponding to database columns 39 | public long id = -1; 40 | public String firstname = ""; 41 | public String lastname = ""; 42 | public String bio = ""; 43 | 44 | /** 45 | * No need to do anything, fields are already set to default values above 46 | */ 47 | public Person() { 48 | } 49 | 50 | /** 51 | * Convert information from the database into a Person object. 52 | */ 53 | public Person(final Cursor cursor) { 54 | // Indices expected to match order in FIELDS! 55 | this.id = cursor.getLong(0); 56 | this.firstname = cursor.getString(1); 57 | this.lastname = cursor.getString(2); 58 | this.bio = cursor.getString(3); 59 | } 60 | 61 | /** 62 | * Return the fields in a ContentValues object, suitable for insertion 63 | * into the database. 64 | */ 65 | public ContentValues getContent() { 66 | final ContentValues values = new ContentValues(); 67 | // Note that ID is NOT included here 68 | values.put(COL_FIRSTNAME, firstname); 69 | values.put(COL_LASTNAME, lastname); 70 | values.put(COL_BIO, bio); 71 | 72 | return values; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/providerexample/database/PersonProvider.java: -------------------------------------------------------------------------------- 1 | package com.example.providerexample.database; 2 | 3 | 4 | import android.content.ContentProvider; 5 | import android.content.ContentValues; 6 | import android.database.Cursor; 7 | import android.net.Uri; 8 | 9 | public class PersonProvider extends ContentProvider { 10 | 11 | // All URIs share these parts 12 | public static final String AUTHORITY = "com.example.providerexample.provider"; 13 | public static final String SCHEME = "content://"; 14 | 15 | // URIs 16 | // Used for all persons 17 | public static final String PERSONS = SCHEME + AUTHORITY + "/person"; 18 | public static final Uri URI_PERSONS = Uri.parse(PERSONS); 19 | // Used for a single person, just add the id to the end 20 | public static final String PERSON_BASE = PERSONS + "/"; 21 | 22 | public PersonProvider() { 23 | } 24 | 25 | @Override 26 | public int delete(Uri uri, String selection, String[] selectionArgs) { 27 | // Implement this to handle requests to delete one or more rows. 28 | throw new UnsupportedOperationException("Not yet implemented"); 29 | } 30 | 31 | @Override 32 | public String getType(Uri uri) { 33 | // TODO: Implement this to handle requests for the MIME type of the data 34 | // at the given URI. 35 | throw new UnsupportedOperationException("Not yet implemented"); 36 | } 37 | 38 | @Override 39 | public Uri insert(Uri uri, ContentValues values) { 40 | // TODO: Implement this to handle requests to insert a new row. 41 | throw new UnsupportedOperationException("Not yet implemented"); 42 | } 43 | 44 | @Override 45 | public boolean onCreate() { 46 | return true; 47 | } 48 | 49 | @Override 50 | public Cursor query(Uri uri, String[] projection, String selection, 51 | String[] selectionArgs, String sortOrder) { 52 | Cursor result = null; 53 | if (URI_PERSONS.equals(uri)) { 54 | result = DatabaseHandler 55 | .getInstance(getContext()) 56 | .getReadableDatabase() 57 | .query(Person.TABLE_NAME, Person.FIELDS, null, null, null, 58 | null, null, null); 59 | result.setNotificationUri(getContext().getContentResolver(), URI_PERSONS); 60 | } else if (uri.toString().startsWith(PERSON_BASE)) { 61 | final long id = Long.parseLong(uri.getLastPathSegment()); 62 | result = DatabaseHandler 63 | .getInstance(getContext()) 64 | .getReadableDatabase() 65 | .query(Person.TABLE_NAME, Person.FIELDS, 66 | Person.COL_ID + " IS ?", 67 | new String[] { String.valueOf(id) }, null, null, 68 | null, null); 69 | result.setNotificationUri(getContext().getContentResolver(), URI_PERSONS); 70 | } else { 71 | throw new UnsupportedOperationException("Not yet implemented"); 72 | } 73 | 74 | return result; 75 | } 76 | 77 | @Override 78 | public int update(Uri uri, ContentValues values, String selection, 79 | String[] selectionArgs) { 80 | // TODO: Implement this to handle requests to update one or more rows. 81 | throw new UnsupportedOperationException("Not yet implemented"); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacecowboy/AndroidTutorialContentProvider/3c0674586ebb807d060968ca6566af0d16baa88d/app/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-ldpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacecowboy/AndroidTutorialContentProvider/3c0674586ebb807d060968ca6566af0d16baa88d/app/src/main/res/drawable-ldpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacecowboy/AndroidTutorialContentProvider/3c0674586ebb807d060968ca6566af0d16baa88d/app/src/main/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacecowboy/AndroidTutorialContentProvider/3c0674586ebb807d060968ca6566af0d16baa88d/app/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/rounded_corners_white.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_person_detail.xml: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_person_list.xml: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_person_twopane.xml: -------------------------------------------------------------------------------- 1 | 10 | 11 | 21 | 22 | 29 | 30 | 35 | 36 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_person_detail.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 10 | 11 | 19 | 20 | 28 | 29 | 37 | 38 | 46 | 47 | 55 | 56 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_person_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/layout/person_listitem.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 23 | 24 | 32 | 33 | 40 | 41 | 51 | 52 | -------------------------------------------------------------------------------- /app/src/main/res/menu/list_activity.xml: -------------------------------------------------------------------------------- 1 | 2 |

3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/values-large/refs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | @layout/activity_person_twopane 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/values-sw600dp/refs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | @layout/activity_person_twopane 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/values-v11/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/values-v14/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ProviderExample 5 | Person Detail 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 14 | 15 | 16 | 19 | 20 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | repositories { 4 | jcenter() 5 | } 6 | dependencies { 7 | classpath 'com.android.tools.build:gradle:1.0.1' 8 | } 9 | } 10 | 11 | allprojects { 12 | repositories { 13 | jcenter() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacecowboy/AndroidTutorialContentProvider/3c0674586ebb807d060968ca6566af0d16baa88d/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Apr 10 15:27:10 PDT 2013 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Things I wish I'd known about sooner: Lists, SQLite and ContentProviders 2 | 3 | This tutorial is good if you want to learn about SQLite and how to use it effectively 4 | in Android. I have upgraded the project to Android Studio so some of the 5 | Eclipse-centric content is slightly out-dated, but the gist of it is the same. 6 | All the same functionality is present in Android Studio as it was in Eclipse, 7 | with the same kind of wizards etc. You can also enable a setting in Android Studio 8 | so most of the key commands are the same. 9 | 10 | ## Introduction 11 | 12 | Lists must be one of the most ubiquitous UI-elements. What app doesn't have them? 13 | If they don't they probably have some kind of grid view. This tutorial will apply 14 | to them both. The final result will look like this: 15 | 16 | ![On Galaxy Nexus](readme_img/gnex_framed.png) 17 | 18 | ![On Nexus7](readme_img/nexus7_framed.png) 19 | 20 | The structure of the tutorial might not be common. I am going to start by 21 | creating the app, documenting all the steps on the way. Once that's done, 22 | I will explain some things in more detail. 23 | 24 | To start with, we're going to have to start a new project. This means there 25 | will be a small amount of boiler plate to deal with before we can actually 26 | get our code running. 27 | 28 | Somewhere in the middle, I will be taking a detour to make those nice looking layouts. 29 | If you don't care about the layout stuff, just skip that section. 30 | 31 | The final step is to make the app use the new layouts, and load the data 32 | from our database into them. 33 | 34 | Once the app is complete, I can explain a few things in more detail by 35 | pointing to the code just created. 36 | 37 | 38 | ### Motivation 39 | Many tutorials only show how to use ArrayAdapters and I find that problematic. 40 | ArrayAdapters are fine if you're showing a static list, but I think most devs 41 | have something more dynamic in mind. Simple rule of thumb is, if the data 42 | backing the list can *change* while the list is displaying it, then you want 43 | to use a ContentProvider. While it isn't the only way to get everything working, 44 | together with Loaders, it's just very smooth. The data is fetched on a 45 | background thread and is always up to date. And the system does it all 46 | for you (as long as you have a ContentProvider). What's not to like? 47 | 48 | ### What's a ContentProvider 49 | A ContentProvider is seperate from the actual data. In this example, and many 50 | real world applications, the data is stored in an SQLite database. But it doesn't 51 | have to be. We will use the Provider to supply our list with content. The advantage 52 | over a simple ArrayAdapter will be that if the data changes, the list will change, 53 | all by itself, as it happens. 54 | 55 | ### What's a Loader 56 | Loader is a class offered by the Android Framework. It loads (surprise) data 57 | in a background thread and offers a callback interface to allow you to 58 | use that data once it's loaded. We will focus entirely on the case of using 59 | the Loader together with a database cursor. You can use a Loader to load 60 | anything in the background though. So if you have data stored as txt files, 61 | you can use a Loader then as well. 62 | 63 | ### What's SQLite and why do I want to store data in a database 64 | SQLite is a dialect of SQL, which stands for *Structured Query Language*. There are 65 | many dialects of SQL and this is the one supported by Android. It is a light and 66 | fast version of SQL which does not support all features present in other dialects, 67 | but you can do anything even though you might have to phrase it differently. 68 | 69 | The reason you want to store your data in a SQLite database in the first place is 70 | because it is **fast**. I mean deadly **fast**. Properly coded, you can do full text 71 | searches among *millions* of entries in real time. 72 | 73 | ## Let's start our project 74 | I will be using Eclipse together with the standard SDK only. Target will be Jelly Bean MR2 75 | (Android 4.2), but compatibility will be preserved back to Ice Cream Sandwich. 76 | I am assuming that you have managed to install Eclipse together with the SDK and 77 | are good to go. If not, follow the steps [here](http://developer.android.com/sdk/installing/bundle.html). Make sure you have downloaded 78 | the SDK for the Android version you intend to compile against (in our case, android-17). 79 | This is not the same as the least required android version to run your app. 80 | 81 | ### Create a new project 82 | Go to the menu, select New, Other, Android Application Project. 83 | 84 | ![New project](readme_img/newandroid.png) 85 | ![New project details](readme_img/newandroid2.png) 86 | 87 | I am going to go 88 | with the MasterDetail style to get a nice two-pane view on tablets for free. 89 | 90 | ![Setting the default layout](readme_img/newandroid3.png) 91 | 92 | I selected to name my "items" Persons. 93 | 94 | ### A note about Android versions 95 | Many outdated tutorials and things talk about supporting 96 | Android versions as far back as 1.6. Don't. If you really want to reach 95% of the market, 97 | set your minimum requirement to Gingerbread 2.3.3 (this is android-10 in the SDK). The 98 | reason is that several nice APIs and tools were introduced then and not using them is 99 | only stupid. If you only want to make an app for your own enjoyment and actually 100 | have a **modern** device, or were smart enough to get the Nexus S back in the day, 101 | put your least required version to ICS (android-14). ICS also introduced a lot of great 102 | APIs which you don't want to miss out on. Many of them can be used with the support package 103 | but to get the action bar and good looks from ICS, you'll need some library like 104 | ActionBarSherlock, and that's a tutorial for another day. The project wizard 105 | adds the support library by default, so staying backwards compatible shouldn't 106 | be too hard. 107 | 108 | ## Let's make our database 109 | By selecting the MasterDetail style in the wizard, we got a lot of code for free. 110 | You will only need to concern yourself with the two fragment classes, named 111 | *PersonDetailFragment* and *PersonListFragment* in my case. The listFragment 112 | extends ListFragment (surprise), but I'll show how to do this in the general case 113 | in case you want to use a generic fragment (or activity) later. 114 | 115 | The wizard created a dummy datastore which is just a fixed array of items. The 116 | first thing we want to do is to replace this with the SQLite database. 117 | 118 | ### Create the DatabaseHandler 119 | First create a new package for your database (not necessary, it's just nice). 120 | I'll name it "com.example.providerexample.database". 121 | 122 | ![New package](readme_img/newpkg.png) 123 | 124 | Next, in that package create a new class called "DatabaseHandler". 125 | 126 | ![New class](readme_img/newdbhandler.png) 127 | 128 | Once you 129 | have the class open, make it extend "SQLiteOpenHelper" and save. 130 | Eclipse should now complain about SQLiteOpenHelper needing to be 131 | imported. Put the text cursor 132 | somewhere in "SQLiteOpenHelper", hit **CTRL-1**, and select 133 | "import SQLiteOpenHelper.." 134 | 135 | ![Extending the super class](readme_img/newdbhandler2.png) 136 | 137 | Save. Now, we need to implement the needed methods. Put the text cursor instead 138 | somewhere in "DatabaseHandler", **CTRL-1**, "Add unimplemented methods..". 139 | 140 | ![Auto fixing errors](readme_img/newdbhandler3.png) 141 | 142 | The last thing missing is a constructor. Instead of adding a default one, add 143 | the following code to the top of the class: 144 | 145 | ```java 146 | private static DatabaseHandler singleton; 147 | 148 | public static DatabaseHandler getInstance(final Context context) { 149 | if (singleton == null) { 150 | singleton = new DatabaseHandler(context); 151 | } 152 | return singleton; 153 | } 154 | 155 | private static final int DATABASE_VERSION = 1; 156 | private static final String DATABASE_NAME = "providerExample"; 157 | 158 | private final Context context; 159 | 160 | public DatabaseHandler(Context context) { 161 | super(context, DATABASE_NAME, null, DATABASE_VERSION); 162 | // Good idea to use process context here 163 | this.context = context.getApplicationContext(); 164 | } 165 | ``` 166 | 167 | Your project should no longer show any errors. 168 | 169 | Before we make any further changes to the DatabaseHandler, let's make a 170 | Person class. We are now doing a kind of DAO/ORM approach 171 | (*Data As Objects* and *Object Relational Mapping* respectively), basically 172 | a Class will be responsible for converting the data stored in the database 173 | into variables useable in the Java code. The advantage of this approach, 174 | as compared to dealing with the database directly everytime is primarily 175 | safety and convenience. Putting all the code associated with a type of 176 | data in one place is good practice. If the data defnition has to change, then 177 | that class should be the only place that needs changes ideally. 178 | 179 | ### Creating the Person class 180 | So, once again create a new Class in the database package. I name my class 181 | "Person" to be consistent. I now decide that every person will have a 182 | first name, last name (never make that assumption in real life) and a "bio". 183 | 184 | The class is fairly simple but it's easier to show the finished result: 185 | 186 | ```java 187 | package com.example.providerexample.database; 188 | 189 | import android.content.ContentValues; 190 | import android.database.Cursor; 191 | 192 | /** 193 | * A class representation of a row in table "Person". 194 | */ 195 | public class Person { 196 | 197 | // SQL convention says Table name should be "singular", so not Persons 198 | public static final String TABLE_NAME = "Person"; 199 | // Naming the id column with an underscore is good to be consistent 200 | // with other Android things. This is ALWAYS needed 201 | public static final String COL_ID = "_id"; 202 | // These fields can be anything you want. 203 | public static final String COL_FIRSTNAME = "firstname"; 204 | public static final String COL_LASTNAME = "lastname"; 205 | public static final String COL_BIO = "bio"; 206 | 207 | // For database projection so order is consistent 208 | public static final String[] FIELDS = { COL_ID, COL_FIRSTNAME, COL_LASTNAME, 209 | COL_BIO }; 210 | 211 | /* 212 | * The SQL code that creates a Table for storing Persons in. 213 | * Note that the last row does NOT end in a comma like the others. 214 | * This is a common source of error. 215 | */ 216 | public static final String CREATE_TABLE = 217 | "CREATE TABLE " + TABLE_NAME + "(" 218 | + COL_ID + " INTEGER PRIMARY KEY," 219 | + COL_FIRSTNAME + " TEXT NOT NULL DEFAULT ''," 220 | + COL_LASTNAME + " TEXT NOT NULL DEFAULT ''," 221 | + COL_BIO + " TEXT NOT NULL DEFAULT ''" 222 | + ")"; 223 | 224 | // Fields corresponding to database columns 225 | public long id = -1; 226 | public String firstname = ""; 227 | public String lastname = ""; 228 | public String bio = ""; 229 | 230 | /** 231 | * No need to do anything, fields are already set to default values above 232 | */ 233 | public Person() { 234 | } 235 | 236 | /** 237 | * Convert information from the database into a Person object. 238 | */ 239 | public Person(final Cursor cursor) { 240 | // Indices expected to match order in FIELDS! 241 | this.id = cursor.getLong(0); 242 | this.firstname = cursor.getString(1); 243 | this.lastname = cursor.getString(2); 244 | this.bio = cursor.getString(3); 245 | } 246 | 247 | /** 248 | * Return the fields in a ContentValues object, suitable for insertion 249 | * into the database. 250 | */ 251 | public ContentValues getContent() { 252 | final ContentValues values = new ContentValues(); 253 | // Note that ID is NOT included here 254 | values.put(COL_FIRSTNAME, firstname); 255 | values.put(COL_LASTNAME, lastname); 256 | values.put(COL_BIO, bio); 257 | 258 | return values; 259 | } 260 | } 261 | ``` 262 | 263 | This class contains all the necessary information to create the required database 264 | things for Persons. Note that the fields themselves are public. This is a really 265 | simple class and I therefor consider getters/setters for those variables to be 266 | highly unnecessary. 267 | 268 | I also want to write the result of the CREATE_TABLE string in plain text here: 269 | 270 | ```SQL 271 | CREATE TABLE Person( 272 | _id INTEGER PRIMARY KEY, 273 | firstname TEXT NOT NULL DEFAULT '', 274 | lastname TEXT NOT NULL DEFAULT '', 275 | bio TEXT NOT NULL DEFAULT '') 276 | ``` 277 | 278 | This create a table in the database. Every table of a database must have an ID. 279 | By saying that *_id* is an *integer* and the *primary key*, we make it into the 280 | ID. *primary key* has the property that every newly inserted row gets a unique ID. 281 | The database will **never** allow two rows to have the same ID. This is a very 282 | good thing. 283 | 284 | The other columns all share the same definitions, they are *TEXT* which defaults 285 | to the empty string (just like they do in the class Person). But note that it 286 | also has *NOT NULL* defined. This means that they are not allowed to be NULL, ever. 287 | If you try to set a firstname to NULL, the database will protest and crash. 288 | Putting restrictions on the fields inside the database is a good idea, because 289 | you can then be sure of what will come out of it. However, think it through 290 | properly beforehand to make sure you don't set yourself up for stupid things later. 291 | 292 | ### Finishing the DatabaseHandler 293 | Let's turn our attention back to the DatabaseHandler we created earlier. First 294 | thing we need to do is put something inside *onCreate*. This method is called 295 | the *first* time the database is opened. We need to create our table inside it. 296 | Because we wrote the relevant code in the Person class, this is very straight 297 | forward. This is the complete *onCreate* method: 298 | 299 | ```java 300 | @Override 301 | public void onCreate(SQLiteDatabase db) { 302 | db.execSQL(Person.CREATE_TABLE); 303 | } 304 | ``` 305 | 306 | Let's leave *onUpgrade* for now. This method is called when the database version 307 | has changed. The version is defined at the top of the class in constant 308 | **DATABASE_VERSION = 1**. If you need to make changes to your database, increment 309 | this value and put any relevant code to *upgrade* the database in the *onUpgrade* 310 | method. We will get to an example of this later. All you have to have in mind now 311 | is that you can change the database as your app matures. You can add/remove tables, 312 | and also add columns to existing tables. The only thing you can't do is remove 313 | columns from existing tables (not a big problem, just ignore them in that case) or 314 | add/change/remove the *restrictions* you defined in the table creation 315 | (like *NOT NULL*). 316 | 317 | OK, the table will be created for us, good. But we can't really do anything yet. 318 | We need to be able to do stuff with persons. To start with, we want to be able to 319 | do the following: add/update a person, remove a person and of course get a person. 320 | 321 | Let's add methods corresponding to those cases: 322 | 323 | ```java 324 | public synchronized Person getPerson(final long id) { 325 | // TODO 326 | return null; 327 | } 328 | 329 | public synchronized boolean putPerson(final Person person) { 330 | // TODO 331 | return false; 332 | } 333 | 334 | public synchronized int removePerson(final Person person) { 335 | // TODO 336 | return 0; 337 | } 338 | ``` 339 | 340 | Some might notice that I do not have separate insert and update methods. 341 | Your case might differ but I find that if my code wants to save a person, 342 | then it wants to save it regardless if it is already present in the 343 | database or not. 344 | 345 | The methods are *synchronized* because we are going to use loaders to load 346 | the data, which run on separate background threads. So we want to make sure 347 | that only one thread is reading/writing at any given time. 348 | 349 | Let's start with *getPerson*. All we want to do is query the database 350 | for a row with the specified id. 351 | 352 | ```java 353 | public synchronized Person getPerson(final long id) { 354 | final SQLiteDatabase db = this.getReadableDatabase(); 355 | final Cursor cursor = db.query(Person.TABLE_NAME, 356 | Person.FIELDS, Person.COL_ID + " IS ?", 357 | new String[] { String.valueOf(id) }, null, null, null, null); 358 | if (cursor == null || cursor.isAfterLast()) { 359 | return null; 360 | } 361 | 362 | Person item = null; 363 | if (cursor.moveToFirst()) { 364 | item = new Person(cursor); 365 | } 366 | cursor.close(); 367 | 368 | return item; 369 | } 370 | ``` 371 | 372 | Here the advantage of the Person's cursor constructor should be clear. But make 373 | sure the cursor is pointing at the correct row before with *moveToFirst* or 374 | *moveToNext* depending on your use case. The method returns **null** if no 375 | row in the database matches the specified ID. IDs start at 1, so -1 is 376 | always a safe default value. The delete method is similarly simple: 377 | 378 | ```java 379 | public synchronized int removePerson(final Person person) { 380 | final SQLiteDatabase db = this.getWritableDatabase(); 381 | final int result = db.delete(Person.TABLE_NAME, 382 | Person.COL_ID + " IS ?", 383 | new String[] { Long.toString(person.id) }); 384 | 385 | return result; 386 | } 387 | ``` 388 | 389 | The result is the number 390 | of rows that were deleted. In this case it should never be anything except 391 | zero or one. Finally, *putPerson*. It's a little longer, but that's only 392 | because it is both an insert and an update method. 393 | 394 | First, we try to update the person. If that fails, we insert it instead. 395 | 396 | ```java 397 | public synchronized boolean putPerson(final Person person) { 398 | boolean success = false; 399 | int result = 0; 400 | final SQLiteDatabase db = this.getWritableDatabase(); 401 | 402 | if (person.id > -1) { 403 | result += db.update(Person.TABLE_NAME, person.getContent(), 404 | Person.COL_ID + " IS ?", 405 | new String[] { String.valueOf(person.id) }); 406 | } 407 | 408 | if (result > 0) { 409 | success = true; 410 | } else { 411 | // Update failed or wasn't possible, insert instead 412 | final long id = db.insert(Person.TABLE_NAME, null, 413 | person.getContent()); 414 | 415 | if (id > -1) { 416 | person.id = id; 417 | success = true; 418 | } 419 | } 420 | 421 | return success; 422 | } 423 | ``` 424 | 425 | Our ORM layer is complete. 426 | 427 | Using Person as a base, it is fairly easy to add additional tables to the database. 428 | All we need are get,put and remote methods in the database handler and we're set. 429 | Changes made to Person basically never have to touch the database handler ever. 430 | This will be very nice during development when you realize that you forgot 431 | a column that you needed. Only changes needed in the Person class, reinstall the 432 | app and you're good to go. 433 | 434 | 435 | ## Making a ContentProvider 436 | We will only be concerned with reading data from the database with our provider 437 | at first, so making it will be quick. First we need to create the shell of the 438 | class. The newer versions of the Android SDK make this easy. Go to New -> 439 | Other. Then select "Android Object". 440 | 441 | ![new android object](readme_img/newandroidobject.png) 442 | 443 | Next select "ContentProvider" from the list! 444 | 445 | ![new content provider](readme_img/newcontentprovider.png) 446 | 447 | It's important to keep in mind what you type as your authority. It should 448 | basically be "your.package.provider", but that's just a convention I read 449 | somewhere. It would be anything but must be unique to your application. 450 | I went with *com.example.providerexample.provider*. Exported isn't needed 451 | as we will only use the provider internally so far. 452 | 453 | ![choosing authority](readme_img/newcontentprovider2.png) 454 | 455 | By default, the wizard places the class in your main package. I want it to 456 | be in the database package. Remember if you move it, you must change the 457 | corresponding package name in the *AndroidManifest.xml* or your app will 458 | crash on start. 459 | 460 | Now you have the shell of a Provider done! This is the complete generated class: 461 | 462 | ```java 463 | public class PersonProvider extends ContentProvider { 464 | public PersonProvider() { 465 | } 466 | 467 | @Override 468 | public int delete(Uri uri, String selection, String[] selectionArgs) { 469 | // Implement this to handle requests to delete one or more rows. 470 | throw new UnsupportedOperationException("Not yet implemented"); 471 | } 472 | 473 | @Override 474 | public String getType(Uri uri) { 475 | // TODO: Implement this to handle requests for the MIME type of the data 476 | // at the given URI. 477 | throw new UnsupportedOperationException("Not yet implemented"); 478 | } 479 | 480 | @Override 481 | public Uri insert(Uri uri, ContentValues values) { 482 | // TODO: Implement this to handle requests to insert a new row. 483 | throw new UnsupportedOperationException("Not yet implemented"); 484 | } 485 | 486 | @Override 487 | public boolean onCreate() { 488 | // TODO: Implement this to initialize your content provider on startup. 489 | return false; 490 | } 491 | 492 | @Override 493 | public Cursor query(Uri uri, String[] projection, String selection, 494 | String[] selectionArgs, String sortOrder) { 495 | // TODO: Implement this to handle query requests from clients. 496 | throw new UnsupportedOperationException("Not yet implemented"); 497 | } 498 | 499 | @Override 500 | public int update(Uri uri, ContentValues values, String selection, 501 | String[] selectionArgs) { 502 | // TODO: Implement this to handle requests to update one or more rows. 503 | throw new UnsupportedOperationException("Not yet implemented"); 504 | } 505 | } 506 | ``` 507 | 508 | Since we only care about reading persons so far, we only need to concern 509 | ourselves with the *onCreate* and *query* methods. We also need to come 510 | up with a *URI*, basically a path that we can use to differentiate between 511 | different queries. 512 | 513 | Start by adding the following fields at the top of the Provider: 514 | 515 | ```java 516 | // All URIs share these parts 517 | public static final String AUTHORITY = "com.example.providerexample.provider"; 518 | public static final String SCHEME = "content://"; 519 | 520 | // URIs 521 | // Used for all persons 522 | public static final String PERSONS = SCHEME + AUTHORITY + "/person"; 523 | public static final Uri URI_PERSONS = Uri.parse(PERSONS); 524 | // Used for a single person, just add the id to the end 525 | public static final String PERSON_BASE = PERSONS + "/"; 526 | ``` 527 | 528 | Change the *onCreate* method to: 529 | 530 | ```java 531 | @Override 532 | public boolean onCreate() { 533 | return true; 534 | } 535 | ``` 536 | 537 | The *query* method is not that complicated either. This is the shell we 538 | are working from: 539 | 540 | ```java 541 | @Override 542 | public Cursor query(Uri uri, String[] projection, String selection, 543 | String[] selectionArgs, String sortOrder) { 544 | Cursor result = null; 545 | if (URI_PERSONS.equals(uri)) { 546 | 547 | } 548 | else if (uri.toString().startsWith(PERSON_BASE)) { 549 | 550 | } 551 | else { 552 | throw new UnsupportedOperationException("Not yet implemented"); 553 | } 554 | 555 | return result; 556 | } 557 | ``` 558 | 559 | Either you can get a cursor of all persons, or you can get a cursor with 560 | a single person. The single person operation is already implemented in 561 | getPerson(), so doing basically the same we get: 562 | 563 | ```java 564 | final long id = Long.parseLong(uri.getLastPathSegment()); 565 | result = DatabaseHandler 566 | .getInstance(getContext()) 567 | .getReadableDatabase() 568 | .query(Person.TABLE_NAME, Person.FIELDS, 569 | Person.COL_ID + " IS ?", 570 | new String[] { String.valueOf(id) }, null, null, 571 | null, null); 572 | ``` 573 | 574 | Getting all persons is even simpler: 575 | 576 | ```java 577 | result = DatabaseHandler 578 | .getInstance(getContext()) 579 | .getReadableDatabase() 580 | .query(Person.TABLE_NAME, Person.FIELDS, null, null, null, 581 | null, null, null); 582 | ``` 583 | 584 | You can note that I am outright ignoring the selection and order parameters 585 | of the provider method, as well as the projection. I don't care about selections 586 | so far because there is no use case for them yet. The projection I ignore 587 | because the only valid projection is defined in the Person class, as far as 588 | I care. This is very convenient if we ever make use of a ViewBinder. 589 | 590 | We're not completely done yet however. The provider works, in the sense that 591 | it will return cursors with the result. But we also want that to update 592 | whenever the database updates. To do that, we have to notify listeners 593 | on the URIs that things have changed. To do that, turn back to the 594 | DatabaseHandler. 595 | 596 | Every time something changes, we must notify the listeners. Only two methods 597 | change the content, and that is *removePerson* and *putPerson*. First, make 598 | a new method that does the actual notifying: 599 | 600 | ```java 601 | private void notifyProviderOnPersonChange() { 602 | context.getContentResolver().notifyChange( 603 | PersonProvider.URI_PERSONS, null, false); 604 | } 605 | ``` 606 | 607 | And call that method any time something changes. *putPerson* gets this added 608 | before its return statement: 609 | 610 | ```java 611 | if (success) { 612 | notifyProviderOnPersonChange(); 613 | } 614 | ``` 615 | 616 | And similarly for *removePerson*: 617 | 618 | ```java 619 | if (result > 0) { 620 | notifyProviderOnPersonChange(); 621 | } 622 | ``` 623 | 624 | Last, make the Provider listen for such notifications. Add the following 625 | line to both blocks in *query*: 626 | 627 | ```Java 628 | result.setNotificationUri(getContext().getContentResolver(), URI_PERSONS); 629 | ``` 630 | 631 | That's it. We're done. Here are the complete *PersonProvider* and *DatabaseHandler* classes before we move on to displaying the content: 632 | 633 | ```java 634 | public class DatabaseHandler extends SQLiteOpenHelper { 635 | 636 | private static DatabaseHandler singleton; 637 | 638 | public static DatabaseHandler getInstance(final Context context) { 639 | if (singleton == null) { 640 | singleton = new DatabaseHandler(context); 641 | } 642 | return singleton; 643 | } 644 | 645 | private static final int DATABASE_VERSION = 1; 646 | private static final String DATABASE_NAME = "providerExample"; 647 | 648 | private final Context context; 649 | 650 | public DatabaseHandler(Context context) { 651 | super(context, DATABASE_NAME, null, DATABASE_VERSION); 652 | this.context = context; 653 | } 654 | 655 | @Override 656 | public void onCreate(SQLiteDatabase db) { 657 | db.execSQL(Person.CREATE_TABLE); 658 | } 659 | 660 | @Override 661 | public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 662 | } 663 | 664 | public synchronized Person getPerson(final long id) { 665 | final SQLiteDatabase db = this.getReadableDatabase(); 666 | final Cursor cursor = db.query(Person.TABLE_NAME, Person.FIELDS, 667 | Person.COL_ID + " IS ?", new String[] { String.valueOf(id) }, 668 | null, null, null, null); 669 | if (cursor == null || cursor.isAfterLast()) { 670 | return null; 671 | } 672 | 673 | Person item = null; 674 | if (cursor.moveToFirst()) { 675 | item = new Person(cursor); 676 | } 677 | cursor.close(); 678 | return item; 679 | } 680 | 681 | public synchronized boolean putPerson(final Person person) { 682 | boolean success = false; 683 | int result = 0; 684 | final SQLiteDatabase db = this.getWritableDatabase(); 685 | 686 | if (person.id > -1) { 687 | result += db.update(Person.TABLE_NAME, person.getContent(), 688 | Person.COL_ID + " IS ?", 689 | new String[] { String.valueOf(person.id) }); 690 | } 691 | 692 | if (result > 0) { 693 | success = true; 694 | } else { 695 | // Update failed or wasn't possible, insert instead 696 | final long id = db.insert(Person.TABLE_NAME, null, 697 | person.getContent()); 698 | 699 | if (id > -1) { 700 | person.id = id; 701 | success = true; 702 | } 703 | } 704 | 705 | if (success) { 706 | notifyProviderOnPersonChange(); 707 | } 708 | 709 | return success; 710 | } 711 | 712 | public synchronized int removePerson(final Person person) { 713 | final SQLiteDatabase db = this.getWritableDatabase(); 714 | final int result = db.delete(Person.TABLE_NAME, 715 | Person.COL_ID + " IS ?", 716 | new String[] { Long.toString(person.id) }); 717 | 718 | if (result > 0) { 719 | notifyProviderOnPersonChange(); 720 | } 721 | return result; 722 | } 723 | 724 | private void notifyProviderOnPersonChange() { 725 | context.getContentResolver().notifyChange( 726 | PersonProvider.URI_PERSONS, null, false); 727 | } 728 | } 729 | ``` 730 | 731 | ```java 732 | public class PersonProvider extends ContentProvider { 733 | 734 | // All URIs share these parts 735 | public static final String AUTHORITY = "com.example.providerexample.provider"; 736 | public static final String SCHEME = "content://"; 737 | 738 | // URIs 739 | // Used for all persons 740 | public static final String PERSONS = SCHEME + AUTHORITY + "/person"; 741 | public static final Uri URI_PERSONS = Uri.parse(PERSONS); 742 | // Used for a single person, just add the id to the end 743 | public static final String PERSON_BASE = PERSONS + "/"; 744 | 745 | public PersonProvider() { 746 | } 747 | 748 | @Override 749 | public int delete(Uri uri, String selection, String[] selectionArgs) { 750 | // Implement this to handle requests to delete one or more rows. 751 | throw new UnsupportedOperationException("Not yet implemented"); 752 | } 753 | 754 | @Override 755 | public String getType(Uri uri) { 756 | // TODO: Implement this to handle requests for the MIME type of the data 757 | // at the given URI. 758 | throw new UnsupportedOperationException("Not yet implemented"); 759 | } 760 | 761 | @Override 762 | public Uri insert(Uri uri, ContentValues values) { 763 | // TODO: Implement this to handle requests to insert a new row. 764 | throw new UnsupportedOperationException("Not yet implemented"); 765 | } 766 | 767 | @Override 768 | public boolean onCreate() { 769 | return true; 770 | } 771 | 772 | @Override 773 | public Cursor query(Uri uri, String[] projection, String selection, 774 | String[] selectionArgs, String sortOrder) { 775 | Cursor result = null; 776 | if (URI_PERSONS.equals(uri)) { 777 | result = DatabaseHandler 778 | .getInstance(getContext()) 779 | .getReadableDatabase() 780 | .query(Person.TABLE_NAME, Person.FIELDS, null, null, null, 781 | null, null, null); 782 | result.setNotificationUri(getContext().getContentResolver(), URI_PERSONS); 783 | } else if (uri.toString().startsWith(PERSON_BASE)) { 784 | final long id = Long.parseLong(uri.getLastPathSegment()); 785 | result = DatabaseHandler 786 | .getInstance(getContext()) 787 | .getReadableDatabase() 788 | .query(Person.TABLE_NAME, Person.FIELDS, 789 | Person.COL_ID + " IS ?", 790 | new String[] { String.valueOf(id) }, null, null, 791 | null, null); 792 | result.setNotificationUri(getContext().getContentResolver(), URI_PERSONS); 793 | } else { 794 | throw new UnsupportedOperationException("Not yet implemented"); 795 | } 796 | 797 | return result; 798 | } 799 | 800 | @Override 801 | public int update(Uri uri, ContentValues values, String selection, 802 | String[] selectionArgs) { 803 | // TODO: Implement this to handle requests to update one or more rows. 804 | throw new UnsupportedOperationException("Not yet implemented"); 805 | } 806 | } 807 | ``` 808 | 809 | ## Making some layouts 810 | Let's get ready to use our new database. Obviously the layouts provided by the wizard 811 | aren't optimized to display information about our Persons. Let's remedy that! 812 | 813 | ### Detail view 814 | Persons are display in "fragment_person_detail.xml", replace the contents 815 | with the following to get a more interesting view: 816 | 817 | ```XML 818 | 822 | 823 | 827 | 828 | 836 | 837 | 845 | 846 | 854 | 855 | 863 | 864 | 872 | 873 | 881 | 882 | 883 | 884 | ``` 885 | 886 | This will look like this: 887 | 888 | ![Fancy person detail view](readme_img/persondetailview.png) 889 | 890 | The whole thing is wrapped in a scrollview to allow people to have really long 891 | biographies even on small screens. 892 | 893 | ### List item view 894 | Using Androids provided one or two line list items is so boring. Let's make 895 | something nicer. 896 | 897 | #### Create a drawable 898 | Create a folder called "drawable" in the res folder. It should live beside the other 899 | drawable folders you already got. 900 | 901 | Then create a new XML file named "rounded_corners_white.xml" in drawables. Put 902 | the following in it: 903 | 904 | ```XML 905 | 906 | 907 | 908 | 909 | 912 | 913 | 914 | 915 | 916 | 917 | 918 | 919 | 920 | 921 | 924 | 925 | 926 | 927 | 928 | 929 | 930 | 931 | 932 | 933 | 934 | 935 | ``` 936 | 937 | #### Create the list item 938 | 939 | Create a new layout xml file called "person_listitem.xml", make the 940 | root view a linear vertical layout. 941 | 942 | ![New layout file](readme_img/newlayoutfile.png) 943 | 944 | You want the file contents to look like: 945 | 946 | ```XML 947 | 948 | 960 | 961 | 969 | 970 | 978 | 979 | 986 | 987 | 997 | 998 | 999 | ``` 1000 | 1001 | #### Create a new list layout 1002 | Create a new layout xml called "fragment_person_list.xml". This is 1003 | its content: 1004 | 1005 | ```XML 1006 | 1007 | 1018 | ``` 1019 | 1020 | This is a straight up list view with a grey background and it draws 1021 | selectors on top. The items in the list will now look like this: 1022 | 1023 | ![List item layout](readme_img/cardItem.png) 1024 | 1025 | Because of the background, I want to move the location of some margins so the 1026 | correct background is rendered at that location. 1027 | 1028 | *activity_person_twopange* only margins and layout weights changed: 1029 | ```XML 1030 | 1039 | 1040 | 1050 | 1051 | 1058 | 1059 | 1064 | 1065 | 1066 | ``` 1067 | 1068 | And same deal for *activity_person_list*: 1069 | ```XML 1070 | 1078 | ``` 1079 | 1080 | ## Make the app use the new layouts and load the data 1081 | By now you should have one or two compiler errors because we removed 1082 | some things that was used in the fragments. Now we fix the errors 1083 | by making use of our new layouts instead. 1084 | 1085 | ### Fixing details fragment 1086 | Open "PersonDetailFragment.java" and you should be presented with this class: 1087 | 1088 | ```java 1089 | public class PersonDetailFragment extends Fragment { 1090 | /** 1091 | * The fragment argument representing the item ID that this fragment 1092 | * represents. 1093 | */ 1094 | public static final String ARG_ITEM_ID = "item_id"; 1095 | 1096 | /** 1097 | * The dummy content this fragment is presenting. 1098 | */ 1099 | private DummyContent.DummyItem mItem; 1100 | 1101 | /** 1102 | * Mandatory empty constructor for the fragment manager to instantiate the 1103 | * fragment (e.g. upon screen orientation changes). 1104 | */ 1105 | public PersonDetailFragment() { 1106 | } 1107 | 1108 | @Override 1109 | public void onCreate(Bundle savedInstanceState) { 1110 | super.onCreate(savedInstanceState); 1111 | 1112 | if (getArguments().containsKey(ARG_ITEM_ID)) { 1113 | // Load the dummy content specified by the fragment 1114 | // arguments. In a real-world scenario, use a Loader 1115 | // to load content from a content provider. 1116 | mItem = DummyContent.ITEM_MAP.get(getArguments().getString(ARG_ITEM_ID)); 1117 | } 1118 | } 1119 | 1120 | @Override 1121 | public View onCreateView(LayoutInflater inflater, ViewGroup container, 1122 | Bundle savedInstanceState) { 1123 | View rootView = inflater.inflate(R.layout.fragment_person_detail, container, false); 1124 | 1125 | // Show the dummy content as text in a TextView. 1126 | if (mItem != null) { 1127 | ((TextView) rootView.findViewById(R.id.person_detail)).setText(mItem.content); 1128 | } 1129 | 1130 | return rootView; 1131 | } 1132 | } 1133 | ``` 1134 | 1135 | Starting from the top, we have to change from the dummy class to the 1136 | Person class, use the databasehandler to get the person object and 1137 | finally use the new layout we made earlier. 1138 | 1139 | Additionally, we will save any updates we make in the onPause method. 1140 | That way, the content is saved everytime the screen goes off or the 1141 | fragment goes away. 1142 | 1143 | The result: 1144 | 1145 | ```java 1146 | public class PersonDetailFragment extends Fragment { 1147 | /** 1148 | * The fragment argument representing the item ID that this fragment 1149 | * represents. 1150 | */ 1151 | public static final String ARG_ITEM_ID = "item_id"; 1152 | 1153 | /** 1154 | * The person this fragment is presenting. 1155 | */ 1156 | private Person mItem; 1157 | 1158 | /** 1159 | * The UI elements showing the details of the Person 1160 | */ 1161 | private TextView textFirstName; 1162 | private TextView textLastName; 1163 | private TextView textBio; 1164 | 1165 | /** 1166 | * Mandatory empty constructor for the fragment manager to instantiate the 1167 | * fragment (e.g. upon screen orientation changes). 1168 | */ 1169 | public PersonDetailFragment() { 1170 | } 1171 | 1172 | @Override 1173 | public void onCreate(Bundle savedInstanceState) { 1174 | super.onCreate(savedInstanceState); 1175 | 1176 | if (getArguments().containsKey(ARG_ITEM_ID)) { 1177 | // Should use the contentprovider here ideally 1178 | mItem = DatabaseHandler.getInstance(getActivity()).getPerson(getArguments().getLong(ARG_ITEM_ID)); 1179 | } 1180 | } 1181 | 1182 | @Override 1183 | public View onCreateView(LayoutInflater inflater, ViewGroup container, 1184 | Bundle savedInstanceState) { 1185 | View rootView = inflater.inflate(R.layout.fragment_person_detail, container, false); 1186 | 1187 | if (mItem != null) { 1188 | textFirstName = ((TextView) rootView.findViewById(R.id.textFirstName)); 1189 | textFirstName.setText(mItem.firstname); 1190 | 1191 | textLastName = ((TextView) rootView.findViewById(R.id.textLastName)); 1192 | textLastName.setText(mItem.lastname); 1193 | 1194 | textBio = ((TextView) rootView.findViewById(R.id.textBio)); 1195 | textBio.setText(mItem.bio); 1196 | } 1197 | 1198 | return rootView; 1199 | } 1200 | 1201 | @Override 1202 | public void onPause() { 1203 | super.onPause(); 1204 | updatePersonFromUI(); 1205 | } 1206 | 1207 | private void updatePersonFromUI() { 1208 | if (mItem != null) { 1209 | mItem.firstname = textFirstName.getText().toString(); 1210 | mItem.lastname = textLastName.getText().toString(); 1211 | mItem.bio = textBio.getText().toString(); 1212 | 1213 | DatabaseHandler.getInstance(getActivity()).putPerson(mItem); 1214 | } 1215 | } 1216 | } 1217 | ``` 1218 | 1219 | #### Why not use the provider here? 1220 | It would be good practice to use a Loader to fetch the Person from the Provider, 1221 | if only just because the data would be loaded on a background thread. Right 1222 | now the fragment is fetching the person on the UI thread. 1223 | 1224 | First, I wanted to show how to use an SQLite database without a ContentProvider. 1225 | As you can see, the changes from some kind of ArrayAdapter situation are quite 1226 | minimal in the fragments/activities. 1227 | 1228 | Second, fetching the data on a background thread is just about the only 1229 | benefit we will get from using a Loader in the detail fragment. We **DON'T** 1230 | want the data to auto update in here like we want it to do in the List. 1231 | Because consider if it is possible that the data is changed somewhere else, 1232 | maybe you implement some kind of synchronization service. It would be really 1233 | bad user experience to have your entire item change as you were editing it. 1234 | 1235 | Fetching a single item on the UI thread is fine for now. If you have more 1236 | advanced ideas in mind, yes you should use a Loader. 1237 | 1238 | ### Fixing list view 1239 | First we need to use the layout defined in "fragment_person_list.xml". This is 1240 | more or less only because I want a grey background in the list. 1241 | 1242 | Add: 1243 | ```java 1244 | @Override 1245 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 1246 | return inflater.inflate(R.layout.fragment_person_list, null); 1247 | } 1248 | ``` 1249 | 1250 | Next we set the adapter using a *SimpleCursorAdapter*. 1251 | Initially it has a null cursor. The data is loaded using a *Loader*. 1252 | So in *onCreate*, change from: 1253 | 1254 | ```java 1255 | // TODO: replace with a real list adapter. 1256 | setListAdapter(new ArrayAdapter(getActivity(), 1257 | android.R.layout.simple_list_item_activated_1, 1258 | android.R.id.text1, DummyContent.ITEMS)); 1259 | ``` 1260 | 1261 | to: 1262 | ```java 1263 | setListAdapter(new SimpleCursorAdapter(getActivity(), 1264 | R.layout.person_listitem, null, new String[] { 1265 | Person.COL_FIRSTNAME, Person.COL_LASTNAME, 1266 | Person.COL_BIO }, new int[] { R.id.cardFirstName, 1267 | R.id.cardLastName, R.id.cardDescription }, 0)); 1268 | 1269 | // Load the content 1270 | getLoaderManager().initLoader(0, null, new LoaderCallbacks() { 1271 | @Override 1272 | public Loader onCreateLoader(int id, Bundle args) { 1273 | return new CursorLoader(getActivity(), 1274 | PersonProvider.URI_PERSONS, Person.FIELDS, null, null, 1275 | null); 1276 | } 1277 | 1278 | @Override 1279 | public void onLoadFinished(Loader loader, Cursor c) { 1280 | ((SimpleCursorAdapter) getListAdapter()).swapCursor(c); 1281 | } 1282 | 1283 | @Override 1284 | public void onLoaderReset(Loader arg0) { 1285 | ((SimpleCursorAdapter) getListAdapter()).swapCursor(null); 1286 | } 1287 | }); 1288 | ``` 1289 | 1290 | As a last step, update the *onListItemClick* method to utilize the Cursor instead. 1291 | 1292 | ```java 1293 | @Override 1294 | public void onListItemClick(ListView listView, View view, int position, 1295 | long id) { 1296 | super.onListItemClick(listView, view, position, id); 1297 | 1298 | // Notify the active callbacks interface (the activity, if the 1299 | // fragment is attached to one) that an item has been selected. 1300 | mCallbacks.onItemSelected(getListAdapter().getItemId(position)); 1301 | } 1302 | ``` 1303 | 1304 | But, we must also change the callback interface to use a Long instead of 1305 | a String to match our database ID and what the Fragment expects. 1306 | 1307 | ![Change to long](readme_img/changetolongarg.png) 1308 | 1309 | Make suitable changes everywhere. In the same file update the dummyCallback: 1310 | 1311 | ```java 1312 | private static Callbacks sDummyCallbacks = new Callbacks() { 1313 | @Override 1314 | public void onItemSelected(long id) { 1315 | } 1316 | }; 1317 | ``` 1318 | 1319 | And in PersonListActivity update *onItemSelected*: 1320 | 1321 | ```java 1322 | @Override 1323 | public void onItemSelected(long id) { 1324 | if (mTwoPane) { 1325 | // In two-pane mode, show the detail view in this activity by 1326 | // adding or replacing the detail fragment using a 1327 | // fragment transaction. 1328 | Bundle arguments = new Bundle(); 1329 | arguments.putLong(PersonDetailFragment.ARG_ITEM_ID, id); 1330 | PersonDetailFragment fragment = new PersonDetailFragment(); 1331 | fragment.setArguments(arguments); 1332 | getSupportFragmentManager().beginTransaction() 1333 | .replace(R.id.person_detail_container, fragment) 1334 | .commit(); 1335 | 1336 | } else { 1337 | // In single-pane mode, simply start the detail activity 1338 | // for the selected item ID. 1339 | Intent detailIntent = new Intent(this, PersonDetailActivity.class); 1340 | detailIntent.putExtra(PersonDetailFragment.ARG_ITEM_ID, id); 1341 | startActivity(detailIntent); 1342 | } 1343 | } 1344 | ``` 1345 | ## Add a new button 1346 | Running the app now should present an empty list. Not that interesting! 1347 | We need to be able to add some items. Just implement a menu item for that: 1348 | 1349 | ![New menu file](readme_img/newmenu.png) 1350 | 1351 | ```XML 1352 | 1353 | 1354 | 1355 | 1360 | 1361 | 1362 | ``` 1363 | 1364 | Inflate the menu and click handler in *PersonListActivity*: 1365 | 1366 | ```Java 1367 | @Override 1368 | public boolean onCreateOptionsMenu(Menu menu) { 1369 | MenuInflater inflater = getMenuInflater(); 1370 | inflater.inflate(R.menu.list_activity, menu); 1371 | return true; 1372 | } 1373 | 1374 | @Override 1375 | public boolean onOptionsItemSelected(MenuItem item) { 1376 | boolean result = false; 1377 | if (R.id.newPerson == item.getItemId()) { 1378 | result = true; 1379 | // Create a new person. 1380 | Person p = new Person(); 1381 | DatabaseHandler.getInstance(this).putPerson(p); 1382 | // Open a new fragment with the new id 1383 | onItemSelected(p.id); 1384 | } 1385 | 1386 | return result; 1387 | } 1388 | ``` 1389 | 1390 | Change to LongExtras in *PersonListActivity.java* 1391 | ```Java 1392 | @Override 1393 | protected void onCreate(Bundle savedInstanceState) { 1394 | super.onCreate(savedInstanceState); 1395 | setContentView(R.layout.activity_person_detail); 1396 | 1397 | // Show the Up button in the action bar. 1398 | getActionBar().setDisplayHomeAsUpEnabled(true); 1399 | 1400 | // savedInstanceState is non-null when there is fragment state 1401 | // saved from previous configurations of this activity 1402 | // (e.g. when rotating the screen from portrait to landscape). 1403 | // In this case, the fragment will automatically be re-added 1404 | // to its container so we don't need to manually add it. 1405 | // For more information, see the Fragments API guide at: 1406 | // 1407 | // http://developer.android.com/guide/components/fragments.html 1408 | // 1409 | if (savedInstanceState == null) { 1410 | // Create the detail fragment and add it to the activity 1411 | // using a fragment transaction. 1412 | Bundle arguments = new Bundle(); 1413 | arguments.putLong(PersonDetailFragment.ARG_ITEM_ID, 1414 | getIntent().getLongExtra(PersonDetailFragment.ARG_ITEM_ID, -1)); 1415 | PersonDetailFragment fragment = new PersonDetailFragment(); 1416 | fragment.setArguments(arguments); 1417 | getSupportFragmentManager().beginTransaction() 1418 | .add(R.id.person_detail_container, fragment) 1419 | .commit(); 1420 | } 1421 | } 1422 | ``` 1423 | 1424 | ## Add some data 1425 | Before shipping off your newly created app you might want to add some data to it. 1426 | You can do so by updating the onCreate method of the DatabaseHandler. 1427 | First you create a new person object, populate it with the desired values and 1428 | finally insert it using the insert method of the db object. Eventually it should look like below. 1429 | 1430 | Do note that ideally any predefined data should be loaded from resources 1431 | and not be hardcoded like done here. But doing so is beyond this tutorial. 1432 | 1433 | ```java 1434 | @Override 1435 | public void onCreate(SQLiteDatabase db) { 1436 | db.execSQL(Person.CREATE_TABLE); 1437 | 1438 | Person person = new Person(); 1439 | person.firstname = "Sylvester"; 1440 | person.lastname = "Stallone"; 1441 | person.bio = "..."; 1442 | db.insert(Person.TABLE_NAME, null, person.getContent()); 1443 | 1444 | person.firstname = "Danny"; 1445 | person.lastname = "DeVito"; 1446 | person.bio = "..."; 1447 | db.insert(Person.TABLE_NAME, null, person.getContent()); 1448 | } 1449 | ``` 1450 | 1451 | ## Final result 1452 | Nexus 7 1453 | 1454 | ![Nexus 7](readme_img/nexus7.png) 1455 | 1456 | Galaxy Nexus List 1457 | 1458 | ![Galaxy Nexus](readme_img/gnexlist.png) 1459 | 1460 | Galaxy Nexus Details 1461 | 1462 | ![Galaxy Nexus](readme_img/gnexdetail1.png) 1463 | 1464 | ## Explaining the details 1465 | 1466 | Let's clarify a few of the things I skipped over quite fast. 1467 | 1468 | ### URIs 1469 | 1470 | A URI is pretty much an address. You are quite familiar with URIs like 1471 | *http://www.google.com*. The URIs which point to information handled by 1472 | ContentProviders always start with *content://* instead of *http://*, but 1473 | you see that the structure is the same. This part is what is called the 1474 | **scheme**. A URI pointing to a file on the SD card would have the scheme: 1475 | *file://*. 1476 | 1477 | The next part of the URI is the **authority**. This identifies which entity 1478 | (in our case, which provider in which app) should handle the request. In the 1479 | example above the authority was *www.google.com*. In the case of content 1480 | providers, the authority typically involves your package name (like 1481 | *com.example.providerexample*) in order to guarantee its uniqueness to your 1482 | provider. Some Google document says its convention to put *.provider* at 1483 | the end. It makes sure the system knows to pass the request along to our 1484 | content provider and not one of the others in the system. 1485 | 1486 | The final part of the URI is the **path**. The google address does not have 1487 | a path. If it looked like: *http://www.google.com/a/folder/img.png* then 1488 | */a/folder/img.png* would be the path. This is meaningless to the system 1489 | and only relevant to our content provider. We can define anything we 1490 | want here. We use it to differentiate between different requests. For 1491 | example: */persons* used in a query will retrieve a list of all persons. 1492 | */persons/id* will fetch only a single person with the designated id. 1493 | */squirrels* won't lead to anything since the provider doesn't know 1494 | what to do with that. But it could. It's all up to the developer. 1495 | 1496 | ### SQLite 1497 | 1498 | There are (many) books written about SQL so I'm not even going to try to 1499 | condense everything thing in here. Instead I'll focus on what you have seen 1500 | and what is obviously still missing. 1501 | 1502 | SQLite is merely the language dialect we are using. We are using it because 1503 | it is what Google decided to support natively in Android. The point is 1504 | to create and manage a database. A relational database (there are other kinds) 1505 | stores its information in tables. Each entry corresponds to a row in the 1506 | table, with each kind of information stored in the different columns. 1507 | 1508 | A concrete example: 1509 | 1510 | 1511 | 1512 | 1513 | 1514 | 1515 | 1516 | 1517 | 1518 | 1519 | 1520 | 1521 | 1522 | 1523 | 1524 | 1525 | 1526 |
IDnamephonenumber
1Bob326346
2Alice326346
1527 | 1528 | Created by: 1529 | 1530 | ```SQL 1531 | CREATE TABLE contact( 1532 | INT ID primary key, 1533 | TEXT name, 1534 | TEXT phonenumber) 1535 | ``` 1536 | 1537 | This table would store people with their phone numbers and possibly other 1538 | information as well. Now, if you wanted to support more than one phone number 1539 | per person, this structure would break down. There is no way to make a list 1540 | of phone numbers (without doing string parsing) for a single person here. To 1541 | do that, you'd need to take advantage of the *relational* part of the database. 1542 | We'd have to store the phonenumbers in a separate table. For example: 1543 | 1544 | 1545 | 1546 | 1547 | 1548 | 1549 | 1550 | 1551 | 1552 | 1553 | 1554 | 1555 | 1556 | 1557 |
IDname
1Bob
2Alice
1558 | 1559 | 1560 | 1561 | 1562 | 1563 | 1564 | 1565 | 1566 | 1567 | 1568 | 1569 | 1570 | 1571 | 1572 | 1573 | 1574 | 1575 |
IDcontactidphonenumber
12326346
21326346
1576 | 1577 | 1578 | Created by: 1579 | 1580 | ```SQL 1581 | CREATE TABLE contact( 1582 | INT ID primary key, 1583 | TEXT name); 1584 | 1585 | CREATE TABLE phonenumber( 1586 | INT ID primary key, 1587 | INT contactid, 1588 | TEXT phonenumber); 1589 | ``` 1590 | 1591 | Now we can store an arbitrary amount of phone numbers per contact. This might 1592 | seem messy, and it kind of is. But, once you get the hang of it you can do 1593 | some pretty powerful and convenient things too. One thing that I missed above 1594 | was to have a *CONSTRAINT* on the table. Right now, the phonenumbers table 1595 | has a column called contactid, but the database doesn't care what you insert 1596 | there. Consider instead the following modification to the code: 1597 | 1598 | ```SQL 1599 | CREATE TABLE phonenumber( 1600 | INT ID primary key, 1601 | INT contactid REFERENCES contact, 1602 | TEXT phonenumber) 1603 | ``` 1604 | 1605 | Now we are telling the database that this number should match a valid ID 1606 | in the contacts table. If we do something that will make this statement 1607 | false, the database will throw an exception. This is good because it 1608 | prevents us from doing stupid things. Like filling the database with a 1609 | bunch of orphan phone numbers. We can make things easy on ourselves however 1610 | by adding a *CONFLICT RESOLUTION CLAUSE*: 1611 | 1612 | ```SQL 1613 | CREATE TABLE phonenumber( 1614 | INT ID primary key, 1615 | INT contactid REFERENCES contact ON DELETE CASCADE, 1616 | TEXT phonenumber) 1617 | ``` 1618 | 1619 | If we delete a contact and that contact is linked to phone numbers, now 1620 | those phone numbers are deleted automatically at the same time. So the database 1621 | will not break because we delete a contact. We can also constrain the 1622 | phone numbers to be unique: 1623 | 1624 | ```SQL 1625 | CREATE TABLE phonenumber( 1626 | INT ID primary key, 1627 | INT contactid REFERENCES contact ON DELETE CASCADE, 1628 | TEXT phonenumber, 1629 | UNIQUE(phonenumber)) 1630 | ``` 1631 | 1632 | This means that only one contact can be linked to a single phone number, which 1633 | is probably a lot like reality. But consider something like a hard line, a 1634 | home phone. That is probably linked to an entire family, logically speaking. 1635 | We can make the database understand that by doing: 1636 | 1637 | ```SQL 1638 | CREATE TABLE phonenumber( 1639 | INT ID primary key, 1640 | INT contactid REFERENCES contact ON DELETE CASCADE, 1641 | TEXT phonenumber, 1642 | UNIQUE(contactid, phonenumber)) 1643 | ``` 1644 | 1645 | Now we are only constraining the phonenumber to be unique within a person's 1646 | collection of numbers. So a number can not occur more than once for a single 1647 | persons, which makes sense. But a number can belong to several persons, which 1648 | is true in case of home phones. 1649 | 1650 | #### Queries 1651 | 1652 | To get information from the database, we will be doing a **query**. The 1653 | result is returned in a Cursor. The basic SQL syntax of a query is: 1654 | 1655 | ```SQL 1656 | SELECT columnname1, columnname2 1657 | FROM table 1658 | WHERE something IS something 1659 | ORDER BY columname; 1660 | ``` 1661 | 1662 | This returns a cursor with all the rows that matched the *WHERE*-part. But, 1663 | you can do queries on multiple tables at once and do internal queries as 1664 | part of your *WHERE*-clause. We can also do some arithmetic and string operations. 1665 | In Android we will probably tend to use the suitable helper classes. They 1666 | work fine unless you want to do things that concern multiple tables at 1667 | once (JOINED queries). You have already seen the syntax being of the form: 1668 | 1669 | ```Java 1670 | db.query(tableName, columnsToReturn, "something IS ?", someValue, 1671 | null, null, 1672 | orderByColumnName, 1673 | null); 1674 | ``` 1675 | 1676 | You can see that this matches quite literally the parts from the SQL code. 1677 | The nulls represents possible other constraints 1678 | that I will not mention. 1679 | 1680 | The question mark however is useful. While you could have put your argument 1681 | directly in the string instead of the question mark, that might break at times. 1682 | Namely when that argument is a string. For example: 1683 | 1684 | **WHERE column1 IS Arthur** 1685 | 1686 | would crash because the correct syntax would be 1687 | 1688 | **WHERE column1 IS 'Arthur'** 1689 | 1690 | And those quotes are easy to forget. Putting the question mark in the java 1691 | call means that the implementation will wrap your argument in quotes for you. 1692 | It's basically safer, and means you have a fixed where-clause as a static 1693 | string somewhere, and only change the argument array. 1694 | 1695 | To order the data, just specify the columnname and whether to do it "ASC" or 1696 | "DESC" (forwards or backwards). To sort the names in the list in the app 1697 | alphabetically, just do something like: 1698 | 1699 | ```Java 1700 | final Cursor cursor = db.query(Person.TABLE_NAME, Person.FIELDS, 1701 | Person.COL_ID + " IS ?", new String[] { String.valueOf(id) }, 1702 | null, null, 1703 | Person.COL_FIRSTNAME, 1704 | , null); 1705 | ``` 1706 | 1707 | To do it backwards replace the OrderBy part with (don't forget the space): 1708 | 1709 | ```Java 1710 | Person.COL_FIRSTNAME + " DESC" 1711 | ``` 1712 | 1713 | #### Changing data 1714 | Operating on tables is possible using *insert*, *update* or *delete*. They 1715 | are probably self explanatory from their names. Just like queries, *update* 1716 | and *delete* accept WHERE-clauses. Which means you can update or delete 1717 | a whole bunch of items fulfilling some criteria. Like deleting all 1718 | people over 50 years old if you had that data. 1719 | 1720 | Another thing that is handy to know about is how to change tha database 1721 | tables. SQLite will not allow you to delete columns from tables, but 1722 | you can add columns or rename them. Adding columns after the fact has 1723 | a few restrictions though: 1724 | 1725 | - The column may not have a PRIMARY KEY or UNIQUE constraint. 1726 | - The column may not have a default value of CURRENT_TIME, CURRENT_DATE, CURRENT_TIMESTAMP, or an expression in parentheses. 1727 | - If a NOT NULL constraint is specified, then the column must have a default value other than NULL. 1728 | - If foreign key constraints are enabled and a column with a REFERENCES clause is added, the column must have a default value of NULL. 1729 | 1730 | The only thing relevant is probably the first one, they can not have a 1731 | unique constraint which could be something of a bummer. The syntax you could 1732 | put in your Databasehandler's onUpdate method would be something like: 1733 | 1734 | ```Java 1735 | public void onUpdate(SQLiteDatabase db, int oldVersion, int newVersion) { 1736 | if (oldVersion < 2) { 1737 | db.execSQL("ALTER TABLE contact ADD COLUMN INT age NOT NULL DEFAULT 25"); 1738 | } 1739 | } 1740 | ``` 1741 | 1742 | Just remember to update your original create table statement for fresh installs. 1743 | They will NOT get a call to onUpdate. 1744 | -------------------------------------------------------------------------------- /readme_img/cardItem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacecowboy/AndroidTutorialContentProvider/3c0674586ebb807d060968ca6566af0d16baa88d/readme_img/cardItem.png -------------------------------------------------------------------------------- /readme_img/changetolongarg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacecowboy/AndroidTutorialContentProvider/3c0674586ebb807d060968ca6566af0d16baa88d/readme_img/changetolongarg.png -------------------------------------------------------------------------------- /readme_img/gnex_framed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacecowboy/AndroidTutorialContentProvider/3c0674586ebb807d060968ca6566af0d16baa88d/readme_img/gnex_framed.png -------------------------------------------------------------------------------- /readme_img/gnexdetail1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacecowboy/AndroidTutorialContentProvider/3c0674586ebb807d060968ca6566af0d16baa88d/readme_img/gnexdetail1.png -------------------------------------------------------------------------------- /readme_img/gnexdetail2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacecowboy/AndroidTutorialContentProvider/3c0674586ebb807d060968ca6566af0d16baa88d/readme_img/gnexdetail2.png -------------------------------------------------------------------------------- /readme_img/gnexlist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacecowboy/AndroidTutorialContentProvider/3c0674586ebb807d060968ca6566af0d16baa88d/readme_img/gnexlist.png -------------------------------------------------------------------------------- /readme_img/new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacecowboy/AndroidTutorialContentProvider/3c0674586ebb807d060968ca6566af0d16baa88d/readme_img/new.png -------------------------------------------------------------------------------- /readme_img/newandroid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacecowboy/AndroidTutorialContentProvider/3c0674586ebb807d060968ca6566af0d16baa88d/readme_img/newandroid.png -------------------------------------------------------------------------------- /readme_img/newandroid2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacecowboy/AndroidTutorialContentProvider/3c0674586ebb807d060968ca6566af0d16baa88d/readme_img/newandroid2.png -------------------------------------------------------------------------------- /readme_img/newandroid3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacecowboy/AndroidTutorialContentProvider/3c0674586ebb807d060968ca6566af0d16baa88d/readme_img/newandroid3.png -------------------------------------------------------------------------------- /readme_img/newandroidobject.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacecowboy/AndroidTutorialContentProvider/3c0674586ebb807d060968ca6566af0d16baa88d/readme_img/newandroidobject.png -------------------------------------------------------------------------------- /readme_img/newcontentprovider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacecowboy/AndroidTutorialContentProvider/3c0674586ebb807d060968ca6566af0d16baa88d/readme_img/newcontentprovider.png -------------------------------------------------------------------------------- /readme_img/newcontentprovider2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacecowboy/AndroidTutorialContentProvider/3c0674586ebb807d060968ca6566af0d16baa88d/readme_img/newcontentprovider2.png -------------------------------------------------------------------------------- /readme_img/newdbhandler.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacecowboy/AndroidTutorialContentProvider/3c0674586ebb807d060968ca6566af0d16baa88d/readme_img/newdbhandler.png -------------------------------------------------------------------------------- /readme_img/newdbhandler2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacecowboy/AndroidTutorialContentProvider/3c0674586ebb807d060968ca6566af0d16baa88d/readme_img/newdbhandler2.png -------------------------------------------------------------------------------- /readme_img/newdbhandler3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacecowboy/AndroidTutorialContentProvider/3c0674586ebb807d060968ca6566af0d16baa88d/readme_img/newdbhandler3.png -------------------------------------------------------------------------------- /readme_img/newlayoutfile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacecowboy/AndroidTutorialContentProvider/3c0674586ebb807d060968ca6566af0d16baa88d/readme_img/newlayoutfile.png -------------------------------------------------------------------------------- /readme_img/newmenu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacecowboy/AndroidTutorialContentProvider/3c0674586ebb807d060968ca6566af0d16baa88d/readme_img/newmenu.png -------------------------------------------------------------------------------- /readme_img/newpkg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacecowboy/AndroidTutorialContentProvider/3c0674586ebb807d060968ca6566af0d16baa88d/readme_img/newpkg.png -------------------------------------------------------------------------------- /readme_img/nexus7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacecowboy/AndroidTutorialContentProvider/3c0674586ebb807d060968ca6566af0d16baa88d/readme_img/nexus7.png -------------------------------------------------------------------------------- /readme_img/nexus7_framed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacecowboy/AndroidTutorialContentProvider/3c0674586ebb807d060968ca6566af0d16baa88d/readme_img/nexus7_framed.png -------------------------------------------------------------------------------- /readme_img/persondetailview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacecowboy/AndroidTutorialContentProvider/3c0674586ebb807d060968ca6566af0d16baa88d/readme_img/persondetailview.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | --------------------------------------------------------------------------------