From 2e8f841e3ed50d25af338235c246d0899a93e205 Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Tue, 9 Jun 2026 20:02:01 +0300 Subject: [PATCH 1/2] feat: migrate from XML Views to Jetpack Compose + Navigation Compose MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Complete migration of the entire Android UI layer from XML Views + View Binding + Navigation XML to Jetpack Compose + Navigation Compose. Architecture: - Activities: AppCompatActivity → ComponentActivity with setContent - Fragments: all 14 eliminated, each became a @Composable destination - Navigation: navigation.xml → NavHost composable with 11 routes - View Binding: removed entirely (no XML layouts remain) - ChatKit: replaced with native Compose LazyColumn + ClickableText - Image loading: custom ImageView.load() → Coil 3.4.0 AsyncImage - Compose deps: ungated from 'next' build type to regular implementation New composables (17 files in src/main/.../ui/): - Theme.kt: full Material3 theme matching Juick web CSS design tokens - MainScreen.kt: Scaffold with TopAppBar, NavigationBar, FAB - AppNavigation.kt: NavHost with all 11 routes - FeedScreen.kt: LazyColumn with pull-to-refresh, load-more, ReplyCard - PostCard.kt: full post card with entity-based formatting, ClickableText - ProfileHeader.kt: blog profile header - ChatScreen.kt + ChatBubble: native Compose PM (ChatKit replacement) - ChatsListScreen.kt: PM dialog list - ThreadScreen.kt, NewPostScreen.kt, TagsScreen.kt, NoAuthScreen.kt - SearchScreen.kt, SignInScreen.kt, SignUpScreen.kt, CropSheet.kt - MessageFormatter.kt: entity-based text formatting (mirrors iOS algorithm) Message formatting: - Uses API-provided entities (links, quotes, bold, italic, underline) with exact positions and shortened display text - Block-level quote rendering with pink accent border + background - URL click handling via position lookup, not annotation boundaries - Newline collapsing to avoid extra vertical space - Bold/italic/underline entity types fully supported Design: - Colors match Juick web app CSS (style.css design tokens) - Post cards: white surface, subtle shadow, sharp corners, 20dp pad - Top/bottom bars: surface background with 0.5dp outline dividers - Quote blocks: 3dp accent left border, background fill, dimmed text - NavigationBar: onSurface text selected, tertiary indicator, dimmed inactive - Both sides of chat bubbles share same surfaceVariant background Removed: - 17 XML layouts + 2 menu XMLs + navigation.xml - 14 Fragments + FeedAdapter + CropBottomSheet - ChatKit, SwipeRefreshLayout, fragmentviewbindingdelegate dependencies - 'next' build type (no longer needed) - android.builtInKotlin=false → restored for CI Java 17 compat Tests: - UITest.kt restored and migrated for Compose - FormatPostTextTest.kt: 8 entity-based formatting tests - LinkClickTest.kt: 5 link rendering + URL position tests - MainScreenTest.kt, SignInScreenTest.kt Model changes: - Post.Entity: API entity model (start, end, text, type, url) - Chat/Post/User: removed ChatKit interface implementations CI: - check job uses assembleDebug (all 3 flavors) - Removed concurrency groups to prevent cancelled jobs - Connected tests use pinned emulator build 13610412 Co-Authored-By: Claude Opus 4.8 --- .github/workflows/android.yml | 6 +- .github/workflows/schedule.yml | 4 - build.gradle | 56 +- gradle.properties | 2 +- gradle/libs.versions.toml | 15 +- src/androidTest/AndroidManifest.xml | 2 +- .../android/testing/ChatLinkClickTest.kt | 103 ---- .../android/testing/FormatPostTextTest.kt | 124 +++++ .../juick/android/testing/LinkClickTest.kt | 113 ++++ .../juick/android/testing/MainScreenTest.kt | 63 +++ .../juick/android/testing/SignInScreenTest.kt | 58 ++ .../java/com/juick/android/testing/UITest.kt | 64 ++- .../com/juick/android/GoogleSignInProvider.kt | 2 +- src/main/AndroidManifest.xml | 3 +- src/main/java/com/juick/App.kt | 30 +- .../juick/android/JuickMessageMenuListener.kt | 40 +- .../java/com/juick/android/MainActivity.kt | 521 +++++------------- .../com/juick/android/NotificationSender.kt | 12 +- ...overFragment.kt => OnItemClickListener.kt} | 24 +- .../java/com/juick/android/SignInActivity.kt | 109 ++-- .../java/com/juick/android/SignUpActivity.kt | 59 +- .../juick/android/fragment/ThreadFragment.kt | 355 ------------ .../com/juick/android/screens/FeedAdapter.kt | 414 -------------- .../com/juick/android/screens/FeedFragment.kt | 256 --------- .../android/screens/blog/BlogFragment.kt | 249 --------- .../android/screens/chat/ChatFragment.kt | 185 ------- .../android/screens/chats/ChatsFragment.kt | 135 ----- .../android/screens/chats/NoAuthFragment.kt | 40 -- .../android/screens/home/HomeFragment.kt | 43 -- .../android/screens/post/NewPostFragment.kt | 209 ------- .../android/screens/post/TagsFragment.kt | 118 ---- .../android/screens/search/SearchFragment.kt | 34 -- .../java/com/juick/android/ui/MainScreen.kt | 174 ++++++ src/main/java/com/juick/android/ui/Theme.kt | 104 ++++ .../android/ui/navigation/AppNavigation.kt | 216 ++++++++ .../android/ui/screens/chat/ChatScreen.kt | 145 +++++ .../ui/screens/chats/ChatsListScreen.kt | 131 +++++ .../android/ui/screens/feed/FeedScreen.kt | 224 ++++++++ .../ui/screens/feed/MessageFormatter.kt | 214 +++++++ .../juick/android/ui/screens/feed/PostCard.kt | 144 +++++ .../android/ui/screens/feed/ProfileHeader.kt | 63 +++ .../android/ui/screens/noauth/NoAuthScreen.kt | 50 ++ .../android/ui/screens/post/NewPostScreen.kt | 136 +++++ .../android/ui/screens/search/SearchScreen.kt | 103 ++++ .../android/ui/screens/tags/TagsScreen.kt | 101 ++++ .../android/ui/screens/thread/ThreadScreen.kt | 130 +++++ .../juick/android/ui/signin/SignInScreen.kt | 133 +++++ .../juick/android/ui/signup/SignUpScreen.kt | 82 +++ .../com/juick/android/ui/widget/CropSheet.kt | 137 +++++ .../juick/android/widget/CropBottomSheet.kt | 162 ------ .../juick/android/widget/util/ImageHelper.kt | 73 --- .../util/ImageUtil.kt} | 26 +- src/main/java/com/juick/api/model/Chat.kt | 30 +- src/main/java/com/juick/api/model/Post.kt | 33 +- src/main/java/com/juick/api/model/User.kt | 15 +- src/main/res/layout/activity_login.xml | 101 ---- src/main/res/layout/activity_main.xml | 97 ---- src/main/res/layout/activity_signup.xml | 60 -- src/main/res/layout/content_main.xml | 26 - src/main/res/layout/dialog_crop.xml | 40 -- src/main/res/layout/fragment_chat.xml | 43 -- src/main/res/layout/fragment_dialog_list.xml | 60 -- src/main/res/layout/fragment_me.xml | 65 --- src/main/res/layout/fragment_new_post.xml | 124 ----- src/main/res/layout/fragment_no_auth.xml | 37 -- src/main/res/layout/fragment_posts_page.xml | 174 ------ .../res/layout/fragment_posts_viewpager.xml | 26 - src/main/res/layout/fragment_tags_list.xml | 44 -- src/main/res/layout/fragment_thread.xml | 130 ----- src/main/res/layout/item_post.xml | 193 ------- src/main/res/layout/item_tag.xml | 27 - src/main/res/layout/item_thread_reply.xml | 175 ------ .../res/layout/menu_layout_discussions.xml | 57 -- src/main/res/layout/menu_layout_profile.xml | 37 -- src/main/res/menu/bottom_navigation.xml | 32 -- src/main/res/menu/toolbar.xml | 37 -- src/main/res/navigation/navigation.xml | 140 ----- src/next/google/google-services.json | 36 -- .../com/juick/android/NextSignInActivity.kt | 120 ---- src/next/java/com/juick/android/ui/Theme.kt | 65 --- 80 files changed, 3074 insertions(+), 4946 deletions(-) delete mode 100644 src/androidTest/java/com/juick/android/testing/ChatLinkClickTest.kt create mode 100644 src/androidTest/java/com/juick/android/testing/FormatPostTextTest.kt create mode 100644 src/androidTest/java/com/juick/android/testing/LinkClickTest.kt create mode 100644 src/androidTest/java/com/juick/android/testing/MainScreenTest.kt create mode 100644 src/androidTest/java/com/juick/android/testing/SignInScreenTest.kt rename src/main/java/com/juick/android/{screens/discover/DiscoverFragment.kt => OnItemClickListener.kt} (59%) delete mode 100644 src/main/java/com/juick/android/fragment/ThreadFragment.kt delete mode 100644 src/main/java/com/juick/android/screens/FeedAdapter.kt delete mode 100644 src/main/java/com/juick/android/screens/FeedFragment.kt delete mode 100644 src/main/java/com/juick/android/screens/blog/BlogFragment.kt delete mode 100644 src/main/java/com/juick/android/screens/chat/ChatFragment.kt delete mode 100644 src/main/java/com/juick/android/screens/chats/ChatsFragment.kt delete mode 100644 src/main/java/com/juick/android/screens/chats/NoAuthFragment.kt delete mode 100644 src/main/java/com/juick/android/screens/home/HomeFragment.kt delete mode 100644 src/main/java/com/juick/android/screens/post/NewPostFragment.kt delete mode 100644 src/main/java/com/juick/android/screens/post/TagsFragment.kt delete mode 100644 src/main/java/com/juick/android/screens/search/SearchFragment.kt create mode 100644 src/main/java/com/juick/android/ui/MainScreen.kt create mode 100644 src/main/java/com/juick/android/ui/Theme.kt create mode 100644 src/main/java/com/juick/android/ui/navigation/AppNavigation.kt create mode 100644 src/main/java/com/juick/android/ui/screens/chat/ChatScreen.kt create mode 100644 src/main/java/com/juick/android/ui/screens/chats/ChatsListScreen.kt create mode 100644 src/main/java/com/juick/android/ui/screens/feed/FeedScreen.kt create mode 100644 src/main/java/com/juick/android/ui/screens/feed/MessageFormatter.kt create mode 100644 src/main/java/com/juick/android/ui/screens/feed/PostCard.kt create mode 100644 src/main/java/com/juick/android/ui/screens/feed/ProfileHeader.kt create mode 100644 src/main/java/com/juick/android/ui/screens/noauth/NoAuthScreen.kt create mode 100644 src/main/java/com/juick/android/ui/screens/post/NewPostScreen.kt create mode 100644 src/main/java/com/juick/android/ui/screens/search/SearchScreen.kt create mode 100644 src/main/java/com/juick/android/ui/screens/tags/TagsScreen.kt create mode 100644 src/main/java/com/juick/android/ui/screens/thread/ThreadScreen.kt create mode 100644 src/main/java/com/juick/android/ui/signin/SignInScreen.kt create mode 100644 src/main/java/com/juick/android/ui/signup/SignUpScreen.kt create mode 100644 src/main/java/com/juick/android/ui/widget/CropSheet.kt delete mode 100644 src/main/java/com/juick/android/widget/CropBottomSheet.kt delete mode 100644 src/main/java/com/juick/android/widget/util/ImageHelper.kt rename src/main/java/com/juick/android/{screens/discussions/DiscussionsFragment.kt => widget/util/ImageUtil.kt} (56%) delete mode 100644 src/main/res/layout/activity_login.xml delete mode 100644 src/main/res/layout/activity_main.xml delete mode 100644 src/main/res/layout/activity_signup.xml delete mode 100644 src/main/res/layout/content_main.xml delete mode 100644 src/main/res/layout/dialog_crop.xml delete mode 100644 src/main/res/layout/fragment_chat.xml delete mode 100644 src/main/res/layout/fragment_dialog_list.xml delete mode 100644 src/main/res/layout/fragment_me.xml delete mode 100644 src/main/res/layout/fragment_new_post.xml delete mode 100644 src/main/res/layout/fragment_no_auth.xml delete mode 100644 src/main/res/layout/fragment_posts_page.xml delete mode 100644 src/main/res/layout/fragment_posts_viewpager.xml delete mode 100644 src/main/res/layout/fragment_tags_list.xml delete mode 100644 src/main/res/layout/fragment_thread.xml delete mode 100644 src/main/res/layout/item_post.xml delete mode 100644 src/main/res/layout/item_tag.xml delete mode 100644 src/main/res/layout/item_thread_reply.xml delete mode 100644 src/main/res/layout/menu_layout_discussions.xml delete mode 100644 src/main/res/layout/menu_layout_profile.xml delete mode 100644 src/main/res/menu/bottom_navigation.xml delete mode 100644 src/main/res/menu/toolbar.xml delete mode 100644 src/main/res/navigation/navigation.xml delete mode 100644 src/next/google/google-services.json delete mode 100644 src/next/java/com/juick/android/NextSignInActivity.kt delete mode 100644 src/next/java/com/juick/android/ui/Theme.kt diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 7826a41c..52bf9f58 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -4,10 +4,6 @@ on: pull_request: push: branches: [master] -concurrency: - group: ${{ github.ref }} - cancel-in-progress: true - jobs: check: runs-on: ubuntu-24.04 @@ -21,4 +17,4 @@ jobs: - name: Setup Android SDK uses: android-actions/setup-android@v4 - name: Build - run: ./gradlew assembleNext + run: ./gradlew assembleDebug diff --git a/.github/workflows/schedule.yml b/.github/workflows/schedule.yml index a42a9c48..eb891b66 100644 --- a/.github/workflows/schedule.yml +++ b/.github/workflows/schedule.yml @@ -6,10 +6,6 @@ on: schedule: - cron: '0 0 * * *' -concurrency: - group: ${{ github.ref }} - cancel-in-progress: true - jobs: check_date: runs-on: ubuntu-24.04 diff --git a/build.gradle b/build.gradle index 80cf474c..0f5a57f2 100644 --- a/build.gradle +++ b/build.gradle @@ -14,6 +14,15 @@ if (keystorePropertiesFile.exists()) { keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) } +configurations.all { + resolutionStrategy.eachDependency { + if (requested.group == "androidx.concurrent" && requested.name == "concurrent-futures") { + useVersion("1.2.0") + because("conflict between compose BOM (1.1.0) and test deps (1.2.0)") + } + } +} + android { compileSdk = 36 defaultConfig { @@ -33,7 +42,6 @@ android { addConstant("INTENT_NEW_EVENT_ACTION", "com.juick.NEW_EVENT_ACTION") buildConfigField "boolean", "HIDE_NSFW", "false" - buildConfigField "boolean", "ENABLE_COMPOSE_UI", "false" buildConfigField "boolean", "ENABLE_UPDATER", "true" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -42,7 +50,6 @@ android { } buildFeatures { buildConfig true - viewBinding true compose true } compileOptions { @@ -66,10 +73,6 @@ android { minifyEnabled false signingConfig signingConfigs.release } - next { - initWith debug - buildConfigField "boolean", "ENABLE_COMPOSE_UI", "true" - } store { initWith release buildConfigField "boolean", "ENABLE_UPDATER", "false" @@ -126,8 +129,9 @@ dependencies { implementation libs.navigation.fragment.ktx implementation libs.navigation.ui.ktx implementation libs.savedstate.ktx - implementation libs.swiperefreshlayout implementation libs.lifecycle.viewmodel.compose + implementation libs.lifecycle.runtime.compose + implementation libs.runtime.livedata implementation libs.lifecycle.extensions implementation libs.material @@ -148,24 +152,27 @@ dependencies { implementation libs.retrofit implementation libs.converter.kotlinx.serialization - implementation libs.chatkit huaweiImplementation libs.push - implementation libs.fragmentviewbindingdelegate.kt - implementation libs.android.image.cropper // Compose - nextImplementation libs.activity.compose - nextImplementation libs.constraintlayout.compose - nextImplementation platform(libs.compose.bom) - nextImplementation libs.ui - nextImplementation libs.ui.graphics - nextImplementation libs.ui.viewbinding - nextImplementation libs.ui.tooling.preview - nextImplementation libs.material.icons.extended - nextImplementation libs.material3 - nextImplementation libs.ui.tooling + implementation libs.activity.compose + implementation libs.constraintlayout.compose + implementation platform(libs.compose.bom) + implementation libs.ui + implementation libs.ui.graphics + implementation libs.ui.tooling.preview + implementation libs.material.icons.extended + implementation libs.material3 + debugImplementation libs.ui.tooling + + // Navigation Compose + implementation libs.navigation.compose + + // Coil for image loading + implementation libs.coil.compose + implementation libs.coil.network.okhttp // Core library androidTestImplementation libs.test.core.ktx @@ -174,10 +181,11 @@ dependencies { androidTestImplementation libs.truth androidTestImplementation libs.espresso.core - // Espresso intents for intercepting ACTION_VIEW - androidTestImplementation libs.espresso.intents - // Fragment testing helpers (launchFragmentInContainer) - androidTestImplementation libs.fragment.testing + + // Compose UI testing + androidTestImplementation(platform(libs.compose.bom)) + androidTestImplementation libs.compose.ui.test.junit4 + debugImplementation libs.compose.ui.test.manifest // UI Automator androidTestImplementation libs.uiautomator.v18 } diff --git a/gradle.properties b/gradle.properties index 80cc0346..94fc9956 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ org.gradle.daemon=false -org.gradle.parallel=fasle +org.gradle.parallel=false org.gradle.jvmargs=-Xmx1536M org.gradle.vfs.watch=true android.useAndroidX=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e6539c82..5de426eb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,10 +4,10 @@ androidImageCropper = "4.7.0" appcompat = "1.7.1" browser = "1.10.0" chatkit = "5e99a8bf3c" +coil = "3.4.0" constraintlayoutCompose = "1.1.1" credentialsPlayServicesAuth = "1.6.0" -espressoCore = "3.6.1" -espressoIntents = "3.7.0" +espresso = "3.7.0" firebaseBom = "34.14.1" composeBom = "2026.05.01" fragmentTesting = "1.8.9" @@ -41,13 +41,15 @@ android-image-cropper = { module = "com.vanniktech:android-image-cropper", versi browser = { module = "androidx.browser:browser", version.ref = "browser" } appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } chatkit = { module = "com.github.vitalyster:ChatKit", version.ref = "chatkit" } +coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" } +coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" } compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" } constraintlayout-compose = { module = "androidx.constraintlayout:constraintlayout-compose", version.ref = "constraintlayoutCompose" } converter-kotlinx-serialization = { module = "com.squareup.retrofit2:converter-kotlinx-serialization", version.ref = "retrofit" } core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" } credentials-play-services-auth = { module = "androidx.credentials:credentials-play-services-auth", version.ref = "credentialsPlayServicesAuth" } -espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espressoCore" } -espresso-intents = { module = "androidx.test.espresso:espresso-intents", version.ref = "espressoIntents" } +espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso" } +espresso-intents = { module = "androidx.test.espresso:espresso-intents", version.ref = "espresso" } firebase-messaging = { module = "com.google.firebase:firebase-messaging" } firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" } fragment-testing = { module = "androidx.fragment:fragment-testing", version.ref = "fragmentTesting" } @@ -56,11 +58,16 @@ googleid = { module = "com.google.android.libraries.identity.googleid:googleid", junit = { module = "androidx.test.ext:junit", version.ref = "junit" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleVersion" } +lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycleVersion" } +runtime-livedata = { module = "androidx.compose.runtime:runtime-livedata" } +compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" } +compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" } lifecycle-extensions = { module = "androidx.lifecycle:lifecycle-extensions", version.ref = "lifecycleExtensions" } logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" } material = { module = "com.google.android.material:material", version.ref = "material" } material3 = { module = "androidx.compose.material3:material3" } material-icons-extended = { module = "androidx.compose.material:material-icons-extended" } +navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationFragmentKtx" } navigation-fragment-ktx = { module = "androidx.navigation:navigation-fragment-ktx", version.ref = "navigationFragmentKtx" } navigation-ui-ktx = { module = "androidx.navigation:navigation-ui-ktx", version.ref = "navigationFragmentKtx" } okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } diff --git a/src/androidTest/AndroidManifest.xml b/src/androidTest/AndroidManifest.xml index ae8f37a8..d2084741 100644 --- a/src/androidTest/AndroidManifest.xml +++ b/src/androidTest/AndroidManifest.xml @@ -1,6 +1,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/main/res/layout/activity_main.xml b/src/main/res/layout/activity_main.xml deleted file mode 100644 index 0baf0272..00000000 --- a/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/main/res/layout/activity_signup.xml b/src/main/res/layout/activity_signup.xml deleted file mode 100644 index 3f73ddc0..00000000 --- a/src/main/res/layout/activity_signup.xml +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - - diff --git a/src/main/res/layout/content_main.xml b/src/main/res/layout/content_main.xml deleted file mode 100644 index f31aa9ec..00000000 --- a/src/main/res/layout/content_main.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - \ No newline at end of file diff --git a/src/main/res/layout/dialog_crop.xml b/src/main/res/layout/dialog_crop.xml deleted file mode 100644 index 1ab2b671..00000000 --- a/src/main/res/layout/dialog_crop.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - -