├── .circleci └── config.yml ├── .gitignore ├── LICENSE ├── README.md ├── android └── ponyinject │ ├── .gitignore │ ├── README.md │ ├── app │ ├── .gitignore │ ├── build.gradle.kts │ ├── proguard-rules.pro │ └── src │ │ ├── androidTest │ │ ├── assets │ │ │ ├── episode_all_1.json │ │ │ └── episode_all_2.json │ │ └── kotlin │ │ │ └── me │ │ │ └── tatarka │ │ │ └── inject │ │ │ └── ponyinject │ │ │ ├── EndToEndTest.kt │ │ │ ├── OkHttpIdlingResourceRule.kt │ │ │ ├── assert │ │ │ └── AssertExtensions.kt │ │ │ ├── detail │ │ │ └── DetailFragmentTest.kt │ │ │ └── episodes │ │ │ └── EpisodesFragmentTest.kt │ │ ├── commonTest │ │ └── kotlin │ │ │ └── me │ │ │ └── tatarka │ │ │ └── inject │ │ │ └── ponyinject │ │ │ ├── TestApplicationComponent.kt │ │ │ └── api │ │ │ └── FakeApiService.kt │ │ ├── debug │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── me │ │ │ │ └── tatarka │ │ │ │ └── inject │ │ │ │ └── api │ │ │ │ └── VariantComponent.kt │ │ └── res │ │ │ └── xml │ │ │ └── network_security_config.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── me │ │ │ │ └── tatarka │ │ │ │ └── inject │ │ │ │ └── ponyinject │ │ │ │ ├── ApplicationComponent.kt │ │ │ │ ├── ViewModelExtensions.kt │ │ │ │ ├── api │ │ │ │ ├── ApiComponent.kt │ │ │ │ ├── ApiService.kt │ │ │ │ ├── EpisodesRepository.kt │ │ │ │ └── LocalDateAdapter.kt │ │ │ │ ├── detail │ │ │ │ ├── DetailFragment.kt │ │ │ │ └── DetailViewModel.kt │ │ │ │ ├── episodes │ │ │ │ ├── EpisodesAdapter.kt │ │ │ │ ├── EpisodesFragment.kt │ │ │ │ └── EpisodesViewModel.kt │ │ │ │ ├── main │ │ │ │ ├── InjectFragmentFactory.kt │ │ │ │ ├── MainActivity.kt │ │ │ │ └── MainFragment.kt │ │ │ │ └── ui │ │ │ │ └── TwoPaneOnBackPressedCallback.kt │ │ └── res │ │ │ ├── drawable-v24 │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── drawable │ │ │ └── ic_launcher_background.xml │ │ │ ├── layout │ │ │ ├── detail_fragment.xml │ │ │ ├── episodes_fragment.xml │ │ │ ├── episodes_item.xml │ │ │ ├── main_activity.xml │ │ │ └── main_fragment.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ │ ├── navigation │ │ │ ├── detail_graph.xml │ │ │ └── main_graph.xml │ │ │ ├── values-night │ │ │ └── themes.xml │ │ │ └── values │ │ │ ├── colors.xml │ │ │ ├── strings.xml │ │ │ └── themes.xml │ │ ├── release │ │ └── java │ │ │ └── me │ │ │ └── tatarka │ │ │ └── inject │ │ │ └── api │ │ │ └── VariantComponent.kt │ │ └── test │ │ └── kotlin │ │ └── me │ │ └── tatarka │ │ └── inject │ │ └── ponyinject │ │ ├── CoroutineTestRule.kt │ │ ├── api │ │ └── EpisodesRepositoryTest.kt │ │ ├── detail │ │ └── DetailViewModelTest.kt │ │ └── episodes │ │ └── EpisodesViewModelTest.kt │ ├── build.gradle.kts │ ├── gradle.properties │ ├── gradlew │ ├── gradlew.bat │ └── settings.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── multiplatform ├── echo │ ├── .gitignore │ ├── build.gradle.kts │ ├── gradlew │ ├── gradlew.bat │ ├── settings.gradle.kts │ └── src │ │ └── commonMain │ │ └── kotlin │ │ └── App.kt └── greeter │ ├── .editorconfig │ ├── .gitignore │ ├── README.md │ ├── androidApp │ ├── build.gradle.kts │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── com │ │ │ └── fredporciuncula │ │ │ └── inject │ │ │ └── greeter │ │ │ └── android │ │ │ ├── ApplicationComponentProvider.kt │ │ │ ├── GreeterApplication.kt │ │ │ └── MainActivity.kt │ │ └── res │ │ └── values │ │ └── styles.xml │ ├── build.gradle.kts │ ├── gradle.properties │ ├── gradle │ └── libs.versions.toml │ ├── gradlew │ ├── gradlew.bat │ ├── iosApp │ ├── iosApp.xcodeproj │ │ ├── project.pbxproj │ │ └── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── iosApp │ │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ │ ├── ContentView.swift │ │ ├── Info.plist │ │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ │ └── iOSApp.swift │ ├── settings.gradle.kts │ └── shared │ ├── build.gradle.kts │ └── src │ ├── androidMain │ └── kotlin │ │ └── com │ │ └── fredporciuncula │ │ └── inject │ │ └── greeter │ │ ├── AndroidGreeter.kt │ │ ├── ApplicationComponent.kt │ │ └── PlatformComponent.kt │ ├── commonMain │ └── kotlin │ │ └── com │ │ └── fredporciuncula │ │ └── inject │ │ └── greeter │ │ ├── CommonGreeter.kt │ │ ├── Greeter.kt │ │ └── Platform.kt │ └── iosMain │ └── kotlin │ └── com │ └── fredporciuncula │ └── inject │ └── greeter │ ├── ApplicationComponent.kt │ ├── IosGreeter.kt │ └── PlatformComponent.kt ├── server └── PonyKtor │ ├── .gitignore │ ├── README.md │ ├── build.gradle.kts │ ├── gradle.properties │ ├── gradlew │ ├── gradlew.bat │ ├── ponydb.sqlite │ ├── settings.gradle.kts │ └── src │ ├── main │ └── kotlin │ │ ├── Application.kt │ │ ├── ApplicationComponent.kt │ │ ├── JsonModels.kt │ │ ├── LocalDateSerializer.kt │ │ ├── ParametersExtensions.kt │ │ ├── Router.kt │ │ ├── db │ │ ├── EpisodesDao.kt │ │ ├── PonyDb.kt │ │ └── SongsDao.kt │ │ └── episodes │ │ ├── EpisodesController.kt │ │ └── EpisodesRepository.kt │ └── test │ └── kotlin │ ├── EndToEndTest.kt │ ├── TestApplicationComponent.kt │ ├── assert │ └── AssertExtensions.kt │ ├── db │ ├── FakeEpisodesDao.kt │ └── FakeSongsDao.kt │ └── episodes │ └── EpisodesControllerTest.kt └── settings.gradle.kts /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | orbs: 3 | gradle: circleci/gradle@1.0.10 4 | android: circleci/android@2.5.0 5 | jobs: 6 | build-ponyinject: 7 | working_directory: ~/code/android/ponyinject 8 | docker: 9 | - image: cimg/android:2024.01 10 | environment: 11 | JVM_OPTS: -Xmx3200m 12 | steps: 13 | - checkout: 14 | path: ~/code 15 | - restore_cache: 16 | keys: 17 | - ponyinject-{{ checksum "build.gradle.kts" }} 18 | - run: 19 | name: Run Tests and Checks 20 | command: ./gradlew check 21 | - save_cache: 22 | paths: 23 | - ~/.gradle 24 | key: ponyinject-{{ checksum "build.gradle.kts" }} 25 | build-ponyktor: 26 | working_directory: ~/code/server/PonyKtor 27 | docker: 28 | - image: cimg/openjdk:17.0 29 | environment: 30 | JVM_OPTS: -Xmx3200m 31 | steps: 32 | - checkout: 33 | path: ~/code 34 | - restore_cache: 35 | keys: 36 | - ponyktor-{{ checksum "build.gradle.kts" }} 37 | - run: 38 | name: Run Tests and Checks 39 | command: ./gradlew check 40 | - save_cache: 41 | paths: 42 | - ~/.gradle 43 | key: ponyktor-{{ checksum "build.gradle.kts" }} 44 | build-echo: 45 | working_directory: ~/code/multiplatform/echo 46 | docker: 47 | - image: cimg/openjdk:17.0 48 | environment: 49 | JVM_OPTS: -Xmx3200m 50 | steps: 51 | - checkout: 52 | path: ~/code 53 | - restore_cache: 54 | keys: 55 | - echo-{{ checksum "build.gradle.kts" }} 56 | - run: 57 | name: Run Tests and Checks 58 | command: ./gradlew check 59 | - save_cache: 60 | paths: 61 | - ~/.gradle 62 | key: echo-{{ checksum "build.gradle.kts" }} 63 | build-greeter: 64 | working_directory: ~/code/multiplatform/greeter 65 | macos: 66 | xcode: "15.3.0" 67 | resource_class: macos.x86.medium.gen2 68 | environment: 69 | JVM_OPTS: -Xmx3200m 70 | steps: 71 | - checkout: 72 | path: ~/code 73 | - run: 74 | name: install android tools 75 | command: | 76 | brew install android-commandlinetools 77 | yes | /usr/local/share/android-commandlinetools/cmdline-tools/latest/bin/sdkmanager --licenses || true 78 | echo sdk.dir=/usr/local/share/android-commandlinetools > local.properties 79 | - restore_cache: 80 | keys: 81 | - greeter-{{ checksum "build.gradle.kts" }} 82 | - run: 83 | name: Run Tests and Checks 84 | command: ./gradlew assemble 85 | - save_cache: 86 | paths: 87 | - ~/.gradle 88 | key: greeter-{{ checksum "build.gradle.kts" }} 89 | workflows: 90 | version: 2 91 | build: 92 | jobs: 93 | - build-ponyinject 94 | - build-ponyktor 95 | - build-echo 96 | - build-greeter -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | .idea 3 | .kotlin 4 | local.properties 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kotlin-inject samples 2 | Various samples using [kotlin-inject](https://github.com/evant/kotlin-inject) 3 | -------------------------------------------------------------------------------- /android/ponyinject/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | local.properties 11 | -------------------------------------------------------------------------------- /android/ponyinject/README.md: -------------------------------------------------------------------------------- 1 | # PonyInject 2 | 3 | An Android sample app using [kotlin-inject](https://github.com/evant/kotlin-inject) that shows how 4 | to set up components for the app and tests. -------------------------------------------------------------------------------- /android/ponyinject/app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /android/ponyinject/app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | kotlin("android") 4 | id("com.google.devtools.ksp") 5 | id("androidx.navigation.safeargs.kotlin") 6 | } 7 | 8 | android { 9 | compileSdk = 34 10 | 11 | defaultConfig { 12 | namespace = "me.tatarka.inject.ponyinject" 13 | applicationId = "me.tatarka.inject.ponyinject" 14 | minSdk = 21 15 | targetSdk = 34 16 | versionCode = 1 17 | versionName = "1.0" 18 | 19 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 20 | 21 | buildConfigField("String", "BASE_URL", "\"https://ponyapi.net/\"") 22 | } 23 | 24 | buildTypes { 25 | release { 26 | isMinifyEnabled = false 27 | proguardFiles( 28 | getDefaultProguardFile("proguard-android-optimize.txt"), 29 | "proguard-rules.pro" 30 | ) 31 | } 32 | } 33 | compileOptions { 34 | isCoreLibraryDesugaringEnabled = true 35 | sourceCompatibility = JavaVersion.VERSION_11 36 | targetCompatibility = JavaVersion.VERSION_11 37 | } 38 | kotlinOptions { 39 | jvmTarget = "11" 40 | } 41 | buildFeatures { 42 | viewBinding = true 43 | buildConfig = true 44 | } 45 | 46 | sourceSets { 47 | // share code between unit and android tests 48 | get("test").kotlin { 49 | srcDir("src/commonTest/kotlin") 50 | } 51 | get("androidTest").kotlin { 52 | srcDir("src/commonTest/kotlin") 53 | } 54 | } 55 | testOptions { 56 | unitTests.isReturnDefaultValues = true 57 | managedDevices { 58 | localDevices { 59 | create("pixel2api33") { 60 | device = "Pixel 2" 61 | apiLevel = 33 62 | systemImageSource = "aosp" 63 | } 64 | } 65 | } 66 | } 67 | } 68 | 69 | dependencies { 70 | coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4") 71 | 72 | implementation("me.tatarka.inject:kotlin-inject-runtime:0.7.1") 73 | ksp("me.tatarka.inject:kotlin-inject-compiler-ksp:0.7.1") 74 | 75 | implementation("com.squareup.okhttp3:okhttp:4.11.0") 76 | debugImplementation("com.squareup.okhttp3:logging-interceptor:4.11.0") 77 | implementation("com.squareup.moshi:moshi:1.12.0") 78 | ksp("dev.zacsweers.moshix:moshi-ksp:0.12.0") 79 | implementation("com.squareup.retrofit2:retrofit:2.9.0") 80 | implementation("com.squareup.retrofit2:converter-moshi:2.9.0") 81 | 82 | implementation("io.coil-kt:coil:2.2.2") 83 | 84 | implementation("androidx.core:core-ktx:1.13.1") 85 | implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.2") 86 | implementation("androidx.appcompat:appcompat:1.7.0") 87 | implementation("androidx.fragment:fragment-ktx:1.8.0") 88 | implementation("androidx.navigation:navigation-fragment-ktx:2.7.7") 89 | implementation("androidx.navigation:navigation-ui-ktx:2.7.7") 90 | implementation("com.google.android.material:material:1.12.0") 91 | implementation("androidx.constraintlayout:constraintlayout:2.1.4") 92 | implementation("androidx.slidingpanelayout:slidingpanelayout:1.2.0") 93 | debugImplementation("androidx.fragment:fragment-testing:1.8.0") 94 | implementation("androidx.paging:paging-runtime:3.3.0") 95 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") 96 | 97 | testImplementation("junit:junit:4.13.2") 98 | testImplementation("androidx.arch.core:core-testing:2.2.0") 99 | testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3") 100 | testImplementation("com.willowtreeapps.assertk:assertk:0.28.1") 101 | testImplementation("androidx.paging:paging-testing:3.3.0") 102 | 103 | androidTestImplementation("androidx.test.ext:junit:1.1.5") 104 | androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") 105 | androidTestImplementation("com.squareup.okhttp3:mockwebserver:4.9.1") 106 | androidTestImplementation("com.jakewharton.espresso:okhttp3-idling-resource:1.0.0") 107 | androidTestImplementation("com.willowtreeapps.assertk:assertk:0.28.1") 108 | } 109 | -------------------------------------------------------------------------------- /android/ponyinject/app/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 -------------------------------------------------------------------------------- /android/ponyinject/app/src/androidTest/assets/episode_all_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": 200, 3 | "data": [ 4 | { 5 | "id": 1, 6 | "name": "Friendship is Magic, part 1", 7 | "image": "https://vignette.wikia.nocookie.net/mlp/images/9/9f/Twilight_looks_up_at_the_moon_S1E01.png/revision/latest?cb=20121209043547", 8 | "url": "https://mlp.fandom.com/wiki/Friendship_is_Magic,_part_1", 9 | "season": 1, 10 | "episode": 1, 11 | "overall": 1, 12 | "airdate": "2010-10-10", 13 | "writtenby": "Lauren Faust", 14 | "storyboard": "Tom Sales\nMike West\nSherann Johnson\nSam To" 15 | }, 16 | { 17 | "id": 2, 18 | "name": "Friendship is Magic, part 2", 19 | "image": "https://vignette.wikia.nocookie.net/mlp/images/2/2c/Main_ponies_activated_the_Elements_of_Harmony_S01E02.png/revision/latest?cb=20111205172131", 20 | "url": "https://mlp.fandom.com/wiki/Friendship_is_Magic,_part_2", 21 | "season": 1, 22 | "episode": 2, 23 | "overall": 2, 24 | "airdate": "2010-10-22", 25 | "writtenby": "Lauren Faust", 26 | "storyboard": "Tom Sales\nMike West\nSherann Johnson\nSam To", 27 | "song": [ 28 | "Laughter Song" 29 | ] 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /android/ponyinject/app/src/androidTest/assets/episode_all_2.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": 200, 3 | "data": [] 4 | } -------------------------------------------------------------------------------- /android/ponyinject/app/src/androidTest/kotlin/me/tatarka/inject/ponyinject/EndToEndTest.kt: -------------------------------------------------------------------------------- 1 | package me.tatarka.inject.ponyinject 2 | 3 | import android.app.Application 4 | import androidx.fragment.app.testing.launchFragmentInContainer 5 | import androidx.test.espresso.Espresso.onView 6 | import androidx.test.espresso.action.ViewActions.click 7 | import androidx.test.espresso.assertion.ViewAssertions.matches 8 | import androidx.test.espresso.matcher.ViewMatchers.* 9 | import androidx.test.ext.junit.runners.AndroidJUnit4 10 | import androidx.test.platform.app.InstrumentationRegistry 11 | import me.tatarka.inject.annotations.Component 12 | import me.tatarka.inject.ponyinject.main.InjectFragmentFactory 13 | import me.tatarka.inject.ponyinject.main.MainFragment 14 | import okhttp3.OkHttpClient 15 | import okhttp3.mockwebserver.MockResponse 16 | import okhttp3.mockwebserver.MockWebServer 17 | import okio.Buffer 18 | import okio.buffer 19 | import okio.source 20 | import org.hamcrest.Matchers.allOf 21 | import org.junit.Rule 22 | import org.junit.Test 23 | import org.junit.runner.RunWith 24 | 25 | /** 26 | * An example of an end-to-end test using [MockWebServer] to mock the api. 27 | */ 28 | @RunWith(AndroidJUnit4::class) 29 | class EndToEndTest { 30 | 31 | @get:Rule 32 | val mockServer = MockWebServer() 33 | 34 | private val component by lazy { 35 | val application = 36 | InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as Application 37 | TestComponent::class.create( 38 | ApplicationComponent::class.create(application, baseUrl = mockServer.url("")) 39 | ) 40 | } 41 | 42 | @get:Rule 43 | val idlingResourceRule = OkHttpIdlingResourceRule { component.client } 44 | 45 | @Test 46 | fun selects_item_in_list_help() { 47 | mockServer.enqueue(MockResponse().setBody(asset("episode_all_1.json"))) 48 | mockServer.enqueue(MockResponse().setBody(asset("episode_all_2.json"))) 49 | launchFragmentInContainer( 50 | themeResId = R.style.Theme_PonyInject, 51 | factory = component.fragmentFactory 52 | ) 53 | 54 | onView(withText("Friendship is Magic, part 2")) 55 | .perform(click()) 56 | onView( 57 | allOf( 58 | withId(R.id.title), 59 | withParent(withId(R.id.detail)) 60 | ) 61 | ).check(matches(withText("Friendship is Magic, part 2"))) 62 | } 63 | 64 | @Component 65 | abstract class TestComponent(@Component val parent: ApplicationComponent) { 66 | abstract val fragmentFactory: InjectFragmentFactory 67 | abstract val client: OkHttpClient 68 | } 69 | } 70 | 71 | private fun asset(fileName: String): Buffer { 72 | val context = InstrumentationRegistry.getInstrumentation().context 73 | val buffer = Buffer() 74 | context.assets.open(fileName).source().buffer().use { 75 | buffer.writeAll(it) 76 | } 77 | return buffer 78 | } -------------------------------------------------------------------------------- /android/ponyinject/app/src/androidTest/kotlin/me/tatarka/inject/ponyinject/OkHttpIdlingResourceRule.kt: -------------------------------------------------------------------------------- 1 | package me.tatarka.inject.ponyinject 2 | 3 | import androidx.test.espresso.IdlingRegistry 4 | import androidx.test.espresso.IdlingResource 5 | import com.jakewharton.espresso.OkHttp3IdlingResource 6 | import okhttp3.OkHttpClient 7 | import org.junit.rules.TestWatcher 8 | import org.junit.runner.Description 9 | 10 | /** 11 | * Adds an [IdlingResource] for okhttp to espresso. 12 | */ 13 | class OkHttpIdlingResourceRule(private val client: () -> OkHttpClient) : TestWatcher() { 14 | private var resource: IdlingResource? = null 15 | 16 | override fun starting(description: Description?) { 17 | resource = OkHttp3IdlingResource.create("OkHttp", client()) 18 | IdlingRegistry.getInstance().register(resource) 19 | } 20 | 21 | override fun finished(description: Description?) { 22 | IdlingRegistry.getInstance().unregister(resource) 23 | resource = null 24 | } 25 | } -------------------------------------------------------------------------------- /android/ponyinject/app/src/androidTest/kotlin/me/tatarka/inject/ponyinject/assert/AssertExtensions.kt: -------------------------------------------------------------------------------- 1 | package me.tatarka.inject.ponyinject.assert 2 | 3 | import android.widget.TextView 4 | import assertk.Assert 5 | import assertk.assertions.isEqualTo 6 | import assertk.assertions.prop 7 | 8 | fun Assert.hasText(text: String) = 9 | prop("text") { it.text.toString() }.isEqualTo(text) -------------------------------------------------------------------------------- /android/ponyinject/app/src/androidTest/kotlin/me/tatarka/inject/ponyinject/detail/DetailFragmentTest.kt: -------------------------------------------------------------------------------- 1 | package me.tatarka.inject.ponyinject.detail 2 | 3 | import android.view.LayoutInflater 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import assertk.all 6 | import assertk.assertThat 7 | import assertk.assertions.prop 8 | import me.tatarka.inject.ponyinject.assert.hasText 9 | import me.tatarka.inject.ponyinject.databinding.DetailFragmentBinding 10 | import org.junit.Test 11 | 12 | class DetailFragmentTest { 13 | @Test 14 | fun binds_episode_view_data() { 15 | val context = InstrumentationRegistry.getInstrumentation().targetContext 16 | val binding = DetailFragmentBinding.inflate(LayoutInflater.from(context)) 17 | binding.bind( 18 | EpisodeDetailViewData( 19 | title = "Friendship is Magic, part 2", 20 | airDate = "Oct 22, 2010", 21 | writtenBy = "Lauren Faust", 22 | storyboard = "Tom Sales, Mike West, Sherann Johnson, Sam To", 23 | songs = "Laughter Song", 24 | ) 25 | ) 26 | 27 | assertThat(binding).all { 28 | prop(DetailFragmentBinding::title).hasText("Friendship is Magic, part 2") 29 | prop(DetailFragmentBinding::airdate).hasText("Oct 22, 2010") 30 | prop(DetailFragmentBinding::writtenBy).hasText("Written By: Lauren Faust") 31 | prop(DetailFragmentBinding::storyboard).hasText("Storyboard: Tom Sales, Mike West, Sherann Johnson, Sam To") 32 | prop(DetailFragmentBinding::songs).hasText("Songs: Laughter Song") 33 | } 34 | 35 | } 36 | } -------------------------------------------------------------------------------- /android/ponyinject/app/src/androidTest/kotlin/me/tatarka/inject/ponyinject/episodes/EpisodesFragmentTest.kt: -------------------------------------------------------------------------------- 1 | package me.tatarka.inject.ponyinject.episodes 2 | 3 | import androidx.fragment.app.testing.launchFragmentInContainer 4 | import androidx.test.espresso.Espresso.onView 5 | import androidx.test.espresso.action.ViewActions.click 6 | import androidx.test.espresso.assertion.ViewAssertions.matches 7 | import androidx.test.espresso.matcher.ViewMatchers.* 8 | import androidx.test.ext.junit.runners.AndroidJUnit4 9 | import me.tatarka.inject.annotations.Component 10 | import me.tatarka.inject.ponyinject.R 11 | import me.tatarka.inject.ponyinject.TestApplicationComponent 12 | import me.tatarka.inject.ponyinject.create 13 | import me.tatarka.inject.ponyinject.main.InjectFragmentFactory 14 | import org.hamcrest.Matchers.allOf 15 | import org.junit.Test 16 | import org.junit.runner.RunWith 17 | 18 | /** 19 | * An example of a self-contained fragment test, using test fakes. 20 | */ 21 | @RunWith(AndroidJUnit4::class) 22 | class EpisodesFragmentTest { 23 | @Test 24 | fun selects_item_in_list() { 25 | val component = TestComponent::class.create() 26 | launchFragmentInContainer( 27 | themeResId = R.style.Theme_PonyInject, 28 | factory = component.fragmentFactory 29 | ) 30 | 31 | onView(withText("Friendship is Magic, part 2")).perform(click()) 32 | onView( 33 | allOf( 34 | withId(R.id.title), 35 | withParent(withId(R.id.detail)) 36 | ) 37 | ).check(matches(withText("Friendship is Magic, part 2"))) 38 | } 39 | 40 | @Component 41 | abstract class TestComponent(@Component val parent: TestApplicationComponent = TestApplicationComponent::class.create()) { 42 | abstract val fragmentFactory: InjectFragmentFactory 43 | } 44 | } -------------------------------------------------------------------------------- /android/ponyinject/app/src/commonTest/kotlin/me/tatarka/inject/ponyinject/TestApplicationComponent.kt: -------------------------------------------------------------------------------- 1 | package me.tatarka.inject.ponyinject 2 | 3 | import me.tatarka.inject.annotations.Component 4 | import me.tatarka.inject.annotations.Provides 5 | import me.tatarka.inject.ponyinject.api.ApiService 6 | import me.tatarka.inject.ponyinject.api.FakeApiService 7 | 8 | class TestFakes( 9 | @get:Provides val service: ApiService = FakeApiService() 10 | ) 11 | 12 | @Component 13 | @ApplicationScope 14 | abstract class TestApplicationComponent(@Component val fakes: TestFakes = TestFakes()) -------------------------------------------------------------------------------- /android/ponyinject/app/src/commonTest/kotlin/me/tatarka/inject/ponyinject/api/FakeApiService.kt: -------------------------------------------------------------------------------- 1 | package me.tatarka.inject.ponyinject.api 2 | 3 | import java.time.LocalDate 4 | 5 | open class FakeApiService : ApiService { 6 | 7 | private val episodes = listOf( 8 | Episode( 9 | id = 1, 10 | name = "Friendship is Magic, part 1", 11 | image = "https://vignette.wikia.nocookie.net/mlp/images/9/9f/Twilight_looks_up_at_the_moon_S1E01.png/revision/latest?cb=20121209043547", 12 | airdate = LocalDate.parse("2010-10-10"), 13 | writtenBy = "Lauren Faust", 14 | storyboard = "Tom Sales\nMike West\nSherann Johnson\nSam To", 15 | ), 16 | Episode( 17 | id = 2, 18 | name = "Friendship is Magic, part 2", 19 | image = "https://vignette.wikia.nocookie.net/mlp/images/2/2c/Main_ponies_activated_the_Elements_of_Harmony_S01E02.png/revision/latest?cb=20111205172131", 20 | airdate = LocalDate.parse("2010-10-22"), 21 | writtenBy = "Lauren Faust", 22 | storyboard = "Tom Sales\nMike West\nSherann Johnson\nSam To", 23 | song = listOf("Laughter Song"), 24 | ) 25 | ) 26 | 27 | 28 | override suspend fun episodes(offset: Int?, limit: Int?): Page { 29 | val from = (offset ?: 0).coerceAtMost(episodes.size) 30 | val to = (from + (limit ?: 50)).coerceAtMost(episodes.size) 31 | return Page(data = episodes.subList(from, to)) 32 | } 33 | } -------------------------------------------------------------------------------- /android/ponyinject/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /android/ponyinject/app/src/debug/kotlin/me/tatarka/inject/api/VariantComponent.kt: -------------------------------------------------------------------------------- 1 | package me.tatarka.inject.api 2 | 3 | import me.tatarka.inject.annotations.IntoSet 4 | import me.tatarka.inject.annotations.Provides 5 | import okhttp3.Interceptor 6 | import okhttp3.logging.HttpLoggingInterceptor 7 | 8 | interface VariantComponent { 9 | val logger: Interceptor 10 | @Provides @IntoSet get() = HttpLoggingInterceptor().apply { 11 | level = HttpLoggingInterceptor.Level.BODY 12 | } 13 | } -------------------------------------------------------------------------------- /android/ponyinject/app/src/debug/res/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | localhost 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /android/ponyinject/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /android/ponyinject/app/src/main/kotlin/me/tatarka/inject/ponyinject/ApplicationComponent.kt: -------------------------------------------------------------------------------- 1 | package me.tatarka.inject.ponyinject 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import me.tatarka.inject.annotations.Component 6 | import me.tatarka.inject.annotations.Provides 7 | import me.tatarka.inject.annotations.Scope 8 | import me.tatarka.inject.api.VariantComponent 9 | import me.tatarka.inject.ponyinject.api.ApiComponent 10 | import okhttp3.HttpUrl 11 | import okhttp3.HttpUrl.Companion.toHttpUrl 12 | 13 | /** 14 | * The application-level scope. There will only be one instance of anything annotated with this. 15 | */ 16 | @Scope 17 | annotation class ApplicationScope 18 | 19 | /** 20 | * The main application component. Use [getInstance] to ensure the same instance is shared. 21 | */ 22 | @Component 23 | @ApplicationScope 24 | abstract class ApplicationComponent( 25 | @get:Provides val application: Application, 26 | @get:Provides val baseUrl: HttpUrl 27 | ) : ApiComponent, VariantComponent { 28 | companion object { 29 | private var instance: ApplicationComponent? = null 30 | 31 | /** 32 | * Get a singleton instance of [ApplicationComponent]. 33 | */ 34 | fun getInstance(context: Context) = instance ?: ApplicationComponent::class.create( 35 | context.applicationContext as Application, BuildConfig.BASE_URL.toHttpUrl() 36 | ).also { instance = it } 37 | } 38 | } -------------------------------------------------------------------------------- /android/ponyinject/app/src/main/kotlin/me/tatarka/inject/ponyinject/ViewModelExtensions.kt: -------------------------------------------------------------------------------- 1 | package me.tatarka.inject.ponyinject 2 | 3 | import androidx.fragment.app.Fragment 4 | import androidx.fragment.app.viewModels 5 | import androidx.lifecycle.SavedStateHandle 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.createSavedStateHandle 8 | import androidx.lifecycle.viewmodel.viewModelFactory 9 | 10 | /** 11 | * [viewModels] helper that allows you to pass a single factory function. 12 | */ 13 | inline fun Fragment.viewModels(crossinline factory: () -> VM): Lazy = 14 | viewModels { 15 | viewModelFactory { 16 | addInitializer(VM::class) { factory() } 17 | } 18 | } 19 | 20 | /** 21 | * [viewModels] helper that allows you to pass a single factory function using a [SavedStateHandle]. 22 | */ 23 | inline fun Fragment.viewModels(crossinline factory: (SavedStateHandle) -> VM): Lazy = 24 | viewModels { 25 | viewModelFactory { 26 | addInitializer(VM::class) { 27 | factory(createSavedStateHandle()) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /android/ponyinject/app/src/main/kotlin/me/tatarka/inject/ponyinject/api/ApiComponent.kt: -------------------------------------------------------------------------------- 1 | package me.tatarka.inject.ponyinject.api 2 | 3 | import android.app.Application 4 | import com.squareup.moshi.Moshi 5 | import me.tatarka.inject.annotations.Provides 6 | import me.tatarka.inject.ponyinject.ApplicationScope 7 | import okhttp3.Cache 8 | import okhttp3.HttpUrl 9 | import okhttp3.Interceptor 10 | import okhttp3.OkHttpClient 11 | import retrofit2.Retrofit 12 | import retrofit2.converter.moshi.MoshiConverterFactory 13 | import retrofit2.create 14 | import java.io.File 15 | import java.time.LocalDate 16 | 17 | /** 18 | * Api-specific setup. 19 | */ 20 | interface ApiComponent { 21 | 22 | @Provides 23 | @ApplicationScope 24 | fun okhttpClient( 25 | application: Application, 26 | interceptors: Set, 27 | ): OkHttpClient = OkHttpClient.Builder() 28 | .cache(Cache(File(application.cacheDir, "http"), maxSize = 50L * 1024L * 1024L /*50 MiB*/)) 29 | .apply { interceptors.forEach { addInterceptor(it) } } 30 | .build() 31 | 32 | @Provides 33 | fun moshi(): Moshi = Moshi.Builder() 34 | .add(LocalDate::class.java, LocalDateAdapter()) 35 | .build() 36 | 37 | @Provides 38 | @ApplicationScope 39 | fun retrofit( 40 | // we use a lazy OkHttpClient so it can be initialized off the main thread, 41 | // this speeds up launch times. 42 | client: Lazy, 43 | baseUrl: HttpUrl, 44 | moshi: Moshi, 45 | ): Retrofit = Retrofit.Builder() 46 | .baseUrl(baseUrl) 47 | .callFactory { client.value.newCall(it) } 48 | .addConverterFactory(MoshiConverterFactory.create(moshi)) 49 | .build() 50 | 51 | val Retrofit.apiService: ApiService 52 | @Provides @ApplicationScope get() = create() 53 | } -------------------------------------------------------------------------------- /android/ponyinject/app/src/main/kotlin/me/tatarka/inject/ponyinject/api/ApiService.kt: -------------------------------------------------------------------------------- 1 | package me.tatarka.inject.ponyinject.api 2 | 3 | import com.squareup.moshi.JsonClass 4 | import retrofit2.http.GET 5 | import retrofit2.http.Query 6 | import java.time.LocalDate 7 | 8 | interface ApiService { 9 | @GET("v1/episode/all") 10 | suspend fun episodes(@Query("offset") offset: Int?, @Query("limit") limit: Int?): Page 11 | } 12 | 13 | @JsonClass(generateAdapter = true) 14 | data class Page( 15 | val data: List 16 | ) 17 | 18 | @JsonClass(generateAdapter = true) 19 | data class Episode( 20 | val id: Int, 21 | val name: String, 22 | val image: String, 23 | val airdate: LocalDate, 24 | val writtenBy: String? = null, 25 | val storyboard: String? = null, 26 | val song: List? = null, 27 | ) -------------------------------------------------------------------------------- /android/ponyinject/app/src/main/kotlin/me/tatarka/inject/ponyinject/api/EpisodesRepository.kt: -------------------------------------------------------------------------------- 1 | package me.tatarka.inject.ponyinject.api 2 | 3 | import androidx.paging.* 4 | import kotlinx.coroutines.flow.Flow 5 | import kotlinx.coroutines.flow.MutableStateFlow 6 | import kotlinx.coroutines.flow.map 7 | import me.tatarka.inject.annotations.Inject 8 | import me.tatarka.inject.ponyinject.ApplicationScope 9 | import java.io.IOException 10 | 11 | /** 12 | * Fetches the list of episodes using [paging v3](https://developer.android.com/topic/libraries/architecture/paging/v3-overview) 13 | * You can then also get a single cached episode with a given id. 14 | */ 15 | @Inject 16 | @ApplicationScope 17 | class EpisodesRepository(private val service: ApiService) { 18 | 19 | private val episodeCache = MutableStateFlow(emptyMap()) 20 | 21 | /** 22 | * A paged list of all episodes. 23 | */ 24 | val episodes: Flow> = Pager(PagingConfig(pageSize = 20), 0) { 25 | LimitOffsetPagingSource { offset, limit -> 26 | service.episodes(offset, limit).data 27 | .also { episodes -> updateCache(episodes) } 28 | } 29 | }.flow 30 | 31 | /** 32 | * A single cached episode with the given id. Will return null if the episode has not yet been 33 | * loaded. 34 | */ 35 | fun episode(id: Int): Flow = episodeCache.map { it[id] } 36 | 37 | private fun updateCache(episodes: List) { 38 | val newCache = episodeCache.value.toMutableMap() 39 | for (episode in episodes) { 40 | newCache[episode.id] = episode 41 | } 42 | episodeCache.value = newCache 43 | } 44 | } 45 | 46 | /** 47 | * A [PagingSource] that fetches data using a limit and offset. 48 | */ 49 | private class LimitOffsetPagingSource(private val fetch: suspend (offset: Int, limit: Int) -> List) : 50 | PagingSource() { 51 | override fun getRefreshKey(state: PagingState): Int? = state.anchorPosition 52 | 53 | override suspend fun load(params: LoadParams): LoadResult { 54 | val offset = params.key!! 55 | val limit = params.loadSize 56 | return try { 57 | val data = fetch(offset, limit) 58 | val next = if (data.isEmpty()) null else offset + data.size 59 | LoadResult.Page(data, null, next) 60 | } catch (e: IOException) { 61 | LoadResult.Error(e) 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /android/ponyinject/app/src/main/kotlin/me/tatarka/inject/ponyinject/api/LocalDateAdapter.kt: -------------------------------------------------------------------------------- 1 | package me.tatarka.inject.ponyinject.api 2 | 3 | import com.squareup.moshi.JsonAdapter 4 | import com.squareup.moshi.JsonReader 5 | import com.squareup.moshi.JsonWriter 6 | import java.time.LocalDate 7 | 8 | /** 9 | * Converts to and from a [LocalDate], assuming the ISO-8601 format uuuu-MM-dd. 10 | */ 11 | class LocalDateAdapter : JsonAdapter() { 12 | 13 | override fun fromJson(reader: JsonReader): LocalDate? { 14 | return if (reader.peek() == JsonReader.Token.NULL) { 15 | reader.skipValue() 16 | null 17 | } else { 18 | LocalDate.parse(reader.nextString()) 19 | } 20 | } 21 | 22 | override fun toJson(writer: JsonWriter, value: LocalDate?) { 23 | if (value == null) { 24 | writer.nullValue() 25 | } else { 26 | writer.value(value.toString()) 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /android/ponyinject/app/src/main/kotlin/me/tatarka/inject/ponyinject/detail/DetailFragment.kt: -------------------------------------------------------------------------------- 1 | package me.tatarka.inject.ponyinject.detail 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import androidx.core.view.isVisible 6 | import androidx.fragment.app.Fragment 7 | import androidx.lifecycle.* 8 | import kotlinx.coroutines.flow.launchIn 9 | import kotlinx.coroutines.flow.onEach 10 | import me.tatarka.inject.annotations.Inject 11 | import me.tatarka.inject.ponyinject.R 12 | import me.tatarka.inject.ponyinject.databinding.DetailFragmentBinding 13 | import me.tatarka.inject.ponyinject.viewModels 14 | 15 | /** 16 | * Shows details for a single episode. 17 | */ 18 | @Inject 19 | class DetailFragment(viewModel: (SavedStateHandle) -> DetailViewModel) : 20 | Fragment(R.layout.detail_fragment) { 21 | private val viewModel by viewModels(viewModel) 22 | 23 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 24 | val binding = DetailFragmentBinding.bind(view) 25 | viewModel.episodeDetail.flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED) 26 | .onEach { episode -> binding.bind(episode) } 27 | .launchIn(viewLifecycleOwner.lifecycleScope) 28 | } 29 | } 30 | 31 | /** 32 | * Binds a [EpisodeDetailViewData] to the ui, split out for ease of testing. 33 | */ 34 | fun DetailFragmentBinding.bind(episode: EpisodeDetailViewData?) { 35 | loading.isVisible = episode == null 36 | if (episode == null) return 37 | val res = root.resources 38 | title.text = episode.title 39 | airdate.text = episode.airDate 40 | if (episode.writtenBy != null) { 41 | writtenBy.text = res.getString(R.string.written_by, episode.writtenBy) 42 | } else { 43 | writtenBy.isVisible = false 44 | } 45 | if (episode.storyboard != null) { 46 | storyboard.text = res.getString(R.string.storyboard, episode.storyboard) 47 | } else { 48 | storyboard.isVisible = false 49 | } 50 | if (episode.songs != null) { 51 | songs.text = res.getString(R.string.songs, episode.songs) 52 | } else { 53 | songs.isVisible = false 54 | } 55 | } -------------------------------------------------------------------------------- /android/ponyinject/app/src/main/kotlin/me/tatarka/inject/ponyinject/detail/DetailViewModel.kt: -------------------------------------------------------------------------------- 1 | package me.tatarka.inject.ponyinject.detail 2 | 3 | import androidx.lifecycle.SavedStateHandle 4 | import androidx.lifecycle.ViewModel 5 | import kotlinx.coroutines.flow.map 6 | import me.tatarka.inject.annotations.Assisted 7 | import me.tatarka.inject.annotations.Inject 8 | import me.tatarka.inject.ponyinject.api.Episode 9 | import me.tatarka.inject.ponyinject.api.EpisodesRepository 10 | import java.time.format.DateTimeFormatter 11 | import java.time.format.FormatStyle 12 | 13 | private val DATE_FORMATTER = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM) 14 | 15 | /** 16 | * view model for the episode details page. 17 | */ 18 | @Inject 19 | class DetailViewModel(repository: EpisodesRepository, @Assisted handle: SavedStateHandle) : 20 | ViewModel() { 21 | private val episodeId: Int = handle["episodeId"]!! 22 | 23 | val episodeDetail = repository.episode(episodeId).map { it?.toViewData() } 24 | 25 | private fun Episode.toViewData() = EpisodeDetailViewData( 26 | title = name, 27 | airDate = airdate.format(DATE_FORMATTER), 28 | writtenBy = writtenBy, 29 | storyboard = storyboard?.replace("\n", ", "), 30 | songs = song?.joinToString(", "), 31 | ) 32 | } 33 | 34 | data class EpisodeDetailViewData( 35 | val title: String, 36 | val airDate: String, 37 | val writtenBy: String?, 38 | val storyboard: String?, 39 | val songs: String?, 40 | ) -------------------------------------------------------------------------------- /android/ponyinject/app/src/main/kotlin/me/tatarka/inject/ponyinject/episodes/EpisodesAdapter.kt: -------------------------------------------------------------------------------- 1 | package me.tatarka.inject.ponyinject.episodes 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import androidx.paging.PagingDataAdapter 7 | import androidx.recyclerview.widget.DiffUtil 8 | import androidx.recyclerview.widget.RecyclerView 9 | import coil.load 10 | import me.tatarka.inject.ponyinject.R 11 | import me.tatarka.inject.ponyinject.api.Episode 12 | import me.tatarka.inject.ponyinject.databinding.EpisodesItemBinding 13 | 14 | typealias OnItemClick = (id: Int) -> Unit 15 | 16 | /** 17 | * Shows a list of episodes. 18 | */ 19 | class EpisodesAdapter(private val onItemClick: OnItemClick) : 20 | PagingDataAdapter( 21 | object : DiffUtil.ItemCallback() { 22 | override fun areItemsTheSame(oldItem: Episode, newItem: Episode) = 23 | oldItem.id == newItem.id 24 | 25 | override fun areContentsTheSame(oldItem: Episode, newItem: Episode) = oldItem == newItem 26 | } 27 | ) { 28 | 29 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 30 | return ViewHolder( 31 | LayoutInflater.from(parent.context).inflate(R.layout.episodes_item, parent, false), 32 | onItemClick 33 | ) 34 | } 35 | 36 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 37 | holder.bind(getItem(position)) 38 | } 39 | 40 | class ViewHolder(view: View, onItemClick: OnItemClick) : RecyclerView.ViewHolder(view) { 41 | private val binding = EpisodesItemBinding.bind(view) 42 | private var episode: Episode? = null 43 | 44 | init { 45 | binding.root.setOnClickListener { 46 | episode?.let { onItemClick(it.id) } 47 | } 48 | } 49 | 50 | fun bind(episode: Episode?) { 51 | this.episode = episode 52 | binding.image.load(episode?.image) { 53 | crossfade(true) 54 | } 55 | binding.title.text = episode?.name 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /android/ponyinject/app/src/main/kotlin/me/tatarka/inject/ponyinject/episodes/EpisodesFragment.kt: -------------------------------------------------------------------------------- 1 | package me.tatarka.inject.ponyinject.episodes 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import androidx.fragment.app.Fragment 6 | import androidx.lifecycle.Lifecycle 7 | import androidx.lifecycle.flowWithLifecycle 8 | import androidx.lifecycle.lifecycleScope 9 | import androidx.navigation.NavOptions 10 | import androidx.navigation.fragment.NavHostFragment 11 | import androidx.slidingpanelayout.widget.SlidingPaneLayout 12 | import kotlinx.coroutines.flow.collectLatest 13 | import kotlinx.coroutines.launch 14 | import me.tatarka.inject.annotations.Inject 15 | import me.tatarka.inject.ponyinject.R 16 | import me.tatarka.inject.ponyinject.databinding.EpisodesFragmentBinding 17 | import me.tatarka.inject.ponyinject.detail.DetailFragmentArgs 18 | import me.tatarka.inject.ponyinject.ui.TwoPaneOnBackPressedCallback 19 | import me.tatarka.inject.ponyinject.viewModels 20 | 21 | /** 22 | * Shows a list of episodes and navigates to their detail page when one is clicked. This uses a 23 | * [SlidingPaneLayout] as described in [https://developer.android.com/guide/topics/ui/layout/twopane] 24 | */ 25 | @Inject 26 | class EpisodesFragment(viewModel: () -> EpisodesViewModel) : Fragment(R.layout.episodes_fragment) { 27 | private val viewModel by viewModels(viewModel) 28 | 29 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 30 | val binding = EpisodesFragmentBinding.bind(view) 31 | val adapter = EpisodesAdapter { id -> binding.navigate(id) } 32 | binding.list.adapter = adapter 33 | 34 | // Connect the SlidingPaneLayout to the system back button. 35 | requireActivity().onBackPressedDispatcher.addCallback( 36 | viewLifecycleOwner, 37 | TwoPaneOnBackPressedCallback(binding.root) 38 | ) 39 | 40 | // hook up the adapter with paging data 41 | viewLifecycleOwner.lifecycleScope.launch { 42 | viewModel.episodes 43 | .flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED) 44 | .collectLatest { data -> adapter.submitData(data) } 45 | } 46 | } 47 | 48 | /** 49 | * Navigates to the detail page with the given id. 50 | */ 51 | private fun EpisodesFragmentBinding.navigate(id: Int) { 52 | val navController = 53 | (childFragmentManager.findFragmentById(R.id.navDetail) as NavHostFragment).navController 54 | navController.navigate( 55 | R.id.detail, 56 | DetailFragmentArgs(episodeId = id).toBundle(), 57 | NavOptions.Builder() 58 | // Pop all destinations off the back stack. 59 | .setPopUpTo(navController.graph.startDestinationId, true) 60 | .apply { 61 | // If we're already open and the detail pane is visible, 62 | // crossfade between the destinations. 63 | if (root.isOpen) { 64 | setEnterAnim(androidx.navigation.ui.R.animator.nav_default_enter_anim) 65 | setExitAnim(androidx.navigation.ui.R.animator.nav_default_exit_anim) 66 | } 67 | } 68 | .build() 69 | ) 70 | root.open() 71 | } 72 | } -------------------------------------------------------------------------------- /android/ponyinject/app/src/main/kotlin/me/tatarka/inject/ponyinject/episodes/EpisodesViewModel.kt: -------------------------------------------------------------------------------- 1 | package me.tatarka.inject.ponyinject.episodes 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import androidx.paging.cachedIn 6 | import me.tatarka.inject.annotations.Inject 7 | import me.tatarka.inject.ponyinject.api.EpisodesRepository 8 | 9 | /** 10 | * view model for the episodes page. 11 | */ 12 | @Inject 13 | class EpisodesViewModel(repository: EpisodesRepository) : ViewModel() { 14 | val episodes = repository.episodes.cachedIn(viewModelScope) 15 | } -------------------------------------------------------------------------------- /android/ponyinject/app/src/main/kotlin/me/tatarka/inject/ponyinject/main/InjectFragmentFactory.kt: -------------------------------------------------------------------------------- 1 | package me.tatarka.inject.ponyinject.main 2 | 3 | import androidx.fragment.app.Fragment 4 | import androidx.fragment.app.FragmentFactory 5 | import me.tatarka.inject.annotations.Inject 6 | import me.tatarka.inject.ponyinject.detail.DetailFragment 7 | import me.tatarka.inject.ponyinject.episodes.EpisodesFragment 8 | 9 | /** 10 | * Allows us to use [Inject] constructors on fragments. When you add a new fragment you need to add 11 | * it here. 12 | */ 13 | @Inject 14 | class InjectFragmentFactory( 15 | private val mainFragment: () -> MainFragment, 16 | private val episodesFragment: () -> EpisodesFragment, 17 | private val detailFragment: () -> DetailFragment 18 | ) : FragmentFactory() { 19 | 20 | override fun instantiate(classLoader: ClassLoader, className: String): Fragment { 21 | return when (className) { 22 | name() -> mainFragment() 23 | name() -> episodesFragment() 24 | name() -> detailFragment() 25 | // fall-back to default no-args construction so we can handle 3rd-party fragments. 26 | else -> super.instantiate(classLoader, className) 27 | } 28 | } 29 | 30 | private inline fun name() = T::class.qualifiedName 31 | } -------------------------------------------------------------------------------- /android/ponyinject/app/src/main/kotlin/me/tatarka/inject/ponyinject/main/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package me.tatarka.inject.ponyinject.main 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import me.tatarka.inject.annotations.Component 6 | import me.tatarka.inject.ponyinject.ApplicationComponent 7 | import me.tatarka.inject.ponyinject.R 8 | 9 | /** 10 | * The app's main entry-point. We want to make this as thin as possible to allow easily changing 11 | * the component when under test. Any top-level logic should be in [MainFragment] instead. 12 | * 13 | * @see me.tatarka.inject.ponyinject.EndToEndTest 14 | */ 15 | class MainActivity : AppCompatActivity() { 16 | override fun onCreate(savedInstanceState: Bundle?) { 17 | val component = MainActivityComponent::class.create(ApplicationComponent.getInstance(this)) 18 | supportFragmentManager.fragmentFactory = component.fragmentFactory 19 | super.onCreate(savedInstanceState) 20 | setContentView(R.layout.main_activity) 21 | } 22 | } 23 | 24 | @Component 25 | abstract class MainActivityComponent(@Component val parent: ApplicationComponent) { 26 | abstract val fragmentFactory: InjectFragmentFactory 27 | } 28 | -------------------------------------------------------------------------------- /android/ponyinject/app/src/main/kotlin/me/tatarka/inject/ponyinject/main/MainFragment.kt: -------------------------------------------------------------------------------- 1 | package me.tatarka.inject.ponyinject.main 2 | 3 | import androidx.fragment.app.Fragment 4 | import me.tatarka.inject.annotations.Inject 5 | import me.tatarka.inject.ponyinject.R 6 | 7 | /** 8 | * The root of the app. Hosts a navigation graph. 9 | */ 10 | @Inject 11 | class MainFragment : Fragment(R.layout.main_fragment) -------------------------------------------------------------------------------- /android/ponyinject/app/src/main/kotlin/me/tatarka/inject/ponyinject/ui/TwoPaneOnBackPressedCallback.kt: -------------------------------------------------------------------------------- 1 | package me.tatarka.inject.ponyinject.ui 2 | 3 | import android.view.View 4 | import androidx.activity.OnBackPressedCallback 5 | import androidx.slidingpanelayout.widget.SlidingPaneLayout 6 | 7 | /** 8 | * Hook [SlidingPaneLayout] up to the back button, taken from 9 | * [https://developer.android.com/guide/topics/ui/layout/twopane] 10 | */ 11 | class TwoPaneOnBackPressedCallback( 12 | private val slidingPaneLayout: SlidingPaneLayout 13 | ) : OnBackPressedCallback( 14 | // Set the default 'enabled' state to true only if it is slidable (i.e., the panes 15 | // are overlapping) and open (i.e., the detail pane is visible). 16 | slidingPaneLayout.isSlideable && slidingPaneLayout.isOpen 17 | ), SlidingPaneLayout.PanelSlideListener { 18 | 19 | init { 20 | slidingPaneLayout.addPanelSlideListener(this) 21 | } 22 | 23 | override fun handleOnBackPressed() { 24 | // Return to the list pane when the system back button is pressed. 25 | slidingPaneLayout.closePane() 26 | } 27 | 28 | override fun onPanelSlide(panel: View, slideOffset: Float) {} 29 | 30 | override fun onPanelOpened(panel: View) { 31 | // Intercept the system back button when the detail pane becomes visible. 32 | isEnabled = true 33 | } 34 | 35 | override fun onPanelClosed(panel: View) { 36 | // Disable intercepting the system back button when the user returns to the 37 | // list pane. 38 | isEnabled = false 39 | } 40 | } -------------------------------------------------------------------------------- /android/ponyinject/app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /android/ponyinject/app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /android/ponyinject/app/src/main/res/layout/detail_fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 17 | 18 | 29 | 30 | 42 | 43 | 55 | 56 | 68 | 69 | 81 | -------------------------------------------------------------------------------- /android/ponyinject/app/src/main/res/layout/episodes_fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 13 | 14 | 21 | -------------------------------------------------------------------------------- /android/ponyinject/app/src/main/res/layout/episodes_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 21 | 22 | 35 | -------------------------------------------------------------------------------- /android/ponyinject/app/src/main/res/layout/main_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /android/ponyinject/app/src/main/res/layout/main_fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | -------------------------------------------------------------------------------- /android/ponyinject/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/ponyinject/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/ponyinject/app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evant/kotlin-inject-samples/b98d76a2e282fc8a8e8b4df73121e84fa74a64e8/android/ponyinject/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /android/ponyinject/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evant/kotlin-inject-samples/b98d76a2e282fc8a8e8b4df73121e84fa74a64e8/android/ponyinject/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /android/ponyinject/app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evant/kotlin-inject-samples/b98d76a2e282fc8a8e8b4df73121e84fa74a64e8/android/ponyinject/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /android/ponyinject/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evant/kotlin-inject-samples/b98d76a2e282fc8a8e8b4df73121e84fa74a64e8/android/ponyinject/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /android/ponyinject/app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evant/kotlin-inject-samples/b98d76a2e282fc8a8e8b4df73121e84fa74a64e8/android/ponyinject/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /android/ponyinject/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evant/kotlin-inject-samples/b98d76a2e282fc8a8e8b4df73121e84fa74a64e8/android/ponyinject/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /android/ponyinject/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evant/kotlin-inject-samples/b98d76a2e282fc8a8e8b4df73121e84fa74a64e8/android/ponyinject/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /android/ponyinject/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evant/kotlin-inject-samples/b98d76a2e282fc8a8e8b4df73121e84fa74a64e8/android/ponyinject/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /android/ponyinject/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evant/kotlin-inject-samples/b98d76a2e282fc8a8e8b4df73121e84fa74a64e8/android/ponyinject/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /android/ponyinject/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evant/kotlin-inject-samples/b98d76a2e282fc8a8e8b4df73121e84fa74a64e8/android/ponyinject/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /android/ponyinject/app/src/main/res/navigation/detail_graph.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 10 | 14 | 15 | -------------------------------------------------------------------------------- /android/ponyinject/app/src/main/res/navigation/main_graph.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /android/ponyinject/app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | -------------------------------------------------------------------------------- /android/ponyinject/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FF000000 4 | #FFFFFFFF 5 | #FFCB93DF 6 | #7F548E 7 | #33CB93DF 8 | -------------------------------------------------------------------------------- /android/ponyinject/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | PonyInject 3 | Written By: %s 4 | Storyboard: %s 5 | Songs: %s 6 | -------------------------------------------------------------------------------- /android/ponyinject/app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | -------------------------------------------------------------------------------- /android/ponyinject/app/src/release/java/me/tatarka/inject/api/VariantComponent.kt: -------------------------------------------------------------------------------- 1 | package me.tatarka.inject.api 2 | 3 | import me.tatarka.inject.annotations.Provides 4 | import okhttp3.Interceptor 5 | 6 | interface VariantComponent { 7 | val none: Set 8 | @Provides get() = emptySet() 9 | } -------------------------------------------------------------------------------- /android/ponyinject/app/src/test/kotlin/me/tatarka/inject/ponyinject/CoroutineTestRule.kt: -------------------------------------------------------------------------------- 1 | package me.tatarka.inject.ponyinject 2 | 3 | import kotlinx.coroutines.CoroutineDispatcher 4 | import kotlinx.coroutines.Dispatchers 5 | import kotlinx.coroutines.ExperimentalCoroutinesApi 6 | import kotlinx.coroutines.cancel 7 | import kotlinx.coroutines.test.resetMain 8 | import kotlinx.coroutines.test.setMain 9 | import org.junit.rules.TestWatcher 10 | import org.junit.runner.Description 11 | 12 | /** 13 | * Replaces [Dispatchers.Main] for test so that they can run without robolectric. 14 | */ 15 | @OptIn(ExperimentalCoroutinesApi::class) 16 | class CoroutineTestRule(private val dispatcher: CoroutineDispatcher = Dispatchers.Unconfined) : 17 | TestWatcher() { 18 | 19 | override fun starting(description: Description) { 20 | super.starting(description) 21 | Dispatchers.setMain(dispatcher) 22 | } 23 | 24 | override fun finished(description: Description) { 25 | super.finished(description) 26 | Dispatchers.resetMain() 27 | dispatcher.cancel() 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /android/ponyinject/app/src/test/kotlin/me/tatarka/inject/ponyinject/api/EpisodesRepositoryTest.kt: -------------------------------------------------------------------------------- 1 | package me.tatarka.inject.ponyinject.api 2 | 3 | import androidx.paging.testing.asSnapshot 4 | import assertk.assertFailure 5 | import assertk.assertThat 6 | import assertk.assertions.* 7 | import kotlinx.coroutines.flow.first 8 | import kotlinx.coroutines.runBlocking 9 | import kotlinx.coroutines.test.runTest 10 | import me.tatarka.inject.annotations.Component 11 | import me.tatarka.inject.ponyinject.* 12 | import org.junit.Test 13 | import java.io.IOException 14 | 15 | /** 16 | * An example of a repository test using test-fakes. 17 | */ 18 | class EpisodesRepositoryTest { 19 | 20 | @Test 21 | fun fetches_episodes_from_the_api() = runTest { 22 | val component = TestComponent::class.create() 23 | val repository = component.episodesRepository 24 | val episodes = repository.episodes.asSnapshot() 25 | 26 | assertThat(episodes).extracting(Episode::name).containsExactly( 27 | "Friendship is Magic, part 1", 28 | "Friendship is Magic, part 2" 29 | ) 30 | } 31 | 32 | @Test 33 | fun shows_error_if_fetch_fails() = runTest { 34 | val component = TestComponent::class.create(TestApplicationComponent::class.create( 35 | TestFakes( 36 | service = object : FakeApiService() { 37 | override suspend fun episodes(offset: Int?, limit: Int?): Page { 38 | throw IOException("api call failed") 39 | } 40 | } 41 | ))) 42 | val repository = component.episodesRepository 43 | 44 | assertFailure { repository.episodes.asSnapshot() } 45 | .isInstanceOf(IOException::class) 46 | .hasMessage("api call failed") 47 | } 48 | 49 | @Test 50 | fun fetches_a_single_episode_from_the_cache() = runTest { 51 | val component = TestComponent::class.create() 52 | val repository = component.episodesRepository 53 | repository.episodes.asSnapshot() 54 | val episode = repository.episode(1).first() 55 | 56 | assertThat(episode).isNotNull() 57 | .prop(Episode::name).isEqualTo("Friendship is Magic, part 1") 58 | } 59 | 60 | @Component 61 | abstract class TestComponent(@Component val parent: TestApplicationComponent = TestApplicationComponent::class.create()) { 62 | abstract val episodesRepository: EpisodesRepository 63 | } 64 | } -------------------------------------------------------------------------------- /android/ponyinject/app/src/test/kotlin/me/tatarka/inject/ponyinject/detail/DetailViewModelTest.kt: -------------------------------------------------------------------------------- 1 | package me.tatarka.inject.ponyinject.detail 2 | 3 | import androidx.lifecycle.SavedStateHandle 4 | import androidx.paging.testing.asSnapshot 5 | import assertk.all 6 | import assertk.assertThat 7 | import assertk.assertions.isEqualTo 8 | import assertk.assertions.isNotNull 9 | import assertk.assertions.prop 10 | import kotlinx.coroutines.flow.first 11 | import kotlinx.coroutines.test.runTest 12 | import me.tatarka.inject.annotations.Component 13 | import me.tatarka.inject.ponyinject.CoroutineTestRule 14 | import me.tatarka.inject.ponyinject.TestApplicationComponent 15 | import me.tatarka.inject.ponyinject.api.EpisodesRepository 16 | import me.tatarka.inject.ponyinject.create 17 | import org.junit.Rule 18 | import org.junit.Test 19 | 20 | /** 21 | * An example of a view model test using test-fakes and [SavedStateHandle]. 22 | */ 23 | class DetailViewModelTest { 24 | @get:Rule 25 | val coroutineTestRule = CoroutineTestRule() 26 | 27 | @Test 28 | fun fetches_the_episode_with_the_given_id() = runTest { 29 | val component = TestComponent::class.create() 30 | val repository = component.repository 31 | val viewModel = component.viewModel(SavedStateHandle(mapOf("episodeId" to 2))) 32 | repository.episodes.asSnapshot() 33 | 34 | assertThat(viewModel.episodeDetail.first()).isNotNull().all { 35 | prop(EpisodeDetailViewData::title).isEqualTo("Friendship is Magic, part 2") 36 | prop(EpisodeDetailViewData::airDate).isEqualTo("Oct 22, 2010") 37 | prop(EpisodeDetailViewData::writtenBy).isEqualTo("Lauren Faust") 38 | prop(EpisodeDetailViewData::storyboard).isEqualTo("Tom Sales, Mike West, Sherann Johnson, Sam To") 39 | prop(EpisodeDetailViewData::songs).isEqualTo("Laughter Song") 40 | } 41 | } 42 | 43 | @Component 44 | abstract class TestComponent(@Component val parent: TestApplicationComponent = TestApplicationComponent::class.create()) { 45 | abstract val viewModel: (SavedStateHandle) -> DetailViewModel 46 | 47 | abstract val repository: EpisodesRepository 48 | } 49 | } -------------------------------------------------------------------------------- /android/ponyinject/app/src/test/kotlin/me/tatarka/inject/ponyinject/episodes/EpisodesViewModelTest.kt: -------------------------------------------------------------------------------- 1 | package me.tatarka.inject.ponyinject.episodes 2 | 3 | import androidx.paging.testing.asSnapshot 4 | import assertk.assertThat 5 | import assertk.assertions.containsExactly 6 | import assertk.assertions.extracting 7 | import kotlinx.coroutines.test.runTest 8 | import me.tatarka.inject.annotations.Component 9 | import me.tatarka.inject.ponyinject.CoroutineTestRule 10 | import me.tatarka.inject.ponyinject.TestApplicationComponent 11 | import me.tatarka.inject.ponyinject.api.Episode 12 | import me.tatarka.inject.ponyinject.create 13 | import org.junit.Rule 14 | import org.junit.Test 15 | 16 | /** 17 | * An example of a view model test using test-fakes. 18 | */ 19 | class EpisodesViewModelTest { 20 | @get:Rule 21 | val coroutineTestRule = CoroutineTestRule() 22 | 23 | @Test 24 | fun fetches_episodes() = runTest { 25 | val component = TestComponent::class.create() 26 | val viewModel = component.viewModel 27 | val episodes = viewModel.episodes.asSnapshot() 28 | 29 | assertThat(episodes).extracting(Episode::name).containsExactly( 30 | "Friendship is Magic, part 1", 31 | "Friendship is Magic, part 2" 32 | ) 33 | } 34 | 35 | @Component 36 | abstract class TestComponent(@Component val parent: TestApplicationComponent = TestApplicationComponent::class.create()) { 37 | abstract val viewModel: EpisodesViewModel 38 | } 39 | } -------------------------------------------------------------------------------- /android/ponyinject/build.gradle.kts: -------------------------------------------------------------------------------- 1 | tasks.wrapper { 2 | jarFile = file("../../gradle/wrapper/gradle-wrapper.jar") 3 | } -------------------------------------------------------------------------------- /android/ponyinject/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official -------------------------------------------------------------------------------- /android/ponyinject/gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/../../gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC2039,SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC2039,SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /android/ponyinject/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 1>&2 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 48 | echo. 1>&2 49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 50 | echo location of your Java installation. 1>&2 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 1>&2 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 62 | echo. 1>&2 63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 64 | echo location of your Java installation. 1>&2 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\..\..\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /android/ponyinject/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | gradlePluginPortal() 5 | } 6 | plugins { 7 | id("com.android.application") version "8.2.0" 8 | kotlin("android") version "2.0.0" 9 | id("com.google.devtools.ksp") version "2.0.0-1.0.22" 10 | id("androidx.navigation.safeargs.kotlin") version "2.7.7" 11 | } 12 | } 13 | dependencyResolutionManagement { 14 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 15 | repositories { 16 | mavenCentral() 17 | google() 18 | } 19 | } 20 | rootProject.name = "PonyInject" 21 | include(":app") 22 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M" -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evant/kotlin-inject-samples/b98d76a2e282fc8a8e8b4df73121e84fa74a64e8/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC2039,SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC2039,SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 1>&2 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 48 | echo. 1>&2 49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 50 | echo location of your Java installation. 1>&2 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 1>&2 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 62 | echo. 1>&2 63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 64 | echo location of your Java installation. 1>&2 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /multiplatform/echo/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | -------------------------------------------------------------------------------- /multiplatform/echo/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("multiplatform") version "2.0.0" 3 | id("com.google.devtools.ksp") version "2.0.0-1.0.22" 4 | } 5 | 6 | kotlin { 7 | listOf( 8 | linuxX64(), 9 | macosX64(), macosArm64(), 10 | ).forEach { 11 | it.binaries.executable() 12 | } 13 | 14 | sourceSets { 15 | commonMain { 16 | dependencies { 17 | implementation("me.tatarka.inject:kotlin-inject-runtime-kmp:0.7.1") 18 | } 19 | } 20 | } 21 | } 22 | 23 | // KSP will eventually have better multiplatform support and we'll be able to simply have 24 | // `ksp libs.kotlinInject.compiler` in the dependencies block of each source set 25 | // https://github.com/google/ksp/pull/1021 26 | 27 | dependencies { 28 | add("kspLinuxX64", "me.tatarka.inject:kotlin-inject-compiler-ksp:0.7.1") 29 | add("kspMacosX64", "me.tatarka.inject:kotlin-inject-compiler-ksp:0.7.1") 30 | add("kspMacosArm64", "me.tatarka.inject:kotlin-inject-compiler-ksp:0.7.1") 31 | } 32 | 33 | tasks.wrapper { 34 | jarFile = file("../../gradle/wrapper/gradle-wrapper.jar") 35 | } -------------------------------------------------------------------------------- /multiplatform/echo/gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/../../gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC2039,SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC2039,SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /multiplatform/echo/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 1>&2 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 48 | echo. 1>&2 49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 50 | echo location of your Java installation. 1>&2 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 1>&2 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 62 | echo. 1>&2 63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 64 | echo location of your Java installation. 1>&2 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\..\..\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /multiplatform/echo/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | mavenCentral() 5 | } 6 | } 7 | dependencyResolutionManagement { 8 | repositories { 9 | mavenCentral() 10 | } 11 | } 12 | rootProject.name = "echo" 13 | -------------------------------------------------------------------------------- /multiplatform/echo/src/commonMain/kotlin/App.kt: -------------------------------------------------------------------------------- 1 | import me.tatarka.inject.annotations.Component 2 | import me.tatarka.inject.annotations.Inject 3 | import me.tatarka.inject.annotations.KmpComponentCreate 4 | import me.tatarka.inject.annotations.Provides 5 | 6 | typealias Args = Array 7 | 8 | @Component 9 | abstract class ApplicationComponent(@get:Provides val args: Args) { 10 | abstract val app: App 11 | } 12 | 13 | @KmpComponentCreate 14 | expect fun createApplicationComponent(args: Args): ApplicationComponent 15 | 16 | @Inject 17 | class ArgProcessor(private val args: Args) { 18 | fun process(): String = args.joinToString(" ") 19 | } 20 | 21 | @Inject 22 | class App(private val argProcessor: ArgProcessor) { 23 | fun run() { 24 | println(argProcessor.process()) 25 | } 26 | } 27 | 28 | fun main(args: Array) { 29 | val appComponent = createApplicationComponent(args) 30 | appComponent.app.run() 31 | } -------------------------------------------------------------------------------- /multiplatform/greeter/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org/ 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | -------------------------------------------------------------------------------- /multiplatform/greeter/.gitignore: -------------------------------------------------------------------------------- 1 | # 🤖 Android Studio 2 | *.iml 3 | /.idea/ 4 | 5 | # 🍎 Xcode (from https://github.com/github/gitignore/blob/main/Swift.gitignore) 6 | # User settings 7 | xcuserdata/ 8 | # Obj-C/Swift specific 9 | *.hmap 10 | # App packaging 11 | *.ipa 12 | *.dSYM.zip 13 | *.dSYM 14 | # Playgrounds 15 | timeline.xctimeline 16 | playground.xcworkspace 17 | 18 | # Gradle 19 | .gradle 20 | build 21 | /local.properties 22 | 23 | # macOS 24 | .DS_Store 25 | -------------------------------------------------------------------------------- /multiplatform/greeter/README.md: -------------------------------------------------------------------------------- 1 | # Greeter 2 | 3 | This is a slimmed down version of the [sample](https://github.com/tfcporciuncula/kotlin-inject-greeter) used in the 4 | [*From Dagger & Hilt into the multiplatform world with kotlin-inject*](https://proandroiddev.com/from-dagger-hilt-into-the-multiplatform-world-with-kotlin-inject-647d8e3bddd5) article. 5 | While in the original sample we're trying to showcase as many DI features as possible, 6 | the sample here is simpler and serves merely as an example of a [KMP](https://kotlinlang.org/lp/multiplatform) project using kotlin-inject on both Android and iOS. 7 | -------------------------------------------------------------------------------- /multiplatform/greeter/androidApp/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.kotlin.android) 3 | alias(libs.plugins.android.application) 4 | alias(libs.plugins.ksp) 5 | alias(libs.plugins.compose.compiler) 6 | } 7 | 8 | android { 9 | namespace = "com.fredporciuncula.inject.greeter.android" 10 | compileSdk = 34 11 | defaultConfig { 12 | applicationId = "com.fredporciuncula.inject.greeter.android" 13 | minSdk = 24 14 | targetSdk = 34 15 | versionCode = 1 16 | versionName = "1.0" 17 | } 18 | buildFeatures { 19 | buildConfig = true 20 | } 21 | buildTypes { 22 | release { 23 | isMinifyEnabled = false 24 | } 25 | } 26 | compileOptions { 27 | sourceCompatibility = JavaVersion.VERSION_11 28 | targetCompatibility = JavaVersion.VERSION_11 29 | } 30 | kotlinOptions { 31 | jvmTarget = "11" 32 | } 33 | } 34 | 35 | dependencies { 36 | implementation(projects.shared) 37 | implementation(libs.androidx.activity.compose) 38 | implementation(platform(libs.compose.bom)) 39 | implementation(libs.compose.foundation) 40 | implementation(libs.compose.material3) 41 | implementation(libs.compose.runtime) 42 | implementation(libs.compose.ui) 43 | implementation(libs.compose.ui.toolingPreview) 44 | debugRuntimeOnly(libs.compose.ui.tooling) 45 | 46 | implementation(libs.kotlinInject.runtime) 47 | ksp(libs.kotlinInject.compiler) 48 | } 49 | -------------------------------------------------------------------------------- /multiplatform/greeter/androidApp/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /multiplatform/greeter/androidApp/src/main/java/com/fredporciuncula/inject/greeter/android/ApplicationComponentProvider.kt: -------------------------------------------------------------------------------- 1 | package com.fredporciuncula.inject.greeter.android 2 | 3 | import android.content.Context 4 | import com.fredporciuncula.inject.greeter.ApplicationComponent 5 | 6 | interface ApplicationComponentProvider { 7 | val component: ApplicationComponent 8 | } 9 | 10 | val Context.applicationComponent get() = (applicationContext as ApplicationComponentProvider).component 11 | -------------------------------------------------------------------------------- /multiplatform/greeter/androidApp/src/main/java/com/fredporciuncula/inject/greeter/android/GreeterApplication.kt: -------------------------------------------------------------------------------- 1 | package com.fredporciuncula.inject.greeter.android 2 | 3 | import android.app.Application 4 | import com.fredporciuncula.inject.greeter.ApplicationComponent 5 | import com.fredporciuncula.inject.greeter.Version 6 | import com.fredporciuncula.inject.greeter.create 7 | 8 | class GreeterApplication : Application(), ApplicationComponentProvider { 9 | override val component by lazy(LazyThreadSafetyMode.NONE) { 10 | ApplicationComponent::class.create(applicationContext, Version(BuildConfig.VERSION_NAME)) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /multiplatform/greeter/androidApp/src/main/java/com/fredporciuncula/inject/greeter/android/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.fredporciuncula.inject.greeter.android 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.compose.foundation.background 7 | import androidx.compose.foundation.layout.Box 8 | import androidx.compose.foundation.layout.fillMaxSize 9 | import androidx.compose.material3.Button 10 | import androidx.compose.material3.Text 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.graphics.Color 14 | import com.fredporciuncula.inject.greeter.ApplicationComponent 15 | import com.fredporciuncula.inject.greeter.CommonGreeter 16 | import me.tatarka.inject.annotations.Component 17 | 18 | @Component 19 | abstract class MainActivityComponent(@Component val parent: ApplicationComponent) { 20 | abstract val greeter: CommonGreeter 21 | } 22 | 23 | class MainActivity : ComponentActivity() { 24 | override fun onCreate(savedInstanceState: Bundle?) { 25 | super.onCreate(savedInstanceState) 26 | val greeter = MainActivityComponent::class.create(applicationComponent).greeter 27 | setContent { 28 | Box( 29 | modifier = Modifier 30 | .background(color = Color.White) 31 | .fillMaxSize(), 32 | contentAlignment = Alignment.Center, 33 | ) { 34 | Button( 35 | onClick = { greeter.greet() } 36 | ) { 37 | Text(text = "Greet!") 38 | } 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /multiplatform/greeter/androidApp/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 |