This guide outlines the implementation of a mobile fare calculator and ticketing frontend using native Android development. The application focuses on station selection, routing logic, and dynamic price computation without requiring external data base dependencies.
Project Configuration
Create a fresh Android project targeting a modern SDK level. Since the application operates entirely with in-memory data structures, no Gradle dependencies or JDBC configurations are required. The architecture consists of a single Activity paired with a vertical scrolling layout to accommodate form inputs and transaction outputs.
User Interface Layout
The XML layout defines the visual structure. It utilizes a ScrollView for responsive behavior across different screen sizes, housing two Spinner components for origin and destination selection, a numeric EditText for passenger count, a trigger button, and a TextView for rendering calculation results.
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".TransitTicketingActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Departure Station"
android:textSize="16sp"
android:textStyle="bold"/>
<Spinner
android:id="@+id/origin_station_spinner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Arrival Station"
android:textSize="16sp"
android:textStyle="bold"/>
<Spinner
android:id="@+id/destination_station_spinner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Number of Tickets"
android:textSize="16sp"
android:textStyle="bold"/>
<EditText
android:id="@+id/ticket_amount_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number"
android:hint="Enter quantity"
android:layout_marginBottom="16dp"/>
<Button
android:id="@+id/process_purchase_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Calculate & Issue Ticket"
android:onClick="processPurchase"/>
<TextView
android:id="@+id/transaction_status_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="15sp"
android:paddingTop="16dp"
android:minHeight="60dp"/>
</LinearLayout>
</ScrollView>
Core Implementation Logic
The backend logic resides within the primary Activity class. Key improvements over standard boilerplate include decoupled data initialization, explicit validation routines, and a refined pricing algorithm that handles both intra-line and inter-line routing scenarios.
package com.example.transitfare;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.HashSet;
public class TransitTicketingActivity extends AppCompatActivity {
private Spinner originSpinner;
private Spinner destinationSpinner;
private EditText quantityInput;
private TextView statusDisplay;
private final Map<String, List<String>> networkRoutes = new LinkedHashMap<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_transit_ticketing);
loadRouteData();
initializeUIBindings();
registerSelectionListeners();
}
private void loadRouteData() {
List<String> lineAlpha = List.of(
"Xiwang", "Shiguangjie", "Changchengqiao", "Heping Yiyuan", "Lie Shi Yuan",
"Xinbai Guangchang", "Jiefang Guangchang", "Ping'an Dajie", "Beiguo Shangcheng",
"Bowuguan", "Ti Chang", "Beisong", "Tangu", "Chaohuiqiao", "Baifo", "Liucun", "Huojue Guangchang"
);
List<String> lineBeta = List.of(
"Liuxinzhuang", "Jiahualu", "Gaozhu", "Xisanzhuang", "Bolinzhuang", "Shizhuang",
"Shi'erzhong", "Beiguo Shangcheng", "Changan Gongyuan", "Jianheqiao", "Yitang",
"Tiedao Daxue", "Liu Zhen", "Fuzhou", "Yuanboshiyuan", "Shangwu Zhongxin",
"Huizhan Zhongxin", "Dongzhuang", "Xizhuang", "Xiaohe Dadao"
);
List<String> lineGamma = List.of(
"Xisanzhuang", "Bolinzhuang", "Shi'erzhong", "Xinbai Guangchang", "Shijiazhuang Zhan",
"Huai'an Qiao", "Dongli", "Huai'an Zhonglu", "Yuhua Lu", "Nanwei",
"Cangfenglu Liucun", "Tatan", "Huitong Lu", "Suncun", "Tagong", "Dongwang",
"Nanwang", "Weitong", "Dong'er Huan Nan Lu", "XiYangling", "Zhongyangling",
"Nandou", "Taihang Nan Da Jie", "Lexiang"
);
networkRoutes.put("Line A", lineAlpha);
networkRoutes.put("Line B", lineBeta);
networkRoutes.put("Line C", lineGamma);
}
private void initializeUIBindings() {
originSpinner = findViewById(R.id.origin_station_spinner);
destinationSpinner = findViewById(R.id.destination_station_spinner);
quantityInput = findViewById(R.id.ticket_amount_input);
statusDisplay = findViewById(R.id.transaction_status_text);
populateStationLists();
}
private void populateStationLists() {
Set<String> uniqueStations = new HashSet<>();
for (List<String> stops : networkRoutes.values()) {
uniqueStations.addAll(stops);
}
List<String> sortedList = new ArrayList<>(uniqueStations);
ArrayAdapter<String> adapter = new ArrayAdapter<>(
this, android.R.layout.simple_spinner_item, sortedList
);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
originSpinner.setAdapter(adapter);
destinationSpinner.setAdapter(adapter);
}
private void registerSelectionListeners() {
originSpinner.setOnItemSelectedListener(createEmptyListener());
destinationSpinner.setOnItemSelectedListener(createEmptyListener());
}
private AdapterView.OnItemSelectedListener createEmptyListener() {
return new AdapterView.OnItemSelectedListener() {
@Override public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {}
@Override public void onNothingSelected(AdapterView<?> parent) {}
};
}
public void processPurchase(View view) {
String selectedOrigin = originSpinner.getSelectedItem().toString();
String selectedDest = destinationSpinner.getSelectedItem().toString();
String qtyRaw = quantityInput.getText().toString().trim();
evaluateTransaction(selectedOrigin, selectedDest, qtyRaw);
}
private void evaluateTransaction(String origin, String dest, String quantity) {
if (origin.equalsIgnoreCase(dest)) {
displayError("Origin and destination cannot match.");
return;
}
if (quantity.isEmpty()) {
displayError("Please specify a valid passenger count.");
return;
}
try {
int passengerCount = Integer.parseInt(quantity);
if (passengerCount <= 0) throw new IllegalArgumentException("Invalid count");
double baseRate = calculateBaseFare(origin, dest);
if (baseRate < 0) {
displayError("Routing unavailable. Check station validity.");
return;
}
double finalAmount = baseRate * passengerCount;
displaySuccess(baseRate, passengerCount, finalAmount);
} catch (IllegalArgumentException e) {
displayError("Quantity must be a positive integer.");
}
}
private double calculateBaseFare(String startLoc, String endLoc) {
String startRoute = locateRouteForStation(startLoc);
String endRoute = locateRouteForStation(endLoc);
if (startRoute == null || endRoute == null) return -1.0;
if (startRoute.equals(endRoute)) {
return computeSingleSegmentPrice(startRoute, startLoc, endLoc);
} else {
return computeMultiSegmentPrice(startRoute, startLoc, endRoute, endLoc);
}
}
private double computeSingleSegmentPrice(String routeName, String sStation, String eStation) {
List<String> sequence = networkRoutes.get(routeName);
int idxStart = sequence.indexOf(sStation);
int idxEnd = sequence.indexOf(eStation);
int travelUnits = Math.abs(idxStart - idxEnd);
return Math.ceil((double) travelUnits / 3.0);
}
private double computeMultiSegmentPrice(String r1, String s1, String r2, String s2) {
List<String> connectionPoints = identifyTransferNodes(r1, r2);
if (connectionPoints.isEmpty()) return -1.0;
double minAggregateCost = Double.MAX_VALUE;
for (String nexus : connectionPoints) {
double costLegOne = computeSingleSegmentPrice(r1, s1, nexus);
double costLegTwo = computeSingleSegmentPrice(r2, nexus, s2);
double segmentSum = costLegOne + costLegTwo;
if (segmentSum > 0 && segmentSum < minAggregateCost) {
minAggregateCost = segmentSum;
}
}
return minAggregateCost;
}
private List<String> identifyTransferNodes(String routeX, String routeY) {
List<String> intersections = new ArrayList<>();
List<String> nodesX = networkRoutes.get(routeX);
List<String> nodesY = networkRoutes.get(routeY);
for (String stop : nodesX) {
if (nodesY.contains(stop)) intersections.add(stop);
}
return intersections;
}
private String locateRouteForStation(String target) {
for (Map.Entry<String, List<String>> entry : networkRoutes.entrySet()) {
if (entry.getValue().contains(target)) return entry.getKey();
}
return null;
}
private void displayError(String msg) {
statusDisplay.setTextColor(0xFFD32F2F);
statusDisplay.setText(msg);
}
private void displaySuccess(double rate, int qty, double total) {
statusDisplay.setTextColor(0xFF388E3C);
statusDisplay.setText(String.format(
"Booking Confirmed.\nBase Rate: ¥%.2f\nPassengers: %d\nGrand Total: ¥%.2f",
rate, qty, total
));
Toast.makeText(this, "Transaction processed successfully", Toast.LENGTH_SHORT).show();
}
}
The application logic enforces a strict pricing model where the base fare covers the initial three station intervals, with subsequent increments calculated per three-unit distance. Cross-line travel automatically identifies common transfer points, evaluates alternative routing paths, and selects the most economical fare combination.
