Pre-requisites:
- Android App Development Fundamentals for Beginners
- Guide to Install and Set up Android Studio
- How to Create/Start a New Project in Android Studio?
- Running your first Android app
- RecyclerView in Android with Example
- Shared Preferences in Android with Example
A simple expense calculator lets you add income and expenditures in a simplified manner. This is a glimpse of the application we are going to build. The application contains a single Activity with a RecyclerView, two EditTexts (one to enter amount and the other to enter a short note of the transaction), 1 clickable TextView to specify loss or gain, 1 clickable image to add the transaction to RecyclerView, and finally a custom ActionBar to show the balance. It includes Shared Preferences to store the data locally. A sample video is given below to get an idea about what we are going to do in this article. Note that we are going to implement this project using the Java language.
Step by Step Implementation
Step 1: Create a New Project
To create a new project in Android Studio please refer to How to Create/Start a New Project in Android Studio. Note that select Java as the programming language.
Step 2:
Before moving to the coding section let’s add the necessary dependencies. The only dependency we have to add for the project is for Gson. It is a Java library that can be used to convert Java Objects into their JSON representation. It can also be used to convert a JSON string to an equivalent Java object. Go to app-level build.gradle file and add the following dependency and click on sync now.
implementation 'com.google.code.gson:gson:2.8.6'
Here is a reference,
Step 3:
Let’s add the necessary vector assets and drawable resource files. Go to app > res > drawable and add the following xml files.
ic_delete.xml (Delete Icon)
android:width = "24dp"
android:height = "24dp"
android:viewportWidth = "24"
android:viewportHeight = "24"
android:tint = "?attr/colorControlNormal" >
< path
android:fillColor = "@android:color/white"
android:pathData = "M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z" />
</ vector >
|
Preview:
ic_send.xml (Send Icon)
android:width = "24dp"
android:height = "24dp"
android:viewportWidth = "24"
android:viewportHeight = "24"
android:tint = "?attr/colorPrimarySurface"
android:autoMirrored = "true" >
< path
android:fillColor = "@color/white"
android:pathData = "M2.01,21L23,12 2.01,3 2,10l15,2 -15,2z" />
</ vector >
|
Preview:
ic_balance.xml (Wallet Icon)
< vector android:height = "24dp" android:tint = "#FFFFFF"
android:viewportHeight = "24" android:viewportWidth = "24"
< path android:fillColor = "@android:color/white" android:pathData = "M21,18v1c0,1.1 -0.9,2 -2,2L5,21c-1.11,0 -2,-0.9 -2,-2L3,5c0,-1.1 0.89,-2 2,-2h14c1.1,0 2,0.9 2,2v1h-9c-1.11,0 -2,0.9 -2,2v8c0,1.1 0.89,2 2,2h9zM12,16h10L22,8L12,8v8zM16,13.5c-0.83,0 -1.5,-0.67 -1.5,-1.5s0.67,-1.5 1.5,-1.5 1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5z" />
</ vector >
|
Preview:
etbg.xml (Selector for Edit Text)
<? xml version = "1.0" encoding = "utf-8" ?>
< item android:state_focused = "false" >
< shape android:shape = "rectangle" >
< corners android:radius = "3dp" />
< stroke android:color = "#DDD" android:width = "2dp" />
</ shape >
</ item >
< item android:state_focused = "true" >
< shape android:shape = "rectangle" >
< corners android:radius = "3dp" />
< stroke android:color = "@color/purple_500" android:width = "2dp" />
</ shape >
</ item >
</ selector >
|
Here is a screenshot for reference.
Step 4:
Now let’s add layout resource files for the custom ActionBar and RecyclerView row layout. Go to app > res > layout and add the following xml files. Below is the code for the custom_action_bar.xml file.
<? xml version = "1.0" encoding = "utf-8" ?>
< LinearLayout android:layout_width = "match_parent"
android:layout_height = "wrap_content"
android:background = "@color/purple_500"
android:gravity = "center_vertical"
android:paddingStart = "5dp"
android:paddingTop = "10dp"
android:paddingEnd = "5dp"
android:paddingBottom = "10dp" >
< TextView
android:id = "@+id/textView"
android:layout_width = "wrap_content"
android:layout_height = "match_parent"
android:layout_marginEnd = "10dp"
android:text = "Expense Calculator"
android:textColor = "@color/white"
android:textSize = "20sp"
android:textStyle = "bold" />
< ImageView
android:id = "@+id/imageView"
android:layout_width = "wrap_content"
android:layout_height = "match_parent"
android:layout_marginStart = "10dp"
android:layout_marginEnd = "10dp"
app:srcCompat = "@drawable/ic_balance" />
< TextView
android:id = "@+id/tvBalance"
android:layout_width = "wrap_content"
android:layout_height = "match_parent"
android:gravity = "start"
android:text = "0.00"
android:textColor = "@color/white"
android:textSize = "20sp" />
</ LinearLayout >
|
Preview:
Below is the code for the transaction_row_layout.xml file. (RecyclerView Row Layout)
<? xml version = "1.0" encoding = "utf-8" ?>
< androidx.cardview.widget.CardView android:layout_width = "match_parent"
android:layout_height = "wrap_content"
app:cardBackgroundColor = "#000000"
app:cardUseCompatPadding = "true" >
< LinearLayout
android:layout_width = "match_parent"
android:layout_height = "match_parent"
android:background = "@color/white"
android:orientation = "horizontal"
android:padding = "5dp" >
< LinearLayout
android:layout_width = "0dp"
android:layout_height = "wrap_content"
android:layout_weight = "6"
android:orientation = "vertical" >
< TextView
android:id = "@+id/tvAmount"
android:layout_width = "match_parent"
android:layout_height = "wrap_content"
android:text = "Amount"
android:textColor = "@color/black"
android:textSize = "24sp"
android:textStyle = "bold" />
< TextView
android:id = "@+id/tvMessage"
android:layout_width = "match_parent"
android:layout_height = "wrap_content"
android:text = "Message"
android:textColor = "@color/black" />
</ LinearLayout >
< ImageView
android:id = "@+id/ivDelete"
android:layout_width = "0dp"
android:layout_height = "32dp"
android:layout_gravity = "center"
android:layout_weight = "1"
android:clickable = "true"
app:srcCompat = "@drawable/ic_delete"
tools:ignore = "SpeakableTextPresentCheck,TouchTargetSizeCheck" />
</ LinearLayout >
</ androidx.cardview.widget.CardView >
|
Preview:
Here is a screenshot for reference.
Step 5:
We have added the necessary resource files for the application we are building. Now, Let’s design the UI for our application. Add this xml file to app > res > layout. Below is the code for the activity_main.xml file.
<? xml version = "1.0" encoding = "utf-8" ?>
< androidx.coordinatorlayout.widget.CoordinatorLayout android:layout_width = "match_parent"
android:layout_height = "match_parent"
tools:context = ".MainActivity" >
< androidx.recyclerview.widget.RecyclerView
android:id = "@+id/rvTransactions"
android:layout_width = "match_parent"
android:layout_height = "wrap_content"
app:layout_dodgeInsetEdges = "bottom" >
</ androidx.recyclerview.widget.RecyclerView >
< androidx.cardview.widget.CardView
android:layout_width = "match_parent"
android:layout_height = "wrap_content"
android:layout_gravity = "bottom"
app:cardBackgroundColor = "#DDD"
app:cardPreventCornerOverlap = "false"
app:cardUseCompatPadding = "false"
app:layout_dodgeInsetEdges = "bottom" >
< LinearLayout
android:layout_width = "match_parent"
android:layout_height = "match_parent"
android:layout_marginTop = "1dp"
android:background = "#FFFFFF"
android:orientation = "horizontal"
android:padding = "10dp" >
< TextView
android:id = "@+id/tvSign"
android:layout_width = "0dp"
android:layout_height = "match_parent"
android:layout_weight = "1"
android:clickable = "true"
android:gravity = "center"
android:text = "+₹"
android:textColor = "#00c853"
android:textSize = "24sp"
tools:ignore = "TouchTargetSizeCheck" />
< EditText
android:id = "@+id/etAmount"
android:layout_width = "0dp"
android:layout_height = "match_parent"
android:layout_marginStart = "2dp"
android:layout_marginEnd = "2dp"
android:layout_weight = "3"
android:background = "@drawable/etbg"
android:ems = "10"
android:hint = "Amount"
android:inputType = "number"
android:maxLength = "7"
android:padding = "5dp"
tools:ignore = "SpeakableTextPresentCheck,TouchTargetSizeCheck" />
< EditText
android:id = "@+id/etMessage"
android:layout_width = "0dp"
android:layout_height = "match_parent"
android:layout_marginStart = "2dp"
android:layout_marginEnd = "2dp"
android:layout_weight = "7"
android:background = "@drawable/etbg"
android:ems = "10"
android:hint = "Message"
android:inputType = "textPersonName"
android:maxLength = "50"
android:maxLines = "1"
android:padding = "5dp"
tools:ignore = "SpeakableTextPresentCheck,TouchTargetSizeCheck" />
< ImageView
android:id = "@+id/ivSend"
android:layout_width = "0dp"
android:layout_height = "match_parent"
android:layout_weight = "1"
android:clickable = "true"
app:srcCompat = "@drawable/ic_send"
tools:ignore = "TouchTargetSizeCheck,SpeakableTextPresentCheck" />
</ LinearLayout >
</ androidx.cardview.widget.CardView >
< TextView
android:id = "@+id/tvEmpty"
android:layout_width = "match_parent"
android:layout_height = "wrap_content"
android:layout_marginStart = "10dp"
android:layout_marginTop = "10dp"
android:layout_marginEnd = "10dp"
android:gravity = "center"
android:text = "No transactions found!"
android:textColor = "@color/black"
android:textSize = "20sp" />
</ androidx.coordinatorlayout.widget.CoordinatorLayout >
|
Preview:
Step 6:
We have added every resource file required for this application. Now let’s jump into the coding part. Let’s first create the Model Class for each transaction to be updated in the RecyclerView. It should contain four members as follows.
- Amount (of type Integer)
- Message (a short note about the Transaction)
- Positive (a boolean variable to check if the amount is received ( Positive = true ) or spent (Positive = False)
Here is the Java code for the Model Class along with the corresponding constructor, getters, and setters. Below is the code for the TransactionClass.java file. (Model Class)
package com.cs.expensecalculator;
public class TransactionClass {
private int amount;
private String message;
private boolean positive;
public TransactionClass( int amount, String message, boolean positive) {
this .amount = amount;
this .message = message;
this .positive = positive;
}
public int getAmount() {
return amount;
}
public void setAmount( int amount) {
this .amount = amount;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this .message = message;
}
public boolean isPositive() {
return positive;
}
public void setPositive( boolean positive) {
this .positive = positive;
}
} |
Step 7:
Now it’s time to create an Adapter for this Model Class. Let’s create a Class named TransactionAdapter that extends RecyclerView Adapter, and implement the necessary methods. If you’re new to RecyclerView, check out this article before proceeding. Below is the code for the TransactionAdapter.java file. Comments are added inside the code to understand the code in more detail.
package com.cs.expensecalculator;
import static com.cs.expensecalculator.MainActivity.calculateBalance;
import static com.cs.expensecalculator.MainActivity.checkIfEmpty;
import static com.cs.expensecalculator.MainActivity.setBalance;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Color;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.TViewHolder> {
Context ctx;
// List containing data for recyclerview
ArrayList<TransactionClass> transactionList;
// Constructor for TransactionAdapter
public TransactionAdapter(Context ctx, ArrayList<TransactionClass> transactionList) {
this .ctx = ctx;
this .transactionList = transactionList;
}
// On Create View Holder to Inflate transaction row layout
@NonNull
@Override
public TransactionAdapter.TViewHolder onCreateViewHolder( @NonNull ViewGroup parent, int viewType) {
View v = LayoutInflater.from(ctx).inflate(R.layout.transaction_row_layout,parent, false );
return new TransactionAdapter.TViewHolder(v);
}
@Override
public void onBindViewHolder( @NonNull TransactionAdapter.TViewHolder holder, int position) {
// Setting Message to a TextView in Row Layout
holder.tvMessage.setText(transactionList.get(holder.getAdapterPosition()).getMessage());
// If the transaction is Positive (Received Money) set Text Color to Green
if (transactionList.get(holder.getAdapterPosition()).isPositive())
{
holder.tvAmount.setTextColor(Color.parseColor( "#00c853" ));
// Setting Amount to a TextView in the row layout
holder.tvAmount.setText( "+₹" +Integer.toString(transactionList.get(holder.getAdapterPosition()).getAmount()));
}
// If the transaction is Negative (Spent Money) set Text Color to Red
else {
holder.tvAmount.setTextColor(Color.parseColor( "#F44336" ));
// Setting Amount to a TextView in the row layout
holder.tvAmount.setText( "-₹" +Integer.toString(transactionList.get(holder.getAdapterPosition()).getAmount()));
}
// On Click Listener for Delete Icon
holder.ivDelete.setOnClickListener( new View.OnClickListener() {
@Override
public void onClick(View view) {
// Confirmation Alert to delete a Transaction
AlertDialog dialog = new AlertDialog.Builder(ctx)
.setCancelable( false )
.setTitle( "Are you sure? The transaction will be deleted." )
.setNegativeButton( "Cancel" , new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
dialogInterface.dismiss();
}
})
.setPositiveButton( "Ok" , new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
transactionList.remove(holder.getAdapterPosition());
dialogInterface.dismiss();
notifyDataSetChanged();
checkIfEmpty(getItemCount());
setBalance(transactionList);
}
})
.create();
dialog.show();
}
});
}
// To get size of the list
@Override
public int getItemCount() {
return transactionList.size();
}
// View Holder for a Transaction
public static class TViewHolder extends RecyclerView.ViewHolder{
TextView tvAmount,tvMessage;
ImageView ivDelete;
public TViewHolder( @NonNull View itemView) {
super (itemView);
tvAmount = itemView.findViewById(R.id.tvAmount);
tvMessage = itemView.findViewById(R.id.tvMessage);
ivDelete = itemView.findViewById(R.id.ivDelete);
}
}
} |
Step 8:
We have defined the Model Class and the associated Adapter Class for Recycler View. Now it’s time to initialize everything in MainActivity, and implement functions to set custom action bar, set balance, and store and retrieve data locally using shared preferences in activity’s onStop() and onCreate() methods respectively. Here is the complete code for MainActivity. Below is the code for the MainActivity.java file. Comments are added inside the code to understand the code in more detail.
package com.cs.expensecalculator;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.os.Bundle;
import android.provider.CalendarContract;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.ArrayList;
public class MainActivity extends AppCompatActivity {
TextView tvSign;
public static TextView tvEmpty, tvBalance;
EditText etAmount, etMessage;
ImageView ivSend;
boolean positive = true ;
RecyclerView rvTransactions;
TransactionAdapter adapter;
ArrayList<TransactionClass> transactionList;
// On create method
@Override
protected void onCreate(Bundle savedInstanceState) {
super .onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Function to initialize views
initViews();
// Function to load data from shared preferences
loadData();
// Function to set custom action bar
setCustomActionBar();
// To check if there is no transaction
checkIfEmpty(transactionList.size());
// Initializing recycler view
rvTransactions.setHasFixedSize( true );
rvTransactions.setLayoutManager( new LinearLayoutManager( this ));
adapter = new TransactionAdapter( this ,transactionList);
rvTransactions.setAdapter(adapter);
// On click sign change
tvSign.setOnClickListener( new View.OnClickListener() {
@Override
public void onClick(View view) {
changeSign();
}
});
// On click Send
ivSend.setOnClickListener( new View.OnClickListener() {
@Override
public void onClick(View view) {
// Input Validation
if (etAmount.getText().toString().trim().isEmpty())
{
etAmount.setError( "Enter Amount!" );
return ;
}
if (etMessage.getText().toString().isEmpty())
{
etMessage.setError( "Enter a message!" );
return ;
}
try {
int amt = Integer.parseInt(etAmount.getText().toString().trim());
// Adding Transaction to recycler View
sendTransaction(amt,etMessage.getText().toString().trim(),positive);
checkIfEmpty(transactionList.size());
// To update Balance
setBalance(transactionList);
etAmount.setText( "" );
etMessage.setText( "" );
}
catch (Exception e){
etAmount.setError( "Amount should be integer greater than zero!" );
}
}
});
}
// To set custom action bar
private void setCustomActionBar() {
this .getSupportActionBar().setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
getSupportActionBar().setDisplayShowCustomEnabled( true );
View v = LayoutInflater.from( this ).inflate(R.layout.custom_action_bar, null );
// TextView to show Balance
tvBalance = v.findViewById(R.id.tvBalance);
// Setting balance
setBalance(transactionList);
getSupportActionBar().setCustomView(v);
getSupportActionBar().setElevation( 0 );
}
// To set Balance along with sign (spent(-) or received(+))
public static void setBalance(ArrayList<TransactionClass> transactionList){
int bal = calculateBalance(transactionList);
if (bal< 0 )
{
tvBalance.setText( "- ₹" +calculateBalance(transactionList)*- 1 );
}
else {
tvBalance.setText( "+ ₹" +calculateBalance(transactionList));
}
}
// To load data from shared preference
private void loadData() {
SharedPreferences pref = getSharedPreferences( "com.cs.ec" ,MODE_PRIVATE);
Gson gson = new Gson();
String json = pref.getString( "transactions" , null );
Type type = new TypeToken<ArrayList<TransactionClass>>(){}.getType();
if (json!= null )
{
transactionList=gson.fromJson(json,type);
}
}
// To add transaction
private void sendTransaction( int amt,String msg, boolean positive) {
transactionList.add( new TransactionClass(amt,msg,positive));
adapter.notifyDataSetChanged();
rvTransactions.smoothScrollToPosition(transactionList.size()- 1 );
}
// Function to change sign
private void changeSign() {
if (positive)
{
tvSign.setText( "-₹" );
tvSign.setTextColor(Color.parseColor( "#F44336" ));
positive = false ;
}
else {
tvSign.setText( "+₹" );
tvSign.setTextColor(Color.parseColor( "#00c853" ));
positive = true ;
}
}
// To check if transaction list is empty
public static void checkIfEmpty( int size) {
if (size == 0 )
{
MainActivity.tvEmpty.setVisibility(View.VISIBLE);
}
else {
MainActivity.tvEmpty.setVisibility(View.GONE);
}
}
// To Calculate Balance by iterating through all transactions
public static int calculateBalance(ArrayList<TransactionClass> transactionList)
{
int bal = 0 ;
for (TransactionClass transaction : transactionList)
{
if (transaction.isPositive())
{
bal+=transaction.getAmount();
}
else {
bal-=transaction.getAmount();
}
}
return bal;
}
// Initializing Views
private void initViews() {
transactionList = new ArrayList<TransactionClass>();
tvSign = findViewById(R.id.tvSign);
rvTransactions = findViewById(R.id.rvTransactions);
etAmount = findViewById(R.id.etAmount);
etMessage = findViewById(R.id.etMessage);
ivSend = findViewById(R.id.ivSend);
tvEmpty = findViewById(R.id.tvEmpty);
}
// Storing data locally
// using shared preferences
// in onStop() method
@Override
protected void onStop() {
super .onStop();
SharedPreferences.Editor editor = getSharedPreferences( "com.cs.ec" ,MODE_PRIVATE).edit();
Gson gson = new Gson();
String json = gson.toJson(transactionList);
editor.putString( "transactions" ,json);
editor.apply();
}
} |
That’s it. Now we can run the application. Make sure that your project contains all the following files.
Here is the preview of the final application.
Output: