├── ios ├── Tuist.swift ├── App │ ├── Resources │ │ └── Assets.xcassets │ │ │ ├── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ ├── app_icon.png │ │ │ └── Contents.json │ ├── Configs │ │ ├── Target.xcconfig │ │ └── Project.xcconfig │ └── Sources │ │ └── NewsfeedApp.swift ├── CommonDesign │ ├── Resources │ │ └── Assets.xcassets │ │ │ ├── Contents.json │ │ │ ├── black.colorset │ │ │ └── Contents.json │ │ │ ├── black_2.colorset │ │ │ └── Contents.json │ │ │ ├── black_3.colorset │ │ │ └── Contents.json │ │ │ ├── blue.colorset │ │ │ └── Contents.json │ │ │ ├── blue_2.colorset │ │ │ └── Contents.json │ │ │ ├── blue_3.colorset │ │ │ └── Contents.json │ │ │ ├── blue_4.colorset │ │ │ └── Contents.json │ │ │ ├── blue_5.colorset │ │ │ └── Contents.json │ │ │ ├── blue_6.colorset │ │ │ └── Contents.json │ │ │ ├── brown.colorset │ │ │ └── Contents.json │ │ │ ├── gray.colorset │ │ │ └── Contents.json │ │ │ ├── gray_2.colorset │ │ │ └── Contents.json │ │ │ ├── gray_3.colorset │ │ │ └── Contents.json │ │ │ ├── gray_4.colorset │ │ │ └── Contents.json │ │ │ ├── green.colorset │ │ │ └── Contents.json │ │ │ ├── green_2.colorset │ │ │ └── Contents.json │ │ │ ├── green_3.colorset │ │ │ └── Contents.json │ │ │ ├── green_4.colorset │ │ │ └── Contents.json │ │ │ ├── red.colorset │ │ │ └── Contents.json │ │ │ ├── red_2.colorset │ │ │ └── Contents.json │ │ │ ├── red_3.colorset │ │ │ └── Contents.json │ │ │ ├── white.colorset │ │ │ └── Contents.json │ │ │ ├── yellow.colorset │ │ │ └── Contents.json │ │ │ ├── yellow_2.colorset │ │ │ └── Contents.json │ │ │ ├── yellow_3.colorset │ │ │ └── Contents.json │ │ │ ├── yellow_4.colorset │ │ │ └── Contents.json │ │ │ └── yellow_5.colorset │ │ │ └── Contents.json │ ├── Configs │ │ ├── Target.xcconfig │ │ └── Project.xcconfig │ ├── Project.swift │ └── Sources │ │ └── Colors.swift ├── Workspace.swift ├── CommonSwiftUi │ ├── Sources │ │ ├── Elements │ │ │ ├── AppProgress.swift │ │ │ ├── AppSurface.swift │ │ │ ├── AppAvatar.swift │ │ │ ├── AppScreen.swift │ │ │ ├── AppDeepLink.swift │ │ │ ├── AppIcon.swift │ │ │ ├── AppText.swift │ │ │ ├── AppImage.swift │ │ │ └── AppButton.swift │ │ ├── Theme │ │ │ ├── Background.swift │ │ │ ├── Typography.swift │ │ │ ├── ContentColor.swift │ │ │ ├── Theme.swift │ │ │ └── Shape.swift │ │ └── Test │ │ │ └── CustomSwiftUiTestRuleWrapper.swift │ ├── Configs │ │ ├── Target.xcconfig │ │ └── Project.xcconfig │ └── Project.swift ├── Feed │ ├── Configs │ │ ├── Target.xcconfig │ │ ├── UiTestHost-Target.xcconfig │ │ └── Project.xcconfig │ └── Project.swift ├── Post │ ├── Configs │ │ ├── Target.xcconfig │ │ ├── UiTestHost-Target.xcconfig │ │ └── Project.xcconfig │ ├── Project.swift │ └── Tests │ │ └── Host │ │ └── PostTestHostApp.swift ├── CommonKotlinMultiplatform │ └── Project.swift └── CommonFirebase │ └── Project.swift ├── android ├── common │ ├── design │ │ ├── src │ │ │ └── main │ │ │ │ └── res │ │ │ │ ├── values │ │ │ │ ├── strings.xml │ │ │ │ ├── styles.xml │ │ │ │ └── colors.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ │ └── drawable-v24 │ │ │ │ └── ic_launcher_foreground.xml │ │ └── build.gradle.kts │ ├── navigation │ │ ├── src │ │ │ └── main │ │ │ │ ├── res │ │ │ │ └── values │ │ │ │ │ └── strings.xml │ │ │ │ └── kotlin │ │ │ │ └── com │ │ │ │ └── gchristov │ │ │ │ └── newsfeed │ │ │ │ └── android │ │ │ │ └── common │ │ │ │ └── navigation │ │ │ │ ├── Navigator.kt │ │ │ │ ├── NavigationModule.kt │ │ │ │ └── RealNavigator.kt │ │ └── build.gradle.kts │ ├── compose-test │ │ ├── src │ │ │ └── main │ │ │ │ ├── res │ │ │ │ └── values │ │ │ │ │ └── styles.xml │ │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── gchristov │ │ │ │ │ └── newsfeed │ │ │ │ │ └── android │ │ │ │ │ └── common │ │ │ │ │ └── composetest │ │ │ │ │ ├── ComposeTestActivity.kt │ │ │ │ │ ├── CommonComposeTestClass.kt │ │ │ │ │ ├── Finders.kt │ │ │ │ │ └── CustomComposeTestRule.kt │ │ │ │ └── AndroidManifest.xml │ │ └── build.gradle.kts │ ├── compose │ │ ├── src │ │ │ └── main │ │ │ │ └── kotlin │ │ │ │ └── com │ │ │ │ └── gchristov │ │ │ │ └── newsfeed │ │ │ │ └── android │ │ │ │ └── common │ │ │ │ └── compose │ │ │ │ ├── elements │ │ │ │ ├── AppRipple.kt │ │ │ │ ├── AppProgress.kt │ │ │ │ ├── AppScreen.kt │ │ │ │ ├── AppHtmlText.kt │ │ │ │ ├── AppImage.kt │ │ │ │ ├── AppIcon.kt │ │ │ │ ├── AppSurface.kt │ │ │ │ ├── AppText.kt │ │ │ │ ├── AppPullRefresh.kt │ │ │ │ ├── avatar │ │ │ │ │ ├── AppAvatar.kt │ │ │ │ │ └── ColorGenerator.kt │ │ │ │ ├── AppBar.kt │ │ │ │ └── AppButton.kt │ │ │ │ ├── CommonComposeActivity.kt │ │ │ │ └── theme │ │ │ │ ├── Background.kt │ │ │ │ ├── Typography.kt │ │ │ │ ├── ContentColor.kt │ │ │ │ ├── Shape.kt │ │ │ │ └── Theme.kt │ │ └── build.gradle.kts │ ├── firebase │ │ └── build.gradle.kts │ └── test │ │ └── build.gradle.kts ├── post │ ├── feature │ │ ├── src │ │ │ └── main │ │ │ │ ├── res │ │ │ │ └── values │ │ │ │ │ └── strings.xml │ │ │ │ └── AndroidManifest.xml │ │ └── build.gradle.kts │ └── test-fixtures │ │ └── build.gradle.kts ├── feed │ ├── feature │ │ ├── build.gradle.kts │ │ └── src │ │ │ └── main │ │ │ ├── res │ │ │ └── values │ │ │ │ └── strings.xml │ │ │ └── AndroidManifest.xml │ └── test-fixtures │ │ └── build.gradle.kts └── app │ ├── src │ └── main │ │ ├── kotlin │ │ └── com │ │ │ └── gchristov │ │ │ └── newsfeed │ │ │ └── android │ │ │ └── app │ │ │ ├── NewsfeedApp.kt │ │ │ └── DependencyInjector.kt │ │ └── AndroidManifest.xml │ └── build.gradle.kts ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle-plugins ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradle.properties ├── settings.gradle.kts └── conventions │ └── src │ └── main │ └── kotlin │ └── com │ └── gchristov │ └── newsfeed │ └── gradleplugins │ ├── multiplatform │ ├── MplBuildConfigPlugin.kt │ ├── MplModulePlugin.kt │ ├── MplFeaturePlugin.kt │ ├── MplDataPlugin.kt │ └── MplBasePlugin.kt │ ├── android │ ├── AndroidBinaryPlugin.kt │ ├── AndroidComposePlugin.kt │ ├── AndroidFirebasePlugin.kt │ ├── AndroidModulePlugin.kt │ ├── AndroidFeaturePlugin.kt │ └── AndroidBasePlugin.kt │ ├── BaseExtensionExt.kt │ └── ProjectExt.kt ├── multiplatform ├── common │ ├── mvvm-test │ │ ├── src │ │ │ ├── commonMain │ │ │ │ └── kotlin │ │ │ │ │ └── com │ │ │ │ │ └── gchristov │ │ │ │ │ └── newsfeed │ │ │ │ │ └── multiplatform │ │ │ │ │ └── common │ │ │ │ │ └── mvvmtest │ │ │ │ │ └── CommonViewModelTestClass.kt │ │ │ ├── iosMain │ │ │ │ └── kotlin │ │ │ │ │ └── com │ │ │ │ │ └── gchristov │ │ │ │ │ └── newsfeed │ │ │ │ │ └── multiplatform │ │ │ │ │ └── common │ │ │ │ │ └── mvvmtest │ │ │ │ │ └── CommonViewModelTestClass.kt │ │ │ └── androidMain │ │ │ │ └── kotlin │ │ │ │ └── com │ │ │ │ └── gchristov │ │ │ │ └── newsfeed │ │ │ │ └── multiplatform │ │ │ │ └── common │ │ │ │ └── mvvmtest │ │ │ │ └── CommonViewModelTestClass.kt │ │ └── build.gradle.kts │ ├── kotlin │ │ ├── src │ │ │ ├── androidMain │ │ │ │ └── kotlin │ │ │ │ │ └── com │ │ │ │ │ └── gchristov │ │ │ │ │ └── newsfeed │ │ │ │ │ └── multiplatform │ │ │ │ │ └── common │ │ │ │ │ └── kotlin │ │ │ │ │ └── AppContext.kt │ │ │ └── commonMain │ │ │ │ └── kotlin │ │ │ │ └── com │ │ │ │ └── gchristov │ │ │ │ └── newsfeed │ │ │ │ └── multiplatform │ │ │ │ └── common │ │ │ │ └── kotlin │ │ │ │ ├── di │ │ │ │ ├── DependencyModule.kt │ │ │ │ └── DependencyInjector.kt │ │ │ │ ├── Log.kt │ │ │ │ ├── Serializer.kt │ │ │ │ └── MplCommonKotlinModule.kt │ │ └── build.gradle.kts │ ├── network │ │ ├── src │ │ │ ├── androidMain │ │ │ │ └── AndroidManifest.xml │ │ │ └── commonMain │ │ │ │ └── kotlin │ │ │ │ └── com │ │ │ │ └── gchristov │ │ │ │ └── newsfeed │ │ │ │ └── multiplatform │ │ │ │ └── common │ │ │ │ └── network │ │ │ │ ├── NetworkConfig.kt │ │ │ │ ├── MplCommonNetworkModule.kt │ │ │ │ └── NetworkClient.kt │ │ └── build.gradle.kts │ ├── test │ │ ├── src │ │ │ └── commonMain │ │ │ │ └── kotlin │ │ │ │ └── com │ │ │ │ └── gchristov │ │ │ │ └── newsfeed │ │ │ │ └── multiplatform │ │ │ │ └── common │ │ │ │ └── test │ │ │ │ ├── FakeClock.kt │ │ │ │ ├── FakeCoroutineDispatcher.kt │ │ │ │ └── FakeResponse.kt │ │ └── build.gradle.kts │ ├── mvvm │ │ ├── build.gradle.kts │ │ └── src │ │ │ ├── androidMain │ │ │ └── kotlin │ │ │ │ └── com │ │ │ │ └── gchristov │ │ │ │ └── newsfeed │ │ │ │ └── multiplatform │ │ │ │ └── common │ │ │ │ └── mvvm │ │ │ │ └── ViewModelFactory.kt │ │ │ └── commonMain │ │ │ └── kotlin │ │ │ └── com │ │ │ └── gchristov │ │ │ └── newsfeed │ │ │ └── multiplatform │ │ │ └── common │ │ │ └── mvvm │ │ │ └── CommonViewModel.kt │ ├── firebase │ │ ├── src │ │ │ ├── androidMain │ │ │ │ └── kotlin │ │ │ │ │ └── com │ │ │ │ │ └── gchristov │ │ │ │ │ └── newsfeed │ │ │ │ │ └── multiplatform │ │ │ │ │ └── common │ │ │ │ │ └── firebase │ │ │ │ │ └── AndroidCommonFirebaseModule.kt │ │ │ ├── iosMain │ │ │ │ └── kotlin │ │ │ │ │ └── com │ │ │ │ │ └── gchristov │ │ │ │ │ └── newsfeed │ │ │ │ │ └── multiplatform │ │ │ │ │ └── common │ │ │ │ │ └── firebase │ │ │ │ │ └── IosCommonFirebaseModule.kt │ │ │ └── commonMain │ │ │ │ └── kotlin │ │ │ │ └── com │ │ │ │ └── gchristov │ │ │ │ └── newsfeed │ │ │ │ └── multiplatform │ │ │ │ └── common │ │ │ │ └── firebase │ │ │ │ └── MplCommonFirebaseModule.kt │ │ └── build.gradle.kts │ └── persistence │ │ ├── build.gradle.kts │ │ └── src │ │ ├── iosMain │ │ └── kotlin │ │ │ └── com │ │ │ └── gchristov │ │ │ └── newsfeed │ │ │ └── multiplatform │ │ │ └── common │ │ │ └── persistence │ │ │ └── IosCommonPersistenceModule.kt │ │ ├── androidMain │ │ └── kotlin │ │ │ └── com │ │ │ └── gchristov │ │ │ └── newsfeed │ │ │ └── multiplatform │ │ │ └── common │ │ │ └── persistence │ │ │ └── AndroidCommonPersistenceModule.kt │ │ └── commonMain │ │ └── kotlin │ │ └── com │ │ └── gchristov │ │ └── newsfeed │ │ └── multiplatform │ │ └── common │ │ └── persistence │ │ └── MplCommonPersistenceModule.kt ├── feed │ ├── data │ │ ├── src │ │ │ └── commonMain │ │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── gchristov │ │ │ │ │ └── newsfeed │ │ │ │ │ └── multiplatform │ │ │ │ │ └── feed │ │ │ │ │ └── data │ │ │ │ │ ├── model │ │ │ │ │ ├── FeedFilter.kt │ │ │ │ │ ├── SectionedFeed.kt │ │ │ │ │ └── Feed.kt │ │ │ │ │ ├── db │ │ │ │ │ ├── DbFeedFilter.kt │ │ │ │ │ └── FeedFilterMapper.kt │ │ │ │ │ ├── api │ │ │ │ │ └── ApiFeedResponse.kt │ │ │ │ │ ├── FeedApi.kt │ │ │ │ │ └── usecase │ │ │ │ │ ├── FlattenSectionedFeedUseCase.kt │ │ │ │ │ └── RedecorateSectionedFeedUseCase.kt │ │ │ │ └── sqldelight │ │ │ │ └── com │ │ │ │ └── gchristov │ │ │ │ └── newsfeed │ │ │ │ └── multiplatform │ │ │ │ └── feed │ │ │ │ └── data │ │ │ │ └── FeedSqlDelightDatabase.sq │ │ └── build.gradle.kts │ ├── test-fixtures │ │ └── build.gradle.kts │ └── feature │ │ ├── build.gradle.kts │ │ └── src │ │ └── commonMain │ │ └── kotlin │ │ └── com │ │ └── gchristov │ │ └── newsfeed │ │ └── multiplatform │ │ └── feed │ │ └── feature │ │ ├── SearchWidgetState.kt │ │ └── MplFeedModule.kt ├── post │ ├── test-fixtures │ │ ├── build.gradle.kts │ │ └── src │ │ │ └── commonMain │ │ │ └── kotlin │ │ │ └── com │ │ │ └── gchristov │ │ │ └── newsfeed │ │ │ └── multiplatform │ │ │ └── post │ │ │ └── testfixtures │ │ │ └── PostCreator.kt │ ├── feature │ │ ├── build.gradle.kts │ │ └── src │ │ │ └── commonMain │ │ │ └── kotlin │ │ │ └── com │ │ │ └── gchristov │ │ │ └── newsfeed │ │ │ └── multiplatform │ │ │ └── post │ │ │ └── feature │ │ │ ├── MplPostModule.kt │ │ │ └── PostViewModel.kt │ └── data │ │ ├── src │ │ └── commonMain │ │ │ ├── sqldelight │ │ │ └── com │ │ │ │ └── gchristov │ │ │ │ └── newsfeed │ │ │ │ └── multiplatform │ │ │ │ └── post │ │ │ │ └── data │ │ │ │ └── PostSqlDelightDatabase.sq │ │ │ └── kotlin │ │ │ └── com │ │ │ └── gchristov │ │ │ └── newsfeed │ │ │ └── multiplatform │ │ │ └── post │ │ │ └── data │ │ │ ├── model │ │ │ └── Post.kt │ │ │ ├── api │ │ │ └── ApiPostResponse.kt │ │ │ ├── PostApi.kt │ │ │ └── usecase │ │ │ └── EstimateReadingTimeMinutesUseCase.kt │ │ └── build.gradle.kts ├── auth │ └── data │ │ ├── build.gradle.kts │ │ └── src │ │ └── commonMain │ │ ├── sqldelight │ │ └── com │ │ │ └── gchristov │ │ │ └── newsfeed │ │ │ └── multiplatform │ │ │ └── auth │ │ │ └── data │ │ │ └── AuthSqlDelightDatabase.sq │ │ └── kotlin │ │ └── com │ │ └── gchristov │ │ └── newsfeed │ │ └── multiplatform │ │ └── auth │ │ └── data │ │ ├── MplAuthDataModule.kt │ │ └── AuthRepository.kt └── umbrella │ ├── src │ └── commonMain │ │ └── kotlin │ │ └── com │ │ └── gchristov │ │ └── newsfeed │ │ └── multiplatform │ │ └── umbrella │ │ ├── Dispatcher.kt │ │ └── DependencyInjector.kt │ └── build.gradle.kts ├── gradle.properties ├── .gitignore ├── .github ├── actions │ ├── build-android │ │ └── action.yml │ ├── test-ios │ │ └── action.yml │ ├── setup-gradle │ │ └── action.yml │ ├── unit-test │ │ └── action.yml │ ├── setup-xcode │ │ └── action.yml │ ├── build-ios │ │ └── action.yml │ └── test-android │ │ └── action.yml └── workflows │ └── staging-check.yml ├── tools └── scripts │ ├── secrets.sh │ └── new_app_setup.sh ├── PULL_REQUEST_TEMPLATE.md └── settings.gradle.kts /ios/Tuist.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | 3 | let tuist = Tuist() 4 | -------------------------------------------------------------------------------- /android/common/design/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Newsfeed 3 | 4 | -------------------------------------------------------------------------------- /ios/App/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gchristov/newsfeed-kotlin-multiplatform/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /ios/CommonDesign/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /android/common/navigation/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | news 3 | 4 | -------------------------------------------------------------------------------- /gradle-plugins/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gchristov/newsfeed-kotlin-multiplatform/HEAD/gradle-plugins/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/common/design/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gchristov/newsfeed-kotlin-multiplatform/HEAD/android/common/design/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/common/design/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gchristov/newsfeed-kotlin-multiplatform/HEAD/android/common/design/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/common/design/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gchristov/newsfeed-kotlin-multiplatform/HEAD/android/common/design/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/common/design/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gchristov/newsfeed-kotlin-multiplatform/HEAD/android/common/design/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/common/design/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gchristov/newsfeed-kotlin-multiplatform/HEAD/android/common/design/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /ios/App/Resources/Assets.xcassets/AppIcon.appiconset/app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gchristov/newsfeed-kotlin-multiplatform/HEAD/ios/App/Resources/Assets.xcassets/AppIcon.appiconset/app_icon.png -------------------------------------------------------------------------------- /android/common/design/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gchristov/newsfeed-kotlin-multiplatform/HEAD/android/common/design/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/common/design/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gchristov/newsfeed-kotlin-multiplatform/HEAD/android/common/design/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/common/design/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gchristov/newsfeed-kotlin-multiplatform/HEAD/android/common/design/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/common/design/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gchristov/newsfeed-kotlin-multiplatform/HEAD/android/common/design/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/common/design/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gchristov/newsfeed-kotlin-multiplatform/HEAD/android/common/design/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/common/navigation/src/main/kotlin/com/gchristov/newsfeed/android/common/navigation/Navigator.kt: -------------------------------------------------------------------------------- 1 | package com.gchristov.newsfeed.android.common.navigation 2 | 3 | interface Navigator { 4 | fun openPost(postId: String) 5 | } -------------------------------------------------------------------------------- /gradle-plugins/gradle.properties: -------------------------------------------------------------------------------- 1 | #Gradle 2 | org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=2g 3 | org.gradle.caching=true 4 | org.gradle.configuration-cache=true 5 | org.gradle.parallel=true 6 | 7 | #Kotlin 8 | kotlin.code.style=official -------------------------------------------------------------------------------- /multiplatform/common/mvvm-test/src/commonMain/kotlin/com/gchristov/newsfeed/multiplatform/common/mvvmtest/CommonViewModelTestClass.kt: -------------------------------------------------------------------------------- 1 | package com.gchristov.newsfeed.multiplatform.common.mvvmtest 2 | 3 | expect open class CommonViewModelTestClass() -------------------------------------------------------------------------------- /android/common/design/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.newsfeed.android.module) 3 | } 4 | 5 | android { 6 | defaultConfig { 7 | namespace = "com.gchristov.newsfeed.android.common.design" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /multiplatform/common/kotlin/src/androidMain/kotlin/com/gchristov/newsfeed/multiplatform/common/kotlin/AppContext.kt: -------------------------------------------------------------------------------- 1 | package com.gchristov.newsfeed.multiplatform.common.kotlin 2 | 3 | import android.content.Context 4 | 5 | lateinit var AppContext: Context -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /multiplatform/common/mvvm-test/src/iosMain/kotlin/com/gchristov/newsfeed/multiplatform/common/mvvmtest/CommonViewModelTestClass.kt: -------------------------------------------------------------------------------- 1 | package com.gchristov.newsfeed.multiplatform.common.mvvmtest 2 | 3 | actual open class CommonViewModelTestClass actual constructor() -------------------------------------------------------------------------------- /android/common/compose-test/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /multiplatform/common/network/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /gradle-plugins/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /android/common/compose-test/src/main/kotlin/com/gchristov/newsfeed/android/common/composetest/ComposeTestActivity.kt: -------------------------------------------------------------------------------- 1 | package com.gchristov.newsfeed.android.common.composetest 2 | 3 | import androidx.activity.ComponentActivity 4 | 5 | internal class ComposeTestActivity : ComponentActivity() -------------------------------------------------------------------------------- /android/post/feature/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | %1$d min read 3 | Add to favourites 4 | Remove from favourites 5 | 6 | -------------------------------------------------------------------------------- /multiplatform/common/network/src/commonMain/kotlin/com/gchristov/newsfeed/multiplatform/common/network/NetworkConfig.kt: -------------------------------------------------------------------------------- 1 | package com.gchristov.newsfeed.multiplatform.common.network 2 | 3 | data class NetworkConfig( 4 | val guardianApiKey: String, 5 | val guardianApiUrl: String, 6 | ) 7 | -------------------------------------------------------------------------------- /android/common/compose-test/src/main/kotlin/com/gchristov/newsfeed/android/common/composetest/CommonComposeTestClass.kt: -------------------------------------------------------------------------------- 1 | package com.gchristov.newsfeed.android.common.composetest 2 | 3 | import org.junit.Rule 4 | 5 | open class CommonComposeTestClass { 6 | @get:Rule 7 | val composeRule = createCustomComposeRule() 8 | } -------------------------------------------------------------------------------- /android/common/design/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | -------------------------------------------------------------------------------- /android/common/navigation/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.newsfeed.android.module) 3 | } 4 | 5 | android { 6 | defaultConfig { 7 | namespace = "com.gchristov.newsfeed.android.common.navigation" 8 | } 9 | } 10 | 11 | dependencies { 12 | implementation(projects.multiplatform.common.kotlin) 13 | } 14 | -------------------------------------------------------------------------------- /ios/Workspace.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | 3 | let workspace = Workspace( 4 | name: "Newsfeed", 5 | projects: [ 6 | "CommonSwiftUi", 7 | "CommonDesign", 8 | "CommonFirebase", 9 | "CommonKotlinMultiplatform", 10 | "Post", 11 | "Feed", 12 | "App", 13 | ] 14 | ) 15 | -------------------------------------------------------------------------------- /multiplatform/feed/data/src/commonMain/kotlin/com/gchristov/newsfeed/multiplatform/feed/data/model/FeedFilter.kt: -------------------------------------------------------------------------------- 1 | package com.gchristov.newsfeed.multiplatform.feed.data.model 2 | 3 | data class FeedFilter( 4 | val query: String 5 | ) { 6 | companion object { 7 | val Default = FeedFilter(query = "fintech") 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /android/common/design/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /multiplatform/common/test/src/commonMain/kotlin/com/gchristov/newsfeed/multiplatform/common/test/FakeClock.kt: -------------------------------------------------------------------------------- 1 | package com.gchristov.newsfeed.multiplatform.common.test 2 | 3 | import kotlinx.datetime.Clock 4 | import kotlinx.datetime.Instant 5 | 6 | object FakeClock : Clock { 7 | override fun now(): Instant = Instant.parse("2022-02-22T00:00:00Z") 8 | } -------------------------------------------------------------------------------- /android/common/design/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | #Gradle 2 | org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=2g 3 | org.gradle.caching=true 4 | org.gradle.configuration-cache=true 5 | org.gradle.parallel=true 6 | 7 | #Kotlin 8 | kotlin.code.style=official 9 | kotlin.native.ignoreDisabledTargets=true 10 | 11 | #Android 12 | android.useAndroidX=true 13 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /multiplatform/feed/data/src/commonMain/kotlin/com/gchristov/newsfeed/multiplatform/feed/data/db/DbFeedFilter.kt: -------------------------------------------------------------------------------- 1 | package com.gchristov.newsfeed.multiplatform.feed.data.db 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | internal data class DbFeedFilter( 8 | @SerialName("query") val query: String, 9 | ) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | .idea 5 | .DS_Store 6 | /build 7 | */build 8 | build 9 | **/.build 10 | /captures 11 | .externalNativeBuild 12 | .cxx 13 | local.properties 14 | secrets.properties 15 | google-services.json 16 | GoogleService-Info.plist 17 | .kotlin 18 | *.xcworkspace 19 | Info.plist 20 | *.pbxproj 21 | *.xcodeproj 22 | Derived/ 23 | -------------------------------------------------------------------------------- /android/common/compose/src/main/kotlin/com/gchristov/newsfeed/android/common/compose/elements/AppRipple.kt: -------------------------------------------------------------------------------- 1 | package com.gchristov.newsfeed.android.common.compose.elements 2 | 3 | import androidx.compose.foundation.Indication 4 | import androidx.compose.runtime.Composable 5 | 6 | @Composable 7 | fun rememberRipple(): Indication { 8 | return androidx.compose.material.ripple.rememberRipple() 9 | } -------------------------------------------------------------------------------- /android/feed/feature/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.newsfeed.android.feature) 3 | } 4 | 5 | android { 6 | defaultConfig { 7 | namespace = "com.gchristov.newsfeed.android.feed.feature" 8 | } 9 | } 10 | 11 | dependencies { 12 | implementation(projects.multiplatform.feed.feature) 13 | androidTestImplementation(projects.android.feed.testFixtures) 14 | } -------------------------------------------------------------------------------- /android/feed/test-fixtures/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.newsfeed.android.module) 3 | } 4 | 5 | android { 6 | defaultConfig { 7 | namespace = "com.gchristov.newsfeed.android.feed.testfixtures" 8 | } 9 | } 10 | 11 | dependencies { 12 | implementation(projects.android.common.composeTest) 13 | implementation(projects.android.feed.feature) 14 | } 15 | -------------------------------------------------------------------------------- /android/post/feature/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.newsfeed.android.feature) 3 | } 4 | 5 | android { 6 | defaultConfig { 7 | namespace = "com.gchristov.newsfeed.android.post.feature" 8 | } 9 | } 10 | 11 | dependencies { 12 | implementation(projects.multiplatform.post.feature) 13 | androidTestImplementation(projects.android.post.testFixtures) 14 | } -------------------------------------------------------------------------------- /android/post/test-fixtures/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.newsfeed.android.module) 3 | } 4 | 5 | android { 6 | defaultConfig { 7 | namespace = "com.gchristov.newsfeed.android.post.testfixtures" 8 | } 9 | } 10 | 11 | dependencies { 12 | implementation(projects.android.common.composeTest) 13 | implementation(projects.android.post.feature) 14 | } 15 | -------------------------------------------------------------------------------- /multiplatform/feed/data/src/commonMain/kotlin/com/gchristov/newsfeed/multiplatform/feed/data/db/FeedFilterMapper.kt: -------------------------------------------------------------------------------- 1 | package com.gchristov.newsfeed.multiplatform.feed.data.db 2 | 3 | import com.gchristov.newsfeed.multiplatform.feed.data.model.FeedFilter 4 | 5 | internal fun FeedFilter.toFeedFilter() = DbFeedFilter(query = query) 6 | 7 | internal fun DbFeedFilter.toFeedFilter() = FeedFilter(query = query) -------------------------------------------------------------------------------- /multiplatform/common/mvvm/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.newsfeed.mpl.module) 3 | } 4 | 5 | android { 6 | defaultConfig { 7 | namespace = "com.gchristov.newsfeed.multiplatform.common.mvvm" 8 | } 9 | } 10 | 11 | kotlin { 12 | sourceSets { 13 | commonMain.dependencies { 14 | api(libs.moko.mvvm.livedata) 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /android/common/firebase/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.newsfeed.android.module) 3 | } 4 | 5 | android { 6 | defaultConfig { 7 | namespace = "com.gchristov.newsfeed.android.common.firebase" 8 | } 9 | } 10 | 11 | dependencies { 12 | api(platform(libs.google.firebase)) 13 | implementation(libs.google.crashlytics) 14 | implementation(libs.google.analytics) 15 | } 16 | -------------------------------------------------------------------------------- /android/common/compose-test/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /multiplatform/feed/test-fixtures/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.newsfeed.mpl.module) 3 | } 4 | 5 | android { 6 | defaultConfig { 7 | namespace = "com.gchristov.newsfeed.multiplatform.feed.testfixtures" 8 | } 9 | } 10 | 11 | kotlin { 12 | sourceSets { 13 | commonMain.dependencies { 14 | implementation(projects.multiplatform.feed.data) 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /multiplatform/post/test-fixtures/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.newsfeed.mpl.module) 3 | } 4 | 5 | android { 6 | defaultConfig { 7 | namespace = "com.gchristov.newsfeed.multiplatform.post.testfixtures" 8 | } 9 | } 10 | 11 | kotlin { 12 | sourceSets { 13 | commonMain.dependencies { 14 | implementation(projects.multiplatform.post.data) 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /gradle-plugins/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 2 | 3 | dependencyResolutionManagement { 4 | repositories { 5 | google() 6 | gradlePluginPortal() 7 | } 8 | versionCatalogs { 9 | create("libs") { 10 | from(files("../gradle/libs.versions.toml")) 11 | } 12 | } 13 | } 14 | 15 | rootProject.name = "gradle-plugins" 16 | 17 | include(":conventions") -------------------------------------------------------------------------------- /multiplatform/auth/data/build.gradle.kts: -------------------------------------------------------------------------------- 1 | val packageId = "com.gchristov.newsfeed.multiplatform.auth.data" 2 | 3 | plugins { 4 | alias(libs.plugins.newsfeed.mpl.data) 5 | } 6 | 7 | android { 8 | defaultConfig { 9 | namespace = packageId 10 | } 11 | } 12 | 13 | sqldelight { 14 | databases { 15 | create("AuthSqlDelightDatabase") { 16 | packageName.set(packageId) 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /android/feed/feature/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Added to favourites 4 | No results found. Please\ntry another search term 5 | This week 6 | Last week 7 | This month 8 | -------------------------------------------------------------------------------- /multiplatform/common/test/src/commonMain/kotlin/com/gchristov/newsfeed/multiplatform/common/test/FakeCoroutineDispatcher.kt: -------------------------------------------------------------------------------- 1 | package com.gchristov.newsfeed.multiplatform.common.test 2 | 3 | import kotlinx.coroutines.CoroutineDispatcher 4 | import kotlinx.coroutines.Dispatchers 5 | 6 | /** 7 | * Use this dispatcher to execute coroutines instantly and sequentially in unit tests. 8 | */ 9 | val FakeCoroutineDispatcher: CoroutineDispatcher = Dispatchers.Unconfined -------------------------------------------------------------------------------- /gradle-plugins/conventions/src/main/kotlin/com/gchristov/newsfeed/gradleplugins/multiplatform/MplBuildConfigPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.gchristov.newsfeed.gradleplugins.multiplatform 2 | 3 | import org.gradle.api.Plugin 4 | import org.gradle.api.Project 5 | 6 | class MplBuildConfigPlugin : Plugin { 7 | override fun apply(target: Project) { 8 | target.run { 9 | plugins.apply("com.codingfeline.buildkonfig") 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /multiplatform/post/feature/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.newsfeed.mpl.feature) 3 | } 4 | 5 | android { 6 | defaultConfig { 7 | namespace = "com.gchristov.newsfeed.multiplatform.post.feature" 8 | } 9 | } 10 | 11 | kotlin { 12 | sourceSets { 13 | commonMain.dependencies { 14 | api(projects.multiplatform.post.data) 15 | api(projects.multiplatform.post.testFixtures) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ios/CommonDesign/Resources/Assets.xcassets/black.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.000", 9 | "green" : "0.000", 10 | "red" : "0.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ios/CommonDesign/Resources/Assets.xcassets/black_2.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.082", 9 | "green" : "0.082", 10 | "red" : "0.082" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ios/CommonDesign/Resources/Assets.xcassets/black_3.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.157", 9 | "green" : "0.153", 10 | "red" : "0.145" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ios/CommonDesign/Resources/Assets.xcassets/blue.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.635", 9 | "green" : "0.122", 10 | "red" : "0.482" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ios/CommonDesign/Resources/Assets.xcassets/blue_2.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.659", 9 | "green" : "0.176", 10 | "red" : "0.318" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ios/CommonDesign/Resources/Assets.xcassets/blue_3.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.624", 9 | "green" : "0.247", 10 | "red" : "0.188" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ios/CommonDesign/Resources/Assets.xcassets/blue_4.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.824", 9 | "green" : "0.463", 10 | "red" : "0.098" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ios/CommonDesign/Resources/Assets.xcassets/blue_5.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.820", 9 | "green" : "0.533", 10 | "red" : "0.008" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ios/CommonDesign/Resources/Assets.xcassets/blue_6.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.655", 9 | "green" : "0.592", 10 | "red" : "0.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ios/CommonDesign/Resources/Assets.xcassets/brown.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.216", 9 | "green" : "0.251", 10 | "red" : "0.365" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ios/CommonDesign/Resources/Assets.xcassets/gray.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.380", 9 | "green" : "0.380", 10 | "red" : "0.380" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ios/CommonDesign/Resources/Assets.xcassets/gray_2.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.392", 9 | "green" : "0.353", 10 | "red" : "0.271" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ios/CommonDesign/Resources/Assets.xcassets/gray_3.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.753", 9 | "green" : "0.753", 10 | "red" : "0.753" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ios/CommonDesign/Resources/Assets.xcassets/gray_4.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.937", 9 | "green" : "0.937", 10 | "red" : "0.937" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ios/CommonDesign/Resources/Assets.xcassets/green.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.420", 9 | "green" : "0.475", 10 | "red" : "0.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ios/CommonDesign/Resources/Assets.xcassets/green_2.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.235", 9 | "green" : "0.557", 10 | "red" : "0.220" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ios/CommonDesign/Resources/Assets.xcassets/green_3.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.220", 9 | "green" : "0.624", 10 | "red" : "0.408" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ios/CommonDesign/Resources/Assets.xcassets/green_4.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.169", 9 | "green" : "0.706", 10 | "red" : "0.686" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ios/CommonDesign/Resources/Assets.xcassets/red.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.376", 9 | "green" : "0.106", 10 | "red" : "0.847" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ios/CommonDesign/Resources/Assets.xcassets/red_2.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.357", 9 | "green" : "0.094", 10 | "red" : "0.761" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ios/CommonDesign/Resources/Assets.xcassets/red_3.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.184", 9 | "green" : "0.184", 10 | "red" : "0.827" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ios/CommonDesign/Resources/Assets.xcassets/white.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "1.000", 9 | "green" : "1.000", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ios/CommonDesign/Resources/Assets.xcassets/yellow.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.000", 9 | "green" : "0.800", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ios/CommonDesign/Resources/Assets.xcassets/yellow_2.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.176", 9 | "green" : "0.753", 10 | "red" : "0.984" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ios/CommonDesign/Resources/Assets.xcassets/yellow_3.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.000", 9 | "green" : "0.627", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ios/CommonDesign/Resources/Assets.xcassets/yellow_4.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.000", 9 | "green" : "0.486", 10 | "red" : "0.961" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ios/CommonDesign/Resources/Assets.xcassets/yellow_5.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.098", 9 | "green" : "0.290", 10 | "red" : "0.902" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /multiplatform/auth/data/src/commonMain/sqldelight/com/gchristov/newsfeed/multiplatform/auth/data/AuthSqlDelightDatabase.sq: -------------------------------------------------------------------------------- 1 | CREATE TABLE UserSession ( 2 | userId TEXT NOT NULL PRIMARY KEY, 3 | userName TEXT NOT NULL 4 | ); 5 | CREATE INDEX user_session_id ON UserSession(userId); 6 | 7 | clearSession: 8 | DELETE 9 | FROM UserSession; 10 | 11 | getSession: 12 | SELECT * 13 | FROM UserSession; 14 | 15 | insertSession: 16 | INSERT OR REPLACE INTO UserSession(userId, userName) 17 | VALUES (?, ?); -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/gchristov/newsfeed/android/app/NewsfeedApp.kt: -------------------------------------------------------------------------------- 1 | package com.gchristov.newsfeed.android.app 2 | 3 | import android.app.Application 4 | import com.gchristov.newsfeed.multiplatform.common.kotlin.di.DependencyInjector 5 | 6 | class NewsfeedApp : Application() { 7 | override fun onCreate() { 8 | super.onCreate() 9 | setupDependencyInjection() 10 | } 11 | 12 | private fun setupDependencyInjection() { 13 | DependencyInjector.initialise(this) 14 | } 15 | } -------------------------------------------------------------------------------- /multiplatform/common/firebase/src/androidMain/kotlin/com/gchristov/newsfeed/multiplatform/common/firebase/AndroidCommonFirebaseModule.kt: -------------------------------------------------------------------------------- 1 | package com.gchristov.newsfeed.multiplatform.common.firebase 2 | 3 | import com.gchristov.newsfeed.multiplatform.common.kotlin.AppContext 4 | import dev.gitlive.firebase.Firebase 5 | import dev.gitlive.firebase.FirebaseApp 6 | import dev.gitlive.firebase.initialize 7 | 8 | internal actual fun provideFirebaseApp(): FirebaseApp = 9 | requireNotNull(Firebase.initialize(context = AppContext)) -------------------------------------------------------------------------------- /multiplatform/common/kotlin/src/commonMain/kotlin/com/gchristov/newsfeed/multiplatform/common/kotlin/di/DependencyModule.kt: -------------------------------------------------------------------------------- 1 | package com.gchristov.newsfeed.multiplatform.common.kotlin.di 2 | 3 | import org.kodein.di.DI 4 | 5 | abstract class DependencyModule { 6 | abstract fun name(): String 7 | 8 | abstract fun bindDependencies(builder: DI.Builder) 9 | 10 | val module: DI.Module 11 | get() = DI.Module(name = name()) { 12 | bindDependencies(builder = this) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /ios/CommonSwiftUi/Sources/Elements/AppProgress.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public struct AppCircularProgressIndicator: View { 4 | @EnvironmentObject var theme: Theme 5 | private let tint: Color? 6 | 7 | public init(tint: Color? = nil) { 8 | self.tint = tint 9 | } 10 | 11 | public var body: some View { 12 | ProgressView() 13 | .progressViewStyle(CircularProgressViewStyle(tint: tint ?? theme.contentColors.action)) 14 | .accessibilityLabel("Loading") 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /ios/CommonSwiftUi/Sources/Theme/Background.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import CommonDesign 3 | 4 | public struct Backgrounds { 5 | public let primary: Color 6 | public let surface: Color 7 | } 8 | 9 | func lightBackgrounds() -> Backgrounds { 10 | return Backgrounds( 11 | primary: Color.appGray4, 12 | surface: Color.appWhite 13 | ) 14 | } 15 | 16 | func darkBackgrounds() -> Backgrounds { 17 | return Backgrounds( 18 | primary: Color.appBlack, 19 | surface: Color.appBlack2 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /multiplatform/common/mvvm-test/src/androidMain/kotlin/com/gchristov/newsfeed/multiplatform/common/mvvmtest/CommonViewModelTestClass.kt: -------------------------------------------------------------------------------- 1 | package com.gchristov.newsfeed.multiplatform.common.mvvmtest 2 | 3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule 4 | import kotlinx.coroutines.ExperimentalCoroutinesApi 5 | import org.junit.Rule 6 | 7 | @ExperimentalCoroutinesApi 8 | actual open class CommonViewModelTestClass { 9 | // Allows testing of LiveData 10 | @get:Rule 11 | val rule = InstantTaskExecutorRule() 12 | } -------------------------------------------------------------------------------- /multiplatform/feed/feature/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.newsfeed.mpl.feature) 3 | } 4 | 5 | android { 6 | defaultConfig { 7 | namespace = "com.gchristov.newsfeed.multiplatform.feed.feature" 8 | } 9 | } 10 | 11 | kotlin { 12 | sourceSets { 13 | commonMain.dependencies { 14 | api(projects.multiplatform.feed.data) 15 | api(projects.multiplatform.feed.testFixtures) 16 | api(projects.multiplatform.post.testFixtures) // Needed for fake post repository 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /android/common/compose/src/main/kotlin/com/gchristov/newsfeed/android/common/compose/elements/AppProgress.kt: -------------------------------------------------------------------------------- 1 | package com.gchristov.newsfeed.android.common.compose.elements 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.Modifier 5 | import com.gchristov.newsfeed.android.common.compose.theme.Theme 6 | 7 | @Composable 8 | fun AppCircularProgressIndicator(modifier: Modifier = Modifier) { 9 | androidx.compose.material.CircularProgressIndicator( 10 | modifier = modifier, 11 | color = Theme.contentColors.action, 12 | ) 13 | } -------------------------------------------------------------------------------- /ios/CommonSwiftUi/Sources/Theme/Typography.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public struct Typography { 4 | public let title: Font 5 | public let subtitle: Font 6 | public let body: Font 7 | public let bodyBold: Font 8 | public let caption: Font 9 | } 10 | 11 | func typography() -> Typography { 12 | return Typography( 13 | title: Font.system(size: 24), 14 | subtitle: Font.system(size: 15), 15 | body: Font.system(size: 18), 16 | bodyBold: Font.system(size: 18, weight: .bold), 17 | caption: Font.system(size: 12) 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /multiplatform/common/firebase/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.newsfeed.mpl.module) 3 | } 4 | 5 | android { 6 | defaultConfig { 7 | namespace = "com.gchristov.newsfeed.multiplatform.common.firebase" 8 | } 9 | } 10 | 11 | kotlin { 12 | sourceSets { 13 | commonMain.dependencies { 14 | api(libs.gitlive.firebase.firestore) 15 | api(libs.gitlive.firebase.analytics) 16 | implementation(libs.touchlab.kermit.crashlytics) 17 | implementation(libs.touchlab.crashkios) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /multiplatform/post/data/src/commonMain/sqldelight/com/gchristov/newsfeed/multiplatform/post/data/PostSqlDelightDatabase.sq: -------------------------------------------------------------------------------- 1 | CREATE TABLE Post ( 2 | id TEXT NOT NULL PRIMARY KEY, 3 | date TEXT NOT NULL, 4 | headline TEXT, 5 | body TEXT, 6 | thumbnail TEXT 7 | ); 8 | CREATE INDEX post_id ON Post(id); 9 | 10 | deletePost: 11 | DELETE 12 | FROM Post 13 | WHERE Post.id == ?; 14 | 15 | selectPostWithId: 16 | SELECT * 17 | FROM Post 18 | WHERE Post.id == ?; 19 | 20 | insertPost: 21 | INSERT OR REPLACE INTO Post(id, date, headline, body, thumbnail) 22 | VALUES (?, ?, ?, ?, ?); -------------------------------------------------------------------------------- /multiplatform/feed/feature/src/commonMain/kotlin/com/gchristov/newsfeed/multiplatform/feed/feature/SearchWidgetState.kt: -------------------------------------------------------------------------------- 1 | package com.gchristov.newsfeed.multiplatform.feed.feature 2 | 3 | // We can model this with a sealed class and pass in the current value directly, eg: 4 | // 5 | // sealed class SearchBarState { 6 | // object Closed: SearchBarState() 7 | // data class Opened(val text: String) 8 | // } 9 | // 10 | // TODO: Remodel this and move to AppSearchBar 11 | // https://github.com/gchristov/newsfeed-kmm/issues/20 12 | enum class SearchWidgetState { 13 | OPENED, 14 | CLOSED 15 | } -------------------------------------------------------------------------------- /multiplatform/post/data/build.gradle.kts: -------------------------------------------------------------------------------- 1 | val packageId = "com.gchristov.newsfeed.multiplatform.post.data" 2 | 3 | plugins { 4 | alias(libs.plugins.newsfeed.mpl.data) 5 | } 6 | 7 | android { 8 | defaultConfig { 9 | namespace = packageId 10 | } 11 | } 12 | 13 | sqldelight { 14 | databases { 15 | create("PostSqlDelightDatabase") { 16 | packageName.set(packageId) 17 | } 18 | } 19 | } 20 | 21 | kotlin { 22 | sourceSets { 23 | commonMain.dependencies { 24 | implementation(projects.multiplatform.common.firebase) 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /android/common/compose/src/main/kotlin/com/gchristov/newsfeed/android/common/compose/elements/AppScreen.kt: -------------------------------------------------------------------------------- 1 | package com.gchristov.newsfeed.android.common.compose.elements 2 | 3 | import androidx.compose.material.Scaffold 4 | import androidx.compose.runtime.Composable 5 | import com.gchristov.newsfeed.android.common.compose.theme.Theme 6 | 7 | @Composable 8 | fun AppScreen( 9 | topBar: @Composable () -> Unit = {}, 10 | content: @Composable () -> Unit 11 | ) { 12 | Scaffold( 13 | topBar = topBar, 14 | backgroundColor = Theme.backgrounds.primary, 15 | ) { 16 | content() 17 | } 18 | } -------------------------------------------------------------------------------- /multiplatform/common/firebase/src/iosMain/kotlin/com/gchristov/newsfeed/multiplatform/common/firebase/IosCommonFirebaseModule.kt: -------------------------------------------------------------------------------- 1 | package com.gchristov.newsfeed.multiplatform.common.firebase 2 | 3 | import co.touchlab.crashkios.crashlytics.setCrashlyticsUnhandledExceptionHook 4 | import dev.gitlive.firebase.Firebase 5 | import dev.gitlive.firebase.FirebaseApp 6 | import dev.gitlive.firebase.initialize 7 | 8 | internal actual fun provideFirebaseApp(): FirebaseApp { 9 | // Allows catching unhandled exceptions on iOS 10 | setCrashlyticsUnhandledExceptionHook() 11 | return requireNotNull(Firebase.initialize()) 12 | } -------------------------------------------------------------------------------- /android/common/compose-test/src/main/kotlin/com/gchristov/newsfeed/android/common/composetest/Finders.kt: -------------------------------------------------------------------------------- 1 | package com.gchristov.newsfeed.android.common.composetest 2 | 3 | import androidx.compose.ui.semantics.ProgressBarRangeInfo 4 | import androidx.compose.ui.test.SemanticsNodeInteraction 5 | import androidx.compose.ui.test.SemanticsNodeInteractionsProvider 6 | import androidx.compose.ui.test.hasProgressBarRangeInfo 7 | 8 | /* Indeterminate progress */ 9 | 10 | fun SemanticsNodeInteractionsProvider.onIndeterminateProgress(): SemanticsNodeInteraction { 11 | return onNode(hasProgressBarRangeInfo(ProgressBarRangeInfo.Indeterminate)) 12 | } -------------------------------------------------------------------------------- /ios/CommonSwiftUi/Sources/Elements/AppSurface.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public struct AppSurface: View where Content: View { 4 | @EnvironmentObject var theme: Theme 5 | private let content: () -> Content 6 | 7 | public init(@ViewBuilder content: @escaping () -> Content) { 8 | self.content = content 9 | } 10 | 11 | public var body: some View { 12 | ZStack { 13 | content() 14 | } 15 | .padding(16) 16 | .background(theme.backgrounds.surface) 17 | .clipShape(theme.shapes.surface) 18 | .shadow(radius: 2, x: 0.2, y: 0.5) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /android/common/test/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.newsfeed.android.base) // Avoids circular dependencies in android-module-plugin 3 | } 4 | 5 | android { 6 | defaultConfig { 7 | namespace = "com.gchristov.newsfeed.android.common.test" 8 | } 9 | } 10 | 11 | /* 12 | This module is used in other test modules. Common dependencies are linked to the 'main' 13 | source sets, and marked as `api`, rather than 'test'. This is because 'test' source-specific 14 | dependencies and code are local to the relevant module and cannot be accesses by other modules. 15 | */ 16 | dependencies { 17 | api(libs.junit) 18 | } 19 | -------------------------------------------------------------------------------- /gradle-plugins/conventions/src/main/kotlin/com/gchristov/newsfeed/gradleplugins/android/AndroidBinaryPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.gchristov.newsfeed.gradleplugins.android 2 | 3 | import com.gchristov.newsfeed.gradleplugins.libs 4 | import org.gradle.api.Plugin 5 | import org.gradle.api.Project 6 | 7 | class AndroidApplicationBinaryPlugin : Plugin { 8 | override fun apply(target: Project) { 9 | with (target) { 10 | with (plugins) { 11 | apply("com.android.application") 12 | apply(libs.findPlugin("newsfeed-android-base").get().get().pluginId) 13 | } 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /multiplatform/common/kotlin/src/commonMain/kotlin/com/gchristov/newsfeed/multiplatform/common/kotlin/Log.kt: -------------------------------------------------------------------------------- 1 | package com.gchristov.newsfeed.multiplatform.common.kotlin 2 | 3 | import co.touchlab.kermit.Logger 4 | 5 | fun Logger.debug(tag: String?, message: String) = d(logMessage(tag, message)) 6 | 7 | fun Logger.debug(tag: String?, throwable: Throwable, message: () -> String) = d(throwable) { logMessage(tag, message()) } 8 | 9 | fun Logger.error(tag: String?, throwable: Throwable, message: () -> String) = e(throwable) { logMessage(tag, message()) } 10 | 11 | private fun logMessage(tag: String?, message: String) = "[${tag ?: "Anonymous"}] $message" -------------------------------------------------------------------------------- /multiplatform/post/test-fixtures/src/commonMain/kotlin/com/gchristov/newsfeed/multiplatform/post/testfixtures/PostCreator.kt: -------------------------------------------------------------------------------- 1 | package com.gchristov.newsfeed.multiplatform.post.testfixtures 2 | 3 | import com.gchristov.newsfeed.multiplatform.post.data.Post 4 | 5 | object PostCreator { 6 | fun post( 7 | id: String = "post_123", 8 | title: String = "Post Title", 9 | body: String = "This is a sample post body", 10 | date: String = "2022-02-21T00:00:00Z", 11 | ): Post = Post( 12 | id = id, 13 | date = date, 14 | headline = title, 15 | body = body, 16 | thumbnail = null, 17 | ) 18 | } -------------------------------------------------------------------------------- /gradle-plugins/conventions/src/main/kotlin/com/gchristov/newsfeed/gradleplugins/BaseExtensionExt.kt: -------------------------------------------------------------------------------- 1 | package com.gchristov.newsfeed.gradleplugins 2 | 3 | import com.android.build.gradle.BaseExtension 4 | 5 | internal fun BaseExtension.configureAndroid() { 6 | compileSdkVersion(34) 7 | defaultConfig { 8 | minSdk = 21 9 | targetSdk = 34 10 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 11 | } 12 | // Resolves "N files found with path 'META-INF/XXX'" errors 13 | packagingOptions { 14 | resources.excludes.add("META-INF/AL2.0") 15 | resources.excludes.add("META-INF/LGPL2.1") 16 | } 17 | } -------------------------------------------------------------------------------- /multiplatform/umbrella/src/commonMain/kotlin/com/gchristov/newsfeed/multiplatform/umbrella/Dispatcher.kt: -------------------------------------------------------------------------------- 1 | package com.gchristov.newsfeed.multiplatform.umbrella 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | 5 | /** 6 | * Exposes coroutine dispatchers to native targets, 7 | * 8 | * Exposing the coroutines dependency directly trigger some link errors so we use a simple wrapper. 9 | */ 10 | @Suppress("unused") 11 | object Dispatcher { 12 | @Suppress("unused") 13 | val Default = Dispatchers.Default 14 | 15 | @Suppress("unused") 16 | val Main = Dispatchers.Main 17 | 18 | @Suppress("unused") 19 | val Unconfined = Dispatchers.Unconfined 20 | } -------------------------------------------------------------------------------- /.github/actions/build-android/action.yml: -------------------------------------------------------------------------------- 1 | name: 'build-android' 2 | runs: 3 | using: "composite" 4 | steps: 5 | - name: Set up Gradle 6 | uses: ./.github/actions/setup-gradle 7 | - name: Build Android 8 | shell: bash 9 | run: | 10 | set -Eeuo pipefail 11 | ./gradlew android:app:assembleDebug 12 | - name: Artifacts 13 | uses: actions/upload-artifact@v4 14 | if: always() # Ensure all artifacts are collected, even after errors 15 | with: 16 | name: Android Build 17 | path: | 18 | **/*.apk 19 | **/build 20 | **/secrets.properties 21 | **/google-services.json -------------------------------------------------------------------------------- /.github/actions/test-ios/action.yml: -------------------------------------------------------------------------------- 1 | name: 'test-ios' 2 | runs: 3 | using: "composite" 4 | steps: 5 | - name: Set up Gradle 6 | uses: ./.github/actions/setup-gradle 7 | - name: Set up Xcode 8 | uses: ./.github/actions/setup-xcode 9 | - name: Test iOS 10 | shell: bash 11 | run: | 12 | set -Eeuo pipefail 13 | cd ios 14 | tuist test 15 | cd .. 16 | - name: Artifacts 17 | uses: actions/upload-artifact@v4 18 | if: always() # Ensure all artifacts are collected, even after errors 19 | with: 20 | name: iOS Tests 21 | path: | 22 | /Users/runner/.local/state/tuist/**/*.log -------------------------------------------------------------------------------- /tools/scripts/secrets.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | # Exports required CI environment secrets to local secrets so that the project can use them. 4 | # Should be invoked from the root of the project as all paths are relative. 5 | 6 | # network credentials (the > is intentional to append to a file) 7 | echo GUARDIAN_API_KEY="$GUARDIAN_API_KEY" >> multiplatform/common/network/secrets.properties 8 | echo GUARDIAN_API_URL="$GUARDIAN_API_URL" >> multiplatform/common/network/secrets.properties 9 | 10 | # Firebase credentials 11 | echo "$GOOGLE_SERVICES_JSON" >> android/app/google-services.json 12 | echo "$GOOGLE_SERVICE_INFO_PLIST" >> ios/App/Resources/GoogleService-Info.plist 13 | -------------------------------------------------------------------------------- /ios/App/Configs/Target.xcconfig: -------------------------------------------------------------------------------- 1 | ASSETCATALOG_COMPILER_APPICON_NAME=AppIcon 2 | CODE_SIGN_STYLE=Automatic 3 | DEVELOPMENT_TEAM=5G893F5HW7 4 | ENABLE_PREVIEWS=YES 5 | INFOPLIST_FILE=app/Info.plist 6 | OTHER_LDFLAGS= 7 | PRODUCT_BUNDLE_IDENTIFIER=com.gchristov.newsfeed 8 | PRODUCT_NAME=$(TARGET_NAME) 9 | SUPPORTED_PLATFORMS=iphoneos iphonesimulator 10 | SUPPORTS_MACCATALYST=NO 11 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD=NO 12 | SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD=NO 13 | SWIFT_VERSION=5.0 14 | TARGETED_DEVICE_FAMILY=1 15 | 16 | LD_RUNPATH_SEARCH_PATHS[config=Debug]=$(inherited) @executable_path/Frameworks 17 | LD_RUNPATH_SEARCH_PATHS[config=Release]=$(inherited) @executable_path/Frameworks -------------------------------------------------------------------------------- /multiplatform/common/persistence/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.newsfeed.mpl.module) 3 | } 4 | 5 | android { 6 | defaultConfig { 7 | namespace = "com.gchristov.newsfeed.multiplatform.common.persistence" 8 | } 9 | } 10 | 11 | kotlin { 12 | sourceSets { 13 | commonMain.dependencies { 14 | implementation(libs.sqlDelight.core) 15 | api(libs.multiplatformSettings) 16 | } 17 | androidMain.dependencies { 18 | implementation(libs.sqlDelight.android) 19 | } 20 | iosMain.dependencies { 21 | implementation(libs.sqlDelight.native) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ios/CommonSwiftUi/Sources/Elements/AppAvatar.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import LetterAvatarKit 3 | 4 | public struct AppAvatar: View { 5 | private let name: String 6 | private let size: CGSize 7 | 8 | public init( 9 | name: String, 10 | size: CGSize 11 | ) { 12 | self.name = name 13 | self.size = size 14 | } 15 | 16 | public var body: some View { 17 | let avatar = LetterAvatarMaker() 18 | .setCircle(true) 19 | .setUsername(name) 20 | .useSingleLetter(true) 21 | .setSize(size) 22 | .build() ?? UIImage() 23 | 24 | Image(uiImage: avatar) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /multiplatform/common/kotlin/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.newsfeed.mpl.base) 3 | } 4 | 5 | android { 6 | defaultConfig { 7 | namespace = "com.gchristov.newsfeed.multiplatform.common.kotlin" 8 | } 9 | } 10 | 11 | kotlin { 12 | sourceSets { 13 | commonMain.dependencies { 14 | api(libs.kodein) 15 | api(libs.touchlab.kermit) 16 | api(libs.kotlinx.coroutines.core) 17 | api(libs.kotlinx.datetime) 18 | api(libs.kotlinx.serialization) 19 | api(libs.arrow.core) 20 | } 21 | androidMain.dependencies { 22 | api(libs.kotlinx.coroutines.android) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /android/common/navigation/src/main/kotlin/com/gchristov/newsfeed/android/common/navigation/NavigationModule.kt: -------------------------------------------------------------------------------- 1 | package com.gchristov.newsfeed.android.common.navigation 2 | 3 | import android.content.Context 4 | import com.gchristov.newsfeed.multiplatform.common.kotlin.di.DependencyModule 5 | import org.kodein.di.DI 6 | import org.kodein.di.bindFactory 7 | 8 | object NavigationModule : DependencyModule() { 9 | override fun name() = "common-navigation" 10 | 11 | override fun bindDependencies(builder: DI.Builder) { 12 | builder.apply { 13 | bindFactory { context: Context -> 14 | RealNavigator(context = context) 15 | } 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /multiplatform/common/mvvm-test/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.newsfeed.mpl.module) 3 | } 4 | 5 | android { 6 | defaultConfig { 7 | namespace = "com.gchristov.newsfeed.multiplatform.common.mvvmtest" 8 | } 9 | } 10 | 11 | kotlin { 12 | /* 13 | This module is used in other test modules. Common dependencies are linked to the 'main' 14 | source sets, and marked as `api`, rather than 'test'. This is because 'test' source-specific 15 | dependencies and code are local to the relevant module and cannot be accesses by other modules. 16 | */ 17 | sourceSets { 18 | commonMain.dependencies { 19 | api(libs.moko.mvvm.test) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /multiplatform/common/persistence/src/iosMain/kotlin/com/gchristov/newsfeed/multiplatform/common/persistence/IosCommonPersistenceModule.kt: -------------------------------------------------------------------------------- 1 | package com.gchristov.newsfeed.multiplatform.common.persistence 2 | 3 | import app.cash.sqldelight.db.SqlDriver 4 | import app.cash.sqldelight.driver.native.NativeSqliteDriver 5 | import com.russhwolf.settings.NSUserDefaultsSettings 6 | import com.russhwolf.settings.Settings 7 | 8 | internal actual fun provideSqlDriver(properties: SqlDriverProperties): SqlDriver = 9 | NativeSqliteDriver( 10 | schema = properties.schema, 11 | name = properties.databaseName 12 | ) 13 | 14 | internal actual fun provideSharedPreferences(): Settings = NSUserDefaultsSettings.Factory().create() -------------------------------------------------------------------------------- /gradle-plugins/conventions/src/main/kotlin/com/gchristov/newsfeed/gradleplugins/android/AndroidComposePlugin.kt: -------------------------------------------------------------------------------- 1 | package com.gchristov.newsfeed.gradleplugins.android 2 | 3 | import com.android.build.gradle.BaseExtension 4 | import org.gradle.api.Plugin 5 | import org.gradle.api.Project 6 | import org.gradle.kotlin.dsl.configure 7 | 8 | class AndroidComposePlugin : Plugin { 9 | override fun apply(target: Project) { 10 | with(target) { 11 | extensions.configure { 12 | buildFeatures.compose = true 13 | with (plugins) { 14 | apply("org.jetbrains.kotlin.plugin.compose") 15 | } 16 | } 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /multiplatform/feed/data/build.gradle.kts: -------------------------------------------------------------------------------- 1 | val packageId = "com.gchristov.newsfeed.multiplatform.feed.data" 2 | 3 | plugins { 4 | alias(libs.plugins.newsfeed.mpl.data) 5 | } 6 | 7 | android { 8 | defaultConfig { 9 | namespace = packageId 10 | } 11 | } 12 | 13 | kotlin { 14 | sourceSets { 15 | commonMain.dependencies { 16 | api(projects.multiplatform.post.data) 17 | implementation(projects.multiplatform.auth.data) 18 | implementation(projects.multiplatform.common.firebase) 19 | } 20 | } 21 | } 22 | 23 | sqldelight { 24 | databases { 25 | create("FeedSqlDelightDatabase") { 26 | packageName.set(packageId) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /ios/CommonSwiftUi/Sources/Elements/AppScreen.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public struct AppScreen: View where Content: View { 4 | @EnvironmentObject var theme: Theme 5 | private let content: () -> Content 6 | 7 | public init(@ViewBuilder content: @escaping () -> Content) { 8 | self.content = content 9 | } 10 | 11 | public var body: some View { 12 | ZStack { 13 | content() 14 | } 15 | .frame( 16 | minWidth: 0, 17 | maxWidth: .infinity, 18 | minHeight: 0, 19 | maxHeight: .infinity 20 | ) 21 | .background(theme.backgrounds.primary.ignoresSafeArea()) // Safe are includes navigation view 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## What does this pull request change? 4 | 5 | 6 | 7 | ## Demo 8 | 9 | 10 | 11 | Before|After 12 | -|- 13 |