├── .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 |
--------------------------------------------------------------------------------
/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 | 
17 |
18 | 
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 | 
85 | 
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 | 
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 | 
123 |
124 | Next, in that package create a new class called "DatabaseHandler".
125 |
126 | 
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 | 
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 | 
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 | 
442 |
443 | Next select "ContentProvider" from the list!
444 |
445 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
1350 |
1351 | ```XML
1352 |
1353 |
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 | 
1455 |
1456 | Galaxy Nexus List
1457 |
1458 | 
1459 |
1460 | Galaxy Nexus Details
1461 |
1462 | 
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 | ID |
1513 | name |
1514 | phonenumber |
1515 |
1516 |
1517 | 1 |
1518 | Bob |
1519 | 326346 |
1520 |
1521 |
1522 | 2 |
1523 | Alice |
1524 | 326346 |
1525 |
1526 |
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 | ID |
1547 | name |
1548 |
1549 |
1550 | 1 |
1551 | Bob |
1552 |
1553 |
1554 | 2 |
1555 | Alice |
1556 |
1557 |
1558 |
1559 |
1560 |
1561 | ID |
1562 | contactid |
1563 | phonenumber |
1564 |
1565 |
1566 | 1 |
1567 | 2 |
1568 | 326346 |
1569 |
1570 |
1571 | 2 |
1572 | 1 |
1573 | 326346 |
1574 |
1575 |
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 |
--------------------------------------------------------------------------------