App Setup: Gradle, Packages & MVVM
Step 1: Create or open the project
You can build every file yourself by following these lessons, or open the finished project from the download and read along. To start fresh:
- 1Android Studio → New Project → Empty Views Activity (this template uses XML layouts)
- 2Name KartEasy, package com.codingeasily.karteasy, language Kotlin, minimum SDK API 26, build language Kotlin DSL
- 3Replace the generated Gradle files with the ones below and click Sync Now
- 4Or: File → Open → select the
android/KartEasyfolder from the ZIP
Step 2: Version catalog and dependencies
All library versions live in one file, gradle/libs.versions.toml. The module file refers to them as libs.retrofit, libs.glide and so on.
[versions]
agp = "8.7.3"
kotlin = "2.0.21"
coreKtx = "1.15.0"
appcompat = "1.7.0"
material = "1.12.0"
constraintlayout = "2.2.0"
recyclerview = "1.4.0"
viewpager2 = "1.1.0"
swiperefresh = "1.1.0"
activityKtx = "1.9.3"
fragmentKtx = "1.8.5"
lifecycle = "2.8.7"
navigation = "2.8.5"
datastore = "1.1.1"
splashscreen = "1.0.1"
coroutines = "1.9.0"
retrofit = "2.11.0"
okhttp = "4.12.0"
glide = "4.16.0"
junit = "4.13.2"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "splashscreen" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
androidx-recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "recyclerview" }
androidx-viewpager2 = { group = "androidx.viewpager2", name = "viewpager2", version.ref = "viewpager2" }
androidx-swiperefreshlayout = { group = "androidx.swiperefreshlayout", name = "swiperefreshlayout", version.ref = "swiperefresh" }
androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activityKtx" }
androidx-fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "fragmentKtx" }
androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycle" }
androidx-lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "lifecycle" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" }
androidx-navigation-fragment-ktx = { group = "androidx.navigation", name = "navigation-fragment-ktx", version.ref = "navigation" }
androidx-navigation-ui-ktx = { group = "androidx.navigation", name = "navigation-ui-ktx", version.ref = "navigation" }
androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" }
kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" }
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
retrofit-converter-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" }
okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" }
glide = { group = "com.github.bumptech.glide", name = "glide", version.ref = "glide" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }| Library | Used for |
|---|---|
| material, constraintlayout, recyclerview, viewpager2, swiperefreshlayout | the UI: cards, chips, grids, banner slider, pull to refresh |
| lifecycle, activity-ktx, fragment-ktx | ViewModel, by viewModels(), lifecycle-aware collection |
| navigation | moving between screens, back stack, deep links, bottom nav + drawer |
| retrofit + converter-gson + okhttp-logging | calling the PHP API, JSON → Kotlin, logs, offline cache |
| glide | loading product images from URLs |
| datastore-preferences | saving the login token and settings |
| core-splashscreen | the Android 12+ style splash screen on every version |
// Top-level build file: only declares plugins, the app module applies them.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
}pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "KartEasy"
include(":app")plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
}
android {
namespace = "com.codingeasily.karteasy"
compileSdk = 35
defaultConfig {
applicationId = "com.codingeasily.karteasy"
minSdk = 26
targetSdk = 35
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
// Upload key for the Play Store. Create it once with Build > Generate Signed Bundle,
// then put the 4 values in ~/.gradle/gradle.properties (never commit passwords).
signingConfigs {
create("release") {
val storeFilePath = providers.gradleProperty("KARTEASY_STORE_FILE").orNull
if (storeFilePath != null) {
storeFile = file(storeFilePath)
storePassword = providers.gradleProperty("KARTEASY_STORE_PASSWORD").orNull
keyAlias = providers.gradleProperty("KARTEASY_KEY_ALIAS").orNull
keyPassword = providers.gradleProperty("KARTEASY_KEY_PASSWORD").orNull
}
}
}
buildTypes {
debug {
// Emulator -> your computer running "php -S 0.0.0.0:8000" inside the backend folder
buildConfigField("String", "BASE_URL", "\"http://10.0.2.2:8000/\"")
applicationIdSuffix = ".debug"
resValue("string", "app_name", "KartEasy Debug")
}
release {
// Your live backend (must end with a slash)
buildConfigField("String", "BASE_URL", "\"https://yourdomain.com/karteasy/\"")
resValue("string", "app_name", "KartEasy")
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
if (providers.gradleProperty("KARTEASY_STORE_FILE").isPresent) {
signingConfig = signingConfigs.getByName("release")
}
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
viewBinding = true
buildConfig = true
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.core.splashscreen)
implementation(libs.androidx.appcompat)
implementation(libs.material)
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.recyclerview)
implementation(libs.androidx.viewpager2)
implementation(libs.androidx.swiperefreshlayout)
implementation(libs.androidx.activity.ktx)
implementation(libs.androidx.fragment.ktx)
implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(libs.androidx.lifecycle.livedata.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.navigation.fragment.ktx)
implementation(libs.androidx.navigation.ui.ktx)
implementation(libs.androidx.datastore.preferences)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.retrofit)
implementation(libs.retrofit.converter.gson)
implementation(libs.okhttp.logging)
implementation(libs.glide)
testImplementation(libs.junit)
}Step 3: Debug and release API URL
The buildConfigField lines create BuildConfig.BASE_URL. Debug builds talk to your computer, release builds to your live server — you never change code to switch.
debug (Run ▶ in Android Studio)
- BASE_URL = http://10.0.2.2:8000/
- app name "KartEasy Debug"
- package …karteasy.debug (both can be installed)
- HTTP logging ON
release (Play Store)
- BASE_URL = https://yourdomain.com/karteasy/
- app name "KartEasy"
- R8 shrinks and obfuscates code
- HTTP logging OFF
Inside the Android emulator, localhost is the emulator itself. The special address 10.0.2.2 means "the computer running the emulator", where your php -S server is listening.
Step 4: Manifest and network security
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<!-- NetworkMonitor checks if the phone is online -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- Android 13+ asks before showing the "Order placed" notification -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:name=".KartApp"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:supportsRtl="true"
android:theme="@style/Theme.KartEasy">
<!-- Launcher: shows the splash screen, then decides where to go -->
<activity
android:name=".MainActivity"
android:exported="true"
android:theme="@style/Theme.KartEasy.Splash"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- Creates intent filters for the <deepLink> entries in nav_main.xml -->
<nav-graph android:value="@navigation/nav_main" />
</activity>
<activity
android:name=".ui.onboarding.OnboardingActivity"
android:exported="false" />
<activity
android:name=".ui.auth.AuthActivity"
android:exported="false"
android:windowSoftInputMode="adjustResize" />
</application>
</manifest>Android blocks plain http:// by default. We allow it only for the development addresses, so the live app is forced to use HTTPS:
<?xml version="1.0" encoding="utf-8"?>
<!-- HTTPS everywhere. Plain HTTP is allowed only for your own computer while developing
(10.0.2.2 = the computer, seen from the Android emulator). -->
<network-security-config>
<base-config cleartextTrafficPermitted="false" />
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="false">10.0.2.2</domain>
<domain includeSubdomains="false">localhost</domain>
</domain-config>
</network-security-config>Step 5: Package structure
📁 app/src/main/java/com/codingeasily/karteasy/ KartApp.kt Application: creates shared objects once MainActivity.kt splash, toolbar, drawer, bottom nav, badge 📁 data/ 📁 api/ ApiService.kt ApiClient.kt Result.kt 📁 model/ Models.kt (all JSON classes) 📁 repo/ ShopRepository.kt (only class that calls the API) 📁 session/ SessionManager.kt (DataStore: token, theme …) 📁 notify/ Notifier.kt 📁 util/ Format.kt Validator.kt NetworkMonitor.kt Extensions.kt 📁 ui/ 📁 onboarding/ auth/ home/ category/ product/ 📁 cart/ wishlist/ checkout/ address/ orders/ account/ 📁 app/src/main/res/ 📁 layout/ (35 files) drawable/ (47) navigation/ nav_main, nav_auth 📁 menu/ values/ values-night/ values-sw600dp/ xml/ anim/ color/
MVVM: who does what
| Layer | Knows about | Never does |
|---|---|---|
| Fragment + XML | views, clicks, the ViewModel | network calls, business rules |
| ViewModel | screen state, the repository | touch views (so it survives rotation) |
| Repository | ApiService, SessionManager | anything about screens |
Step 6: KartApp (shared objects)
The Application class is created before any screen and lives as long as the app. We create the session, network monitor and repository there once, and every screen reuses them.
package com.codingeasily.karteasy
import android.app.Application
import com.codingeasily.karteasy.data.api.ApiClient
import com.codingeasily.karteasy.data.repo.ShopRepository
import com.codingeasily.karteasy.data.session.SessionManager
import com.codingeasily.karteasy.notify.Notifier
import com.codingeasily.karteasy.util.NetworkMonitor
/**
* Application class = simple "service locator".
* Objects created here live as long as the app and are shared by every screen:
* (application as KartApp).repository
*/
class KartApp : Application() {
lateinit var session: SessionManager
private set
lateinit var network: NetworkMonitor
private set
lateinit var repository: ShopRepository
private set
override fun onCreate() {
super.onCreate()
session = SessionManager(this)
network = NetworkMonitor(this)
repository = ShopRepository(ApiClient.create(this, session, network), session)
Notifier.createChannels(this)
}
}// Anywhere in a Fragment or ViewModel:
val repo = requireContext().kartApp.repository // Extensions.kt: Context.kartApp
// In an AndroidViewModel:
class HomeViewModel(app: Application) : AndroidViewModel(app) {
private val repo = app.kartApp.repository
}Step 7: Run app + backend together
Connect the phone to the same Wi-Fi, find your PC's IP (ipconfig / ifconfig, e.g. 192.168.1.5), change the debug BASE_URL to http://192.168.1.5:8000/ and add that IP to network_security_config.xml.
