Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions .github/workflows/android-kotlin-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Android Kotlin CI

on:
push:
paths:
- 'mobile/kotlin-app/**'
- '.github/workflows/android-kotlin-ci.yml'
pull_request:
paths:
- 'mobile/kotlin-app/**'
- '.github/workflows/android-kotlin-ci.yml'
workflow_dispatch:

jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: mobile/kotlin-app

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
cache: gradle

- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@v4

- name: Build debug APK
run: ./gradlew clean assembleDebug

- name: Build release APK
run: ./gradlew assembleRelease

- name: Upload APK artifacts
uses: actions/upload-artifact@v4
with:
name: kotlin-apk
path: |
mobile/kotlin-app/app/build/outputs/apk/debug/*.apk
mobile/kotlin-app/app/build/outputs/apk/release/*.apk
20 changes: 11 additions & 9 deletions mobile/kotlin-app/README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
# Kotlin Android wrapper app
# Kotlin Android app (mobile-optimized web wrapper)

This is a native Kotlin Android app that replicates the existing Sunset web project on phone by loading the deployed web app in a full-screen `WebView`.
This Android app preserves the same Sunset product idea by loading the existing web experience, but adds mobile-first navigation and interaction patterns:

## Steps
- Bottom tab navigation to key sections (Home, Courses, About, AI Lab)
- Pull-to-refresh support
- Top loading progress indicator
- Tuned WebView settings and custom Android user-agent suffix

1. Set your production URL in `app/build.gradle.kts`:
- `WEB_APP_URL = "https://YOUR_DEPLOYED_WEB_APP_URL"`
## Setup

1. Set `WEB_APP_URL` in `app/build.gradle.kts` to your deployed site URL.
2. Open `mobile/kotlin-app` in Android Studio.
3. Build and run.

## Why this approach
## CI

- Reuses your existing Next.js codebase.
- Fastest way to get parity between web and Android.
- Keeps one product surface while still shipping through Play Store.
A GitHub Actions workflow is included at `.github/workflows/android-kotlin-ci.yml` to build debug and release APKs automatically on pushes/PRs that touch the Kotlin Android app.
1 change: 1 addition & 0 deletions mobile/kotlin-app/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,5 @@ dependencies {
implementation("androidx.core:core-ktx:1.13.1")
implementation("androidx.appcompat:appcompat:1.7.0")
implementation("com.google.android.material:material:1.12.0")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
}
Original file line number Diff line number Diff line change
@@ -1,40 +1,103 @@
package com.sunset.replica

import android.annotation.SuppressLint
import android.graphics.Bitmap
import android.os.Bundle
import android.webkit.WebChromeClient
import android.webkit.WebResourceRequest
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.ProgressBar
import androidx.appcompat.app.AppCompatActivity
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.google.android.material.bottomnavigation.BottomNavigationView

class MainActivity : AppCompatActivity() {

private lateinit var webView: WebView
private lateinit var pullToRefresh: SwipeRefreshLayout
private lateinit var loadingIndicator: ProgressBar

private val pages = mapOf(
R.id.nav_home to "",
R.id.nav_courses to "/courses",
R.id.nav_about to "/about",
R.id.nav_ai to "/new-design"
)

@SuppressLint("SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

webView = findViewById(R.id.webView)
webView.webChromeClient = WebChromeClient()
webView.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
return false
}
pullToRefresh = findViewById(R.id.pullToRefresh)
loadingIndicator = findViewById(R.id.loadingIndicator)

configureWebView()
configureBottomNavigation()

pullToRefresh.setOnRefreshListener {
webView.reload()
}

if (savedInstanceState == null) {
navigateTo(R.id.nav_home)
} else {
webView.restoreState(savedInstanceState)
}
}

private fun configureBottomNavigation() {
val bottomNavigation: BottomNavigationView = findViewById(R.id.bottomNavigation)
bottomNavigation.setOnItemSelectedListener { item ->
navigateTo(item.itemId)
true
}
}

private fun navigateTo(navItemId: Int) {
val path = pages[navItemId] ?: ""
val destination = "${BuildConfig.WEB_APP_URL.trimEnd('/')}$path"
if (webView.url != destination) {
webView.loadUrl(destination)
}
}

@SuppressLint("SetJavaScriptEnabled")
private fun configureWebView() {
with(webView.settings) {
javaScriptEnabled = true
domStorageEnabled = true
mediaPlaybackRequiresUserGesture = false
cacheMode = WebSettings.LOAD_DEFAULT
useWideViewPort = true
loadWithOverviewMode = true
builtInZoomControls = false
displayZoomControls = false
userAgentString = "$userAgentString SunsetAndroid/1.1"
}

if (savedInstanceState == null) {
webView.loadUrl(BuildConfig.WEB_APP_URL)
webView.webChromeClient = object : WebChromeClient() {
override fun onProgressChanged(view: WebView?, newProgress: Int) {
loadingIndicator.progress = newProgress
loadingIndicator.visibility = if (newProgress < 100) ProgressBar.VISIBLE else ProgressBar.GONE
}
}

webView.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean = false

override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
pullToRefresh.isRefreshing = true
}

override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
pullToRefresh.isRefreshing = false
}
}
}

Expand All @@ -43,16 +106,8 @@ class MainActivity : AppCompatActivity() {
super.onSaveInstanceState(outState)
}

override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
webView.restoreState(savedInstanceState)
}

@Deprecated("Deprecated in Java")
override fun onBackPressed() {
if (webView.canGoBack()) {
webView.goBack()
} else {
super.onBackPressed()
}
if (webView.canGoBack()) webView.goBack() else super.onBackPressed()
}
}
35 changes: 29 additions & 6 deletions mobile/kotlin-app/app/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
@@ -1,11 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:orientation="vertical">

<WebView
android:id="@+id/webView"
<ProgressBar
android:id="@+id/loadingIndicator"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_height="4dp"
android:max="100" />

</FrameLayout>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/pullToRefresh"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">

<WebView
android:id="@+id/webView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottomNavigation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:labelVisibilityMode="labeled"
app:menu="@menu/bottom_navigation_menu" />

</LinearLayout>
19 changes: 19 additions & 0 deletions mobile/kotlin-app/app/src/main/res/menu/bottom_navigation_menu.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/nav_home"
android:icon="@android:drawable/ic_menu_view"
android:title="@string/nav_home" />
<item
android:id="@+id/nav_courses"
android:icon="@android:drawable/ic_menu_agenda"
android:title="@string/nav_courses" />
<item
android:id="@+id/nav_about"
android:icon="@android:drawable/ic_menu_info_details"
android:title="@string/nav_about" />
<item
android:id="@+id/nav_ai"
android:icon="@android:drawable/ic_menu_manage"
android:title="@string/nav_ai_lab" />
</menu>
6 changes: 5 additions & 1 deletion mobile/kotlin-app/app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
<resources>
<string name="app_name">Sunset Replica</string>
<string name="app_name">Sunset Mobile</string>
<string name="nav_home">Home</string>
<string name="nav_courses">Courses</string>
<string name="nav_about">About</string>
<string name="nav_ai_lab">AI Lab</string>
</resources>
Loading