Android Health App : Handling User Input

android_imirire_user_input_events_menu_displayedThe last turorial in this series left us with a pretty user interface that is dumb in one way : it didn’t handle user input ,in other words we want the calorie total count to be constantly updated as users input grams in EditTexts for chosen foods.We are going to change that in this tutorial.We will also look at how ridiculously easy it is to add menus to your apps so the user can perfrom other stuff on your app.Let’s get our hands dirty making our app better.

Handling Click Events on List Item Views.

A friendly note first, if you are just jumping right in ,the source code for the last tutorial can be retrieved from the tutorial series git repository.We build on top of that here.On our list items ,we need to handle three events :(1) when the user clicks on the check box changing its state (2) when the text inside the gram input EditText changes and (3) when the user clicks on the gram inpt EditText .

Open up your MainActivity.java file and have look at the constructor of FoodHolder.

public FoodHolder(View itemView)
{
    super(itemView);
    foodItemCheckBox = (CheckBox) itemView.findViewById(R.id.list_item_food_checkbox);
    foodNameTextView = (TextView) itemView.findViewById(R.id.list_item_food_name);
    gramInputField = (EditText) itemView.findViewById(R.id.list_item_gram_input_field);
}

The function simply gets references to the views inside the list item.Add a CheckedChangeListener to foodItemCheckBox as shown below.

public FoodHolder(View itemView)
{
    super(itemView);
    foodItemCheckBox = (CheckBox) itemView.findViewById(R.id.list_item_food_checkbox);
    foodItemCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
        @Override
        public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {

        }
    });

    foodNameTextView = (TextView) itemView.findViewById(R.id.list_item_food_name);
    gramInputField = (EditText) itemView.findViewById(R.id.list_item_gram_input_field);
}

Notice the onCheckedChanged() function?Its inside that function that you do stuff you want to do when the user clicks on the check box of one list item.It passes two arguments to you and right now we are interested in the second one.It tells you whether the checkbox is checked or not.What do we want to do when the user clicks on the checkbox?

(1) We want to save if the current food is checked or not

(2) We want to clear the number displayed in the gram input EditText .Wh? Well … when the user clicks on that EditText ,he/she usually wants to change the text in there.By clearing whatever was there ,we give the user a clean start.This came from the suggestions I got from a few people who are using Imirire.

(3) If the user deselects an item that was previously selected ,we want to clear the data in the gram input EditText and update the gramCount for the foodItem in the model.

(4)Finally ,we update the Calory count displat TextView on top of the application.This is done in a private function to MainActivity class that is not implemented yet.

Add the function at the bottom of MainActivity class.

package com.blikoon.imirire;

//IMPORT DIRECTIVES ARE HERE.

import java.util.List;

public class MainActivity extends Activity {

    private RecyclerView mFoodListRecyclerView;
    private FoodAdapter mAdapter;
    private TextView caloryDisplayTextView;


   ...........//CODE HERE OMITTED FOR CLARITY

    private void updateDisplay()
    {
        //Not implemented yet.
    }
}//MainActivity ends here.

Update the constructor of FoodHolder according to our design as shown below:

public FoodHolder(View itemView)
{
    super(itemView);
    foodItemCheckBox = (CheckBox) itemView.findViewById(R.id.list_item_food_checkbox);
    foodItemCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
        @Override
        public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {

            mFood.setIsChecked(isChecked);
            gramInputField.setText(String.valueOf(""));
            if (!isChecked) {
                //If the user unchecks a food
                // 1.clear its gram input field
                // 2.Set the food gram count to 0
                mFood.setGramCount(0.0);
                // mAdapter.notifyDataSetChanged();
                gramInputField.setText(String.valueOf(0.0));

            }
            updateDisplay();
        }
    });

    foodNameTextView = (TextView) itemView.findViewById(R.id.list_item_food_name);
    gramInputField = (EditText) itemView.findViewById(R.id.list_item_gram_input_field);
}

The checkbox is out of the way ,now we focus on The gram input EditText.We want to know when the text inside changes ,and when it does ,we want to retrieve the current value and use it to compute the total calorie count that should be displayed in the calorie display TextView on top.To be able to do that we need to add a TextChangeListener to gramInputField .Change FoodHolder constructor to add the TextChangeListener as shown below.

public FoodHolder(View itemView)
{
    super(itemView);
    foodItemCheckBox = (CheckBox) itemView.findViewById(R.id.list_item_food_checkbox);
    foodItemCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
        @Override
        public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {

            mFood.setIsChecked(isChecked);
            gramInputField.setText(String.valueOf(""));
            if (!isChecked) {
                //If the user unchecks a food
                // 1.clear its gram input field
                // 2.Set the food gram count to 0
                mFood.setGramCount(0.0);
                // mAdapter.notifyDataSetChanged();
                gramInputField.setText(String.valueOf(0.0));

            }
            updateDisplay();
        }
    });

    foodNameTextView = (TextView) itemView.findViewById(R.id.list_item_food_name);
    gramInputField = (EditText) itemView.findViewById(R.id.list_item_gram_input_field);
    gramInputField.addTextChangedListener(new TextWatcher() {
        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
            
        }

        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {

        }

        @Override
        public void afterTextChanged(Editable s) {

        }
    });
}

We add a TextWatcher to act as our TextChangeListener.The TextWatcher has to implement the shown three methods and which one you use depends on what you want to achieve.In our case we are interested in retrieving the latest value so afterTextChanged makes more sense to use.My updated afterTextChanged method implementation is shown below.

@Override
public void afterTextChanged(Editable s) {
    String dataString = s.toString();

    if(dataString.isEmpty())
    {
        Log.d("Imirire","Empty string");
        if(gramInputField.hasFocus())
        {
            Log.d("Imirire","gramInputField Has Focus");
            mFood.setGramCount(0.0);
            updateDisplay();

        }
    }else
    {
        double itemGramCount = Double.parseDouble(s.toString());
        mFood.setGramCount(itemGramCount);
        updateDisplay();

    }

}

We retrieve the value and store it into a string variable.If the value is an empty string ,we simply update the food gramCount to zero and update the total calorie count display.If its not empty we update the gram count with the value(converted to a double) an update the display.I splited this into an if else clause because parsing an empty string into a double caused the application to crash (it took me sometime to figure that out) .

The last event we want to handle is when the gramInputField EditText itself is clicked.We simply want to clear the value.The fully updated FoodHolder constructor is shown below.

public FoodHolder(View itemView)
{
    super(itemView);
    foodItemCheckBox = (CheckBox) itemView.findViewById(R.id.list_item_food_checkbox);
    foodItemCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
        @Override
        public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {

            mFood.setIsChecked(isChecked);
            gramInputField.setText(String.valueOf(""));
            if (!isChecked) {
                //If the user unchecks a food
                // 1.clear its gram input field
                // 2.Set the food gram count to 0
                mFood.setGramCount(0.0);
                // mAdapter.notifyDataSetChanged();
                gramInputField.setText(String.valueOf(0.0));

            }
            updateDisplay();
        }
    });

    foodNameTextView = (TextView) itemView.findViewById(R.id.list_item_food_name);
    gramInputField = (EditText) itemView.findViewById(R.id.list_item_gram_input_field);
    gramInputField.addTextChangedListener(new TextWatcher() {
        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {

        }

        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {

        }

        @Override
        public void afterTextChanged(Editable s) {
            String dataString = s.toString();

            if(dataString.isEmpty())
            {
                Log.d("Imirire","Empty string");
                if(gramInputField.hasFocus())
                {
                    Log.d("Imirire","gramInputField Has Focus");
                    mFood.setGramCount(0.0);
                    updateDisplay();

                }
            }else
            {

                double itemGramCount = Double.parseDouble(s.toString());
                mFood.setGramCount(itemGramCount);
                updateDisplay();

            }

        }
    });

    gramInputField.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            gramInputField.setText(String.valueOf(""));

        }
    });
}

The click listener simply updates the value of the text field to zero.Run the application and test if the events we just added work as expected.Click on the check box to see if the gramInputField gets cleared.Also ,click on a gramInputField with previous data to see if it gets cleared.My run session is shown below.

android_imirire_user_input_events_test

Our total calorie display isn’t updating as the user inputs data.We change this next.To do this we need some way to compute the total calorie count and pass it to the display.The best place to do that is inside our adapter class .We create a function called computeCaloryTotal() and make is public so it can be called from other places in our code.The function follows the silly algebra formula shown below.

android_imirire_user_input_events_compute_calory_total

Add the function inside your FoodAdapter class as shown below.

private class FoodAdapter extends RecyclerView.Adapter<FoodHolder>
{
    private List<Food> mFoods;

    public FoodAdapter (List<Food> foods)
    {
        mFoods = foods;
    }

    public FoodHolder onCreateViewHolder( ViewGroup parent,
                                          int viewType)
    {
        LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
        View view = layoutInflater
                .inflate(R.layout.list_item_food, parent,
                        false);
        return new FoodHolder(view);
    }

    public void onBindViewHolder(FoodHolder holder ,int position)
    {
        Food food = mFoods.get(position);
        holder.bindFood(food);
    }

    @Override
    public int getItemCount()
    {
        return mFoods.size();
    }

    public double computeCaloryTotal()
    {
        double calTotal=0;
        for(int i=0;i<mFoods.size();i++)
        {
            Food food = mFoods.get(i);
            if(food.isChecked()) {
                double internalCal = mFoods.get(i).getCalCount() * mFoods.get(i).getGramCount() / 100;
                calTotal += internalCal;
            }
        }
        return  calTotal;
    }

}

We go through the list of foods computing the calorie count and creating a sum of all the food calories.With this function in place ,now it can be used inside our updateDisplay() function so the calorie total is displayed live as the user changes input.Change your updateDisplay() function as shown below.

private void updateDisplay()
{
    double calTotal = mAdapter.computeCaloryTotal();
    //caloryDisplayTextView.setText(String.valueOf(calTotal));
    caloryDisplayTextView.setText(String.format("%.2f", calTotal));
    Log.d("Imirire", "Calling updateDisplay ,the total is :"+
            String.format("%.2f", calTotal));
}

We simply call computeCaloryTotal and format the returned value to be displayed with two decimal points in the calory total display TextView.Run your application and try to check some foods and input gram counts.

android_imirire_user_input_events_calory_update_live

The display is being updated live and that’s great.You may have noticed that at app startup the displayed value is “777.5” ,what we put in the TextView to be shown for test purposes.Go back to your activity_main.xml layout file and change this to “0.00” so it makes more sense.

Adding Menus:AppCompatActivity

Android provides several types of menus .One of them that we will use is the option menu.The menu that gets displayed when the user clicks on the hardware button.We don’t have those title bar (or whatever you call it ) menus in Imirire because the app is designed to fully occupy the screen ,so we completely rely on the system menu button.You may have to check your specific hardware for what menu button you have.The hardware menu button for my Genymotion virtual is shown below for reference.

android_imirire_user_input_events_menu_button

To be able to work with menus with the least amount of work ,we need to change our MainActivity so it extends AppCompatActivity and not simply Activity.It is possible to work with menus from direct subclasses of Activity but we are taking the AppCompatActivity as it is the easiest for the moment.But AppCompatActivity introduces a problem we have to deal with.To see that change your MainActivity to subclass AppcompatActivity as shown below.

public class MainActivity extends AppCompatActivity {

If you run the application you see that a title bar that we don’t want is introduced.

android_imirire_user_input_events_title_bar

This title bar comes with AppCompatActivity .Some applications use it and yes it is very useful when you need it.But today we don’t want it and we are disabling it.To get rid of it ,simple call getSupportActionBar().hide in your onCreateFunction.

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    getSupportActionBar().hide();
    mFoodListRecyclerView = (RecyclerView)
            findViewById(R.id.food_list_recycler_view);
    mFoodListRecyclerView.addItemDecoration(
            new DividerItemDecoration(getBaseContext(), DividerItemDecoration.VERTICAL_LIST));
    mFoodListRecyclerView.setLayoutManager(new LinearLayoutManager(getBaseContext()));

    //Display textView
    caloryDisplayTextView = (TextView)findViewById(R.id.calorie_display);

    updateUi();
}

Run the app to test again and the title bar or the action bar as you can guess from the function that disables it ,is gone.This is what we want.There sure might be other ways and even better ways to disable this but this is what we are using today.We may update this later when new options arize.

Adding Menus.

With the AppCompatActivity business out of the way now we wory about menus.To work with option menus in Android Studio ,there are two steps you need to take.

(1) create a menu xml resource file

(2) inflate the menu in java code.

To create the menu xml file ,double click on your res folder and select New->Android resource file as shown in the figure below.

android_imirire_user_input_events_add_menu_resource

 

Choose the resource type to be menu ,chose a name and hit ok.A menu folder is created for you and inside it ,there is a file named option_menu.xml file.Open the file and change its contents to be as shown below.

option_menu.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto"
      xmlns:tools="http://schemas.android.com/tools"
    >

    <item
        android:id="@+id/clear_data"
        android:title="Clear Data"
        android:visible="true"/>
    <item
        android:id="@+id/add_food"
        android:title="Add Food"
        android:visible="true"/>
    <item
        android:id="@+id/settings"
        android:title="Settings"
        android:visible="true"/>

</menu>

We want for three menu options to be shown when the user clicks the hardware(or virtual) menu button.One to clear the food data ,resulting in all the foods being cleared ,one to add a new food( to be covered later) and one for settings.The option_menu.xml file has one menu tag and inside it three items with the names reflecting the menus we want.If this doesn’t make sense you might find it useful to check the android documentation on how menus work.

With the option_menu.xml menu file created ,we need to go to MainActivity.java and use that menu file.To work with option menus inside an Activity ,there are two essential functions you need to implement: onCreateOptionsMenu() which is called when the user clicks on the hardware menu button ,it creates the menu and onOptionsItemSelected() which is called when one menu item is clicked.Go ahead and override these functions in your MainActivity class.

MainActivity.java

package com.blikoon.imirire;

//IMPORTS OMITTED HERE

import java.util.List;

public class MainActivity extends AppCompatActivity {

    private RecyclerView mFoodListRecyclerView;
    private FoodAdapter mAdapter;
    private TextView caloryDisplayTextView;

...........//IRRELEVANT CODE OMMITED HERE.
   
    @Override
    public boolean onCreateOptionsMenu(Menu menu)
    {
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.option_menu, menu);
        return  true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle item selection
        switch (item.getItemId()) {
            case R.id.clear_data:
                clearData();
                return true;
            case R.id.add_food:
                addFood();
                return true;
            default:
                return super.onOptionsItemSelected(item);
        }
    }

    private void clearData()
    {
      //Not implemented yet.  
    }

    private void addFood()
    {
        //Not implemented yet
    }

    private void updateDisplay()
    {
        //Not implemented yet.
        double calTotal = mAdapter.computeCaloryTotal();
        //caloryDisplayTextView.setText(String.valueOf(calTotal));
        caloryDisplayTextView.setText(String.format("%.2f", calTotal));
        Log.d("Imirire", "Calling updateDisplay ,the total is :"+
                String.format("%.2f", calTotal));
    }
}

onCreateOptionsMenu() simple takes the menu xml file we created previously and turns it into a Menu object that is directly displayed on screen.It returns true to inform the system that it has done its job.

onOptionsItemSelected() is called when a user clicks on one menu item.In the argument it passes in a reference to the MenuItem object that was clicked.We get the id of the MenuItem and compare it to the IDs we have specified in the menu xml file to know which item was clicked.If it is “Clear Data” we call a function called clearData ,whose details are not implemented yet.If the user clicked on “Add Food” ,we call the addFood() function which is also not implemented yet.We dont’t handle the settings menu option for the moment.If you run your application and click on the hardware menu button ,you can see your menu displayed on screen as shown below.

android_imirire_user_input_events_menu_displayed

The menus are being displayed nice and clean.All that remains in implementing the functions that get called when menu items are clicked.We will implement clearData() only in this tutorial as the others would take us out of the scope of this tutorial.

Clearing data is really going through the list of foods ,unchecking each and setting its gram count to zero ,we add a function that does just that in our FoodAdapter class just below computeCaloryTotal() as follows.

public void clearSelectedFoods()
{
    for(int i=0;i<mFoods.size();i++)
    {
        Food food = mFoods.get(i);
        if(food.isChecked())
        {
            // set its gram count to 0
            // set the food as unchecked
            food.setGramCount(0);
            food.setIsChecked(false);

        }
    }
}

and inside the clearData() function : (1) call clearSelectedFoods on the adapter instance (2) inform the recyclerView that something has changed and (3) update the diaplayed calorie total count as shown below.

private void clearData()
{
    mAdapter.clearSelectedFoods();
    mAdapter.notifyDataSetChanged();
    updateDisplay();

}

Run your application ,select and edit the calorie count for a bunch of foods ,click on the hardware menu button and select “Clear Data”.Your user interface should be updated accordingly.If yours is not working as it should ,now …you’ve got yourself some debugging to do.And if you keep running into trouble getting this to work don’t hesitate to drop me a line in the comments below.The next steps will be adding foods and designing what the settings would look like or even storing data in an SQLite database.The source code for this tutorial is available at my git repository.I hope this ‘s been informative to you and I would like to thank you for reading.

Posted in android, Tutorials and tagged , , , .

Daniel Gakwaya loves computer Hardware/Software.He is a Software Engineer at BLIKOON and lead developer of bliboard-The whiteboard system currently marketed by the company.He is known to hack around on any piece of tech that happens to pick his interest. More on his tech endeavors here
Follow him on Twitter

One Comment

  1. hi Daniel Gakwaya

    i like your app i need your help i want to see edittext value array and food.getFoodName to show another activity when i select check box.when i click a button to show me how i do that please help me answer
    eg in another activity when i checked button click to show me 2 rice
    9 peanuts
    10 beans
    total 90.94

Comments are closed.