diff --git a/app/benchmark-rules.pro b/app/benchmark-rules.pro
index 96b67f2d1..48d2ca28d 100644
--- a/app/benchmark-rules.pro
+++ b/app/benchmark-rules.pro
@@ -3,7 +3,6 @@
# Obsfuscation must be disabled for the build variant that generates Baseline Profile, otherwise
# wrong symbols would be generated. The generated Baseline Profile will be properly applied when generated
# without obfuscation and your app is being obfuscated.
--dontobfuscate
# Please add these rules to your existing keep rules in order to suppress warnings.
# This is generated automatically by the Android Gradle plugin.
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 6aec2d1bc..095efdb38 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -70,6 +70,7 @@ android {
}
dependencies {
+ implementation(projects.app.nativelib)
implementation(projects.feature.interests)
implementation(projects.feature.foryou)
implementation(projects.feature.bookmarks)
diff --git a/app/lib/.gitignore b/app/lib/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/app/lib/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/app/lib/build.gradle.kts b/app/lib/build.gradle.kts
new file mode 100644
index 000000000..a95692d3c
--- /dev/null
+++ b/app/lib/build.gradle.kts
@@ -0,0 +1,13 @@
+plugins {
+ id("java-library")
+ alias(libs.plugins.kotlin.jvm)
+}
+java {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+}
+kotlin {
+ compilerOptions {
+ jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11
+ }
+}
diff --git a/app/lib/src/main/java/com/example/lib/MyClass.kt b/app/lib/src/main/java/com/example/lib/MyClass.kt
new file mode 100644
index 000000000..99ce9e67c
--- /dev/null
+++ b/app/lib/src/main/java/com/example/lib/MyClass.kt
@@ -0,0 +1,4 @@
+package com.example.lib
+
+class MyClass {
+}
\ No newline at end of file
diff --git a/app/nativelib/.gitignore b/app/nativelib/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/app/nativelib/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/app/nativelib/build.gradle.kts b/app/nativelib/build.gradle.kts
new file mode 100644
index 000000000..6778ee488
--- /dev/null
+++ b/app/nativelib/build.gradle.kts
@@ -0,0 +1,54 @@
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
+}
+
+android {
+ namespace = "com.example.nativelib"
+ compileSdk = 36
+
+ defaultConfig {
+ minSdk = 21
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+ externalNativeBuild {
+ cmake {
+ cppFlags("")
+ }
+ }
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ externalNativeBuild {
+ cmake {
+ path("src/main/cpp/CMakeLists.txt")
+ version = "3.22.1"
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+ kotlinOptions {
+ jvmTarget = "11"
+ }
+}
+
+dependencies {
+
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.appcompat)
+ implementation(libs.material)
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.androidx.test.espresso.core)
+}
\ No newline at end of file
diff --git a/app/nativelib/consumer-rules.pro b/app/nativelib/consumer-rules.pro
new file mode 100644
index 000000000..e69de29bb
diff --git a/app/nativelib/proguard-rules.pro b/app/nativelib/proguard-rules.pro
new file mode 100644
index 000000000..481bb4348
--- /dev/null
+++ b/app/nativelib/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/app/nativelib/src/androidTest/java/com/example/nativelib/ExampleInstrumentedTest.kt b/app/nativelib/src/androidTest/java/com/example/nativelib/ExampleInstrumentedTest.kt
new file mode 100644
index 000000000..dd8f87ac7
--- /dev/null
+++ b/app/nativelib/src/androidTest/java/com/example/nativelib/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.example.nativelib
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("com.example.nativelib.test", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/app/nativelib/src/main/AndroidManifest.xml b/app/nativelib/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..a5918e68a
--- /dev/null
+++ b/app/nativelib/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/nativelib/src/main/cpp/CMakeLists.txt b/app/nativelib/src/main/cpp/CMakeLists.txt
new file mode 100644
index 000000000..73f56e9dc
--- /dev/null
+++ b/app/nativelib/src/main/cpp/CMakeLists.txt
@@ -0,0 +1,38 @@
+# For more information about using CMake with Android Studio, read the
+# documentation: https://d.android.com/studio/projects/add-native-code.html.
+# For more examples on how to use CMake, see https://github.com/android/ndk-samples.
+
+# Sets the minimum CMake version required for this project.
+cmake_minimum_required(VERSION 3.22.1)
+
+# Declares the project name. The project name can be accessed via ${ PROJECT_NAME},
+# Since this is the top level CMakeLists.txt, the project name is also accessible
+# with ${CMAKE_PROJECT_NAME} (both CMake variables are in-sync within the top level
+# build script scope).
+project("nativelib")
+
+# Creates and names a library, sets it as either STATIC
+# or SHARED, and provides the relative paths to its source code.
+# You can define multiple libraries, and CMake builds them for you.
+# Gradle automatically packages shared libraries with your APK.
+#
+# In this top level CMakeLists.txt, ${CMAKE_PROJECT_NAME} is used to define
+# the target library name; in the sub-module's CMakeLists.txt, ${PROJECT_NAME}
+# is preferred for the same purpose.
+#
+# In order to load a library into your app from Java/Kotlin, you must call
+# System.loadLibrary() and pass the name of the library defined here;
+# for GameActivity/NativeActivity derived applications, the same library name must be
+# used in the AndroidManifest.xml file.
+add_library(${CMAKE_PROJECT_NAME} SHARED
+ # List C/C++ source files with relative paths to this CMakeLists.txt.
+ nativelib.cpp
+)
+
+# Specifies libraries CMake should link to your target library. You
+# can link libraries from various origins, such as libraries defined in this
+# build script, prebuilt third-party libraries, or Android system libraries.
+target_link_libraries(${CMAKE_PROJECT_NAME}
+ # List libraries link to the target library
+ android
+ log)
\ No newline at end of file
diff --git a/app/nativelib/src/main/cpp/nativelib.cpp b/app/nativelib/src/main/cpp/nativelib.cpp
new file mode 100644
index 000000000..5522a3b41
--- /dev/null
+++ b/app/nativelib/src/main/cpp/nativelib.cpp
@@ -0,0 +1,10 @@
+#include
+#include
+
+extern "C" JNIEXPORT jstring JNICALL
+Java_com_example_nativelib_NativeLib_stringFromJNI(
+ JNIEnv* env,
+ jobject /* this */) {
+ std::string hello = "Hello from C++";
+ return env->NewStringUTF(hello.c_str());
+}
\ No newline at end of file
diff --git a/app/nativelib/src/main/java/com/example/nativelib/NativeLib.kt b/app/nativelib/src/main/java/com/example/nativelib/NativeLib.kt
new file mode 100644
index 000000000..71e6384f8
--- /dev/null
+++ b/app/nativelib/src/main/java/com/example/nativelib/NativeLib.kt
@@ -0,0 +1,18 @@
+package com.example.nativelib
+
+class NativeLib {
+
+ /**
+ * A native method that is implemented by the 'nativelib' native library,
+ * which is packaged with this application.
+ */
+ external fun stringFromJNI(): String
+
+ companion object {
+ // Used to load the 'nativelib' library on application startup.
+ init {
+ System.loadLibrary("nativelib")
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/nativelib/src/test/java/com/example/nativelib/ExampleUnitTest.kt b/app/nativelib/src/test/java/com/example/nativelib/ExampleUnitTest.kt
new file mode 100644
index 000000000..d6145e7f9
--- /dev/null
+++ b/app/nativelib/src/test/java/com/example/nativelib/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.example.nativelib
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/MainActivity.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/MainActivity.kt
index ecc23d80e..b2ada79a8 100644
--- a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/MainActivity.kt
+++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/MainActivity.kt
@@ -16,12 +16,15 @@
package com.google.samples.apps.nowinandroid
+import android.os.Build.VERSION_CODES
import android.os.Bundle
+import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
+import androidx.annotation.RequiresApi
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -50,7 +53,10 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
+import com.example.nativelib.NativeLib
+
import javax.inject.Inject
+import kotlin.reflect.KVisibility
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@@ -79,6 +85,8 @@ class MainActivity : ComponentActivity() {
val splashScreen = installSplashScreen()
super.onCreate(savedInstanceState)
+ accessSecretMessage(LibraryClass())
+
// We keep this as a mutable state, so that we can track changes inside the composition.
// This allows us to react to dark/light mode changes.
var themeSettings by mutableStateOf(
diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/SecretBox.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/SecretBox.kt
new file mode 100644
index 000000000..8fc7b792e
--- /dev/null
+++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/SecretBox.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.nowinandroid
+
+import android.util.Log
+
+val TAG: String = "R8"
+
+/**
+ * A class with private members to demonstrate reflection.
+ */
+class LibraryClass {
+ private val secretMessage: Message = Message("R8 will remove me")
+}
+
+data class Message(
+ val message: String,
+ val id: Int = 0
+)
+
+// In your app code:
+fun accessSecretMessage(instance: LibraryClass) {
+ // Use Java reflection from Kotlin to access the private field
+ val secretField = instance::class.java.getDeclaredField("secretMessage")
+ secretField.isAccessible = true
+ val message = secretField.get(instance) as Message
+ Log.d(TAG, message.toString())
+}
diff --git a/build.gradle.kts b/build.gradle.kts
index b7989bab4..7b1789be5 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -60,5 +60,6 @@ plugins {
alias(libs.plugins.roborazzi) apply false
alias(libs.plugins.secrets) apply false
alias(libs.plugins.room) apply false
- alias(libs.plugins.module.graph) apply true // Plugin applied to allow module graph generation
+ alias(libs.plugins.module.graph) apply true
+ alias(libs.plugins.kotlin.android) apply false // Plugin applied to allow module graph generation
}
diff --git a/core/datastore/consumer-proguard-rules.pro b/core/datastore/consumer-proguard-rules.pro
index 173273916..30c4fdc2f 100644
--- a/core/datastore/consumer-proguard-rules.pro
+++ b/core/datastore/consumer-proguard-rules.pro
@@ -1,4 +1,12 @@
# Keep DataStore fields
-keepclassmembers class * extends com.google.protobuf.GeneratedMessageLite* {
;
-}
\ No newline at end of file
+}
+
+-keepclassmembers,includedescriptorclasses class com.google.samples.apps.nowinandroid.LibraryClass {
+ private * secretMessage;
+}
+
+-printconfiguration r8/full-r8-config.txt # Prints the entire configuration for the app
+-printusage r8/usage.txt # Prints where R8 removed code from the app
+-printseeds r8/seeds.txt
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 477dde916..6a621ce71 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -18,7 +18,7 @@ androidxEspresso = "3.6.1"
androidxHiltNavigationCompose = "1.2.0"
androidxLifecycle = "2.8.7"
androidxLintGradle = "1.0.0-alpha03"
-androidxMacroBenchmark = "1.3.4"
+androidxMacroBenchmark = "1.4.0-beta02"
androidxMetrics = "1.0.0-beta01"
androidxNavigation = "2.8.5"
androidxProfileinstaller = "1.4.1"
@@ -59,6 +59,9 @@ room = "2.6.1"
secrets = "2.0.1"
truth = "1.4.4"
turbine = "1.2.0"
+junit = "4.13.2"
+junitVersion = "1.2.1"
+material = "1.12.0"
[bundles]
androidx-compose-ui-test = ["androidx-compose-ui-test", "androidx-compose-ui-testManifest"]
@@ -161,6 +164,9 @@ firebase-performance-gradlePlugin = { group = "com.google.firebase", name = "per
kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" }
ksp-gradlePlugin = { group = "com.google.devtools.ksp", name = "com.google.devtools.ksp.gradle.plugin", version.ref = "ksp" }
room-gradlePlugin = { group = "androidx.room", name = "room-gradle-plugin", version.ref = "room" }
+junit = { group = "junit", name = "junit", version.ref = "junit" }
+androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
+material = { group = "com.google.android.material", name = "material", version.ref = "material" }
[plugins]
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
@@ -198,3 +204,4 @@ nowinandroid-android-room = { id = "nowinandroid.android.room" }
nowinandroid-android-test = { id = "nowinandroid.android.test" }
nowinandroid-hilt = { id = "nowinandroid.hilt" }
nowinandroid-jvm-library = { id = "nowinandroid.jvm.library" }
+kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 2b8c6e45c..e8c31b2d9 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -83,3 +83,4 @@ check(JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_17)) {
https://developer.android.com/build/jdks#jdk-config-in-studio
""".trimIndent()
}
+include(":app:nativelib")
diff --git a/stacktrace b/stacktrace
new file mode 100644
index 000000000..889269b57
--- /dev/null
+++ b/stacktrace
@@ -0,0 +1,36 @@
+java.lang.RuntimeException: Unable to start activity ComponentInfo{com.google.samples.apps.nowinandroid/com.google.samples.apps.nowinandroid.MainActivity}: java.util.NoSuchElementException: List is empty.
+ at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:4048)
+ at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:4235)
+ at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:112)
+ at android.app.servertransaction.TransactionExecutor.executeNonLifecycleItem(TransactionExecutor.java:174)
+ at android.app.servertransaction.TransactionExecutor.executeTransactionItems(TransactionExecutor.java:109)
+ at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:81)
+ at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2636)
+ at android.os.Handler.dispatchMessage(Handler.java:107)
+ at android.os.Looper.loopOnce(Looper.java:232)
+ at android.os.Looper.loop(Looper.java:317)
+ at android.app.ActivityThread.main(ActivityThread.java:8705)
+ at java.lang.reflect.Method.invoke(Native Method)
+ at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:580)
+ at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:886)
+Caused by: java.util.NoSuchElementException: List is empty.
+ at i4.k.m0(SourceFile:22)
+ at i4.k.l0(SourceFile:12)
+ at com.google.samples.apps.nowinandroid.MainActivity.onCreate(SourceFile:54)
+ at android.app.Activity.performCreate(Activity.java:9002)
+ at android.app.Activity.performCreate(Activity.java:8980)
+ at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1526)
+ at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:4030)
+ at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:4235)
+ at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:112)
+ at android.app.servertransaction.TransactionExecutor.executeNonLifecycleItem(TransactionExecutor.java:174)
+ at android.app.servertransaction.TransactionExecutor.executeTransactionItems(TransactionExecutor.java:109)
+ at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:81)
+ at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2636)
+ at android.os.Handler.dispatchMessage(Handler.java:107)
+ at android.os.Looper.loopOnce(Looper.java:232)
+ at android.os.Looper.loop(Looper.java:317)
+ at android.app.ActivityThread.main(ActivityThread.java:8705)
+ at java.lang.reflect.Method.invoke(Native Method)
+ at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:580)
+ at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:886)
\ No newline at end of file