Building an Android App to Manage Your Parts Collection
If you are a Maker like me, you probably have a big pile of hardware somewhere in your workshop: screws, nuts, bearings, electrical components, maybe some development boards. With each new project, more parts are added to what is more than likely a fairly disorganized stash of metal and silicon. In my own workshop I had a giant cardboard box filled with bags of parts and components. Finding any specific part was a daunting task involving wading through the box and passing by hundreds of other parts until I found the part I was seeking.
[picture of parts box]Maybe your collection of hardware is more organized than mine was; but do you know how many of each part you have without having to count and recount your parts over and over? If you take on a new project and find out you will need 24 M3 x 12mm screws, how would you know if you had enough or needed to order more? How much money have you wasted over the years ordering parts you already had on hand only to end up with four times the quantity you needed in the first place?
I decided to create an Android app to help me keep track of all my parts. This way, when I take on a new project, I can simply search in the application to check what parts I already have, and how many of each part. The application is called Stacks and this post is about how the app is designed. I am by no means a professional programmer, but it is still my hope that this retrospective provides useful information for others designing similar, or not so similar, apps of their own.
Finished App Preview
Planning
Whenever I start any kind of project, whether it is a coding project, written project, or a build of some kind, I like to roughly plan out the project first. When designing an Android app specifically, I think it is very important to plan out the flow of the app before you ever open Android Studio, in terms of the way the user will navigate the app. Creating an intuitive organization for the app is critically important because if your information architecture confuses your users, you will not keep any users for long.
I don’t use any sophisticated tools when planning out my projects. I mostly work with pen and paper, sketching out layouts, activities, using arrows for navigation, and adding notes.
As you can see from the image, the planning process does not stop when the coding starts. I continue to take notes and modify my sketches throughout the project. The other thing about planning projects is that it needn’t take a long time. Planning is mostly about thinking through the high-level details of the project in order to get a basic road map to stay on task while working out all the fine details in code.
Databases
Stacks is ostensibly a database management app. All of the information about the user’s collection of parts in Stacks, and all the actions taken by the user while using the application are driven by two databases. The first database stores the part data. Every part in the user’s collection is a row in the parts database.
ID | Name | Category | Quantity |
---|---|---|---|
1 | part name 1 | category 1 | 12 |
2 | part name 2 | category 1 | 45 |
3 | part name 3 | category 3 | 6 |
4 | part name 4 | category 2 | 34 |
5 | part name 5 | category 2 | 16 |
So, any time the user is looking at a part, adding a part, looking at a category, doing a search, or really any other activity in the Stacks application, they are interacting with the parts database.
A second database is used to store information about the categories.
ID | Name | Image |
---|---|---|
1 | category 1 | imagename.png |
2 | category 2 | imagename.png |
3 | category 3 | imagename.png |
The Database Handler Functions
If the databases themselves are a kind of skeleton for the Stacks application, then the app’s brain is a pair of Java classes for handling interactions between the user interface and the databases. None of the user-facing activities interact directly with the database, rather, the UI activities use methods from database handler classes, and the handler classes run queries on the databases. This pattern complies with the Model-View-Controller architecture and it allows the UI components of the app to be developed and modified without changing the databases themselves, by using the same set of methods from the handler classes.
The database handler classes both have a similar structure. Their first responsibility is to create the parts and categories tables when the app is initially installed and launched, or if the user chooses to delete all the parts from their collection and start again from scratch. Stacks uses SQLite databases. Therefore, the part database handler and the category database handler classes utilize SQLite queries to create the tables in the database.
public class PartsDatabaseHandler extends SQLiteOpenHelper { // All Static variables // Database Version private static final int DATABASE_VERSION = 1; // Database Name private static final String DATABASE_NAME = "partsDatabase"; // Contacts table name private static final String TABLE_PARTS = "parts"; // Contacts Table Columns names private static final String KEY_ID = "id"; private static final String KEY_NAME = "name"; private static final String KEY_CATEGORY = "category"; private static final String KEY_QUANTITY = "quantity"; private static final String CREATE_PARTS_TABLE = "CREATE TABLE " + TABLE_PARTS + "(" + KEY_ID + " INTEGER PRIMARY KEY, " + KEY_NAME + " TEXT," + KEY_CATEGORY + " TEXT," + KEY_QUANTITY + " INTEGER" + ")"; public PartsDatabaseHandler(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } // Creating Tables @Override public void onCreate(SQLiteDatabase db) { db.execSQL(CREATE_PARTS_TABLE); }
After the database tables are created, following the structure outlined earlier in this post, the real work of the database handler classes begins. Each class contains a set of Create, Retreive, Update, Delete (CRUD) operation methods. It is these CRUD methods that allow the user-facing activities in the app to interact with the database. Some of these methods are very simple, for example, the parts database handler class contains a method to add a new part to the parts table.
// Adding new part Boolean addPart (Part part) { SQLiteDatabase db = this.getWritableDatabase(); ContentValues values = new ContentValues(); values.put(KEY_NAME, part.getName()); // Part name values.put(KEY_CATEGORY, part.getCategory()); // Part category values.put(KEY_QUANTITY, part.getQuantity()); // Part quantity // Inserting Row db.insert(TABLE_PARTS, null, values); db.close(); // Closing database connection return true; }
There are methods for deleting a part, adding a category, updating a part or category, and retrieving parts and categories. These are straightforward CRUD methods used throughout the Stash app. There are also a few higher-level methods used for convenience. For example, the parts database handler class has a method for getting a list of parts in a given category. It would certainly be possible to use the basic CRUD methods to obtain this list, but it would require additional logic in the user interface activity. The higher-level CRUD methods are included for simplicity.
// Getting a list of all parts in a particular category public List<Part> getCategoryParts(String categoryFilter) { List<Part> partList = new ArrayList<Part>(); // Select All Query String selectQuery = "SELECT * FROM " + TABLE_PARTS; SQLiteDatabase db = this.getWritableDatabase(); Cursor cursor = db.rawQuery(selectQuery, null); // looping through all rows and adding to list if the category equals the search category if (cursor.moveToFirst()) { while (!cursor.isAfterLast()) { if(cursor.getString(cursor.getColumnIndex(KEY_CATEGORY)).equals(categoryFilter)) { int id = Integer.parseInt(cursor.getString(cursor.getColumnIndex(KEY_ID))); String name = cursor.getString(cursor.getColumnIndex(KEY_NAME)); String category = cursor.getString(cursor.getColumnIndex(KEY_CATEGORY)); int quantity = Integer.parseInt(cursor.getString(cursor.getColumnIndex(KEY_QUANTITY))); partList.add(new Part(id, name, category, quantity)); } cursor.moveToNext(); } } // return contact list cursor.close(); return partList; }
The Stacks application uses database handler functions for both the database containing the users parts collection and the database containing information about the part categories.
The Main Activity
So, two database handler classes and two databases run in the background of the Stacks application. Thanks to the database handler classes, the user-facing activities are relatively simple. The first of the app’s activities is the category view presented to the user when the application initially launches.
This activity uses fragments to create a flexible layout, a mechanism used by many of the activities in the Stacks application. The activity first checks how many categories exist. Depending upon how many categories are in the users’s collection, the activity will display different fragments. The activity can load special fragments for collections with zero or one categories, but these fragments are likely to be used only when a user runs the Stacks app for the first time, or is just beginning to use the app. Most of the time, the category will display a fragment that builds rows of category images.
// If there are multiple categories, and the total number of categories is divisible by two else if(categories.size() > 1){ // Loop through categories, adding a button for each // Loop through categories and create rows of buttons for(int y = 0; y < categories.size() - 1; y=y+2) { // Get the category images String image1Name = category_db.getImage(categories.get(y)); String image2Name = category_db.getImage(categories.get(y+1)); FragmentManager fragmentManager = getSupportFragmentManager(); FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); CategoryRowsFragment fragment = new CategoryRowsFragment() .newInstance(categories.get(y), image1Name, categories.get(y+1), image2Name); fragmentTransaction.add(R.id.fragmentContainer, fragment); fragmentTransaction.commit(); } // If there are an odd number of categories, add the last category to a new row if(categories.size() % 2 == 1) { // Get the category image String oddImageName = category_db.getImage(categories.get(categories.size() - 1)); FragmentManager fragmentManager = getSupportFragmentManager(); FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); oddCategoryFragment fragment = new oddCategoryFragment() .newInstance(categories.get(categories.size() - 1), oddImageName); fragmentTransaction.add(R.id.fragmentContainer, fragment); fragmentTransaction.commit(); } }
The fragment itself consists of two image buttons in a horizontal row. As shown in the code above, the main activity will load multiple instances of the fragment depending upon how many categories exist in the user’s collection. The fragment itself takes two category names and two category names as parameters and uses this information to populate the image buttons.
@Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); // Apply the category name and image to the buttons Button category1Btn = (Button) getView().findViewById(R.id.category1Btn); category1Btn.setText(name1); // Get the image for the category Resources resources = this.getResources(); final int resourceId1 = resources.getIdentifier(image1, "drawable", PACKAGE_NAME); Drawable top1 = getResources().getDrawable(resourceId1); category1Btn.setCompoundDrawablesWithIntrinsicBounds(null, top1 , null, null); // Create onClick methods for the buttons // When a category button is pressed, start the CategoryActivity and pass the category clicked category1Btn.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { // User clicks the "Add part" button, go to AddPartActivity Intent categoryIntent = new Intent(getActivity(), CategoryActivity.class); categoryIntent.putExtra("categoryName", name1); startActivity(categoryIntent); } }); Button category2Btn = (Button) getView().findViewById(R.id.category2Btn); category2Btn.setText(name2); // Get the image for the category final int resourceId2 = resources.getIdentifier(image2, "drawable", PACKAGE_NAME); Drawable top2 = getResources().getDrawable(resourceId2); category2Btn.setCompoundDrawablesWithIntrinsicBounds(null, top2 , null, null); // Create onClick methods for the buttons // When a category button is pressed, start the CategoryActivity and pass the category clicked category2Btn.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { // User clicks the "Add part" button, go to AddPartActivity Intent categoryIntent = new Intent(getActivity(), CategoryActivity.class); categoryIntent.putExtra("categoryName", name2); startActivity(categoryIntent); } }); }
Then, when a user clicks one of the category image button, the app starts an activity for displaying the parts in that category.
The Category Activity
When the user selects the name of a category from the main activity, the Stacks app will start an activity for displaying the parts in that category. First, the name of the category selected by the user is passed as an intent to the category activity. Second, the category activity uses a method in the parts database handler method to get a list of all the parts in the selected category. Third, to display the list of parts to the user, the activity uses a custom array adapter.
private void getPartsList(final String categoryName) { // Create instance of PartsDatabaseHandler final PartsDatabaseHandler db = new PartsDatabaseHandler(CategoryActivity.this); // Get a list of categories List<Part> partsList = db.getCategoryParts(categoryName); // Return the list of unique categories to be drawn on the screen listParts(partsList); } // After getting a list of parts in the current category from the database, display that // list of parts in the UI private void listParts(List<Part> partsList) { // Get the listView ListView partsListView = (ListView) findViewById(R.id.partsListView); // A custom ArrayAdapter will be used to display the Part objects in the listView // Create the adapter to convert the array to views PartsAdapter adapter = new PartsAdapter(this, partsList); // Attach the adapter to a ListView partsListView.setAdapter(adapter); }
The custom array adapter is used several times throughout the app. It is the same array adapter used by the search activity. The custom list item created by the adapter consists of a Constraint Layout which includes a Text View for the name of a part, another Text View for the quantity of the part, and two buttons, one for increasing the quantity of the part and one for decreasing the quantity.
The category activity loops through the list of parts in the category selected by the user and passes each part into the array adapter to populate a List View that is the only element used in the layout file for the category activity.
Adding Parts
Of course, the Stacks application would do nothing if it did not have a way for users to enter parts into their collections. Because adding parts is an important function for the app, from any activity in the app, the activity for adding parts can be accessed from a plus icon in the Toolbar. The activity for adding parts consists of several Edit Text elements: one for the part name, one for the part category, and one for the initial part quantity. At the bottom of the activity is a button for saving the part, and the category, if it is a new one, to the databases using functions provided by the database handler classes.
Several actions occur when the user presses the save button. First, the activity checks to make sure a part with the same name is not already in the database. This check consists of simply calling a method from the parts database handler class that returns null if a result is not returned. Therefore, if the call does not return null, a part with the name entered already exists in the database so the app presents a dialog. The dialog gives the user the option to view the page for that part, or cancel adding the part.
Part existingPart = part_db.getPart(enteredName); if (existingPart != null) { // Display a dialog confirming that the user wishes to delete all parts AlertDialog.Builder builder = new AlertDialog.Builder(AddPartActivity.this); builder.setTitle(enteredName + " already exists"); builder.setMessage("Your collection already contains " + existingPart.getQuantity() + " " + enteredName + ". Would you like to see that part in your collection?"); builder.setPositiveButton("View Part", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { dialog.dismiss(); // View the Part page for the part with the name entered Intent partIntent = new Intent(AddPartActivity.this, PartActivity.class); partIntent.putExtra("partName", enteredName); startActivity(partIntent); } }); builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { dialog.dismiss(); } }); AlertDialog alert = builder.create(); alert.show(); }
After checking that the part entered does not already exist in the database, the next check is to see if the category exists. This is done similarly to checking the part, by simply using methods from the category database handler class. If the category exists, the activity simply proceeds to add the part to that category in the database. However, if the category does not exist, the user is presented with a dialog box asking if the category should be added or not.
else { if (category_db.getCategory(enteredCategory) == null) { // Display a dialog asking the user whether or not a new category should be created final AlertDialog.Builder builder = new AlertDialog.Builder(AddPartActivity.this); builder.setTitle(enteredCategory + " does not exist."); builder.setMessage("Your collection does not have a category called, '" + enteredCategory + ".' Would you like to create this category?"); builder.setPositiveButton("Yep", new DialogInterface.OnClickListener() { public void onClick(final DialogInterface dialog, int id) { dialog.dismiss(); [...]
If the user chooses to add the part to the database, the next prompt is to select an image for the category. It is this image that is used in the main activity.
// Display a dialog for selecting a category image final AlertDialog.Builder builder1 = new AlertDialog.Builder(AddPartActivity.this); builder1.setTitle("Choose a Category Image"); builder1.setItems(R.array.category_images, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int selection) { String[] categoryStrings = getResources().getStringArray(R.array.category_images); Part newPart = new Part(enteredName, enteredCategory, enteredQuantity); switch (selection) { case 0: category_db.addCategoryByName(enteredCategory, categoryStrings[0].toLowerCase()); part_db.addPart(newPart); Toast.makeText(AddPartActivity.this, enteredName + " added to your collection and " + categoryStrings[0] + " created.", Toast.LENGTH_SHORT) .show(); dialog.dismiss(); break; case 1: category_db.addCategoryByName(enteredCategory, categoryStrings[1].toLowerCase()); part_db.addPart(newPart); Toast.makeText(AddPartActivity.this, enteredName + " added to your collection and " + categoryStrings[1] + " created.", Toast.LENGTH_SHORT) .show(); dialog.dismiss(); break; case 2: category_db.addCategoryByName(enteredCategory, categoryStrings[2].toLowerCase()); part_db.addPart(newPart); Toast.makeText(AddPartActivity.this, enteredName + " added to your collection and " + categoryStrings[2] + " created.", Toast.LENGTH_SHORT) .show(); dialog.dismiss(); break; [...]
Finally, when the part and/or category is added to the database, the action is confirmed with a Toast message.
0 Comments