├── .editorconfig ├── .github └── workflows │ ├── build.yml │ └── gradle-wrapper.yaml ├── .gitignore ├── .idea ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml └── inspectionProfiles │ ├── ktlint.xml │ └── profiles_settings.xml ├── APISummaryFormat.md ├── CLAUDE.md ├── CodeCommentCleanup.md ├── DroidconIosDocumentation.md ├── IOSDEV.md ├── LICENSE.txt ├── MEDIA.md ├── README.md ├── StartupANRIssue.md ├── StructuredInstructionFormats.md ├── TimeZonesAndroid.txt ├── TimeZonesiOS.txt ├── android ├── .gitignore ├── build.gradle.kts ├── mock-google-services.json ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ └── co │ │ └── touchlab │ │ └── droidcon │ │ └── android │ │ ├── MainActivity.kt │ │ ├── MainApp.kt │ │ ├── service │ │ └── impl │ │ │ ├── AndroidAnalyticsService.kt │ │ │ ├── DefaultFirebaseMessagingService.kt │ │ │ └── DefaultParseUrlViewService.kt │ │ └── util │ │ └── NotificationLocalizedStringFactory.kt │ ├── lint.xml │ └── res │ ├── drawable-night │ ├── about_droidcon.xml │ └── about_kotlin.xml │ ├── drawable-nodpi │ └── about_touchlab.png │ ├── drawable │ ├── about_droidcon.xml │ ├── about_kotlin.xml │ ├── ic_baseline_insert_invitation_24.xml │ ├── ic_launcher_foreground.xml │ ├── ic_splash_screen.xml │ ├── linkedin.xml │ └── twitter.xml │ ├── mipmap-anydpi-v26 │ └── ic_launcher.xml │ ├── values-night │ └── colors.xml │ └── values │ ├── colors.xml │ ├── strings.xml │ └── themes.xml ├── build.gradle.kts ├── docs └── HyperDrivev1.md ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── ios ├── Droidcon │ ├── .gitignore │ ├── Droidcon.xcodeproj │ │ ├── project.pbxproj │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Droidcon.xcscheme │ └── Droidcon │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ ├── Accent.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ └── dcLondon24-Icon.png │ │ ├── AttendButton.colorset │ │ │ └── Contents.json │ │ ├── AttendButton_Foreground.colorset │ │ │ └── Contents.json │ │ ├── Attending │ │ │ ├── AttendingConflict.colorset │ │ │ │ └── Contents.json │ │ │ ├── AttendingNormal.colorset │ │ │ │ └── Contents.json │ │ │ ├── AttendingPast.colorset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── Divider.colorset │ │ │ └── Contents.json │ │ ├── ElevatedBackground.colorset │ │ │ └── Contents.json │ │ ├── ElevatedBackgroundDisabled.colorset │ │ │ └── Contents.json │ │ ├── ElevatedHeaderBackground.colorset │ │ │ └── Contents.json │ │ ├── Feedback │ │ │ ├── Contents.json │ │ │ ├── Feedback_Dissatisfied.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── baseline_sentiment_very_dissatisfied_black_48dp.png │ │ │ ├── Feedback_Normal.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── baseline_sentiment_satisfied_black_48dp.png │ │ │ └── Feedback_Satisfied.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── baseline_sentiment_satisfied_alt_black_48dp.png │ │ ├── LaunchScreen │ │ │ ├── Contents.json │ │ │ ├── LaunchScreen_Background.colorset │ │ │ │ └── Contents.json │ │ │ └── LaunchScreen_Icon.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── London 24-Splash screen.svg │ │ ├── Logos │ │ │ ├── Contents.json │ │ │ ├── about_droidcon.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── droidcon_logo_light.pdf │ │ │ │ └── droidcon_logo_night.pdf │ │ │ ├── about_kotlin.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── small Kotlin Full Color Logo on Black RGB.svg │ │ │ │ └── small Kotlin Full Color Logo on White RGB.svg │ │ │ ├── about_touchlab.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── TL_Gradient.png │ │ │ ├── linkedin.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── linked-in.png │ │ │ └── twitter.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── twitter.png │ │ ├── NavBar │ │ │ ├── Contents.json │ │ │ ├── NavBar_Background.colorset │ │ │ │ └── Contents.json │ │ │ └── NavBar_Foreground.colorset │ │ │ │ └── Contents.json │ │ ├── Shadow.colorset │ │ │ └── Contents.json │ │ ├── TabBar │ │ │ ├── Contents.json │ │ │ ├── TabBar_Background.colorset │ │ │ │ └── Contents.json │ │ │ ├── TabBar_Foreground.colorset │ │ │ │ └── Contents.json │ │ │ └── TabBar_Foreground_Selected.colorset │ │ │ │ └── Contents.json │ │ └── TextFieldBackground.colorset │ │ │ └── Contents.json │ │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ │ ├── ComposeController.swift │ │ ├── Droidcon.entitlements │ │ ├── DroidconApp.swift │ │ ├── IOSAnalyticsService.swift │ │ ├── Info.plist │ │ ├── Koin.swift │ │ ├── Settings.bundle │ │ ├── Root.plist │ │ └── en.lproj │ │ │ └── Root.strings │ │ ├── Utils │ │ └── LifecycleManager.swift │ │ └── en.lproj │ │ └── Localizable.strings ├── build.gradle.kts └── src │ └── iosMain │ └── kotlin │ └── co │ └── touchlab │ └── droidcon │ └── ios │ ├── DependencyInjection.kt │ ├── service │ └── DefaultParseUrlViewService.kt │ └── util │ ├── NotificationLocalizedStringFactory.kt │ └── formatter │ └── IOSDateFormatter.kt ├── privacy.html ├── settings.gradle.kts ├── shared-ui ├── build.gradle.kts ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidMain │ ├── AndroidManifest.xml │ ├── kotlin │ │ └── co │ │ │ └── touchlab │ │ │ └── droidcon │ │ │ └── ui │ │ │ ├── settings │ │ │ └── PlatformSpecificSettings.kt │ │ │ ├── theme │ │ │ └── Type.android.kt │ │ │ └── util │ │ │ ├── Dialog.kt │ │ │ ├── LocalImage.jvm.kt │ │ │ ├── MainView.kt │ │ │ └── NavigationBackPressWrapper.kt │ └── res │ │ └── font │ │ ├── montserrat_bold.ttf │ │ ├── montserrat_light.ttf │ │ ├── montserrat_medium.ttf │ │ ├── montserrat_regular.ttf │ │ └── montserrat_semi_bold.ttf │ ├── commonMain │ ├── composeResources │ │ └── drawable │ │ │ └── venue-map-1.jpg │ └── kotlin │ │ └── co.touchlab.droidcon │ │ ├── ui │ │ ├── BottomNavigationView.kt │ │ ├── FeedbackDialog.kt │ │ ├── FirstRunConferenceSelector.kt │ │ ├── MainComposeView.kt │ │ ├── UiModule.kt │ │ ├── session │ │ │ ├── SessionBlockView.kt │ │ │ ├── SessionDetailView.kt │ │ │ ├── SessionListView.kt │ │ │ └── SpeakerDetailView.kt │ │ ├── settings │ │ │ ├── AboutView.kt │ │ │ ├── ConferenceSelector.kt │ │ │ ├── PlatformSpecificSettings.kt │ │ │ └── SettingsView.kt │ │ ├── sponsors │ │ │ ├── SponsorDetailView.kt │ │ │ └── SponsorsView.kt │ │ ├── theme │ │ │ ├── Colors.kt │ │ │ ├── Dimensions.kt │ │ │ ├── Theme.kt │ │ │ ├── Type.kt │ │ │ └── Typography.kt │ │ ├── util │ │ │ ├── Dialog.kt │ │ │ ├── Image.kt │ │ │ ├── LocalImage.kt │ │ │ ├── NavigationBackPressWrapper.kt │ │ │ ├── ObserveAsState.kt │ │ │ └── WebLinkText.kt │ │ └── venue │ │ │ └── VenueView.kt │ │ ├── util │ │ ├── LocalDateTime+startOfMinute.kt │ │ └── NavigationController.kt │ │ └── viewmodel │ │ ├── ApplicationViewModel.kt │ │ ├── FeedbackDialogViewModel.kt │ │ ├── WaitForLoadedContextModel.kt │ │ ├── session │ │ ├── AgendaViewModel.kt │ │ ├── BaseSessionListViewModel.kt │ │ ├── ScheduleViewModel.kt │ │ ├── SessionBlockViewModel.kt │ │ ├── SessionDayViewModel.kt │ │ ├── SessionDetailScrollStateStorage.kt │ │ ├── SessionDetailViewModel.kt │ │ ├── SessionListItemViewModel.kt │ │ ├── SpeakerDetailViewModel.kt │ │ └── SpeakerListItemViewModel.kt │ │ ├── settings │ │ ├── AboutItemViewModel.kt │ │ ├── AboutViewModel.kt │ │ └── SettingsViewModel.kt │ │ └── sponsor │ │ ├── SponsorDetailViewModel.kt │ │ ├── SponsorGroupItemViewModel.kt │ │ ├── SponsorGroupViewModel.kt │ │ └── SponsorListViewModel.kt │ └── iosMain │ └── kotlin │ └── co │ └── touchlab │ └── droidcon │ └── ui │ ├── ComposeRootController.kt │ ├── settings │ └── PlatformSpecificSettings.kt │ ├── theme │ └── Type.ios.kt │ └── util │ ├── Dialog.kt │ ├── LocalImage.kt │ ├── NavigationBackPressWrapper.kt │ └── ToSkiaImage.kt ├── shared ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidMain │ ├── AndroidManifest.xml │ ├── kotlin │ │ └── co │ │ │ └── touchlab │ │ │ └── droidcon │ │ │ ├── Koin.android.kt │ │ │ ├── domain │ │ │ └── repository │ │ │ │ └── impl │ │ │ │ └── SqlDelightDriverFactory.android.kt │ │ │ ├── service │ │ │ ├── AndroidNotificationService.kt │ │ │ ├── NotificationPublisher.kt │ │ │ └── NotificationRescheduler.kt │ │ │ └── util │ │ │ ├── AssetResourceReader.kt │ │ │ ├── ClasspathResourceReader.kt │ │ │ ├── IdentifiableIntent.kt │ │ │ ├── Platform.android.kt │ │ │ └── formatter │ │ │ └── AndroidDateFormatter.kt │ └── res │ │ ├── drawable │ │ └── ic_baseline_insert_invitation_24.xml │ │ └── values │ │ └── strings.xml │ ├── commonMain │ ├── kotlin │ │ └── co │ │ │ └── touchlab │ │ │ └── droidcon │ │ │ ├── Koin.kt │ │ │ ├── application │ │ │ ├── composite │ │ │ │ ├── AboutItem.kt │ │ │ │ └── Settings.kt │ │ │ ├── gateway │ │ │ │ ├── SettingsGateway.kt │ │ │ │ └── impl │ │ │ │ │ └── DefaultSettingsGateway.kt │ │ │ ├── repository │ │ │ │ ├── AboutRepository.kt │ │ │ │ ├── SettingsRepository.kt │ │ │ │ └── impl │ │ │ │ │ ├── DefaultAboutRepository.kt │ │ │ │ │ └── DefaultSettingsRepository.kt │ │ │ └── service │ │ │ │ ├── Notification.kt │ │ │ │ ├── NotificationSchedulingService.kt │ │ │ │ ├── NotificationService.kt │ │ │ │ └── impl │ │ │ │ └── DefaultNotificationSchedulingService.kt │ │ │ ├── composite │ │ │ └── Url.kt │ │ │ ├── domain │ │ │ ├── composite │ │ │ │ ├── ScheduleItem.kt │ │ │ │ └── SponsorGroupWithSponsors.kt │ │ │ ├── entity │ │ │ │ ├── Conference.kt │ │ │ │ ├── DomainEntity.kt │ │ │ │ ├── Profile.kt │ │ │ │ ├── Room.kt │ │ │ │ ├── Session.kt │ │ │ │ ├── Sponsor.kt │ │ │ │ └── SponsorGroup.kt │ │ │ ├── gateway │ │ │ │ ├── SessionGateway.kt │ │ │ │ ├── SponsorGateway.kt │ │ │ │ └── impl │ │ │ │ │ ├── DefaultSessionGateway.kt │ │ │ │ │ └── DefaultSponsorGateway.kt │ │ │ ├── repository │ │ │ │ ├── ConferenceRepository.kt │ │ │ │ ├── ProfileRepository.kt │ │ │ │ ├── Repository.kt │ │ │ │ ├── RoomRepository.kt │ │ │ │ ├── SessionRepository.kt │ │ │ │ ├── SponsorGroupRepository.kt │ │ │ │ ├── SponsorRepository.kt │ │ │ │ └── impl │ │ │ │ │ ├── BaseRepository.kt │ │ │ │ │ ├── SqlDelightConferenceRepository.kt │ │ │ │ │ ├── SqlDelightDriverFactory.kt │ │ │ │ │ ├── SqlDelightProfileRepository.kt │ │ │ │ │ ├── SqlDelightRoomRepository.kt │ │ │ │ │ ├── SqlDelightSessionRepository.kt │ │ │ │ │ ├── SqlDelightSponsorGroupRepository.kt │ │ │ │ │ ├── SqlDelightSponsorRepository.kt │ │ │ │ │ └── adapter │ │ │ │ │ └── InstantSqlDelightAdapter.kt │ │ │ └── service │ │ │ │ ├── AnalyticsService.kt │ │ │ │ ├── ConferenceConfigProvider.kt │ │ │ │ ├── DateTimeService.kt │ │ │ │ ├── FeedbackService.kt │ │ │ │ ├── ScheduleService.kt │ │ │ │ ├── ServerApi.kt │ │ │ │ ├── SyncService.kt │ │ │ │ ├── UserIdProvider.kt │ │ │ │ └── impl │ │ │ │ ├── DefaultApiDataSource.kt │ │ │ │ ├── DefaultConferenceConfigProvider.kt │ │ │ │ ├── DefaultDateTimeService.kt │ │ │ │ ├── DefaultFeedbackService.kt │ │ │ │ ├── DefaultScheduleService.kt │ │ │ │ ├── DefaultServerApi.kt │ │ │ │ ├── DefaultSyncService.kt │ │ │ │ ├── DefaultUserIdProvider.kt │ │ │ │ ├── ResourceReader.kt │ │ │ │ ├── dto │ │ │ │ ├── AboutDto.kt │ │ │ │ ├── ConferencesDto.kt │ │ │ │ ├── ScheduleDto.kt │ │ │ │ ├── SpeakersDto.kt │ │ │ │ ├── SponsorSessionsDto.kt │ │ │ │ └── SponsorsDto.kt │ │ │ │ └── json │ │ │ │ ├── AboutJsonResourceDataSource.kt │ │ │ │ └── JsonResourceReader.kt │ │ │ ├── dto │ │ │ └── WebLink.kt │ │ │ ├── service │ │ │ ├── DeepLinkNotificationHandler.kt │ │ │ └── ParseUrlViewService.kt │ │ │ └── util │ │ │ ├── AppChecker.kt │ │ │ ├── Platform.kt │ │ │ └── formatter │ │ │ └── DateFormatter.kt │ ├── resources │ │ └── about.json │ └── sqldelight │ │ └── co │ │ └── touchlab │ │ └── droidcon │ │ └── db │ │ ├── Conference.sq │ │ ├── Profile.sq │ │ ├── Room.sq │ │ ├── Session.sq │ │ ├── SessionSpeaker.sq │ │ ├── Sponsor.sq │ │ ├── SponsorGroup.sq │ │ └── SponsorRepresentative.sq │ └── iosMain │ └── kotlin │ └── co │ └── touchlab │ └── droidcon │ ├── Koin.ios.kt │ ├── MainScope.kt │ ├── domain │ └── repository │ │ └── impl │ │ └── SqlDelightDriverFactory.ios.kt │ ├── service │ └── IOSNotificationService.kt │ └── util │ ├── AppInit.kt │ ├── BundleResourceReader.kt │ ├── Platform.ios.kt │ └── WrapMultiThreadCallback.kt ├── tlsmall.png ├── updateDates.py └── venuemaps ├── dcldn-2024-venue-map-1.jpg └── dcnyc-2024-venue-map-1.jpg /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.{kt,kts}] 4 | end_of_line = lf 5 | ij_kotlin_packages_to_use_import_on_demand = true 6 | ij_kotlin_allow_trailing_comma = true 7 | ij_kotlin_allow_trailing_comma_on_call_site = true 8 | ij_kotlin_imports_layout = * 9 | ij_kotlin_indent_before_arrow_on_new_line = false 10 | ij_kotlin_line_break_after_multiline_when_entry = true 11 | indent_size = 4 12 | indent_style = space 13 | insert_final_newline = true 14 | parameter-list-wrapping = true 15 | ktlint_argument_list_wrapping_ignore_when_parameter_count_greater_or_equal_than = 8 16 | ktlint_chain_method_rule_force_multiline_when_chain_operator_count_greater_or_equal_than = 4 17 | ktlint_code_style = android_studio 18 | ktlint_enum_entry_name_casing = upper_or_camel_cases 19 | ktlint_function_naming_ignore_when_annotated_with = Composable 20 | ktlint_function_signature_body_expression_wrapping = default 21 | ktlint_ignore_back_ticked_identifier = false 22 | max_line_length = 140 23 | 24 | 25 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | pull_request: 4 | push: 5 | branches: [ main ] # Trigger on pushes to the main branch 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | runs-on: macos-latest 11 | steps: 12 | - name: Checkout source code 13 | uses: actions/checkout@v4 14 | 15 | - name: Set up JDK 17 16 | uses: actions/setup-java@v4 17 | with: 18 | java-version: '17' 19 | distribution: 'corretto' 20 | 21 | - name: Setup Gradle 22 | uses: gradle/actions/setup-gradle@v4 23 | 24 | - name: Setup Android SDK 25 | uses: android-actions/setup-android@v3 26 | 27 | - name: Check, Assemble Android and compile iOS 28 | run: ./gradlew ktlintCheck assembleDebug compileKotlinIosX64 --no-daemon -------------------------------------------------------------------------------- /.github/workflows/gradle-wrapper.yaml: -------------------------------------------------------------------------------- 1 | name: gradle-wrapper 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - 'gradlew' 7 | - 'gradlew.bat' 8 | - 'gradle/wrapper/**' 9 | 10 | jobs: 11 | validate: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: gradle/actions/wrapper-validation@v4 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # .gitignore Cleanup 2 | 3 | # Built application files 4 | *.apk 5 | *.ap_ 6 | 7 | # Files for the ART/Dalvik VM 8 | *.dex 9 | 10 | # Java class files 11 | *.class 12 | 13 | # Generated files 14 | bin/ 15 | gen/ 16 | out/ 17 | build/ 18 | captures/ 19 | .cxx 20 | .externalNativeBuild 21 | 22 | # Gradle files 23 | .gradle/ 24 | /local.properties 25 | local.properties 26 | 27 | # IDE files 28 | ## Android Studio 29 | *.iws 30 | *.ipr 31 | *.iml 32 | .idea/ 33 | .navigation/ 34 | 35 | ## Eclipse 36 | proguard/ 37 | .classpath 38 | .project 39 | 40 | ## iOS/Mac 41 | .DS_Store 42 | timeline.xctimeline 43 | playground.xcworkspace 44 | *.xcworkspace 45 | Pods/ 46 | 47 | # Keystore files 48 | *.jks 49 | 50 | # Log Files 51 | *.log 52 | 53 | # Fastlane 54 | fastlane/report.xml 55 | fastlane/Preview.html 56 | fastlane/screenshots 57 | fastlane/test_output 58 | 59 | # Project-specific files 60 | buildboth.sh 61 | google-services.json 62 | GoogleService-Info.plist 63 | .kotlin 64 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # DroidconKotlin Development Guide 2 | 3 | ## Build Commands 4 | - Build: `./gradlew build` 5 | - Clean build: `./gradlew clean build` 6 | - Check (includes lint): `./gradlew check` 7 | - Android lint: `./gradlew lint` 8 | - Run tests: `./gradlew test` 9 | - ktlint check: `./gradlew ktlintCheck` 10 | - ktlint format: `./gradlew ktlintFormat` 11 | - Build ios: `cd /Users/kevingalligan/devel/DroidconKotlin/ios/Droidcon && xcodebuild -scheme Droidcon -sdk iphonesimulator` 12 | 13 | ## Modules 14 | 15 | - android: The Android app 16 | - ios: The iOS app 17 | - shared: Shared logic code 18 | - shared-ui: UI implemented with Compose Multiplatform and used by both Android and iOS 19 | 20 | ## Libraries 21 | 22 | - Hyperdrive: KMP-focused architecture library. It is open source but rarely used by other apps. See docs/HyperDrivev1.md 23 | 24 | ## Code Style 25 | - Kotlin Multiplatform project (Android/iOS) 26 | - Use ktlint for formatting (version 1.4.0) 27 | - Follow dependency injection pattern with Koin 28 | - Repository pattern for data access 29 | - Compose UI for shared UI components 30 | - Class/function names: PascalCase for classes, camelCase for functions 31 | - Interface implementations: Prefix with `Default` (e.g., `DefaultRepository`) 32 | - Organize imports by package, no wildcard imports 33 | - Type-safe code with explicit type declarations 34 | - Coroutines for asynchronous operations 35 | - Proper error handling with try/catch blocks 36 | 37 | ## Claude Document Formats and Instructions 38 | See APISummaryFormat.md and StructuredInstructionFormats.md 39 | 40 | ## Architecture Notes 41 | - App startup logic is handled in `co.touchlab.droidcon.viewmodel.ApplicationViewModel` 42 | 43 | ## Current Task 44 | 45 | Cleaning up the app and prepping for release -------------------------------------------------------------------------------- /IOSDEV.md: -------------------------------------------------------------------------------- 1 | # Minimal iOS Dev Instructions 2 | 3 | The absolute bare minimum install instructions, for an iOS dev who maybe hasn't done any Android 4 | 5 | 6 | 1 - Install the Android SDK 7 | 8 | 2 - Go here and download the .zip file for Mac under “Command Line Tools Only” 9 | On the command line, run the following commands: 10 | ```sdkmanager “platform-tools” 11 | sdkmanager “platforms;android-28” 12 | cd ~/Library/Android/sdk 13 | export ANDROID_HOME=~/Library/Android/sdk 14 | export PATH=$PATH:$ANDROID_HOME/tools:$ANDROID_HOME/platorm-tools 15 | source ~/.bash_profile 16 | ``` 17 | 3 - Download IntelliJ Community: https://www.jetbrains.com/idea/download/#section=mac 18 | 19 | 4 - `git clone https://github.com/touchlab/DroidconKotlin` 20 | 21 | 5 - Go to the “Terminal” tab on the bottom and run: `./gradlew build` 22 | 23 | 6 - Right click on “DroidconKotlin” in the “Project” tab on the left and select “Reveal in Finder” 24 | 25 | 7 - Go to "/iosApp/” and open “iosApp.xcworkspace” 26 | 27 | 8 - Run the app! 28 | -------------------------------------------------------------------------------- /MEDIA.md: -------------------------------------------------------------------------------- 1 | # Media 2 | 3 | A sample of blog posts and videos about the application project. 4 | 5 | [Touchlab Blog - Droidcon NYC iOS app with Compose](https://touchlab.co/droidcon-nyc-ios-app-with-compose/) 6 | 7 | [Touchlab Blog - Compose UI for iOS](https://touchlab.co/compose-ui-for-ios/) 8 | 9 | [Medium - Droidcon NYC App!](https://medium.com/@kpgalligan/droidcon-nyc-app-da868bdef387) 10 | 11 | [Medium - Kotlin Multiplatform in the App Store!](https://medium.com/@kpgalligan/kotlin-multiplatform-in-the-app-store-c3a50c24f93b) 12 | 13 | [Youtube - Droidcon SF](https://www.youtube.com/watch?v=c8IkWGmlcNE) 14 | 15 | [Youtube - Kotlinconf](https://www.youtube.com/watch?v=Dul17VSiejo) 16 | 17 | [Youtube - Kotlin Multiplatform @ Android Summit](https://www.youtube.com/watch?v=oeREzhXx7uw) 18 | 19 | [Youtube - Droidcon App Kotlin Multiplatform](https://www.youtube.com/watch?v=YAeDK3Ei0Lk&feature=youtu.be) 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Droidcon Mobile Clients 2 | 3 | ## General Info 4 | 5 | This project has a pair of native mobile applications for Global Droidcon events. It is built with Compose Multiplatform to run on both Android and iOS. 6 | 7 | > ## Subscribe! 8 | > 9 | > We build solutions that get teams started smoothly with Kotlin Multiplatform and ensure their success in production. Join our community to learn how your peers are adopting KMP. 10 | [Sign up here](https://touchlab.co/?s=shownewsletter)! 11 | 12 | ## Building 13 | 14 | The apps need a Firebase account set up to run. You'll need to get the `google-services.json` and put it in `android/google-services.json` for Android, and 15 | the `GoogleService-Info.plist` and put that in `ios/Droidcon/Droidcon/GoogleService-Info.plist` for iOS. 16 | 17 | ## Compose UI for both! 18 | 19 | This app has come a long way! It was one of the earliest KMP apps in the iOS app store, and was certainly the first Compose Multiplatform app in the app store, as it was built and released before there was technically a tech preview. 20 | 21 | [Check out the blog post](https://touchlab.co/droidcon-nyc-ios-app-with-compose/) 22 | 23 | CMP has come a long way. Back in 2022, the Compose experience on iOS wasn't great. We kept a SwiftUI version of the UI as the main UI for the iOS app, and let you turn on CMP as an early preview. As of March 2025, the SwiftUI version has been removed entirely. Why do the extra work? But, if you want to do a side-by-side, grab a version from main through Feb 2025 and check it out. Prior versions were built and released for each conference, but this updated version is designed to be used for all Droidcon events. As that was a fairly major update, we decided to drop the parallel SwiftUI. 24 | 25 | ## Media 26 | 27 | [Blog posts and videos ->](MEDIA.md) 28 | 29 | ## About 30 | 31 | Droidcon Mobile App brought to you by... 32 | 33 | [![Touchlab Logo](tlsmall.png "Touchlab Logo")](https://touchlab.co) 34 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /android/mock-google-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "project_number": "606665771234", 4 | "project_id": "mock-firebase-project", 5 | "storage_bucket": "mock-firebase-project.appspot.com" 6 | }, 7 | "client": [ 8 | { 9 | "client_info": { 10 | "mobilesdk_app_id": "1:606665771229:android:c1f0f09aa42abc12", 11 | "android_client_info": { 12 | "package_name": "co.touchlab.droidcon.london" 13 | } 14 | }, 15 | "oauth_client": [ 16 | { 17 | "client_id": "123455771229-sc9bpuefbjceq7i1qabk7gssstefrdlv.apps.googleusercontent.com", 18 | "client_type": 1, 19 | "android_info": { 20 | "package_name": "co.touchlab.droidcon.london", 21 | "certificate_hash": "7f254b538565cfe6c28a88744985f015b1534980" 22 | } 23 | }, 24 | { 25 | "client_id": "123455771229-sc9bpuefbjceq7i1qabk7gssstefrdlv.apps.googleusercontent.com", 26 | "client_type": 3 27 | } 28 | ], 29 | "api_key": [ 30 | { 31 | "current_key": "AIzaSyCNzhU2a9gMc_JHurHbywOrRI9Vj4VQZZZ" 32 | } 33 | ], 34 | "services": { 35 | "appinvite_service": { 36 | "other_platform_oauth_client": [ 37 | { 38 | "client_id": "413392989754-04u9tv32474rj0pmbfksirt4ti02a64r.apps.googleusercontent.com", 39 | "client_type": 3 40 | } 41 | ] 42 | } 43 | } 44 | } 45 | ], 46 | "configuration_version": "1" 47 | } -------------------------------------------------------------------------------- /android/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | 15 | 16 | 22 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 38 | 39 | 40 | 41 | 42 | 43 | 46 | 47 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /android/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/touchlab/DroidconKotlin/537585897db8c4f24df70efd89254858cee5f121/android/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /android/src/main/java/co/touchlab/droidcon/android/MainApp.kt: -------------------------------------------------------------------------------- 1 | package co.touchlab.droidcon.android 2 | 3 | import android.app.Activity 4 | import android.app.Application 5 | import android.content.Context 6 | import android.content.SharedPreferences 7 | import co.touchlab.droidcon.android.service.impl.AndroidAnalyticsService 8 | import co.touchlab.droidcon.android.service.impl.DefaultParseUrlViewService 9 | import co.touchlab.droidcon.android.util.NotificationLocalizedStringFactory 10 | import co.touchlab.droidcon.application.service.NotificationSchedulingService 11 | import co.touchlab.droidcon.domain.service.AnalyticsService 12 | import co.touchlab.droidcon.domain.service.impl.ResourceReader 13 | import co.touchlab.droidcon.initKoin 14 | import co.touchlab.droidcon.service.ParseUrlViewService 15 | import co.touchlab.droidcon.ui.uiModule 16 | import co.touchlab.droidcon.util.ClasspathResourceReader 17 | import com.google.firebase.analytics.ktx.analytics 18 | import com.google.firebase.ktx.Firebase 19 | import com.russhwolf.settings.ExperimentalSettingsApi 20 | import com.russhwolf.settings.ObservableSettings 21 | import com.russhwolf.settings.SharedPreferencesSettings 22 | import kotlinx.coroutines.CoroutineScope 23 | import kotlinx.coroutines.Dispatchers 24 | import kotlinx.coroutines.SupervisorJob 25 | import org.koin.core.component.KoinComponent 26 | import org.koin.dsl.module 27 | 28 | @OptIn(ExperimentalSettingsApi::class) 29 | class MainApp : 30 | Application(), 31 | KoinComponent { 32 | private val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) 33 | 34 | override fun onCreate() { 35 | super.onCreate() 36 | initKoin( 37 | module { 38 | single { this@MainApp } 39 | single> { MainActivity::class.java } 40 | single { 41 | get().getSharedPreferences("DROIDCON_SETTINGS_2024", Context.MODE_PRIVATE) 42 | } 43 | single { SharedPreferencesSettings(delegate = get()) } 44 | 45 | single { 46 | DefaultParseUrlViewService() 47 | } 48 | 49 | single { 50 | ClasspathResourceReader() 51 | } 52 | 53 | single { 54 | NotificationLocalizedStringFactory(context = get()) 55 | } 56 | 57 | single { 58 | AndroidAnalyticsService(firebaseAnalytics = Firebase.analytics) 59 | } 60 | } + uiModule, 61 | ) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /android/src/main/java/co/touchlab/droidcon/android/service/impl/AndroidAnalyticsService.kt: -------------------------------------------------------------------------------- 1 | package co.touchlab.droidcon.android.service.impl 2 | 3 | import android.os.Bundle 4 | import co.touchlab.droidcon.domain.service.AnalyticsService 5 | import com.google.firebase.analytics.FirebaseAnalytics 6 | 7 | class AndroidAnalyticsService(private val firebaseAnalytics: FirebaseAnalytics) : AnalyticsService { 8 | 9 | override fun logEvent(name: String, params: Map) { 10 | val bundle = Bundle() 11 | params.keys.forEach { key -> 12 | when (val obj = params[key]) { 13 | is String -> bundle.putString(key, obj) 14 | is Boolean -> bundle.putBoolean(key, obj) 15 | is Int -> bundle.putInt(key, obj) 16 | is Long -> bundle.putLong(key, obj) 17 | is Double -> bundle.putDouble(key, obj) 18 | is BooleanArray -> bundle.putBooleanArray(key, obj) 19 | is IntArray -> bundle.putIntArray(key, obj) 20 | is LongArray -> bundle.putLongArray(key, obj) 21 | is DoubleArray -> bundle.putDoubleArray(key, obj) 22 | else -> throw IllegalArgumentException("Unsupported type $obj with key $key") 23 | } 24 | } 25 | firebaseAnalytics.logEvent(name, bundle) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /android/src/main/java/co/touchlab/droidcon/android/service/impl/DefaultFirebaseMessagingService.kt: -------------------------------------------------------------------------------- 1 | package co.touchlab.droidcon.android.service.impl 2 | 3 | import android.app.NotificationManager 4 | import androidx.core.app.NotificationCompat 5 | import androidx.core.content.getSystemService 6 | import co.touchlab.droidcon.application.service.Notification 7 | import co.touchlab.droidcon.service.AndroidNotificationService 8 | import com.google.firebase.messaging.FirebaseMessagingService 9 | import com.google.firebase.messaging.RemoteMessage 10 | import kotlinx.coroutines.MainScope 11 | import kotlinx.coroutines.launch 12 | import org.koin.android.ext.android.inject 13 | 14 | class DefaultFirebaseMessagingService : FirebaseMessagingService() { 15 | private val notificationService: AndroidNotificationService by inject() 16 | 17 | override fun onNewToken(token: String) { 18 | super.onNewToken(token) 19 | } 20 | 21 | override fun onMessageReceived(message: RemoteMessage) { 22 | super.onMessageReceived(message) 23 | 24 | if (message.data.isNotEmpty() && message.data[Notification.Keys.NOTIFICATION_TYPE] == Notification.Values.REFRESH_DATA_TYPE) { 25 | MainScope().launch { 26 | notificationService.handleNotification( 27 | Notification.Remote.RefreshData, 28 | ) 29 | } 30 | } 31 | 32 | // If we have notification, we're running in foreground and should show it ourselves. 33 | val originalNotification = message.notification ?: return 34 | val notification = NotificationCompat.Builder(this, message.notification?.channelId ?: "") 35 | .setContentTitle(originalNotification.title) 36 | .setContentText(originalNotification.body) 37 | .apply { 38 | originalNotification.channelId?.let { 39 | setChannelId(it) 40 | } 41 | } 42 | .build() 43 | 44 | val notificationManager = getSystemService() 45 | notificationManager?.notify(0, notification) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /android/src/main/java/co/touchlab/droidcon/android/service/impl/DefaultParseUrlViewService.kt: -------------------------------------------------------------------------------- 1 | package co.touchlab.droidcon.android.service.impl 2 | 3 | import android.util.Patterns 4 | import co.touchlab.droidcon.dto.WebLink 5 | import co.touchlab.droidcon.service.ParseUrlViewService 6 | 7 | class DefaultParseUrlViewService : ParseUrlViewService { 8 | 9 | private val urlRegex = Patterns.WEB_URL.toRegex() 10 | 11 | override fun parse(text: String): List = urlRegex.findAll(text).map { WebLink(it.range, it.value) }.toList() 12 | } 13 | -------------------------------------------------------------------------------- /android/src/main/java/co/touchlab/droidcon/android/util/NotificationLocalizedStringFactory.kt: -------------------------------------------------------------------------------- 1 | package co.touchlab.droidcon.android.util 2 | 3 | import android.content.Context 4 | import co.touchlab.droidcon.R 5 | import co.touchlab.droidcon.application.service.NotificationSchedulingService 6 | 7 | class NotificationLocalizedStringFactory(private val context: Context) : NotificationSchedulingService.LocalizedStringFactory { 8 | 9 | override fun reminderTitle(roomName: String?): String { 10 | val ending = roomName?.let { context.getString(R.string.notification_reminder_title_in_room, it) } ?: "" 11 | return context.getString(R.string.notification_reminder_title_base, ending) 12 | } 13 | 14 | override fun reminderBody(sessionTitle: String): String = context.getString(R.string.notification_reminder_body, sessionTitle) 15 | 16 | override fun feedbackTitle(): String = context.getString(R.string.notification_feedback_title) 17 | 18 | override fun feedbackBody(): String = context.getString(R.string.notification_feedback_body) 19 | } 20 | -------------------------------------------------------------------------------- /android/src/main/lint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/src/main/res/drawable-nodpi/about_touchlab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/touchlab/DroidconKotlin/537585897db8c4f24df70efd89254858cee5f121/android/src/main/res/drawable-nodpi/about_touchlab.png -------------------------------------------------------------------------------- /android/src/main/res/drawable/about_droidcon.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 27 | 30 | 33 | 34 | -------------------------------------------------------------------------------- /android/src/main/res/drawable/ic_baseline_insert_invitation_24.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /android/src/main/res/drawable/linkedin.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 16 | 17 | -------------------------------------------------------------------------------- /android/src/main/res/drawable/twitter.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 15 | 16 | -------------------------------------------------------------------------------- /android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FF202020 4 | 5 | -------------------------------------------------------------------------------- /android/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFffffff 4 | #FF7de1c3 5 | 6 | -------------------------------------------------------------------------------- /android/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Droidcon 3 | 4 | Upcoming event%s 5 | " in %s" 6 | %s is starting soon. 7 | Feedback Time! 8 | Your Feedback is Requested. 9 | 10 | Droidcon London 2024 11 | 12 | 13 | -------------------------------------------------------------------------------- /android/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | 16 | 17 |