Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added Workshops/Android/GemmaAppImage.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions Workshops/Android/GemmaAppImage/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
1 change: 1 addition & 0 deletions Workshops/Android/GemmaAppImage/app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
62 changes: 62 additions & 0 deletions Workshops/Android/GemmaAppImage/app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
}

android {
namespace = "com.example.gemmaappimage"
compileSdk = 36

defaultConfig {
applicationId = "com.example.gemmaappimage"
minSdk = 24
targetSdk = 36
versionCode = 1
versionName = "1.0"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
release {
isMinifyEnabled = false

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

For release builds, it's highly recommended to enable minification (isMinifyEnabled = true). This helps reduce the size of your APK by shrinking and obfuscating the code, which is a crucial optimization for production apps. While it might be disabled for easier debugging in a sample, it's a best practice to demonstrate in a template.

            isMinifyEnabled = true

proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
Comment on lines +34 to +36

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The kotlinOptions block is a legacy way to configure Kotlin compilation options. The modern and recommended approach is to use the kotlin extension with compilerOptions. This is also inconsistent with the GemmaAppText project, which uses the modern syntax.

    kotlin {
        compilerOptions {
            jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11)
        }
    }

buildFeatures {
compose = true
}
}

dependencies {

implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation(libs.mediapipe.tasks.text)
implementation(libs.mediapipe.tasks.genai)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
}
21 changes: 21 additions & 0 deletions Workshops/Android/GemmaAppImage/app/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.example.gemmaappimage

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.gemmaappimage", appContext.packageName)
}
}
27 changes: 27 additions & 0 deletions Workshops/Android/GemmaAppImage/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.GemmaAppImage">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.GemmaAppImage">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package com.example.gemmaappimage

import android.graphics.BitmapFactory
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.gemmaappimage.ui.theme.GemmaAppImageTheme

val images = arrayOf(
R.drawable.surprise,
R.drawable.kitchen,
R.drawable.sunset,
)

val imageDescriptions = arrayOf(
R.string.surprise_description,
R.string.kitchen_description,
R.string.sunset_description,
)

@Composable
fun ChatScreen() {
val selectedImage = remember { mutableIntStateOf(0) }
val placeholderPrompt = stringResource(R.string.prompt_placeholder)
val placeholderResult = stringResource(R.string.result_placeholder)
var prompt by rememberSaveable { mutableStateOf(placeholderPrompt) }
var result by rememberSaveable { mutableStateOf(placeholderResult) }
val context = LocalContext.current.applicationContext

val chatViewModel: ChatViewModel = viewModel(factory = ChatViewModel.getFactory(context))
val uiState by chatViewModel.uiState.collectAsState()

Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Column(
modifier = Modifier.padding(innerPadding)
) {
Text(
text = stringResource(R.string.app_name),
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(16.dp)
)

LazyRow(
modifier = Modifier.fillMaxWidth()
) {
itemsIndexed(images) { index, image ->
var imageModifier = Modifier
.padding(start = 8.dp, end = 8.dp)
.requiredSize(200.dp)
.clickable {
selectedImage.intValue = index
}
if (index == selectedImage.intValue) {
imageModifier =
imageModifier.border(
BorderStroke(
4.dp,
MaterialTheme.colorScheme.primary
)
)
}
Image(
painter = painterResource(image),
contentDescription = stringResource(imageDescriptions[index]),
modifier = imageModifier
)
}
}
Row(
modifier = Modifier.padding(all = 16.dp)
) {
TextField(
value = prompt,
label = { Text(stringResource(R.string.label_prompt)) },
onValueChange = { prompt = it },
modifier = Modifier
.weight(0.8f)
.padding(end = 16.dp)
.align(Alignment.CenterVertically)
)

if (uiState is UiState.Generating) {
Button(
onClick = {
chatViewModel.stopRespones()
},
enabled = prompt.isNotEmpty(),
modifier = Modifier
.align(Alignment.CenterVertically)
) {
Text(text = stringResource(R.string.action_stop))
}
} else {
Button(
onClick = {
val bitmap = BitmapFactory.decodeResource(
context.resources,
images[selectedImage.intValue]
)
chatViewModel.resetSession()
chatViewModel.sendPrompt(bitmap, prompt)
},
Comment on lines +130 to +136

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

BitmapFactory.decodeResource is a blocking I/O operation that runs on the main thread here. This can cause the UI to freeze or lead to an "Application Not Responding" (ANR) error, especially with large images. This operation should be moved to a background thread.

A good practice is to delegate this work to the ViewModel, which can use viewModelScope to handle it asynchronously without blocking the UI.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gemini-code-assist Makes sense. I'll fix it in the next commit. Thanks!

enabled = prompt.isNotEmpty(),
modifier = Modifier
.align(Alignment.CenterVertically)
) {
Text(text = stringResource(R.string.action_go))
}
}
}
if (uiState is UiState.Loading) {
CircularProgressIndicator(modifier = Modifier.align(Alignment.CenterHorizontally))
} else {
var textColor = MaterialTheme.colorScheme.onSurface
if (uiState is UiState.Error) {
textColor = MaterialTheme.colorScheme.error
result = (uiState as UiState.Error).errorMessage
} else if (uiState is UiState.Generating) {
textColor = MaterialTheme.colorScheme.onSurface
result = (uiState as UiState.Generating).partialResult
} else if (uiState is UiState.Success) {
textColor = MaterialTheme.colorScheme.onSurface
result = (uiState as UiState.Success).outputText
}
val scrollState = rememberScrollState()
Text(
text = result,
textAlign = TextAlign.Start,
color = textColor,
modifier = Modifier
.padding(16.dp)
.fillMaxSize()
.verticalScroll(scrollState)
)
Comment on lines +148 to +168

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This block of code mutates the result state variable as a side effect during composition, which violates a core principle of Compose. The UI should be a direct function of the state, without side effects during composition.

Instead of mutating a rememberSaveable variable, you should derive the textColor and resultText directly from the uiState within the composition. This makes the UI more predictable and stable.

This change would also allow you to remove the result state variable declared on line 61.

                val (textColor, resultText) = when (val state = uiState) {
                    is UiState.Error -> MaterialTheme.colorScheme.error to state.errorMessage
                    is UiState.Generating -> MaterialTheme.colorScheme.onSurface to state.partialResult
                    is UiState.Success -> MaterialTheme.colorScheme.onSurface to state.outputText
                    is UiState.Initial -> MaterialTheme.colorScheme.onSurface to placeholderResult
                }

                val scrollState = rememberScrollState()
                Text(
                    text = resultText,
                    textAlign = TextAlign.Start,
                    color = textColor,
                    modifier = Modifier
                        .padding(16.dp)
                        .fillMaxSize()
                        .verticalScroll(scrollState)
                )

}
}
}
}

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
GemmaAppImageTheme {
ChatScreen()
}
}
Loading