Android E-Commerce App with Joomla VirtueMart Integration
This tutorial demonstrates how to build a fully functional Android shopping app that connects directly to a Joomla VirtueMart store. Products are displayed using a RecyclerView layout and enhanced with features like image loading (Glide), network requests (Volley), and real-time search.
The backend fetches data from a MySQL database, converts it into JSON using a custom PHP script, and delivers it to the Android app. The app showcases a modern UI, smooth animations, and product detail pages using CollapsingToolBarLayout.
Ideal for developers looking to bridge Joomla CMS with native Android applications in a clean, efficient way.
This Android tutorial connects a Joomla VirtueMart store to an Android front end by:
- Using a PHP API (JoomlaApi.php) to query VirtueMart's MySQL structure and return formatted product data as JSON.
- Accessing joined tables like virtuemart_products, virtuemart_product_prices, and virtuemart_medias to output product name, image, stock, price, and category.
- Enhancing Android UI with:
- RecyclerView for dynamic product lists
- Volley for network communication
- Glide for image loading
- HtmlTextView to render HTML-based product descriptions
- A built-in SearchView to filter results locally
- CollapsingToolbarLayout for rich product detail views
- Using custom model classes (e.g., Anime.java) to map JSON data to objects
- Including activities like:
- Shop (Main product list)
- AnimeActivity (Product detail screen)
- SplashActivity (animated intro)
The project uses an activity_shop.xml layout with RecyclerView and fully supports filtering/searching products based on name, studio, description, or category.
Volley Library: Volley is an HTTP library that makes networking for Android applications easier and, more importantly, faster.
Glide: Glide is a fast and efficient open-source image loading and media management framework for Android that wraps media decoding, memory and disk caching, and resource pooling into a simple and easy-to-use interface.
Demo Site:
https://demo.virtuemart.net/
Template used in this demo:
https://www.smartaddons.com/free-joomla-templates
Access the MySql database and convert to Json:
To load Virtuemart products in the Android RecyclerView, you need to access the MySql database and load the data via Json.
To do this, use the following method:
Create a db.php file with access to your database.
Replace JOOMLA_db_prefix with the prefix of your installation.
date_default_timezone_set('Europe/Lisbon');
include('db.php');
JommlaApi.php
$sqlProdutos = "SELECT JOOMLA_db_prefix_virtuemart_products_en_gb.product_name AS name, JOOMLA_db_prefix_virtuemart_products_en_gb.product_desc AS description, JOOMLA_db_prefix_virtuemart_medias.file_url AS img, JOOMLA_db_prefix_virtuemart_categories_en_gb.category_name AS categorie, JOOMLA_db_prefix_virtuemart_products.product_in_stock AS Rating, JOOMLA_db_prefix_virtuemart_product_prices.product_override_price AS studio,JOOMLA_db_prefix_virtuemart_products.product_in_stock AS episode FROM JOOMLA_db_prefix_virtuemart_products
In Android Studio, create a new activity (empty_activity)
1. Importing Required Libraries:
Inside build.gradle (Project: yourappname) add the mavenCentral() repository:
repositories {
mavenCentral()
google()
jcenter()
}
e em build.gradle (Módulo: app) adicione as seguintes implementações:
dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'com.android.support:appcompat-v7:27.1.0' implementation 'com.android.support.constraint:constraint-layout:1.0.2' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.1' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' // Glide library implementation 'com.github.bumptech.glide:glide:4.6.1' annotationProcessor 'com.github.bumptech.glide:compiler:4.6.1' // Volley library implementation 'com.android.volley:volley:1.0.0' // Recyclerview Library implementation 'com.android.support:recyclerview-v7:27.0.2'
// Apresenta HTML na descrição do Produto implementation 'org.sufficientlysecure:html-textview:4.0' }
2. Create a model Class
public class Anime { private String name ; private String Description; private String rating; private String nb_episode; private String categorie; private String studio ; private String image_url; public Anime() { } public Anime(String name, String description, String rating, String nb_episode, String categorie, String studio, String image_url) { this.name = name; Description = description; this.rating = rating; this.nb_episode = nb_episode; this.categorie = categorie; this.studio = studio; this.image_url = image_url; } public String getName() { return name; } public String getDescription() { return Description; } public String getRating() { return rating; } public String getNb_episode() { return nb_episode; } public String getCategorie() { return categorie; } public String getStudio() { return studio; } public String getImage_url() { return image_url; } public void setName(String name) { this.name = name; } public void setDescription(String description) { Description = description; } public void setRating(String rating) { this.rating = rating; } public void setNb_episode(String nb_episode) { this.nb_episode = nb_episode; } public void setCategorie(String categorie) { this.categorie = categorie; } public void setStudio(String studio) { this.studio = studio; } public void setImage_url(String image_url) { this.image_url = image_url; } }
2.3. Product Listing
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/container" android:layout_width="match_parent" android:layout_height="120dp" android:layout_marginTop="0dp" android:background="@color/white" android:orientation="horizontal" android:padding="0dp"> <ImageView android:id="@+id/thumbnail" android:layout_width="150dp" android:layout_height="120dp" android:background="#fff" android:layout_margin="1sp" android:contentDescription="@string/todo" /> <LinearLayout android:layout_width="match_parent" android:layout_height="119dp" android:layout_marginBottom="1dp" android:background="@color/colorPrimaryDark" android:orientation="vertical" android:padding="4dp"> <TextView android:id="@+id/anime_name" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/anime_title" android:textColor="@color/white" android:textSize="18sp" android:textStyle="bold" /> <TextView android:id="@+id/categorie" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="5dp" android:text="@string/category" android:textColor="@color/white" /> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="5dp" android:textSize="16sp" android:fontFamily="@font/ubuntu" android:textColor="@color/white" android:text="@string/ref"/> <TextView android:id="@+id/rating" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="5dp" android:paddingLeft="5dp" android:paddingRight="5dp" android:text="@string/_0_0" android:textColor="@color/white" android:textSize="15sp" android:textStyle="bold" /> </LinearLayout> <TextView android:id="@+id/studio" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="5dp" android:text="@string/studio" android:textStyle="bold" android:textSize="16sp" android:textColor="@color/white" /> </LinearLayout> </LinearLayout>
2.4. Product Details
The details activity will contain a collapsigntoolbarlayout and to implement this view we need to add the design library to our project
open build.gradle(module:app) and add the following line:
// Design Library implementation 'com.android.support:design:27.0.2'
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/container" android:layout_width="match_parent" android:layout_height="120dp" android:layout_marginTop="0dp" android:background="@color/white" android:orientation="horizontal" android:padding="0dp"> <ImageView android:id="@+id/thumbnail" android:layout_width="150dp" android:layout_height="120dp" android:background="#fff" android:layout_margin="1sp" android:contentDescription="@string/todo" /> <LinearLayout android:layout_width="match_parent" android:layout_height="119dp" android:layout_marginBottom="1dp" android:background="@color/colorPrimaryDark" android:orientation="vertical" android:padding="4dp"> <TextView android:id="@+id/anime_name" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/anime_title" android:textColor="@color/white" android:textSize="18sp" android:textStyle="bold" /> <TextView android:id="@+id/categorie" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="5dp" android:text="@string/category" android:textColor="@color/white" /> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="5dp" android:textSize="16sp" android:fontFamily="@font/ubuntu" android:textColor="@color/white" android:text="@string/ref"/> <TextView android:id="@+id/rating" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="5dp" android:paddingLeft="5dp" android:paddingRight="5dp" android:text="@string/_0_0" android:textColor="@color/white" android:textSize="15sp" android:textStyle="bold" /> </LinearLayout> <TextView android:id="@+id/studio" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="5dp" android:text="@string/studio" android:textStyle="bold" android:textSize="16sp" android:textColor="@color/white" /> </LinearLayout> </LinearLayout>
2.1 AnimeActivity
public class AnimeActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_anime); ActionBar actionBar = getSupportActionBar(); actionBar.setDisplayShowHomeEnabled(true); getSupportActionBar().setTitle("Shop Demo"); String name = Objects.requireNonNull(getIntent().getExtras()).getString("anime_name"); String description = getIntent().getExtras().getString("anime_description"); String studio = getIntent().getExtras().getString("anime_studio") ; String category = getIntent().getExtras().getString("anime_category"); String nb_episode = getIntent().getExtras().getString("anime_nb_episode") ; String rating = getIntent().getExtras().getString("anime_rating") ; String image_url = getIntent().getExtras().getString("anime_img") ; // ini views CollapsingToolbarLayout collapsingToolbarLayout = findViewById(R.id.collapsingtoolbar_id); collapsingToolbarLayout.setTitleEnabled(true); TextView tv_name = findViewById(R.id.aa_anime_name); TextView tv_studio = findViewById(R.id.aa_studio); TextView tv_categorie = findViewById(R.id.aa_categorie) ; TextView tv_rating = findViewById(R.id.aa_rating) ; ImageView image = findViewById(R.id.aa_thumbnail); // setting values to each view tv_name.setText(name); tv_categorie.setText(category); tv_rating.setText(rating); tv_studio.setText(studio); HtmlTextView tv_description = findViewById(R.id.aa_description); tv_description.setHtml(Objects.requireNonNull(description), new HtmlHttpImageGetter(tv_description)); collapsingToolbarLayout.setTitle(name); RequestOptions requestOptions = new RequestOptions().centerCrop().placeholder(R.drawable.i9_logo_dark).error(R.drawable.i9_logo_dark); // set image using Glide Glide.with(this).load(image_url).apply(requestOptions).into(image); } @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.main_menu, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id==R.id.BTHome) { Intent int1 = new Intent(AnimeActivity.this, SplashActivity.class); startActivity(int1); overridePendingTransition(R.anim.left_in, R.anim.left_out); } return super.onOptionsItemSelected(item); } }
2.1.1 Itens - (activity_anime.xml)
<?xml version="1.0" encoding="utf-8"?> <androidx.coordinatorlayout.widget.CoordinatorLayout 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" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".AnimeActivity"> <com.google.android.material.appbar.AppBarLayout android:layout_width="match_parent" android:layout_height="180dp" android:background="@color/colorPrimaryDark" android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"> <com.google.android.material.appbar.CollapsingToolbarLayout android:id="@+id/collapsingtoolbar_id" android:layout_width="match_parent" android:layout_height="match_parent" app:contentScrim="?attr/colorPrimary" app:expandedTitleMargin="10dp" app:expandedTitleMarginEnd="10dp" app:expandedTitleMarginStart="8dp" app:expandedTitleTextAppearance="@style/CollapsedAppBar" app:layout_scrollFlags="exitUntilCollapsed|scroll" android:textStyle="bold" app:title="@string/anime_title"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:paddingLeft="4dp" android:paddingTop="4dp" android:paddingRight="4dp" android:paddingBottom="4dp"> <ImageView android:id="@+id/aa_thumbnail" android:layout_width="180dp" android:layout_height="match_parent" android:background="#fff" android:layout_margin="4sp" android:contentDescription="@string/app_name" /> <LinearLayout android:layout_width="match_parent" android:layout_height="140dp" android:layout_margin="0dp" android:padding="4dp" android:orientation="vertical"> <TextView android:id="@+id/aa_anime_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/anime_title" android:textColor="@color/white" android:textSize="18sp" android:fontFamily="@font/ubuntu" android:textAllCaps="true" /> <TextView android:id="@+id/aa_categorie" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="0dp" android:text="@string/category" android:textSize="14dp" android:fontFamily="@font/ubuntu" android:textColor="@color/white" /> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="4dp" android:textSize="16sp" android:fontFamily="@font/ubuntu" android:textColor="@color/white" android:text="@string/ref"/> <TextView android:id="@+id/aa_rating" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="6dp" android:paddingEnd="4dp" android:paddingStart="0dp" android:text="0.0" android:textColor="@color/white" android:textSize="18sp" android:fontFamily="@font/ubuntu" android:textStyle="bold" /> </LinearLayout> <TextView android:id="@+id/aa_studio" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="5dp" android:text="@string/studio" android:paddingStart="0dp" android:textSize="20dp" android:fontFamily="@font/ubuntu" android:textStyle="bold" android:textColor="@color/white" /> </LinearLayout> </LinearLayout> <androidx.appcompat.widget.Toolbar android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:theme="@style/ThemeOverlay.AppCompat.Light" app:layout_collapseMode="pin"> </androidx.appcompat.widget.Toolbar> </com.google.android.material.appbar.CollapsingToolbarLayout> </com.google.android.material.appbar.AppBarLayout> <androidx.core.widget.NestedScrollView android:layout_width="match_parent" android:layout_height="match_parent" android:padding="6dp" app:layout_behavior="@string/appbar_scrolling_view_behavior"> <org.sufficientlysecure.htmltextview.HtmlTextView android:gravity="center" android:layout_gravity="center_vertical|top" android:text="Anime Description" android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/aa_description" android:fontFamily="@font/ubuntu" android:textSize="16sp" /> </androidx.core.widget.NestedScrollView> </androidx.coordinatorlayout.widget.CoordinatorLayout>
2.2 RecyclerView Adapter class
public class RecyclerViewAdapter extends RecyclerView.Adapter implements Filterable { private final Context mContext ; private final List mData ; private List mDataFull ; RequestOptions option; public RecyclerViewAdapter(Context mContext, List mData) { this.mContext = mContext; this.mData = mData; this.mDataFull = mData; // Request option for Glide option = new RequestOptions().centerCrop().placeholder(R.drawable.loading_shape).error(R.drawable.loading_shape); } @Override public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view ; LayoutInflater inflater = LayoutInflater.from(mContext); view = inflater.inflate(R.layout.anime_row_item,parent,false) ; final MyViewHolder viewHolder = new MyViewHolder(view) ; viewHolder.view_container.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Intent i = new Intent(mContext, AnimeActivity.class); i.putExtra("anime_name",mDataFull.get(viewHolder.getAdapterPosition()).getName()); i.putExtra("anime_description",mDataFull.get(viewHolder.getAdapterPosition()).getDescription()); i.putExtra("anime_studio",mDataFull.get(viewHolder.getAdapterPosition()).getStudio()); i.putExtra("anime_category",mDataFull.get(viewHolder.getAdapterPosition()).getCategorie()); i.putExtra("anime_nb_episode",mDataFull.get(viewHolder.getAdapterPosition()).getNb_episode()); i.putExtra("anime_rating",mDataFull.get(viewHolder.getAdapterPosition()).getRating()); i.putExtra("anime_img",mDataFull.get(viewHolder.getAdapterPosition()).getImage_url()); mContext.startActivity(i); } }); return viewHolder; } @Override public void onBindViewHolder(MyViewHolder holder, int position) { holder.tv_name.setText(mDataFull.get(position).getName()); holder.tv_rating.setText(mDataFull.get(position).getRating()); holder.tv_studio.setText(mDataFull.get(position).getStudio()); holder.tv_category.setText(mDataFull.get(position).getCategorie()); // Load Image from the internet and set it into Imageview using Glide Glide.with(mContext).load(mDataFull.get(position).getImage_url()).apply(option).into(holder.img_thumbnail); } @Override public int getItemCount() { return mDataFull.size(); } @Override public Filter getFilter() { return new Filter() { @Override protected FilterResults performFiltering(CharSequence charSequence) { String charString = charSequence.toString(); if (charString.isEmpty()) { mDataFull = mData ; } else { List filteredList = new ArrayList<>(); for (Anime name : mData) { if (name.getName().toLowerCase().contains(charString.toLowerCase()) || name.getStudio().toLowerCase().contains(charString.toLowerCase()) || name.getDescription().toLowerCase().contains(charSequence) || name.getCategorie().toLowerCase().contains(charString.toLowerCase())) { filteredList.add(name); } } mDataFull = filteredList ; } FilterResults filterResults = new FilterResults(); filterResults.values = mDataFull; return filterResults ; } @Override protected void publishResults(CharSequence constraint, FilterResults results) { mDataFull = (List) results.values; notifyDataSetChanged(); } }; } public static class MyViewHolder extends RecyclerView.ViewHolder { TextView tv_name ; TextView tv_rating ; TextView tv_studio ; TextView tv_category; ImageView img_thumbnail; LinearLayout view_container; public MyViewHolder(View itemView) { super(itemView); view_container = itemView.findViewById(R.id.container); tv_name = itemView.findViewById(R.id.anime_name); tv_category = itemView.findViewById(R.id.categorie); tv_rating = itemView.findViewById(R.id.rating); tv_studio = itemView.findViewById(R.id.studio); img_thumbnail = itemView.findViewById(R.id.thumbnail); } } }
2.3. (Activity Shop)
public class Shop extends AppCompatActivity { private final String JSON_URL = "LINK PARA O SEU FICHEIRO PHP/JSON (Descrito Acima)" ; private JsonArrayRequest request ; private RequestQueue requestQueue ; private List<Anime> lstAnime ; private RecyclerView recyclerView ; private RecyclerViewAdapter adapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_shop); ActionBar actionBar = getSupportActionBar(); actionBar.setDisplayShowHomeEnabled(true); getSupportActionBar().setTitle("Shop Demo"); lstAnime = new ArrayList<>() ; recyclerView = findViewById(R.id.recyclerviewid); jsonrequest(); } private void jsonrequest() { request = new JsonArrayRequest(JSON_URL, new Response.Listener<JSONArray>() { @Override public void onResponse(JSONArray response) { JSONObject jsonObject = null ; for (int i = 0 ; i < response.length(); i++ ) { try { jsonObject = response.getJSONObject(i) ; Anime anime = new Anime() ; anime.setName(jsonObject.getString("name")); anime.setDescription(jsonObject.getString("description")); anime.setRating(jsonObject.getString("Rating")); anime.setCategorie(jsonObject.getString("categorie")); anime.setNb_episode(jsonObject.getString("episode")); anime.setStudio(jsonObject.getString("studio")); anime.setImage_url(jsonObject.getString("img")); lstAnime.add(anime); } catch (JSONException e) { e.printStackTrace(); } } setuprecyclerview(lstAnime); } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { } }); requestQueue = Volley.newRequestQueue(Shop.this); requestQueue.add(request) ; } private void setuprecyclerview(List<Anime> lstAnime) { adapter = new RecyclerViewAdapter(this,lstAnime) ; recyclerView.setLayoutManager(new LinearLayoutManager(this)); recyclerView.setAdapter(adapter); } @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.main_menu, menu); inflater.inflate(R.menu.search_menu, menu); MenuItem searchItem = menu.findItem(R.id.action_search); SearchView searchView = (SearchView) searchItem.getActionView(); searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String query) { return false; } @Override public boolean onQueryTextChange(String newText) { if (adapter != null) { adapter.getFilter().filter(newText); } return false; } }); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id==R.id.BTHome) { Intent int1 = new Intent(Shop.this, SplashActivity.class); startActivity(int1); overridePendingTransition(R.anim.left_in, R.anim.left_out); } return super.onOptionsItemSelected(item); } }
activity_shop.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout 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="com.i9web.VMDemo.Shop"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/recyclerviewid" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginLeft="0dp" android:layout_marginTop="0dp" android:layout_marginRight="0dp"> </androidx.recyclerview.widget.RecyclerView> </LinearLayout>
2.4: SplashActivity
Actividade de abertura da App, redireciona para a página de produtos.
public class SplashActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_splash); Handler handler = new Handler(); handler.postDelayed(new Runnable() { @Override public void run() { mostrarMainActivity(); } }, 5000); ImageView img = findViewById(R.id.img_top); Animation aniSlide = AnimationUtils.loadAnimation(getApplicationContext(),R.anim.fade_in); img.startAnimation(aniSlide); } private void mostrarMainActivity() { Intent intent = new Intent( SplashActivity.this, Shop.class ); startActivity(intent); finish(); } }
activity_splash.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout 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" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.i9web.VMDemo.SplashActivity" android:orientation="vertical" android:gravity="center_horizontal|center_vertical" android:background="@color/black_overlay"> <ImageView android:layout_width="wrap_content" android:id="@+id/img_top" android:src="@drawable/i9_logo_dark" android:layout_height="wrap_content" android:layout_marginTop="10dp" android:layout_marginBottom="10dp" android:scaleType="centerInside" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/app_name"/> </LinearLayout>